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",
"details": {}
}Validation failures (422) include a details object describing the offending fields.
Error codes
| HTTP | code | Meaning |
|---|---|---|
401 | UNAUTHORIZED | Missing or invalid X-API-Key |
403 | FORBIDDEN | Outside the key's scope, or a cross-org reference |
403 | PLAN_LIMIT_REACHED | A plan limit (customer count, API calls) was hit |
404 | NOT_FOUND | Customer, conversation, or memory does not exist |
409 | CONFLICT | Unique constraint clash (e.g. duplicate identifier) |
409 | IDEMPOTENCY_IN_PROGRESS | A capture with the same idempotencyKey is still running |
422 | VALIDATION_ERROR | Body or query failed schema validation; see details |
429 | — | Rate limit exceeded; back off and retry |
503 | DATABASE_UNAVAILABLE | Datastore offline; retry with backoff |
500 | INTERNAL_ERROR | Unexpected server error |
Rate limits
Limits are per-minute. Every response carries X-RateLimit-Limit and
X-RateLimit-Remaining. Exceeding a limit returns 429 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:
| Plan | Capture limit, per agent |
|---|---|
| Trial | 100 / min |
| Starter | 600 / min |
| Growth | 1,000 / min |
| Pro | 1,200 / min |
| Scale | 2,000 / min |
| Custom | Unlimited |
| Endpoint group | Limit |
|---|---|
POST /capture, memory ingest (/memories/ingest) | Your plan rate, per agent |
Recall — /customers/lookup, /customers/handoff, /customers/**/memories/lookup | 600 / min |
| Everything else (customers, conversations, CRUD) | 600 / min |
Public embed — GET /embed/handoff | 60 / min per IP |
Each capture call enqueues background AI work, so send a few conversation turns per call rather than one call per message. It costs less, goes further against your limit, and produces better memories than one-line captures.
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 within 24 hours returns the original cached result instead of re-processing.
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 SDK retries 429 and 5xx responses automatically with exponential backoff
(configurable via maxRetries, default 3). For your own HTTP clients:
429— respect theRetry-Afterheader if present, otherwise back off exponentially503— transient datastore issue; retry with backoff409 IDEMPOTENCY_IN_PROGRESS— wait 1–2 seconds and retry with the same key4xx(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
- Authentication — API key scopes and security model
- Capture — idempotency in the capture flow
- AI agents — retry patterns in agentic tool calls