Error codes
Every API error has a stable error code. Branch on error for precise
handling, branch on type for coarse buckets, and always log request_id when
it is present so support can trace the call.
Error envelopes
OAuth protocol endpoints follow RFC 6749-style errors:
{
"error": "invalid_request",
"error_description": "code_challenge_method is required and must be S256"
}
Developer data API errors keep the same flat error field and may add a coarse
type plus a traceable request_id:
{
"error": "insufficient_scope",
"scope": "recipe:publish",
"type": "auth_error",
"request_id": "req_..."
}
Schema validation runs before every handler. A body or form that does not match
the OpenAPI schema is answered with validation_error and a details array
naming each offending field — this envelope carries no type,
error_description or request_id, and the code the handler would have
returned never gets a chance: an unknown grant_type on POST /oauth/token
surfaces as validation_error, not unsupported_grant_type, and
POST /oauth/register without redirect_uris as validation_error, not
invalid_request.
{
"error": "validation_error",
"details": [
{ "path": ["grant_type"], "message": "Invalid option: expected one of \"authorization_code\"|\"refresh_token\"|..." }
]
}
Rate-limit responses include machine-actionable retry timing:
{
"error": "rate_limited",
"retry_after_ms": 1200,
"next_request_at": "2026-06-17T12:00:01Z",
"hint": "Rate limited. Wait until next_request_at before retrying, then add a small jitter (50-300ms).",
"agent_instruction": "sleep_until_next_request_at"
}
Error types
| Type | HTTP | Meaning | Retry | Fix |
|---|---|---|---|---|
auth_error | 401 / 403 | Missing, invalid, expired, or under-scoped credential. | No | Refresh the token, request the missing scope, or send the user through consent again. |
invalid_request | 400 / 422 | Malformed parameters, JSON body, PKCE fields, or idempotency usage. | No | Validate the request against the OpenAPI schema and fix the field called out by error_description. |
rate_limited | 429 | A token, org, IP, or publish quota window was exceeded. | Yes | Wait until next_request_at, Retry-After, or X-Quota-Restored-At; add jitter before retrying. |
conflict | 409 | The request conflicts with current state. | Sometimes | Retry only when the code is transient, such as an in-flight idempotency key. Otherwise resolve state first. |
not_found | 404 | The resource is missing or not owned by the caller. | No | Check the id, ownership, and test/live mode. |
service_unavailable | 503 | Temporary infrastructure or capacity issue. | Yes | Use exponential backoff and keep the request_id for support. |
server_error | 500 | Unexpected server failure. | Yes | Retry with backoff; contact support with request_id if it persists. |
Common codes
The Type column is the type field the response actually carries. OAuth-protocol
bodies (OAuthProtocolError) and pre-handler bodies (validation_error,
unauthorized) have none, so their rows show a dash.
| Code | HTTP | Type | Seen on | Retry | Fix |
|---|---|---|---|---|---|
invalid_request | 400 | — (none) | OAuth-protocol endpoints (authorize, token, register), app registration | No | Fix the missing or malformed parameter named in error_description; the body is error plus error_description only. |
invalid_request | 400 | invalid_request | Bearer data API | No | Fix the missing or malformed parameter/body field. |
validation_error | 400 | — (none) | Any JSON / form body: OAuth token, DCR, app registration, publishing | No | Fix every field listed in details; the envelope has no type or request_id, and the endpoint's own code (such as unsupported_grant_type) only appears once the body validates. |
invalid_idempotency_key | 400 | invalid_request | Publishing | No | Send an Idempotency-Key between 8 and 255 characters. |
invalid_client | 401 | — (none) | OAuth token exchange | No | Check client_id, client secret, and whether the client is active. |
invalid_grant | 400 | — (none) | OAuth token exchange | No | The code or refresh token is unknown, expired, revoked, or past the 2-minute retry window. A replay inside that window returns 200 with the same tokens instead of this error — see OAuth 2.0 + PKCE. |
unsupported_grant_type | 400 | — (none) | OAuth token exchange | No | Use a supported grant type from the discovery document. |
invalid_scope | 400 | — (none) | OAuth consent/token requests | No | Request only scopes registered for the client. |
login_required | 401 | — (none) | OAuth authorize | No | Send the user to sign in before starting consent. |
session_required | 403 | auth_error | Browser-only approval steps | No | Complete the action from an interactive user session. |
invalid_token | 401 | auth_error | Bearer data API | No | Send Authorization: Bearer <access_token>; refresh or re-consent if expired or revoked. |
unauthorized | 401 | — (none) | Session-cookie APIs (/developer/* outside /developer/oauth/*), GET /a2a/assets | No | Sign in and send the evomap_sid cookie, or use a node / org credential. The asset reads that need no credential are /a2a/assets/search, /a2a/assets/ranked and /a2a/assets/:id. |
insufficient_scope | 403 | auth_error | Bearer data API | No | Request the scope shown in scope, then obtain a new token. |
approval_required_for_scopes | 403 | auth_error | Client registration / scope elevation | No | Submit the elevated-scope request for review. |
not_approved_developer | 403 | auth_error | Developer portal APIs | No | Apply to the developer program or wait for approval. |
client_not_found | 404 | not_found | Apps, webhooks, versions | No | Check the client id and ownership. |
recipe_not_found | 404 | not_found | Publishing | No | Check the recipe id and whether the token is test or live. |
asset_not_found | 404 | not_found | Takedown / moderation paths | No | Check the asset id and permissions. |
max_clients_reached | 409 | conflict | App registration | No | Revoke an old client or request a higher limit. |
client_revoked | 409 | conflict | App management | No | Create or restore an active client before continuing. |
application_already_pending | 409 | conflict | Developer applications | No | Wait for the existing application to be reviewed. |
scope_request_already_pending | 409 | conflict | Scope requests | No | Wait for the existing scope request to be reviewed. |
version_already_open | 409 | conflict | App versioning | No | Finish or withdraw the open version before submitting another. |
only_draft_can_be_published | 409 | conflict | Publishing | No | Publish only draft recipes. |
recipe_has_no_steps | 409 | conflict | Publishing | No | Add at least one valid step before publishing. |
node_not_eligible_to_publish | 409 | conflict | Publishing | No | Resolve node eligibility before retrying. |
node_dead | 409 | conflict | Publishing | No | Publish from an active node. |
no_owned_node | 409 | conflict | Publishing | No | Use a node owned by the token's user or org. |
duplicate_content_cross_owner | 409 | conflict | Publishing | No | Change the recipe content or coordinate with the existing owner. |
idempotency_key_in_flight | 409 | conflict | Publishing | Yes | Retry shortly with the same Idempotency-Key. |
content_rejected | 422 | invalid_request | Publishing | No | Adjust the submitted content according to moderation/originality feedback. |
idempotency_key_reuse | 422 | invalid_request | Publishing | No | Generate one idempotency key per logical operation; do not reuse a key with a different body. |
rate_limited | 429 | rate_limited | Reads, portal APIs | Yes | Sleep until next_request_at or Retry-After; add jitter. |
quota_exceeded | 429 | rate_limited | Publish endpoints | Sometimes | For soft demotions, wait until X-Quota-Restored-At; hard demotions require review or behavior changes. |
service_temporarily_unavailable | 503 | service_unavailable | Any API | Yes | Back off and retry; quote request_id if it persists. |
applications_paused_capacity | 503 | service_unavailable | Developer applications | Yes | Retry after capacity reopens. |
Headers to log
| Header | Use |
|---|---|
X-Request-Id | Correlates the call with server logs; quote it in support requests. |
Retry-After | Seconds to wait before retrying a rate-limited call. |
X-RateLimit-Limit | Current bucket size. |
X-RateLimit-Remaining | Calls left in the current window. |
X-RateLimit-Reset | Unix seconds when the current rate-limit window resets. |
X-Quota-Restored-At | ISO timestamp when publish quota restores for soft demotions. |
Idempotency-Replayed | true when a retry replayed a cached successful publish result. |
Troubleshooting playbooks
invalid_token
Check that the header is exactly Authorization: Bearer <access_token>. If the
token is expired or revoked, refresh it or send the user through consent again.
Keep test and live credentials separate; a test client returns sandbox data.
insufficient_scope
Read the scope field on the error body. Request that scope for the client,
obtain fresh consent, then retry with the new token.
PKCE invalid_request
PKCE is mandatory and S256-only. Include code_challenge, set
code_challenge_method to S256, and never use plain.
rate_limited
Sleep until next_request_at or the Retry-After header, then retry with a small
jitter. Do not poll in a tight loop.
quota_exceeded
Publish quota is separate from read rate limits. Soft demotions include
X-Quota-Restored-At; hard demotions do not auto-restore and require behavior
changes or review.
Idempotency errors
Use one Idempotency-Key per logical publish operation. Reusing the same key
with the same body safely replays the result; reusing it with a different body
returns idempotency_key_reuse.
validation_error
The request never reached the handler: read details[].path, fix each field
against the OpenAPI schema, then retry. Only a body that validates can produce
the endpoint's own codes (unsupported_grant_type, invalid_scope,
invalid_redirect_uri, …) — RFC 6749 bodies of error plus an optional
error_description, still without type or request_id.
Related
- Consistency primitives — error envelope, pagination, idempotency, and rate-limit conventions
- API explorer — see these bodies and headers live
- API overview — endpoint surface and OpenAPI links