Cheela Labs
REFERENCE

HTTP API

Four auth planes, and which one an endpoint belongs to is the most important thing about it. A credential valid on one plane is refused on the others.

Base URL and auth

Base URL
https://api.cheelalabs.com

Every authenticated request carries a bearer token. Which token depends on the plane:

PLANECREDENTIALOPENS
PublicnoneHealth, the manifest, the capability broker
Runtime — deploych_sk_…Deployments, runtime status, heartbeat
Runtime — publicch_pk_…/v1/runtime/execute only
OwnerDashboard session or account API keyRuntimes, executions, traces, projects, analytics, billing
Header
Authorization: Bearer ch_sk_...
The planes are enforced, not conventional

A deploy key sent to /v1/runtime/execute is refused, and so is a public key sent to /v1/deployments. The rejection is deliberately vague — the caller learns the key is not valid here, not which of the two they happen to be holding.

Public endpoints

GET/public

Service identity. Useful as a liveness probe.

200
{ "service": "...", "version": "..." }
GET/healthpublic
200
{
  "status": "ok",
  "version": "...",
  "uptime": 51234.7,
  "timestamp": "2026-01-01T00:00:00.000Z"
}
GET/v1/runtime/:runtimeId/manifestpublic

The runtime’s Agent Discovery Specification manifest, built from its latest deployment. Unauthenticated on purpose — this document is meant to be fetched by strangers and republished on your own domain.

Cached briefly (s-maxage=300) so a redeploy is visible without waiting out a long TTL. Returns 404 if the runtime has no deployment, and 400 if the deployment carries no website or adp.namespace.

POST/v1/capabilities/:runtimeId/:capabilitypublic

The capability broker — the address published in every manifest. No model and no agent loop: the caller has already decided what it wants.

Request
{
  "input": { "query": "wool socks" },
  "metadata": { "source": "partner-agent" },
  "endUserToken": "session_abc"
}
200
{ "executionId": "exec_...", "output": { "products": [] }, "durationMs": 142.7 }
502
{ "executionId": "exec_...", "error": { "message": "..." }, "durationMs": 88.1 }
  • 404 for an unknown runtime, an unknown capability, or a runtime with no endpoint — one shape for all three, so an anonymous caller cannot probe which runtimes exist.
  • 401 when the capability is marked requiresEndUser and no endUserToken was sent. Refused before anything is metered.

Executing

POST/v1/runtime/executech_pk_

Runs the full agent loop. The runtime is taken from the authenticated key, never from the body, so a caller can only execute the runtime whose key they hold.

Request
{
  "messages": [
    { "role": "user", "parts": [{ "type": "text", "content": "Where is order 8812?" }] }
  ],
  "metadata": { "surface": "web" },
  "endUserToken": "session_abc"
}
200
{
  "executionId": "exec_...",
  "status": "completed",
  "messages": [ ... ],
  "finishReason": "stop",
  "inputTokens": 812,
  "outputTokens": 96,
  "totalTokens": 908,
  "durationMs": 3140
}

Send Accept: text/event-stream for server-sent events. The stream always ends with a done event carrying the same result shape.

A failed execution is still 200

Check status in the body. The request succeeded; the execution did not.

POST/v1/runtime/capabilitych_pk_

One capability, no model. What the widget polls while waiting on a pending spec — a checkout finishing on your own payment page. Same key, origin allowlist and rate limit as /v1/runtime/execute, and the capability is looked up on the authenticated runtime rather than taken from the body.

Request
{
  "capability": "order-status",
  "input": { "orderId": "ord_123" },
  "endUserToken": "session_abc"
}
200
{ "executionId": "exec_...", "output": { "status": "paid", "cheela": { "settled": true } }, "durationMs": 41.2 }
  • Metered as one capability call with zero tokens — no provider runs, but the call is real load on your endpoint.
  • Unlike the broker, this spends the ordinary execution allowance rather than the anonymous sub-share, so your own widget cannot be starved by third-party traffic against your published manifest.
  • 404 names the capability, because the caller already holds this runtime’s key and there is no enumeration to prevent.
  • A capability failure is 200 with an error field, as with execute.
POST/v1/executionsowner

The owner-level equivalent. Takes runtimeId in the body, and checks it belongs to you.

Request
{ "runtimeId": "rt_...", "messages": [ ... ], "metadata": {} }

Deploying

POST/v1/deploymentsch_sk_

What cheela deploy calls. Creates a new deployment version from a manifest.

Request
{
  "manifest": {
    "schemaVersion": 2,
    "capabilities": [
      {
        "name": "catalog-search",
        "version": "1.2.0",
        "description": "...",
        "requiresEndUser": false,
        "actions": [{ "name": "search", "inputSchema": {}, "outputSchema": {} }]
      }
    ],
    "runtime": { "sdkVersion": "..." },
    "endpoint": "https://app.example.com/cheela/execute",
    "website": { "name": "Acme", "url": "https://www.acme.com" },
    "adp": { "namespace": "com.acme" },
    "metadata": { "cliVersion": "..." }
  }
}
201
{
  "version": 3,
  "status": "active",
  "capabilities": [{ "name": "catalog-search", "hasInputSchema": true }]
}

Capability names are validated here as well as in the CLI and the runtime. actions accepts at most one entry — a capability is 1:1 with an action, and accepting a list that would be ignored is how a second action’s schemas get silently dropped.

GET/v1/runtime/statusch_sk_
200
{
  "runtimeId": "rt_...",
  "deployment": { "version": 3, "status": "active", "deployedAt": "..." },
  "capabilities": ["catalog-search", "order-status"],
  "connection": { "status": "online", "transport": "http" },
  "health": "healthy",
  "provider": { "name": "openrouter", "model": "..." }
}

Calling this also counts as a check-in.

POST/v1/runtime/heartbeatch_sk_

Explicit check-in with no body. One indexed write.

200
{ "status": "ok", "lastSeenAt": "..." }

Runtimes

POST/v1/runtimesowner

Creates a runtime identity. Idempotent on runtimeId.

Request
{ "name": "storefront", "version": "1.0.0", "projectId": "proj_..." }
201
{
  "runtimeId": "rt_...",
  "projectId": "proj_...",
  "tier": "free",
  "secret": "...",
  "deployKey": "ch_sk_...",
  "publicKey": "ch_pk_...",
  "deployKeyPrefix": "ch_sk_yxyD...GHI",
  "publicKeyPrefix": "ch_pk_a1b2...XYZ"
}

deployKey and publicKey appear only when actually minted. Re-registering an existing id keeps its keys and returns neither — use reveal or rotate.

Rejects provider and tier rather than accepting and dropping them. 403 if creating this runtime would exceed your plan’s ceiling.

GET/v1/runtimesowner

Cursor-paginated. Query: limit, cursor, projectId.

GET/v1/runtimes/:runtimeIdowner

Full detail including capabilities, allowed origins, connection, health, and key prefixes — never whole keys.

PUT/v1/runtimes/:runtimeId/allowed-originsowner
Request
{ "origins": ["https://www.example.com"] }

Bare origins, up to 50. A path or trailing slash is rejected with a message naming the origin to use instead. An empty list means unrestricted.

Key management

ENDPOINTBODYEFFECT
POST /v1/runtimes/:runtimeId/reveal-key{ "type": "deploy" | "public" }Returns the key in full. POST, not GET, so it stays out of history and logs.
POST /v1/runtimes/:runtimeId/rotate-key{ "type": "deploy" | "public" }Mints a replacement. The old key works through a grace period.
POST /v1/runtimes/:runtimeId/revoke-key{ "type": "deploy" | "public" }Invalidates immediately, with no replacement.
POST /v1/runtimes/:runtimeId/rotate-secretMints a new signing secret for your endpoint.

Executions and traces

GET/v1/executionsowner

Query: limit (max 200), cursor, status (running, completed, failed), from, to.

200
{
  "nextCursor": "...",
  "executions": [
    {
      "executionId": "exec_...",
      "runtimeId": "rt_...",
      "status": "completed",
      "finishReason": "stop",
      "durationMs": 3140,
      "capabilityCalls": 2,
      "startedAt": "...",
      "completedAt": "..."
    }
  ]
}
GET/v1/executions/:executionIdowner

The trace: turn shapes, token counts, and each capability call.

Message content is not stored

Cheela does not persist what anybody wrote. In place of the conversation, a trace holds messageShape — one entry per turn with its role and the type of each part, and nothing else. It is enough to see that a run went user → assistant → tool, and not enough to read it back.

Capability input and output payloads are stored, and tool arguments often carry the user’s text. Keep that in mind when deciding what a capability accepts.

404, not 403, on someone else's execution

A 403 would confirm the id exists and turn this route into an enumeration oracle.

GET/v1/traces/runtime/:runtimeIdowner

Traces for one runtime. Same pagination and filters as above.

GET/v1/traces/:executionIdowner

Projects

ENDPOINTPURPOSE
GET /v1/projectsList your projects.
POST /v1/projectsCreate one.
GET /v1/projects/:projectIdFetch one.
PATCH /v1/projects/:projectIdRename or update.

A default project is created on first use, so every other endpoint works without mentioning projects at all.

Analytics and billing

GET/v1/analytics/summaryowner

Query: from, to, bucket (hour or day).

The window is clamped server-side against your tier, and the resolved window comes back on range — so a narrowed request is visible rather than silently changing what the numbers mean.

ENDPOINTPURPOSE
GET /v1/billing/plansAvailable plans and their limits.
GET /v1/billing/usageCurrent-period usage, with periodStart and periodEnd.
POST /v1/billing/checkoutStart an upgrade.
POST /v1/billing/verifyConfirm a completed payment.

Error shape

Every error has the same envelope.

Error
{
  "error": {
    "code": "validation_error",
    "message": "Invalid execution request",
    "details": { "fieldErrors": { "messages": ["Required"] } }
  }
}
CODESTATUS
validation_error400
unauthorized401
forbidden403
not_found404
rate_limit_exceeded429
execution_error502
internal_error500

details is present on validation failures and absent otherwise. Internal errors never carry it — an unhandled failure returns a generic message rather than echoing anything about your infrastructure.

Full list with causes in Errors and limits.

Pagination

Listing endpoints are cursor-based. Pass nextCursor back as ?cursor=; it is null on the last page.

Terminal
curl "https://api.cheelalabs.com/v1/executions?limit=50" \
  -H "Authorization: Bearer $API_KEY"

curl "https://api.cheelalabs.com/v1/executions?limit=50&cursor=$NEXT" \
  -H "Authorization: Bearer $API_KEY"

limit caps at 200 for executions and traces. Offsets are not supported — they skip and duplicate rows when new executions arrive mid-page, which for an append-heavy collection is constant.