Errors & limits

Error codes, rate limits, idempotency, and retry guidance.

Error format

All errors return JSON with a stable, machine-readable code:

{
  "error": "Customer not found",
  "code": "NOT_FOUND"
}

Validation failures (422) add a details object describing the offending fields. Plan-limit errors may add an upgradeUrl.

Error codes

HTTPcodeMeaning
401UNAUTHORIZEDMissing or invalid X-API-Key
403FORBIDDENOutside the key's scope, a cross-org reference, or the key's agent is paused or archived
403PLAN_LIMIT_REACHEDA plan limit (customer count, API calls) was hit
404NOT_FOUNDCustomer, conversation, or memory does not exist
409CONFLICTUnique constraint clash (e.g. duplicate email, phone, or external ID)
409IDEMPOTENCY_IN_PROGRESSA capture with the same idempotencyKey is still running
409IDEMPOTENCY_KEY_REUSEDThe idempotencyKey was already used for a capture with a different payload
409ERASURE_EXPORT_SOURCES_UNKNOWNCustomer deletion is blocked by an unexpired export with unknown sources
409ERASURE_MERGE_LINEAGE_AMBIGUOUSCustomer deletion is blocked by ambiguous merge history
409ERASURE_GRAPH_SOURCES_UNKNOWNCustomer deletion is blocked by shared graph records with incomplete ownership
409ERASURE_ANALYTICS_SOURCES_UNKNOWNCustomer deletion is blocked by analytics history with unknown sources
422VALIDATION_ERRORBody or query failed schema validation; see details
429RATE_LIMITEDRate limit exceeded; back off and retry
503SERVICE_UNAVAILABLEA backing service is temporarily unavailable; retry with backoff
503DATABASE_UNAVAILABLEDatastore offline; retry with backoff
500INTERNAL_ERRORUnexpected server error

The ERASURE_* conflicts leave the customer's data in place for operator review — they are not transient, so don't retry them automatically. See Customers.

Rate limits

Limits are per-minute. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining. Exceeding a limit returns 429 RATE_LIMITED with a Retry-After header.

Capture scales with your plan, and each agent gets its own budget — a busy support agent can't eat into what your sales agent has left. Both run at the full rate for your plan:

PlanCapture limit, per agent
Trial100 / min
Starter600 / min
Growth1,000 / min
Pro1,200 / min
Scale2,000 / min
CustomUnlimited
Endpoint groupLimit
POST /capture, memory ingest (/memories/ingest)Your plan rate, per agent
Recall — /customers/lookup, /customers/handoff, /customers/**/memories/lookup600 / min
Live transfer routing — POST /handoff/route600 / min
Everything else (customers, conversations, CRUD)600 / min
Public embed — /embed/handoff endpoints60 / min per IP

Recall endpoints are deliberately generous (600/min) because agents call them on every turn.

Idempotency

POST /capture accepts an optional idempotencyKey (≤128 chars). Replaying the same key with the same payload within 24 hours returns the original cached result instead of re-processing. Reusing a key with a different payload returns 409 IDEMPOTENCY_KEY_REUSED.

If a request with that key is still in flight, you get 409 IDEMPOTENCY_IN_PROGRESS — wait briefly and retry.

await client.capture(
  [{ role: 'user', content: userMessage }],
  { customerId },
  { idempotencyKey: `turn-${conversationId}-${turnIndex}` },
)

Use idempotency keys anywhere captures may be retried — webhook-driven flows, unreliable networks, or any fire-and-forget pattern that may replay.

Retry guidance

The TypeScript and Python SDKs retry 429, 5xx, and timed-out requests automatically with a jittered, increasing delay (maxRetries / max_retries, default 3). They do not read Retry-After. The Rust SDK does not retry automatically. For your own HTTP clients:

  • 429 — respect the Retry-After header if present, otherwise back off exponentially
  • 503 — transient service issue; retry with backoff
  • 409 IDEMPOTENCY_IN_PROGRESS — wait 1–2 seconds and retry with the same key
  • 4xx (other) — don't retry; fix the request

Phone number normalization

Phone numbers sent to any endpoint are normalized best-effort to E.164:

  • 10 digits → +1… (US/CA assumed)
  • 11 digits starting with 1 → +…
  • Otherwise + is prepended

Send E.164 directly (+14155550101) to avoid ambiguity.

What's next