Octomatica API

Octomatica API

Embed an Octo AI agent inside your own app: create isolated, multi-user spaces, give each one your tools and a durable manifest, send prompts, and read replies — billed per end-user.

Base https://dash.octomatica.ru/v1 Auth Bearer octo_live_… Swagger /api/docs

Octomatica is a general agent-space substrate. A space is one isolated agent:

Space = Agent core  +  Channel  +  Toolset  +  Manifest  +  Engine/Model

Telegram is one channel; this HTTP API is another. Your backend creates spaces, configures them, sends turns (prompts), and reads replies. You pay Octo in aggregate and re-bill your own users using the per-end-user breakdown.

Base URL & auth

ItemValue
Base URLhttps://dash.octomatica.ru/v1
Auth headerAuthorization: Bearer octo_live_… on every request
Interactive docs/api/docs (Swagger) · /api/openapi.json
Content typeapplication/json (except file download, which streams bytes)
Errors{ "error": { code, message, details, trace_id } } + an X-Trace-Id response header

A missing or invalid key returns 401 INVALID_KEY. The plaintext key is shown once at creation — store it.

Get a key — and scope it

Create keys in the dashboard → Integrations → API Keys → Create key, then choose Space access:

ChoiceWhat the key can control
Only spaces this key creates (recommended)ONLY spaces it provisions via POST /v1/spaces. It can never touch your existing (Telegram / other) spaces.
One specific spaceA single space slug you name.
All my spacesEvery space your account owns (account-wide, legacy).
Prefer “Only spaces this key creates” for a service/integration key — a leaked key is then blast-radius-limited to the spaces it made. Each key also has a daily $ cap.

The golden path

1. POST /v1/spaces                     → { space_id }      # create an isolated space; store the slug
2. PUT  /v1/spaces/{slug}/manifest     # how the agent behaves / drives your app (durable)
   PUT  /v1/spaces/{slug}/secret       # your app credential (encrypted, write-only)
   PUT  /v1/spaces/{slug}/tools        # allowlist built-ins + register your MCP server
3. POST /v1/turns  {space, text, end_user_ref}   → 202 { turn_id, state:"queued" }
4. GET  /v1/turns/{turn_id}            # poll until state is done | failed | cancelled
5. GET  /v1/turns/{turn_id}/files      # any files it produced → GET /v1/files/{file_id}
   GET  /v1/usage?group_by=end_user    # per-end-user spend, to re-bill your users

Step 2 configures once (or inherits later); steps 3–5 repeat per message. end_user_ref is your own opaque customer id (like OpenAI’s user param) — it drives per-user billing and caps; it is not an Octo account.

Endpoints · Spaces

Method & pathPurpose
POST /v1/spaces201Create an isolated API space. Body {engine?, model?, title?} — engine + model per Engines & models. Returns { "space_id": "<slug>", "origin": "api" }.
GET /v1/spacesList your API spaces (an own-scoped key lists only the ones it created).
GET /v1/spaces/{slug}/turns?limit=50Recent turns for a space (max 200).
PUT /v1/spaces/{slug}/manifestSet the durable manifest. Body { "manifest": "..." }.
GET /v1/spaces/{slug}/manifestRead it.
PUT /v1/spaces/{slug}/secretStore the space’s app credential (write-only, encrypted, never returned). Body { "secret": "..." }.
PUT /v1/spaces/{slug}/toolsSet the toolset (see Tools & MCP).
GET /v1/spaces/{slug}/toolsRead the toolset config.
GET /v1/tools/catalogThe built-in capability keys you may allowlist.
GET /v1/modelsThe engine + model options for POST /v1/spaces (see Engines & models).
Not yet available: single-space GET /v1/spaces/{id} and DELETE/archive. Track slugs yourself — store the returned space_id against your own record. (external_ref in the create body isn’t persisted today.)

Engines & models

A space runs on one engine + model, chosen at POST /v1/spaces and honored on every turn. Send these exact values (also served by GET /v1/models):

EngineModelSend
Claude Code (Anthropic)Opus{"engine":"claude-code","model":"opus"}
OpencodeDeepSeek V4 Pro{"engine":"opencode","model":"deepseek/deepseek-v4-pro"}
OpencodeCodex (GPT-5.5){"engine":"opencode","model":"gpt-5.5"}

engine defaults to claude-code. An unknown engine, or a model that isn’t valid for that engine, is rejected 400 INVALID_REQUEST.

Endpoints · Turns

Method & pathPurpose
POST /v1/turns202Send a prompt. Body {space, text, end_user_ref?, metadata?}. Header Idempotency-Key. Returns {turn_id, state:"queued", trace_id} + a Location header.
GET /v1/turns/{turn_id}Poll status / result (see lifecycle).
POST /v1/turns/{turn_id}/cancelCooperatively cancel a non-terminal turn (else 409 ALREADY_TERMINAL).
GET /v1/turns/{turn_id}/filesList files the turn produced: [{file_id, name, content_type, size, url}].
GET /v1/files/{file_id}Download a produced file (streamed bytes; Content-Disposition filename).

Endpoints · Usage & per-end-user caps

Method & pathPurpose
GET /v1/usage?group_by=…&scope=key|accountSpend + turn-count rollup. scope=key = only this key’s turns; default scope=account = the whole account (for re-billing across your keys).
GET /v1/spaces/{slug}/end-usersEnd-users with cumulative spend + limit.
PATCH /v1/spaces/{slug}/end-users/{ref}Set an end-user’s cumulative cap. Body { "total_limit_usd": 5.0 } (null = uncapped).

Turn lifecycle

queued → running → done        (result ready)
                  → failed      (error set)
queued →→→→→ cancelled           (you cancelled before it finished)

Poll GET /v1/turns/{turn_id} until state is terminal — done | failed | cancelled (the terminal success state is done, not succeeded). Response:

{
  "turn_id": "turn_ab12...",
  "state": "done",
  "current_activity": "...",         // live progress hint while running
  "result": "the agent's reply text",
  "result_truncated": false,          // true if the reply exceeded 1 MB
  "error": null,                       // { "code": "...", "message": "..." } when state=failed
  "usage": { "input_tokens": ..., "output_tokens": ..., "total_tokens": ...,
             "provider": "...", "model": "..." },
  "billed_cost_usd": 0.0123,
  "metadata": { ...you echoed it back... },
  "created_at": "...", "started_at": "...", "completed_at": "..."
}

Turns are async — poll every ~1–2 s with backoff; expect seconds, not milliseconds.

Idempotency

Send a unique Idempotency-Key header on POST /v1/turns (a UUID per logical message):

  • Re-sending the same key + same body returns the original turn (no duplicate).
  • Re-sending the same key with a different body → 409 IDEMPOTENCY_CONFLICT.
  • Without it, a network-blip retry creates a second paid turn. Always send it.

Tools, MCP & results

PUT /v1/spaces/{slug}/tools body:

{
  "allow": ["web", "image", "files"],              // built-in allowlist; null = unrestricted
  "mcp_servers": [                                  // your own tools, exposed over MCP
    { "name": "acme", "url": "https://mcp.acme.com", "headers": { "...": "..." } }
  ]
}

Built-in capability keys (from GET /v1/tools/catalog): web, image, media, transcription, gdrive, seo, code, files, research. Least-privilege: allow only what the space needs.

The model never sees raw credentials. Store your app credential once via PUT /v1/spaces/{slug}/secret; it’s encrypted at rest and injected server-side when a tool calls your app. The manifest is the durable “how to drive my app” instruction — set once, not re-sent per turn. Tools work on both engines (claude-code + opencode).

Getting results back — call your tools, don’t parse the reply

This is the robust pattern, and the one to build. Define the output you want as one of your tools — an MCP tool or a REST action with a typed input schema — and in the manifest tell the agent to deliver its result by calling that tool (e.g. submit_report(title, sections, findings, …)). Then:

  • The structured data arrives through your endpoint, already typed and validated by you.
  • If a call doesn’t match your schema, return a clear validation error — the agent reads it and retries with corrected arguments (both engines self-heal on tool errors). Your input schema is the contract; the agent converges to it.
  • So you never write a JSON-from-text parser, a schema validator, or a retry loop over the reply — the tool layer gives you all three for free.
The turn’s result is a human-readable summary — show it to a person or log it, but do not parse it as JSON or scrape data from it. Your machine-readable output is whatever the agent sent to your tools during the turn (built-in files deliver artifacts the same way — fetch via GET /v1/turns/{id}/files).

Billing

  • Every turn bills your account (the key’s owner) at billed_cost_usd. Octo bills you in aggregate; you re-bill your own users from the end_user_ref breakdown in GET /v1/usage.
  • Per-key daily cap (default $5/day, resets at UTC midnight) → 429 QUOTA_EXCEEDED with {spent_today_usd, cap_usd, resets_at}.
  • Per-end-user cumulative cap (total_limit_usd, default none) → 429 QUOTA_EXCEEDED once that end-user’s lifetime spend hits it. Raise it to “grant more credits.”
  • Only spend is throttled — there is no requests-per-minute limit.

Error codes

HTTPcodeMeaning
400INVALID_REQUESTMissing space/text, bad group_by, unknown tool key, metadata > 8 KB.
401INVALID_KEYMissing / malformed / unknown / revoked key.
403KEY_NOT_BOUND_TO_SPACEThe key may not control that space (wrong binding, not owned, or — for a key scoped to its own spaces — it didn’t create that space).
404SPACE_NOT_FOUND / TURN_NOT_FOUND / FILE_NOT_FOUNDNot found, or not visible to this key (existence isn’t leaked).
409IDEMPOTENCY_CONFLICTIdempotency-Key reused with a different body.
409ALREADY_TERMINALCancel/complete on an already-finished turn.
413FILE_TOO_LARGEUploaded file over the 25 MB limit.
429QUOTA_EXCEEDEDDaily key cap or per-end-user cap reached (details says which).

Payload limits: metadata ≤ 8 KB (opaque, echoed back) · result ≤ 1 MB (truncated, with result_truncated:true) · file ≤ 25 MB.

Worked example

BASE=https://dash.octomatica.ru/v1
KEY=octo_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 1) create an isolated space (a key scoped to "only spaces this key creates"
#    can never touch anything but the spaces it makes)
SLUG=$(curl -s -X POST $BASE/spaces -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"engine":"claude-code","title":"Acme concierge for user 42"}' | jq -r .space_id)

# 2) give it a manifest + your app credential + your tools
curl -s -X PUT $BASE/spaces/$SLUG/manifest -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"manifest":"You are Acme'"'"'s in-app assistant. Use the acme.* tools to read orders..."}'
curl -s -X PUT $BASE/spaces/$SLUG/secret -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -d '{"secret":"acme_app_key_live_..."}'
curl -s -X PUT $BASE/spaces/$SLUG/tools -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"allow":["web","files"],"mcp_servers":[{"name":"acme","url":"https://mcp.acme.com"}]}'

# 3) run a turn for one of your end-users (idempotent)
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":"acme-user-42","text":"Where is my order #1001?"}' | jq -r .turn_id)

# 4) poll until terminal
while :; do
  R=$(curl -s $BASE/turns/$TURN -H "Authorization: Bearer $KEY")
  S=$(echo "$R" | jq -r .state)
  [ "$S" = done ] || [ "$S" = failed ] || [ "$S" = cancelled ] && { echo "$R" | jq .; break; }
  sleep 1.5
done

Client poll loop (pseudocode):

turn = POST /v1/turns { space, text, end_user_ref, headers:{ "Idempotency-Key": uuid() } }
loop:
    t = GET /v1/turns/{turn.turn_id}
    if t.state in {done, failed, cancelled}: break
    sleep(1.5s, capped backoff)
if   t.state == "done":   deliver t.result   (+ GET /v1/turns/{id}/files if any)
elif t.state == "failed": show t.error.message

Common mistakes

  • Parsing the turn result as JSON. It’s a free-text human summary. Deliver structured data through your own MCP tool / REST action — the agent calls it and retries to match your schema (see Tools, MCP & results). Don’t build a parser + validator + retry over the reply.
  • Waiting on succeeded. The terminal success state is done.
  • No Idempotency-Key. A retry then double-charges. Always send one per logical message.
  • Account-wide key when you meant scoped. Use “Only spaces this key creates” so the key can’t reach your existing spaces.
  • Relying on external_ref in the create body. It isn’t persisted today — keep your own your_user_id → space_id map from the returned slug.
  • Putting secrets in the prompt. Store them via PUT …/secret; the model only ever sees tool results, never the credential.
  • Not handling failed. Always branch on the terminal state; read error.code / message.
Octomatica API · Agent-as-a-Service. Interactive schema at /api/docs. Questions? Contact your Octomatica integration owner.