Errors & rate limits
#Error shape
Every error response — regardless of status code — is:
{ "error": { "code": "not_found", "message": "Run not found", "details": null } }code is a stable, machine-readable string (safe to switch on); message is a human-readable English sentence (interfaces translate by code, not by parsing message — see Data & providers for how the console/web apps localize); details is present only on a handful of error codes that carry structured extra data.
#Common codes
| HTTP | code |
When |
|---|---|---|
| 400 | invalid_request |
Malformed JSON, or a missing/wrong-typed required field |
| 400 | schema_required |
Created/updated a document/custom-form agent without config.outputSchema |
| 401 | unauthorized |
Missing/invalid API key, or missing/expired session |
| 403 | forbidden |
Valid credential, but missing scope (key) or role permission (session) |
| 403 | auth.email_verification_required |
Unverified session user hit the unverified-run limit |
| 404 | not_found |
No such resource in this workspace |
| 409 | conflict (or a specific code below) |
An operation doesn't apply to the resource's current state |
| 409 | run_not_awaiting_input / _approval / _review |
Called /input, /approve, or /review on a run not in that status |
| 409 | run_already_terminal / run_currently_running |
Called /cancel on a run that's already finished, or currently executing |
| 409 | agent_no_version |
Ran (or eval'd) an agent that has no published version |
| 409 | idempotency_in_progress |
A request with the same Idempotency-Key is already being processed |
| 409 | idempotency_key_conflict |
The same Idempotency-Key was reused with a different request body |
| 409 | output_not_available |
Fetched /output before the run produced one |
| 409 | csv_not_supported |
Fetched /output?format=csv for a shape that can't flatten to rows |
| 402 | billing.insufficient_credits |
Workspace balance can't cover the estimate — details: { required, available } |
| 413 | payload_too_large |
Upload exceeds the 25 MB limit |
| 415 | unsupported_media_type |
Upload's content type isn't on the allowlist |
| 429 | rate_limited |
Over 120 req/min for this key — Retry-After header set |
| 429 | too_many_streams |
Workspace already has 20 concurrent GET .../events connections open |
| 500 | internal_error |
Unexpected server error — no further detail is included |
#Idempotency
POST /v1/runs, POST /v1/runs/:id/input, and POST /v1/runs/:id/approve accept an Idempotency-Key header (POST /v1/runs/:id/review does not, currently — see Human-in-the-loop). Send a fresh UUID per logical operation:
- Same key + same workspace + same request body, within 24 hours → the original response is replayed verbatim, side effects and all — safe to retry after a timeout or a dropped connection.
- Same key + a different request body →
409 idempotency_key_conflict. Treat this as a bug in the caller (reusing a key across two different operations), not something to retry. - Same key, concurrently in flight →
409 idempotency_in_progress. Retry after a short delay once the first attempt has completed.
async function createRunOnce(body: unknown, idempotencyKey: string) {
const res = await fetch("https://api.agentflowbind.com/v1/runs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AFB_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey, // generate once per logical run, reuse it across retries
},
body: JSON.stringify(body),
});
return res.json();
}#Rate limits
| Route(s) | Limit |
|---|---|
Any key-authenticated /v1/* request |
Per the calling workspace's plan (entitlements.apiRateLimitPerMin) — see Authentication & scopes for the per-plan numbers |
POST /v1/console/auth/sign-in, /sign-up |
10/min per client IP and 5/min per email — whichever is hit first blocks the request |
GET /v1/runs/:id/events |
20 concurrent open streams per workspace |
A 429 always includes Retry-After (seconds) except the SSE stream cap, which is a hard concurrency limit rather than a time window — close an existing stream (or wait for one to reach a terminal run status) before opening another.