The Shadow-OS agent, as an API
Premium multi-agent reasoning at a fraction of the usual API cost. One REST endpoint, your API key, JSON in & out — with fully isolated conversations so every end-user of your app keeps their own private memory and files. A free monthly tier is included — no credit card required.
https://shadow-os-backend.onrender.comYour API keys
Quickstart
Send a POST request to the endpoint below with your API key in the Authorization header.
https://shadow-os-backend.onrender.com/api/v1/agentAuthentication
Every request must include your secret key — as a Bearer token, or via the X-Shadow-API-Key header. Keep it server-side; never expose it in client-side code.
Request body
inputstringrequiredThe prompt / instruction for the agent.session_idstringoptionalThe conversation id. Each distinct value is its OWN isolated conversation — separate memory & files. Reuse it to continue that conversation with full context. Defaults to "default".scopestringoptional"session" (default) isolates memory & files per session_id. "account" shares one memory/file space across all your calls (and enables your account's connected integrations).Response
Files the agent produces
When the agent builds something — a ZIP, a report, code, an image — the response carries it in files. There is no attachment and no multipart body: the signed download_url is the file. Fetch it to get the bytes.
filesis always present — an empty array when the turn produced nothing.download_urlat the top level is a shortcut to the first file, ornull.- Links are signed and time-limited (
exp+sig) — download promptly, or call again for a fresh link. - Files belong to the
session_idthat created them (or to your account underscope: "account").
Python SDK
The official client wraps every endpoint on this page — the assistant, documents and search, and the full agent platform (create, knowledge, access links, customers, escalations, appointments, analytics). It is typed, ships sync and async clients, and retries cold starts, timeouts and 5xx automatically.
from shadow_os import ShadowOS, AgentConfig
client = ShadowOS() # reads $SHADOW_OS_API_KEY
created = client.agents.create(
name="Nona Pizza",
template="pizzeria", # or persona="..." for full control
fields={"business_name": "Nona", "hours": "Sun-Thu 11:00-23:00"},
config=AgentConfig(memory_mode="single", tone="warm and brief"),
)
agent = client.agent(created.id)
agent.knowledge.add_file("menu.pdf", description="Full menu with prices")
agent.knowledge.add_url("https://nona.example/about")
print(agent.share_link().web) # the link you hand to customersfrom shadow_os import AgentClient
with AgentClient("lnk_...") as agent: # share code or sk-agent- token
reply = agent.send("Do you deliver to Florentin?", member_key="user-42")
print(reply.answer, reply.files)
for conv in agent.for_member("user-42").conversations():
print(conv.session_id, conv.title)for c in agent.customers(): # who talked to it, and what it learned
print(c.display_name, "-", c.profile)
for e in agent.escalations(): # what it could not answer
agent.answer_escalation(e.id, "Yes - we're open until 23:00 on Sunday.")
print(agent.analytics().raw, agent.appointments(), agent.delivery_status())status, code, message and the server's request_id — QuotaExceeded exposes .usage, RateLimited exposes .retry_after. Async is the same surface: AsyncShadowOS, awaited.Conversations & isolation
Building an app that serves many end-users? Every session_id is a fully isolated conversation — its own memory, its own uploaded files, its own context. Nothing bleeds between your users. This is the default; you don't have to do anything to get it.
Uploads follow the same rule — pass the matching session_id (form field) to /api/v1/upload, /api/v1/search and /api/v1/documents so a file is visible only inside its conversation.
Want one shared assistant instead (a single memory across all your calls, plus your account's connected Gmail/Calendar/integrations)? Add "scope": "account" to the body. An isolated conversation is a clean sandbox and intentionally has no access to your account integrations. Billing and quota always stay on your key, regardless of scope.
Conversations & memory
Conversations are durable and listable, and the assistant keeps a living memory of you that persists across all of them — not just a context window. Both are readable from the API.
# A conversation loads its own messages — no need to work out how to join the two.
for conv in client.conversations.list():
print(conv.session_id, conv.title)
for msg in conv.messages():
print(" ", msg.role, msg.content)
# The same thing, if you already have the id:
for msg in client.conversations.history("thread-id"):
print(msg.role, msg.content)
client.conversations.rename("thread-id", "Contract review")
client.conversations.delete("thread-id")
mem = client.memory.summary() # what it has learned about you, across everything
print(mem.summary, mem.count)
client.conversations.progress("thread-id") # live tool/thinking progress of an in-flight turnFiles & workspace
There are two different things you can do with a file, and the difference matters.
client.workspace.index("handbook.pdf")
client.documents.search("refund policy", top_k=5)client.workspace.add("analysis.py")
client.workspace.list()
client.workspace.download("report.xlsx") # something the agent producedYour own model
Every endpoint that runs the agent accepts an optional model — run the whole platform (tools, memory, knowledge, agents) on your provider and your key instead of ours. Works on /api/v1/agent, /api/agent/run, and the web chat.
Model.google(...)gemini-2.5-pro · gemini-2.5-flasheffort → thinking_levelModel.openai(...)gpt-4o · o3 · gpt-4.1effort → reasoning_effortModel.anthropic(...)claude-sonnet-4-5 · claude-opus-4effort → thinking budgetfrom shadow_os import ShadowOS, Model
client = ShadowOS()
client.chat("Analyse this contract",
model=Model.google("gemini-2.5-pro", api_key="AIza..."))
client.chat("Draft the reply",
model=Model.openai("gpt-4o", api_key="sk-..."))
client.chat("Think hard about it",
model=Model.anthropic("claude-sonnet-4-5", api_key="sk-ant-..."),
effort="high")
print(client.providers().available) # what THIS deployment can servePOST /api/v1/agent
{
"input": "Analyse this contract",
"model": {
"provider": "openai", // openai | anthropic | google | openai_compatible
"model": "gpt-4o",
"api_key": "sk-...", // yours; used for this turn, never stored
"effort": "high", // optional: low | medium | high
"base_url": "https://..." // required only for openai_compatible
}
}effort is low · medium · high, and is translated to whatever the provider calls reasoning depth — Gemini's thinking_level, OpenAI's reasoning_effort, Claude's thinking budget. It works with or without a model: on its own it just sets the depth of the platform model. Omit it and the depth is chosen per turn from the request itself.
Self-hosted and OpenAI-compatible endpoints
Anything that speaks the OpenAI API also works — Groq, Together, OpenRouter, vLLM, Ollama, your own gateway. Give it an explicit base_url. Capability varies sharply by model here: the platform drives a real tool loop, and a small local model will often fail to use tools reliably. For production, the three providers above are the supported path.
client.chat("Summarise", model=Model.compatible(
"llama-3.3-70b", api_key="gsk-...",
base_url="https://api.groq.com/openai/v1"))A malformed model returns 400 rather than silently falling back to ours — running you on our Gemini when you asked for your own GPT would bill you for a model you never chose. Note that once you supply a model, your content goes to that provider under their terms; see section 7 of the terms.
Automation
# Ask for it in conversation — this is the normal path.
client.chat("Every weekday at 08:00, email me a summary of my unread mail")
# Then see and control what the agent set up.
for t in client.tasks.list(): # scheduled work
print(t.description, t.target_time, t.status)
client.tasks.reschedule(t.id, "2026-10-01T08:00:00")
client.tasks.cancel(t.id)
for w in client.watches.list(): # monitors it created
print(w.title, w.status, w.interval_minutes)
client.watches.pause(w.id)
for r in client.recipes.list(): # reusable workflows it saved
print(r.name, r.status)
for run in client.recipes.runs(r.id):
print(" ", run.status, run.started_at)Recipes are workflows the agent saves so it can repeat them reliably, with a run history. Watches check something on an interval and alert when a condition fires. Scheduled tasks are one-off or recurring work bound to a time. Creating any of them through the API is possible (client.recipes.create(), client.watches.create()) but it is the harder path — the agent writes better steps from a sentence than you will from a schema.
Agent Platform API
session_id. The Agent Platform here is for a configured product: a persona + shared org knowledge + your connected integrations, driven by one token that doubles as a human magic-link, a WhatsApp chat, and a REST key. Each end-user still gets their own private memory.Configure an agent (persona, knowledge, rules) once, then run it for any number of end-users. Memory and chat history are isolated per member (via member_key) — the agent learns each person separately and never mixes two people — while knowledge and integrations are shared across the org. Strict per-agent, per-member isolation, fail-closed.
POST {API}/api/agent/run
Content-Type: application/json
{
"token": "sk-agent-…", // the agent token (manager or member)
"input": "Do you deliver to Florentin?",
"member_key": "user-42", // ← per-PERSON: private memory + history
"session_id": "main" // ← conversation id (multi-conversation orgs)
}GET /api/agent/info?token=….Full, durable history (members are not guests): every member’s conversations are saved server-side and listable —
GET /api/agent/threads?token=&member_key= lists a member’s conversations;GET /api/agent/history?token=&member_key=&thread_id= returns one conversation’s messages. Both are scoped to that member (fail-closed) — no one reads another member’s threads.POST {API}/api/v1/agents
Authorization: Bearer sk-shadow-…
{ "template": "support", "fields": { "business_name": "Acme" },
"config": { "webhook": "https://you.com/hook", "collect_contact": true } }
→ { "agent": {…}, "manager_token": "sk-agent-…" } // run it via /api/agent/runsk-shadow-…):·
POST /api/v1/agents — create · GET /api/v1/agents — list · PATCH /api/v1/agents/:id — persona, rules & full config· define your own end-users with
member_key (each gets private memory + a living profile) · separate chats with session_id·
GET /api/agent/threads · GET /api/agent/history — a user's conversations·
config.webhook — we POST escalation AND outcome events to your server in real time·
config.integrations — connect your own HTTP tools the agent calls mid-chat · config.collect_contact, config.fallback, config.tone, config.require_login, config.memory_modeManage agents by API
Everything the console does is an endpoint. Authenticate with your API key (Authorization: Bearer sk-shadow-…) or a signed-in session; every call re-checks that you own the agent, so a key reaches exactly your agents and no others. :id is the agent id.
Setup in minutes, and campaigns
Send a public URL, a file, a few words — or any mix. The draft is grounded in what you sent: prices, hours and answers are taken from the source, never invented, and missing lists what the source didn't say. Review it, then create the agent with POST /api/agents and add the facts as knowledge.
curl -X POST https://shadow-os-backend.onrender.com/api/agents/draft \
-H "Authorization: Bearer sk-shadow-..." \
-F "url=https://your-business.com" \
-F "file=@price-list.pdf" \
-F "notes=A pilates studio; help people book a trial class"
→ { "name", "business_type", "summary", "persona", "welcome_message",
"hours", "prices", "faq": [{ "q", "a" }], "contact", "missing": [ … ] }Two campaign types: lead_followup (someone asked, then went quiet for days) and win_back (last visit days ago, nothing booked). The agent writes every message itself — for that one person, in the language they write in, from what they actually asked. note is optional guidance in any language.
PUT /api/agents/:id/campaigns
{ "campaigns": [
{ "id": "lead_followup", "type": "lead_followup", "enabled": true, "days": 2,
"hours": [9, 20], "skip_days": [5], "note": "Mention the free trial class" },
{ "id": "win_back", "type": "win_back", "enabled": true, "days": 30,
"hours": [9, 20], "skip_days": [5], "note": "" }
] }
POST /api/agents/:id/campaigns/lead_followup/run
→ { "sent": 3, "sent_template": 1, "needs_template": 2, "skipped": 4, "failed": 0 }· Sends only between
hours (the agent's time zone), never on skip_days (0 = Monday … 5 = Saturday, 6 = Sunday).· At most one campaign message per person every 5 days, across all campaigns — and one per quiet spell or visit.
· A customer who replies
STOP (or הסר) is never messaged again, on any campaign.· WhatsApp allows free-form messages only within 24 hours of the customer's last message. Beyond that, a campaign opens the chat with your approved WhatsApp template; until one is configured, those people are reported as
needs_template and nothing is sent to them.· Web customers see the message on their next visit.
Full endpoint reference
Every endpoint your key can reach. All of them are wrapped by the Python SDK, so the method name beside each one is what you would call from Python. Authenticate with Authorization: Bearer sk-shadow-… (or X-Shadow-API-Key).
/api/v1/agentRun the agent on a prompt. Accepts model + effort.client.chat()/api/v1/meWho this key belongs to.client.me()/api/v1/modelsProviders this deployment can serve.client.providers()/api/v1/usageQuota + lifetime statistics.client.usage()/api/v1/uploadUpload and index a document.client.documents.upload()/api/v1/documentsList indexed documents.client.documents.list()/api/v1/searchSemantic search.client.documents.search()/api/threads/{user_id}List your conversations.client.conversations.list()/api/chat/history/{user_id}Messages of one conversation.client.conversations.history()/api/threads/{id}/renameRename a conversation.client.conversations.rename()/api/threads/{id}Delete a conversation.client.conversations.delete()/api/memory/{user_id}What the assistant has learned about you.client.memory.summary()/api/progress/{thread_id}Live progress of an in-flight turn.client.conversations.progress()/api/agents/{agent_id}/modelRun an agent on your own provider (key stored encrypted, never returned).—/api/agents/{agent_id}/modelBack to the platform model.—/api/files/listEverything in the workspace.client.workspace.list()/api/files/uploadIndex a file for semantic search.client.workspace.index()/api/files/workspaceStore a raw file the agent can edit and run.client.workspace.add()/api/artifacts/{user_id}/{file}Download something the agent produced.client.workspace.download()/api/v1/agentsCreate an agent (returns a manager token once).client.agents.create()/api/v1/agentsList the agents you own.client.agents.list()/api/agents/{id}Read one agent.client.agents.get()/api/v1/agents/{id}Update name, persona, config.client.agents.update()/api/agents/{id}Delete an agent and everything it holds.client.agents.delete()/api/agent/templatesReady-made templates and their fields.client.templates()/api/agent/tool-catalogTool groups you can enable per agent.client.tool_catalog()/api/agents/{id}/tokensMint an agent token.agent.access.create_token()/api/agents/{id}/tokensList tokens.agent.access.list_tokens()/api/agents/{id}/tokens/{t}Revoke a token.agent.access.revoke_token()/api/agents/{id}/shareCreate a share link.agent.access.create_share_link()/api/agents/{id}/shareList share links.agent.access.list_share_links()/api/agents/{id}/share-linkThe persistent primary link.agent.share_link()/api/agents/{id}/adsClick-to-WhatsApp ads and their leads.agent.ads()/api/agents/{id}/share/{s}Revoke a share link.agent.access.revoke_share_link()/api/agents/{id}/knowledgeAdd knowledge as text.agent.knowledge.add_text()/api/agents/{id}/knowledge/fileAdd a document or image.agent.knowledge.add_file()/api/agents/{id}/knowledge/urlImport a public web page.agent.knowledge.add_url()/api/agents/{id}/knowledgeList knowledge files.agent.knowledge.list()/api/agents/{id}/knowledge/{f}Rename a knowledge file.agent.knowledge.rename()/api/agents/{id}/knowledge/{f}Delete a knowledge file.agent.knowledge.delete()/api/agents/{id}/chat-fileFiles the agent may send but not search.agent.knowledge.add_sendable()/api/agents/{id}/membersCustomers + the profile it built of each.agent.customers()/api/agents/{id}/escalationsWhat it could not answer.agent.escalations()/api/agents/{id}/escalations/{e}/answerAnswer; it relays in its own voice.agent.answer_escalation()/api/agents/{id}/appointmentsBookings it made.agent.appointments()/api/agents/{id}/analyticsUsage analytics.agent.analytics()/api/agents/{id}/delivery-statusOutbound delivery outcomes.agent.delivery_status()/api/agents/{id}/testTalk to it as its manager.agent.talk()/api/agents/{id}/test-historyThat conversation's history.agent.history()/api/agent/infoPublic entry info: name, welcome, mode.AgentClient.info()/api/agent/runSend a message. Accepts model + effort.AgentClient.send()/api/agent/threadsA member's conversations.AgentClient.conversations()/api/agent/historyOne conversation's messages.AgentClient.history()/api/agent/thread/deleteDelete a member's conversation.AgentClient.delete_conversation()/api/recipesCreate a workflow.client.recipes.create()/api/recipesList workflows.client.recipes.list()/api/recipes/{id}Read one workflow.client.recipes.get()/api/recipes/{id}Update it.client.recipes.update()/api/recipes/{id}Delete it.client.recipes.delete()/api/recipes/{id}/runRun it now.client.recipes.run()/api/recipes/{id}/pausePause it.client.recipes.pause()/api/recipes/{id}/runsIts run history.client.recipes.runs()/api/watchesCreate an autonomous monitor.client.watches.create()/api/watchesList monitors.client.watches.list()/api/watches/{id}Pause or resume.client.watches.pause()/resume()/api/watches/{id}Delete a monitor.client.watches.delete()/api/watches/alerts/seenMark alerts read.client.watches.mark_alerts_seen()/api/background-tasks/{user_id}Scheduled work.client.tasks.list()/api/background-tasks/{id}/rescheduleMove it.client.tasks.reschedule()/api/background-tasks/{id}Cancel it.client.tasks.cancel()/api/push/prefsWhich events may reach your devices.client.notifications.prefs()/api/push/prefsChange them.client.notifications.set_prefs()/api/gmail/statusIs Google connected?client.integrations.google()/api/whatsapp/link/statusIs WhatsApp linked?client.integrations.whatsapp()/api/profile/languageLanguage for server-initiated messages.client.integrations.set_language()Quota
/api/v1/agentRun the agent on a prompt (chat, reasoning, tools). · 1 quota unit/api/v1/uploadUpload a document (pdf/txt/md/csv/docx) into your knowledge base. · 1 quota unit/api/v1/searchSemantic search over your uploaded documents. · 1 quota unit/api/v1/documentsList the documents in your knowledge base. · free/api/v1/usageYour current quota + lifetime usage statistics. · freeThe session_id matters: an upload is isolated to that conversation, so the agent can only use it when you send the same id. Omit it and the file lands in the default conversation. Use -F "scope=account" to share it across every call on your key instead.
Errors
Errors use standard HTTP status codes and return a JSON body with a machine-readable error and a human-readable detail.
invalid_keyMissing or invalid API key.quota_exceededFree monthly quota reached.rate_limitedToo many requests — retry after a short wait.bad_requestMalformed body, or the 'input' field is missing.