Errors and limits
The error envelope, stable codes, request ids, rate limits, and idempotent retries.
The envelope
Every non-2xx response has the same body. Branch on error.code; the message wording may change.
{
"error": {
"code": "forbidden_scope",
"message": "This key does not have the read:signals scope.",
"request_id": "req_01J9X4K6ZP3M8Q2R5T7V9W1Y3A"
}
}| Code | Status | Meaning | What to do |
|---|---|---|---|
invalid_request | 400 | Body or parameters failed validation. The message names the field. | Fix the request. Do not retry unchanged. |
unauthorized | 401 | Missing, malformed, revoked, or expired key. | Check the key; confirm with GET /v1/account. |
payment_required | 402 | The period allowance is exhausted. The body carries reset_at. | Wait for the reset or raise the plan. |
forbidden_scope | 403 | The key lacks the scope for this operation. | Create a key with the scope; see key.scopes on GET /v1/account. |
not_found | 404 | No such route, technology slug, or store. For stores: the domain is not in the corpus. | For stores, request an observation with refresh: true. Not evidence the site is not a store. |
conflict | 409 | An Idempotency-Key was reused with a different body. | Use a new key for the new body. |
rate_limited | 429 | Too many requests for this key. Retry-After is in seconds. | Wait Retry-After, then retry. |
not_implemented | 501 | The operation is in the contract but not yet served. | Check the reference; the page exists only for served operations. |
internal | 500 | Our fault. | Retry once after a few seconds; quote request_id to support if it persists. Not billed. |
Request ids
Every response carries an X-Request-Id header, and every error body repeats it. You can supply your own with an X-Request-Id request header (8 to 64 characters, A-Z a-z 0-9 _ -), for example a Clay row id, and it is echoed back. Otherwise the API generates one. Log it with every call; it is how support finds one request among millions.
Rate and concurrency limits
Limits are per key and come from the plan (plan.limits on GET /v1/account).
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests per minute for this key |
X-RateLimit-Remaining | Requests left in the current minute |
X-RateLimit-Reset | Seconds until the minute resets |
X-Usage-Remaining | lookup_cached units left in the period |
Retry-After | On 429 and 503 only: seconds to wait |
Exceeding the per-minute rate returns 429 rate_limited. Exceeding concurrency returns 503 with Retry-After: 1. Both are safe to retry after waiting.
Request bodies are capped at 256 KB. Enrich batches are capped at plan.limits.enrich_batch_max domains.
Idempotent retries
Any POST accepts an Idempotency-Key header. Within 24 hours, the same key with the same body returns the original response, adds Idempotent-Replayed: true, and bills nothing. The same key with a different body returns 409 conflict.
Rules that keep retries safe:
- Send an idempotency key on every
POSTyou might retry, including retries after a client-side timeout, which is the case where the first attempt may already have been billed. - Derive the key from your own stable identifiers (
run-id:batch-index), not from a timestamp. GETrequests need no key; they are safe to repeat.
Timeouts
Synchronous requests are bounded at 10 seconds of database time. POST /v1/stores/enrich never fetches a live site inside the request; refresh: true queues a scan and returns immediately with scan_requested: true. A request that exceeds the bound returns internal with a request id and is not billed.