Errors, rate limits and idempotency
#Error format
Every error has the same envelope:
{
"error": {
"code": "rate_limited",
"message": "Human-readable explanation.",
"requestId": "7f3a…",
"retryable": true,
"details": { }
}
}It arrives in one of two places:
- Tool errors come back as a normal tool result with
isError: true, and the envelope instructuredContent(and as JSON text incontent). Most MCP clients show these to the model so it can react. - Request errors, such as a bad key, a disallowed origin or the rate limit, are returned as an HTTP status (
401,403,429) with the envelope as the JSON body, before any tool runs.
Every response has an x-request-id header. Include it when you contact support.
#Error codes
| Code | Retry? | Meaning and what to do |
|---|---|---|
invalid_input |
No | Arguments don't match the schema, or an idempotency key was reused with different arguments. Fix the call. |
unauthorized |
No | Key missing, wrong, revoked or expired; the key's scopes don't cover the tool; or the paid subscription isn't active. |
not_found |
No | The id doesn't exist or belongs to another account. |
payment_required |
No | The account has no active plan or trial (canceled, or the last payment failed). Fix billing in the app's Billing page, then call again. |
insufficient_credits |
No | The balance can't cover the call. details has availableMicros and requiredMicros. Top up, don't retry. |
requires_approval |
No | Not a failure: the action needs confirmation. See Approvals. |
rate_limited |
Yes | Too many calls. Wait for Retry-After / details.retryAfter seconds. |
retryable_upstream_failure |
Yes | An image, video or AI provider failed or timed out, or the same idempotent call is still running. Retry with backoff. |
internal |
No | Something broke on our side. Try once more later, then contact support with the request id. |
Retry only when retryable is true: exponential backoff starting around 2 seconds, at most 3 attempts, with the same Idempotency-Key.
#Rate limits
| Limit | Default |
|---|---|
| Tool calls per API key | 60 per minute (initialize and tools/list are free) |
generate_reel_storyboard |
60 per account per day (UTC) |
scrape_trending_hooks, trending hooks in resolve_reel_generators |
10 searches per account per hour |
| Request body | 1 MB |
Over the per-key limit you get HTTP 429:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{"error":{"code":"rate_limited","retryable":true,"details":{"rateLimit":60,"retryAfter":12}, …}}Higher per-key limits (up to 600 a minute) are available on request.
#Idempotency
A network error can leave you unsure whether a call ran. Idempotency keys make retries safe: send the same key again and you get the first result back instead of a second charge or a duplicate post.
Send the header on any tool that creates, changes or spends (everything marked Write in the tool reference). It's optional, but recommended whenever your code retries:
Idempotency-Key: 5f0c2d6e-2a8b-4a61-9d0f-6a1c3e7b9c42(X-Idempotency-Key works too.) Read tools ignore it.
| Situation | Result |
|---|---|
| New key | The call runs; its result is stored for 24 hours. |
| Same key, same tool, same arguments | The stored result is returned. Nothing runs or is charged again. |
| Same key, different arguments | invalid_input. Use a new key for a new call. |
| Same key while the first call is still running | retryable_upstream_failure. Wait and retry. |
| Same key after the call failed | The failed call was refunded, so the retry runs and is charged like a new call. |
| Same key after the first call never finished (server crash) | Within 30 minutes its charge is refunded automatically, and the retry runs as a new call. |
| No key | The call runs normally, without duplicate protection. |
Generate a fresh random key (a UUID is ideal) for every new action, and reuse it only to retry that action. Keys are scoped to your account and the tool, so they can't collide with anyone else's.