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
https://api.cheelalabs.comEvery authenticated request carries a bearer token. Which token depends on the plane:
| PLANE | CREDENTIAL | OPENS |
|---|---|---|
| Public | none | Health, the manifest, the capability broker |
| Runtime — deploy | ch_sk_… | Deployments, runtime status, heartbeat |
| Runtime — public | ch_pk_… | /v1/runtime/execute only |
| Owner | Dashboard session or account API key | Runtimes, executions, traces, projects, analytics, billing |
Authorization: Bearer ch_sk_...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
/publicService identity. Useful as a liveness probe.
{ "service": "...", "version": "..." }/healthpublic{
"status": "ok",
"version": "...",
"uptime": 51234.7,
"timestamp": "2026-01-01T00:00:00.000Z"
}/v1/runtime/:runtimeId/manifestpublicThe 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.
/v1/capabilities/:runtimeId/:capabilitypublicThe capability broker — the address published in every manifest. No model and no agent loop: the caller has already decided what it wants.
{
"input": { "query": "wool socks" },
"metadata": { "source": "partner-agent" },
"endUserToken": "session_abc"
}{ "executionId": "exec_...", "output": { "products": [] }, "durationMs": 142.7 }{ "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
requiresEndUserand noendUserTokenwas sent. Refused before anything is metered.
Executing
/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.
{
"messages": [
{ "role": "user", "parts": [{ "type": "text", "content": "Where is order 8812?" }] }
],
"metadata": { "surface": "web" },
"endUserToken": "session_abc"
}{
"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.
Check status in the body. The request succeeded; the execution did not.
/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.
{
"capability": "order-status",
"input": { "orderId": "ord_123" },
"endUserToken": "session_abc"
}{ "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
errorfield, as with execute.
/v1/executionsownerThe owner-level equivalent. Takes runtimeId in the body, and checks it belongs to you.
{ "runtimeId": "rt_...", "messages": [ ... ], "metadata": {} }Deploying
/v1/deploymentsch_sk_What cheela deploy calls. Creates a new deployment version from a manifest.
{
"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": "..." }
}
}{
"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.
/v1/runtime/statusch_sk_{
"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.
/v1/runtime/heartbeatch_sk_Explicit check-in with no body. One indexed write.
{ "status": "ok", "lastSeenAt": "..." }Runtimes
/v1/runtimesownerCreates a runtime identity. Idempotent on runtimeId.
{ "name": "storefront", "version": "1.0.0", "projectId": "proj_..." }{
"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.
/v1/runtimesownerCursor-paginated. Query: limit, cursor, projectId.
/v1/runtimes/:runtimeIdownerFull detail including capabilities, allowed origins, connection, health, and key prefixes — never whole keys.
/v1/runtimes/:runtimeId/allowed-originsowner{ "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
| ENDPOINT | BODY | EFFECT |
|---|---|---|
| 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-secret | — | Mints a new signing secret for your endpoint. |
Executions and traces
/v1/executionsownerQuery: limit (max 200), cursor, status (running, completed, failed), from, to.
{
"nextCursor": "...",
"executions": [
{
"executionId": "exec_...",
"runtimeId": "rt_...",
"status": "completed",
"finishReason": "stop",
"durationMs": 3140,
"capabilityCalls": 2,
"startedAt": "...",
"completedAt": "..."
}
]
}/v1/executions/:executionIdownerThe trace: turn shapes, token counts, and each capability call.
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.
A 403 would confirm the id exists and turn this route into an enumeration oracle.
/v1/traces/runtime/:runtimeIdownerTraces for one runtime. Same pagination and filters as above.
/v1/traces/:executionIdownerProjects
| ENDPOINT | PURPOSE |
|---|---|
| GET /v1/projects | List your projects. |
| POST /v1/projects | Create one. |
| GET /v1/projects/:projectId | Fetch one. |
| PATCH /v1/projects/:projectId | Rename or update. |
A default project is created on first use, so every other endpoint works without mentioning projects at all.
Analytics and billing
/v1/analytics/summaryownerQuery: 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.
| ENDPOINT | PURPOSE |
|---|---|
| GET /v1/billing/plans | Available plans and their limits. |
| GET /v1/billing/usage | Current-period usage, with periodStart and periodEnd. |
| POST /v1/billing/checkout | Start an upgrade. |
| POST /v1/billing/verify | Confirm a completed payment. |
Error shape
Every error has the same envelope.
{
"error": {
"code": "validation_error",
"message": "Invalid execution request",
"details": { "fieldErrors": { "messages": ["Required"] } }
}
}| CODE | STATUS |
|---|---|
| validation_error | 400 |
| unauthorized | 401 |
| forbidden | 403 |
| not_found | 404 |
| rate_limit_exceeded | 429 |
| execution_error | 502 |
| internal_error | 500 |
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.
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.