Sophon

Errors & rate limits

Every error from /v1/* follows the same envelope, every non-trivial route emits the same rate-limit headers, and the rules for a sane client are short.

Error envelope

{
  "ok": false,
  "error": {
    "code": "validation_failed",   // snake_case machine code
    "message": "Human-readable",
    "errors": [                    // optional, only on 400 validation_failed
      {
        "path": "attachments.0.size",
        "code": "too_big",
        "message": "Number must be less than or equal to 26214400"
      }
    ],
    "details": { /* optional, code-specific structured payload */ },
    "retry_after_ms": 5000         // optional, set on 429 / 503
  }
}

The errors[] field-path detail mirrors the request body's nesting — array indices are bare integers (attachments.0.size), nested objects are dotted (payload.user.email). Each entry carries:

  • path: dot-joined field path ("" for root-level type mismatches).
  • code: machine code (zod issue codes pass through verbatim: invalid_type, too_big, too_small, invalid_string, …).
  • message: human-readable.

A client can render exactly the field that failed without parsing prose.

Standard error codes

HTTPCodeMeaning
400invalid_requestMalformed body, schema fail (carries errors[])
400invalid_token_locationToken in URL/query — must be header
401invalid_tokenToken unknown, malformed, revoked
401invalid_signatureWebhook HMAC fail
403permission_deniedToken lacks required scope
403installation_revokedInstallation gone
404session_not_foundSession deleted or never existed
404interaction_not_foundInteraction expired or invalid
409idempotency_conflictSame key, different payload
410installation_revokedMid-stream revoke
410session_deletedMid-stream delete
410interaction_expired>30 min since update — followup window closed
413payload_too_large>1 MB JSON or >25 MB attachment
422tool_not_declaredTool name not in agent manifest
429rate_limitedPer-token, per-installation, or per-IP limit
451content_blockedBackend content filter blocked output
500internal_errorBackend bug
502upstream_errorBridge bus failure or similar
503temporarily_unavailableBackend overloaded — retry with retry_after_ms
503agent_degradedAgent in degraded state, snoozed

The full list lives in the SAP RFC § 15.2.

Error handling rules

  • 4xx: do not retry blindly. Inspect the code.
  • 429 / 503: retry after retry_after_ms with jitter.
  • Other 5xx: retry up to 3× with exponential backoff.
  • 410: drop in-flight work and stop sending more — the thing you were writing to is gone.

Rate limit headers

Every response carries Discord-compatible rate-limit headers (no vendor prefix):

X-RateLimit-Limit: 30                   # capacity (max burst)
X-RateLimit-Remaining: 27               # tokens left after this request
X-RateLimit-Reset: 1730345700           # epoch seconds until full
X-RateLimit-Reset-After: 0.300          # seconds (decimal) until full
X-RateLimit-Bucket: msg                 # bucket id for client-side scheduling
X-RateLimit-Scope: installation         # 'installation' | 'agent'

On 429 the response additionally carries:

Retry-After: 1                          # integer seconds (RFC 9110 § 10.2.3)

Buckets

BucketRoutesCapacityRefill/sec
msgsendMessage, sendMessageEnd3010
deltasendMessageDelta200100
taskcreateTask, updateTask, finishTask6030
approvalrequestApproval102
memorygetMemory, setMemory (deferred)6020
defaulteverything else3010

Buckets are scoped per (scope, owner_id, bucket) — two installations never share a bucket.

How to be a well-behaved client

  1. Read the headers, don't hit 429. Build a local token-bucket scheduler keyed on X-RateLimit-Bucket. Refill at the rate the table specifies. Treat X-RateLimit-Remaining as authoritative — if it's 0, wait for X-RateLimit-Reset-After before sending the next request in that bucket.
  2. Coalesce deltas. If your agent emits a token every 8 ms, batch them into ~30 ms windows before POSTing. The user can't see faster than that anyway, and the delta bucket refills at 100/s.
  3. Retry with idempotency. Every retry must reuse the same idempotency_key (or task_id / approval_id). See Idempotency & resume.
  4. Backoff on 429 / 503. Use Retry-After (or retry_after_ms) with ±25 % jitter. Never tight-loop.
  5. Stop on 410. The session, interaction, or installation you were writing to is gone. Drop the in-flight work and tell the agent to wind down.
  6. Log X-Request-ID on every error. It's the only thing that lets us correlate your bug report with our server logs. See Observability.