Skip to content

Developers

API overview

Embed the agent over the HTTP API: keys, end_user_ref, building on your app's own API, turns and webhooks, timeouts and retries, files, billing in energy.

Last updated:

On this page
  1. What the API gives you
  2. Keys and access
  3. Your first request
  4. Build on your app's own API
  5. Lifecycle and webhooks
  6. Timeouts and retries
  7. Files
  8. Voice input
  9. Live calls from your app
  10. Billing and limits in energy

What the API gives you

The API lets your server create isolated spaces, configure them (manifest, secrets, tools, MCP servers), send requests with files, and get back replies and files the agent produced. Billing goes to your account, broken down by your own customers.

Keys and access

  • Base URL: https://dash.octodus.com/v1
  • Every request: the header Authorization: Bearer octo_live_…
  • Create keys in the dashboard: Integrations, API keys. The key is shown once.
  • The API is for server-side calls: never embed a key in a browser or mobile app.
Key scopeWhat it controls
Only spaces this key creates (recommended)Spaces created through POST /v1/spaces with this key
One spaceThe one space you named
All my spacesEvery space the account owns

end_user_ref is your own customer ID. It does not create an account with us, but spend and per-customer limits are tracked by it.

Your first request

Create a space, send a turn with an Idempotency-Key, and poll it until it reaches a terminal state. Python and TypeScript versions are in the reference.

bash
BASE=https://dash.octodus.com/v1
KEY=octo_live_xxxxxxxxxxxxxxxx

SLUG=$(curl -s -X POST $BASE/spaces -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -d '{"title":"My first space"}' | jq -r .space_id)

TURN=$(curl -s -X POST $BASE/turns -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
  -d '{"space":"'"$SLUG"'","end_user_ref":"customer-42","text":"Hello"}' | jq -r .turn_id)

curl -s $BASE/turns/$TURN -H "Authorization: Bearer $KEY" | jq '{state, result, energy_used}'

Build on your app's own API

  • /v1 does not deliver data. A turn is a trigger ("Handle request 42"), and its result is text for a person: show it or log it, never parse JSON or scrape data out of it.
  • Human workflow first. Every action a person takes in your app's UI (claim a request, fetch its file, write the result lines, set the supplier or object, set the status, send to review) is an operation of your app's own API, with its normal auth. The audit log shows who did it: a person, or the agent acting as Maria's delegate. A person can do the same job with the same requests without the agent. There is no agent-specific result endpoint, result format or submit_result tool.
  • The user delegates that work to the agent. Your app issues a credential for the user's delegate and hands the space its API when it creates the space, in one call (app_api on POST /v1/spaces). The space then reads the inputs, writes results and statuses, re-reads what it wrote and fixes what your app refused: your app answers a bad write with a 4xx and a reason, exactly as for a person.
  • Then your app sends only the trigger: POST /v1/turns with "Handle request 42" and an Idempotency-Key. Your app shows progress from its own database (the statuses the space set). A webhook only tells you that a turn finished.
bash
curl -s -X POST $BASE/spaces -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -d '{"title":"Orders for Maria","app_api":{
    "name":"orders","base_url":"https://orders.acme.com/api",
    "credentials":{"ORDERS_API_TOKEN":"<delegate token>"},"acts_for":"[email protected]"}}'

curl -s -X POST $BASE/turns -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
  -d '{"space":"'"$SLUG"'","text":"Handle request 42"}'

Connect an existing space, rotate the token, list or remove it: PUT, PATCH, GET and DELETE on /v1/spaces/{slug}/app-api. App API in the reference

Lifecycle and webhooks

  • A turn is asynchronous: queued → running → done | failed | cancelled.
  • Instead of polling, register a webhook: the platform posts a signed event on every transition. It carries no data, only "the turn finished", to refresh a screen or notice a failure. Webhooks
  • A turn's result is text for a person. The agent writes the data into your app through its API; GET /v1/turns/{id} is for checking the state, the error and energy_used. Your app's API
  • "stateless": true: the turn runs in a fresh throwaway sub-space with no history, archived once it ends. It is the default for an app's sub-key; "stateless": false keeps memory across calls. Details
  • "model": moves the whole space to that model, not only that turn. It stays until a turn names another model or "auto" (back to the default); a turn without a model keeps it. A model change is then one session switch with the cache kept, not a switch on every turn. In a throwaway (stateless) sub-space the model ends with it. Details

Timeouts and retries

  • Poll or be pushed: poll the turn every 1 to 2 s with growing backoff (up to 10 s), or receive events on a webhook (configured once, or webhook_url on the turn) and keep a slow safety-net poll. Neither carries your data: it is in your app already.
  • Every API call returns at once, so 30 s per HTTP request is plenty. Give the turn itself at least 180 s: most finish in seconds or a minute or two, longer with files and tools. If you stop waiting, the turn still runs and is billed; cancel it if you no longer need it.
  • A poll (GET) that gets 502, 503, 504 or a network error is always safe to retry. Retry POST /v1/turns only with the same Idempotency-Key: you get the original turn back, not a second paid one.
  • A network error is not a result. A POST /v1/turns that timed out may have started the turn: do not mark the job failed, re-send the same request with the same Idempotency-Key later, and it returns that turn. Give document recognition no deadline under hours: show "still running". Send Connection: close so one stalled connection does not hang the next call.
  • A failed turn is sent again, not shown as the request's error. When a turn fails with SUBAGENT_BLOCKED or SUBAGENT_PARTIAL (a worker refused or did not finish), your app re-sends "Handle request 42" with backoff, with a new Idempotency-Key each time. That is safe: the space continues the request from the statuses it already set in your app. Nothing is parsed from the answer; after the last attempt the request goes to review.
  • Never retry a cap 429 before its Retry-After; a customer cap (no Retry-After) and a 402 are not cleared by retrying.

The retry table · Webhooks

Files

Upload a file into the space (multipart, field file, up to 200 MB) and pass its file_id on the turn; files the agent produces come back on the turn as links. File names can be in any language: send the name as plain UTF-8 in the filename parameter (curl -F and fetch with FormData do), it comes back unchanged as name, and a download also carries it as filename* (RFC 5987).

bash
FID=$(curl -s -X POST $BASE/spaces/$SLUG/files -H "Authorization: Bearer $KEY" \
  -F 'file=@./invoice.pdf' | jq -r .file_id)

curl -s -X POST $BASE/turns -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
  -d '{"space":"'"$SLUG"'","text":"Summarise this invoice","file_ids":["'"$FID"'"]}'

Files in the reference · File names

Voice input

A user of your app can speak a message or send an audio file. Upload the recording like any file and pass its file_id as voice_file_id on the turn (text is then optional). It is transcribed server-side with the same speech-to-text as a Telegram voice message, the turn runs on the transcript as the user's message, and the recording stays in the space as an attachment. The transcript comes back in the turn's voice field (status: pending → done, transcript) so your app can show the user their words.

bash
FID=$(curl -s -X POST $BASE/spaces/$SLUG/files -H "Authorization: Bearer $KEY" \
  -F 'file=@./voice.webm;type=audio/webm' | jq -r .file_id)

curl -s -X POST $BASE/turns -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
  -d '{"space":"'"$SLUG"'","voice_file_id":"'"$FID"'"}'

# GET /v1/turns/{id} → "voice": {"status": "done", "transcript": "Where is my order 42?", ...}

Formats: webm/opus, ogg, m4a/mp4, mp3, wav (else 415 UNSUPPORTED_MEDIA_TYPE), up to 100 MB (413 VOICE_TOO_LARGE). A recording with no speech fails the turn with VOICE_EMPTY, one that cannot be transcribed with TRANSCRIPTION_FAILED: ask the user to say it again or type it. Transcription is billed in energy per minute to whoever pays for the turn and is part of the turn's energy_used. Never send your key to the browser: the page records (MediaRecorder) and posts the audio to your backend, which uploads it and creates the turn with its key.

Voice input in the reference

Live calls from your app

A “Call Octo” button in your app: your user talks to Octo by voice in your space, with no account here. Your server asks for a call for that user (end_user_ref) with its key and gets a single-use join_url good for 5 minutes; your page opens it in a popup (or an iframe with allow="microphone"): a voice room opened in the browser (WebRTC), just that user and Octo. The call runs as that user, like their turns; its transcript and summary stay with them, and the call is billed in energy to the key's account.

bash
curl -s -X POST $BASE/spaces/$SLUG/calls -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"end_user_ref":"cust-42","display_name":"Maria","lang":"en"}'

# → {"join_url": "https://…/app-call/…", "expires_at": "…", "max_minutes": 30, ...}
# afterwards: GET $BASE/spaces/$SLUG/calls/{call_id} → state, energy, transcript, summary_turn_id

Live calls in the reference

Billing and limits in energy

  • The API reports costs and caps in energy only. Each turn is billed to the account that owns the space; the energy it cost, every multiplier included, is in energy_used, the spend per customer in GET /v1/usage.
  • Caps are energy: each key has a daily cap (default 50 energy, resets at 00:00 UTC), an app's sub-key may have a monthly one, and a customer can have a lifetime total_limit_energy.
  • 402 INSUFFICIENT_CREDITS: the balance is exhausted and needs a top-up.
  • 429 QUOTA_EXCEEDED: a key, customer or windowed cap; the body carries details.reason and energy amounts (cap_energy, spent_today_energy, remaining_energy), and the Retry-After header says when to try again, never sooner.
  • During platform maintenance a turn is still accepted (202) and waits as queued until maintenance ends; the response carries maintenance and Retry-After when the expected end is known.
  • There is no request rate limit: spend is bounded by the balance and the caps.
  • The old dollar fields (billed_cost_usd, spend_usd, cap_usd and the other *_usd) still arrive beside the energy fields, but are deprecated and go away on 2026-10-28.

All error codes · Limits · Billing