Errors and limits

The error envelope, stable codes, request ids, rate limits, and idempotent retries.

Two chat bubbles

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"
  }
}
CodeStatusMeaningWhat to do
invalid_request400Body or parameters failed validation. The message names the field.Fix the request. Do not retry unchanged.
unauthorized401Missing, malformed, revoked, or expired key.Check the key; confirm with GET /v1/account.
payment_required402The period allowance is exhausted. The body carries reset_at.Wait for the reset or raise the plan.
forbidden_scope403The key lacks the scope for this operation.Create a key with the scope; see key.scopes on GET /v1/account.
not_found404No 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.
conflict409An Idempotency-Key was reused with a different body.Use a new key for the new body.
rate_limited429Too many requests for this key. Retry-After is in seconds.Wait Retry-After, then retry.
not_implemented501The operation is in the contract but not yet served.Check the reference; the page exists only for served operations.
internal500Our 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).

HeaderMeaning
X-RateLimit-LimitRequests per minute for this key
X-RateLimit-RemainingRequests left in the current minute
X-RateLimit-ResetSeconds until the minute resets
X-Usage-Remaininglookup_cached units left in the period
Retry-AfterOn 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 POST you 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.
  • GET requests 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.

On this page