Shadow-OS API
Chat Your agentsTerms & Privacy
Developer API

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.

Base URLhttps://shadow-os-backend.onrender.com
Fraction of the cost
Premium quality from ultra-efficient models.
One simple endpoint
POST your prompt, get a JSON answer.
Keys you control
Up to 4 keys, revoke anytime.

Your API keys

Loading…

Quickstart

Send a POST request to the endpoint below with your API key in the Authorization header.

POSThttps://shadow-os-backend.onrender.com/api/v1/agent
curl -X POST https://shadow-os-backend.onrender.com/api/v1/agent \ -H "Authorization: Bearer sk-shadow-..." \ -H "Content-Type: application/json" \ -d '{"input": "Summarize the latest AI news", "session_id": "demo"}'

Authentication

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.

Authorization: Bearer sk-shadow-... # or X-Shadow-API-Key: sk-shadow-...

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

{ "answer": "I packaged the project into cloud_cost_anomaly_review.zip — download it below.", "code": null, "request_id": "a1b2c3d4", "session_id": "demo", "files": [ { "name": "cloud_cost_anomaly_review.zip", "download_url": "https://…/api/artifacts/…/cloud_cost_anomaly_review.zip?exp=…&sig=…", "mime": "application/zip" } ], "download_url": "https://…/api/artifacts/…/cloud_cost_anomaly_review.zip?exp=…&sig=…", "usage": { "used": 1, "quota": 75, "remaining": 74, "period": "2026-06" } }

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.

  • files is always present — an empty array when the turn produced nothing.
  • download_url at the top level is a shortcut to the first file, or null.
  • Links are signed and time-limited (exp + sig) — download promptly, or call again for a fresh link.
  • Files belong to the session_id that created them (or to your account under scope: "account").
# build something and download whatever came back curl -s -X POST https://shadow-os-backend.onrender.com/api/v1/agent \ -H "Authorization: Bearer $SHADOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input":"Build a small FastAPI project and package it as a zip","session_id":"demo"}' \ | jq -r '.files[].download_url' \ | xargs -I {} curl -sOJL "{}"

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.

pip install shadow-os
Build an agent end to end
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 customers
Talk to it — one private memory per end-user
from 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)
Operate it
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())
Typed errors. Every failure raises a precise exception carrying 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.

New conversation
Send a fresh session_id → a clean, empty, isolated space. No memory or files from any other conversation.
Continue a conversation
Reuse the same session_id → the agent picks up with all of that conversation's memory and files.
Never mixed
Memory, files and RAG are all keyed to the conversation — user A can never see user B's data.
// user A's chat — isolated, remembers only user A { "input": "remember I prefer short answers", "session_id": "user-A" } // user B's chat — a totally separate brain & file space { "input": "what are my preferences?", "session_id": "user-B" } // → "You haven't told me any preferences yet." (A's are invisible here) // continue user A later — full memory + files come back { "input": "what are my preferences?", "session_id": "user-A" } // → "You prefer short answers."

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 turn

Files & workspace

There are two different things you can do with a file, and the difference matters.

Index it — for semantic search
Text is extracted and embedded so the agent can FIND it. Use for knowledge: handbooks, policies, notes.
client.workspace.index("handbook.pdf")
client.documents.search("refund policy", top_k=5)
Add it — for reading, editing and running
The RAW file is kept in the agent's persistent workspace, so it can open it, edit it line by line, and execute it. Use for code and data.
client.workspace.add("analysis.py")
client.workspace.list()
client.workspace.download("report.xlsx")     # something the agent produced

Your 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.

Your key is never stored. It is read from the request, used for that single turn, and discarded — never written to our database, to an agent's config, to a log line, or to a backup. Logs record at most a one-way fingerprint of the model, never the key. That is exactly why it is sent per request rather than configured once: there is nothing to configure.
The providers
Google GeminiModel.google(...)gemini-2.5-pro · gemini-2.5-flasheffort → thinking_level
OpenAIModel.openai(...)gpt-4o · o3 · gpt-4.1effort → reasoning_effort
AnthropicModel.anthropic(...)claude-sonnet-4-5 · claude-opus-4effort → thinking budget
With the SDK
from 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 serve
Over HTTP
POST /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
  }
}
Reasoning effort

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

You do not build automation through this API — you ask for it. The agent creates and runs recurring work itself, from an ordinary message: “every morning at 8, summarise the overnight news and email it to me”, or “tell me if the price on this page drops below 8000”. The endpoints below exist to see and control what it already made — list it, pause it, reschedule it, cancel it — which is what you need to put automation on a dashboard rather than to create it.
# 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

Which one do I use? The Agent API (Get started, above) is the fastest path — one endpoint, isolated per 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.

Run a configured agent
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)
}
member_key — a stable id for each end-user. The agent learns and remembers each one separately (never mixes two people). session_id — keep it constant for one continuous thread, or vary it to run multiple parallel conversations per user. Public agent info: 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.
Create & manage agents — with your API key
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/run
Pure-API lifecycle (Bearer sk-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_mode

Manage 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.

Create & configure
POST/api/agents/draftA website and/or a document in, a draft agent out — see below. Creates nothing.
POST/api/agentsCreate: name, persona, template, fields, config.
GET/api/agentsList your agents.
GET · PATCH · DELETE/api/agents/:idRead, update (persona, config, name), or delete an agent.
GET/api/agent/templatesReady-made templates and the fields each one asks for.
GET/api/agent/tool-catalogThe always-on tools and the optional groups you can switch on (config.tools).
GET · PUT · DELETE/api/agents/:id/modelRun this agent on your own model and key. The key is write-only — reads show its last four characters.
Knowledge
POST/api/agents/:id/knowledgeAdd text: { title, text }.
POST/api/agents/:id/knowledge/fileUpload a PDF, Word, text, Markdown or CSV file (multipart: file, description).
POST/api/agents/:id/knowledge/urlImport a public web page: { url, title? }.
GET/api/agents/:id/knowledgeList what the agent knows.
PATCH · DELETE/api/agents/:id/knowledge/:nameUpdate a document's description, or remove it.
GET · POST/api/agents/:id/factsThe fact book — what the owner taught, kept current and shown to the agent on every turn. POST { text } adds a fact; a newer version of an existing one replaces it.
POST/api/agents/:id/facts/bulkAdd several distinct facts at once: { texts: [ … ] }.
PATCH · DELETE/api/agents/:id/facts/:fidEdit a fact's text (the old wording is kept as history), or remove it.
Learned tools
GET/api/agents/:id/learned-toolsTools the agent taught itself in the Manage chat — HTTP and code — with their last test result. Secrets are never returned.
POST/api/agents/:id/learned-tools/:name/approveActivate a proposed tool. Refused when its test failed.
DELETE/api/agents/:id/learned-tools/:nameRemove a learned tool.
Operate
POST/api/agents/:id/testManage the agent by chat: { input, session_id? }. It acts with your authority — messages a customer, books, moves or cancels, updates its details. Not a preview of customer replies.
GET/api/agents/:id/test-historyYour manager conversation, durable across visits.
GET/api/agents/:id/escalationsQuestions the agent passed to you, plus customers who went quiet.
POST/api/agents/:id/escalations/:eid/answerAnswer one — the agent relays it to the customer.
GET/api/agents/:id/membersEveryone who talks to the agent, with the profile it keeps on each.
GET/api/agents/:id/appointmentsWhat the agent booked.
GET/api/agents/:id/analyticsOutcomes: leads, bookings, quotes, resolved, handoffs.
GET/api/agents/:id/delivery-statusWhatsApp delivery per customer over the last 15 minutes.
Campaigns
GET/api/agents/:id/campaignsThe agent's campaigns, 30-day stats, and whether WhatsApp templates are configured.
PUT/api/agents/:id/campaignsSave them: { campaigns: [ … ] }.
POST/api/agents/:id/campaigns/:cid/previewWho is due right now, and a message the agent wrote for the first of them. Sends nothing.
POST/api/agents/:id/campaigns/:cid/runSend now to whoever is due. Every rule still applies except the time-of-day window.
Share & access
GET/api/agents/:id/share-linkThe agent's permanent link — web and WhatsApp.
GET/api/agents/:id/adsClick-to-WhatsApp ads: the number, the prefilled message to use, and each ad with its leads.
POST · GET/api/agents/:id/shareMint an extra access link, or list them.
DELETE/api/agents/:id/share/:sidRevoke a link.
POST · GET/api/agents/:id/tokensMint (with limits) or list agent tokens for /api/agent/run.
DELETE/api/agents/:id/tokens/:tidRevoke a token.
POST · DELETE/api/agents/:id/logoThe picture shown when the link is shared.

Setup in minutes, and campaigns

From a website or a document to a draft agent

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": [ … ] }
Campaigns — follow up quiet leads, invite customers back

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 }
Rules that always hold:
· 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).

Assistant
POST/api/v1/agentRun the agent on a prompt. Accepts model + effort.client.chat()
GET/api/v1/meWho this key belongs to.client.me()
GET/api/v1/modelsProviders this deployment can serve.client.providers()
GET/api/v1/usageQuota + lifetime statistics.client.usage()
Documents & search
POST/api/v1/uploadUpload and index a document.client.documents.upload()
GET/api/v1/documentsList indexed documents.client.documents.list()
POST/api/v1/searchSemantic search.client.documents.search()
Conversations & memory
GET/api/threads/{user_id}List your conversations.client.conversations.list()
GET/api/chat/history/{user_id}Messages of one conversation.client.conversations.history()
POST/api/threads/{id}/renameRename a conversation.client.conversations.rename()
DELETE/api/threads/{id}Delete a conversation.client.conversations.delete()
GET/api/memory/{user_id}What the assistant has learned about you.client.memory.summary()
GET/api/progress/{thread_id}Live progress of an in-flight turn.client.conversations.progress()
PUT/api/agents/{agent_id}/modelRun an agent on your own provider (key stored encrypted, never returned).—
DELETE/api/agents/{agent_id}/modelBack to the platform model.—
Files & workspace
GET/api/files/listEverything in the workspace.client.workspace.list()
POST/api/files/uploadIndex a file for semantic search.client.workspace.index()
POST/api/files/workspaceStore a raw file the agent can edit and run.client.workspace.add()
GET/api/artifacts/{user_id}/{file}Download something the agent produced.client.workspace.download()
Agents — lifecycle
POST/api/v1/agentsCreate an agent (returns a manager token once).client.agents.create()
GET/api/v1/agentsList the agents you own.client.agents.list()
GET/api/agents/{id}Read one agent.client.agents.get()
PATCH/api/v1/agents/{id}Update name, persona, config.client.agents.update()
DELETE/api/agents/{id}Delete an agent and everything it holds.client.agents.delete()
GET/api/agent/templatesReady-made templates and their fields.client.templates()
GET/api/agent/tool-catalogTool groups you can enable per agent.client.tool_catalog()
Agents — access
POST/api/agents/{id}/tokensMint an agent token.agent.access.create_token()
GET/api/agents/{id}/tokensList tokens.agent.access.list_tokens()
DELETE/api/agents/{id}/tokens/{t}Revoke a token.agent.access.revoke_token()
POST/api/agents/{id}/shareCreate a share link.agent.access.create_share_link()
GET/api/agents/{id}/shareList share links.agent.access.list_share_links()
GET/api/agents/{id}/share-linkThe persistent primary link.agent.share_link()
GET/api/agents/{id}/adsClick-to-WhatsApp ads and their leads.agent.ads()
DELETE/api/agents/{id}/share/{s}Revoke a share link.agent.access.revoke_share_link()
Agents — knowledge
POST/api/agents/{id}/knowledgeAdd knowledge as text.agent.knowledge.add_text()
POST/api/agents/{id}/knowledge/fileAdd a document or image.agent.knowledge.add_file()
POST/api/agents/{id}/knowledge/urlImport a public web page.agent.knowledge.add_url()
GET/api/agents/{id}/knowledgeList knowledge files.agent.knowledge.list()
PATCH/api/agents/{id}/knowledge/{f}Rename a knowledge file.agent.knowledge.rename()
DELETE/api/agents/{id}/knowledge/{f}Delete a knowledge file.agent.knowledge.delete()
POST/api/agents/{id}/chat-fileFiles the agent may send but not search.agent.knowledge.add_sendable()
Agents — operations
GET/api/agents/{id}/membersCustomers + the profile it built of each.agent.customers()
GET/api/agents/{id}/escalationsWhat it could not answer.agent.escalations()
POST/api/agents/{id}/escalations/{e}/answerAnswer; it relays in its own voice.agent.answer_escalation()
GET/api/agents/{id}/appointmentsBookings it made.agent.appointments()
GET/api/agents/{id}/analyticsUsage analytics.agent.analytics()
GET/api/agents/{id}/delivery-statusOutbound delivery outcomes.agent.delivery_status()
POST/api/agents/{id}/testTalk to it as its manager.agent.talk()
GET/api/agents/{id}/test-historyThat conversation's history.agent.history()
Talking to an agent (agent token / share code)
GET/api/agent/infoPublic entry info: name, welcome, mode.AgentClient.info()
POST/api/agent/runSend a message. Accepts model + effort.AgentClient.send()
GET/api/agent/threadsA member's conversations.AgentClient.conversations()
GET/api/agent/historyOne conversation's messages.AgentClient.history()
POST/api/agent/thread/deleteDelete a member's conversation.AgentClient.delete_conversation()
Automation
POST/api/recipesCreate a workflow.client.recipes.create()
GET/api/recipesList workflows.client.recipes.list()
GET/api/recipes/{id}Read one workflow.client.recipes.get()
PATCH/api/recipes/{id}Update it.client.recipes.update()
DELETE/api/recipes/{id}Delete it.client.recipes.delete()
POST/api/recipes/{id}/runRun it now.client.recipes.run()
POST/api/recipes/{id}/pausePause it.client.recipes.pause()
GET/api/recipes/{id}/runsIts run history.client.recipes.runs()
POST/api/watchesCreate an autonomous monitor.client.watches.create()
GET/api/watchesList monitors.client.watches.list()
PATCH/api/watches/{id}Pause or resume.client.watches.pause()/resume()
DELETE/api/watches/{id}Delete a monitor.client.watches.delete()
POST/api/watches/alerts/seenMark alerts read.client.watches.mark_alerts_seen()
GET/api/background-tasks/{user_id}Scheduled work.client.tasks.list()
POST/api/background-tasks/{id}/rescheduleMove it.client.tasks.reschedule()
DELETE/api/background-tasks/{id}Cancel it.client.tasks.cancel()
Notifications & integrations
GET/api/push/prefsWhich events may reach your devices.client.notifications.prefs()
PUT/api/push/prefsChange them.client.notifications.set_prefs()
GET/api/gmail/statusIs Google connected?client.integrations.google()
GET/api/whatsapp/link/statusIs WhatsApp linked?client.integrations.whatsapp()
POST/api/profile/languageLanguage for server-initiated messages.client.integrations.set_language()

Quota

POST/api/v1/agentRun the agent on a prompt (chat, reasoning, tools). · 1 quota unit
POST/api/v1/uploadUpload a document (pdf/txt/md/csv/docx) into your knowledge base. · 1 quota unit
POST/api/v1/searchSemantic search over your uploaded documents. · 1 quota unit
GET/api/v1/documentsList the documents in your knowledge base. · free
GET/api/v1/usageYour current quota + lifetime usage statistics. · free
Upload a file (multipart):
curl -X POST https://shadow-os-backend.onrender.com/api/v1/upload \ -H "Authorization: Bearer sk-shadow-..." \ -F "file=@report.pdf" \ -F "session_id=demo" # same session_id you pass to /api/v1/agent

The 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.

{ "error": "quota_exceeded", "detail": "Free monthly quota reached. Resets next month.", "request_id": "a1b2c3d4" }
401invalid_keyMissing or invalid API key.
402quota_exceededFree monthly quota reached.
429rate_limitedToo many requests — retry after a short wait.
400bad_requestMalformed body, or the 'input' field is missing.

Reference

Access
Registered users only — create keys while signed in
Auth
Header Authorization: Bearer sk-shadow-… (or X-Shadow-API-Key)
Body
input (required) · session_id (optional — its own isolated memory + files) · scope (optional — "session" default | "account" shared)
Free quota
75 requests / user / month, then HTTP 402
Rate limit
Max 10 requests / 20s per user → HTTP 429
Keys
Up to 4 active keys per account
Errors
401 invalid key · 402 quota exceeded · 429 rate-limited