Errors and limits
Every failure Cheela can report, and every ceiling it enforces.
Error shape
{
"error": {
"code": "validation_error",
"message": "Invalid execution request",
"details": { "fieldErrors": { "messages": ["Required"] } }
}
}details appears on validation failures and is otherwise absent. Internal errors never carry it — an unhandled failure returns a generic message rather than echoing anything about the infrastructure that produced it.
API error codes
| CODE | STATUS | CAUSE |
|---|---|---|
| validation_error | 400 | The body failed its schema. `details` names the fields. |
| unauthorized | 401 | Missing or invalid key, a key not valid on this route, or a capability requiring an end user called anonymously. |
| forbidden | 403 | The resource belongs to another owner, or creating it would exceed your plan's runtime ceiling. |
| not_found | 404 | Unknown runtime, capability, execution, or project. Also returned instead of 403 where a 403 would confirm an id exists. |
| rate_limit_exceeded | 429 | Too many requests, or the hourly execution allowance is spent. |
| execution_error | 502 | A capability call through the public broker failed. |
| internal_error | 500 | Something unexpected. Logged server-side. |
Fetching another owner’s execution returns 404, not 403 — a 403 would confirm the id exists and turn the route into an enumeration oracle. The public broker does the same for unknown runtimes, unknown capabilities, and runtimes with no endpoint: one shape for all three, so an anonymous caller cannot probe which runtimes exist.
Two that look like errors and are not
- A failed execution returns 200. The request succeeded; the execution did not. Read
statusanderrorfrom the body. - A capability that throws returns 200 from your handler. Cheela turns it into a
tool_resulterror the model can read and recover from.
Signature failures
Returned by verifyCheelaSignature as result.reason, and by the handlers as a 401 body.
| REASON | WHAT TO CHECK |
|---|---|
| missing_headers | One of the four x-cheela-* headers did not arrive. Usually a proxy or CDN stripping unknown headers. |
| runtime_mismatch | The signature was issued for a different runtime than the one you pinned with `runtimeId`. |
| timestamp_invalid | The timestamp header was not a number. |
| timestamp_outside_tolerance | More than five minutes of clock skew, in either direction. Check NTP on your server. |
| nonce_replayed | A genuine replay — or several instances behind a load balancer without a shared nonce store. |
| signature_mismatch | The wrong secret, or far more often a body that was parsed and re-serialized before verification. |
The last one is the one to suspect first. Full detail in Serve capability calls.
Handler responses
What the runtime handlers return to Cheela.
| STATUS | BODY | MEANING |
|---|---|---|
| 200 | { output } | Ran and returned. |
| 200 | { output: null, error } | The capability threw. The loop continues. |
| 400 | { error: "invalid_json" } | Body was not JSON. |
| 400 | { error: "missing_capability" } | No capability named in the body. |
| 400 | { error: "raw_body_required" } | Express only. Mounted with express.json() instead of express.raw(). |
| 401 | { error: reason } | Verification failed. One of the six reasons above. |
Client errors
| CLASS | THROWN WHEN |
|---|---|
| CheelaNetworkError | The request never completed. Carries the cause. |
| CheelaAuthError | 401 or 403. Carries `status`. |
| CheelaApiError | Any other non-2xx. Carries `status`. |
| CheelaConfigError | Client config failed to parse — usually a missing key. |
| ValidationError | From @cheela/sdk, when a schema rejects a value. |
CheelaClientError is the base for the first three. Catch it to catch everything transport-related.
Plan limits
| FREE | PRO | ENTERPRISE | |
|---|---|---|---|
| Executions / hour | 100 | 2,000 | unlimited |
| Rollover window | 2 hours | 24 hours | — |
| Burst capacity | 200 | 48,000 | — |
| Runtimes | 1 | 10 | unlimited |
An execution counts once no matter how many steps the model takes. Capability calls appear in usage and analytics, but they are not a limit — a ceiling nobody enforces is worse than no ceiling, because it drifts away from what was sold.
How quota works
A token bucket, not a per-hour counter. Capacity is your hourly rate multiplied by your rollover window, so a quiet hour pays for a busy one — which is the only way to express “you may burst to 48,000 but only sustain 2,000/hour”.
Usage responses carry periodStart and periodEnd so an interface can say when the allowance resets rather than leaving people to guess.
Broker sub-allowance
Anonymous calls through the public broker draw on a smaller share as well as the main allowance. Without it, traffic against a public manifest could exhaust the owner’s whole quota and take their own widget down with it — a remote denial of service against anyone who publishes one.
Runtime ceilings
Only creating a new runtime counts. Re-registration never does, socheela deploy keeps working for someone sitting exactly at their limit. Owners already over a limit keep everything they have — the check gates creation, it never deletes.
Execution bounds
| BOUND | DEFAULT | EFFECT |
|---|---|---|
| Steps per execution | 25 | The agent loop stops with finishReason: "length". |
| Parallel capability calls | capped | Tool calls in one step are dispatched concurrently up to a fixed ceiling. |
| Signature tolerance | 5 minutes | Clock skew allowed in either direction. |
| Page size | 200 | Maximum `limit` on executions and traces. |
| Allowed origins | 50 | Maximum entries per runtime. |
There is no quota check between steps, deliberately. A step consumes nothing a quota could refuse, so the cost of one execution is bounded structurally by the step budget and the parallel-call cap instead — re-checking per step meant aggregating the owner’s whole usage once per step rather than once per execution.
Reaching the step budget is "length", not "error": the loop hit its ceiling, which is a budget outcome rather than a failure.