Skip to main content

Error response shape

All non-2xx responses share this envelope:
Branch on error.code only. error.message is human-readable English and may evolve. details is structured supplementary info — fields vary per code. request_id is also returned as the X-Request-Id response header. Include it in any support request.

HTTP → code matrix

Per-code troubleshooting

History processing failure is separate from HTTP failure

A successful GET /v1/history-lookups/{id} returns HTTP200 even when the stored lookup has status: "failed". Inspect its failure.code (no_record, provider_unavailable, or provider_error), keep any available version-2 aggregate, and read per-provider sources. A failed primary lookup consumes no credit, including when supplemental Car365 or Encar facts are returned. Supplemental provider failure alone does not invalidate a ready primary report. Network timeouts do not prove POST was rejected. Resume GET when you have the ID. A plate’s same idempotency key/body recovers the original job; a VIN repeat does so only while that job is in flight. If the original VIN lookup may have finished, locate it through GET-list, the accepted reference, identifier, and time range before intentionally submitting again. A completed VIN request can create a new lookup in expanded collection. A client timeout does not cancel the server job. See History integration.

Telling the 429s apart

429 rate_limited covers conditions with opposite remedies. Discriminate on the body and the Retry-After header:
Do not write a blanket “429 → exponential backoff” handler, and do not treat every 429 with details.limit as permanent. Backoff loops forever on the budget case, and the daily history-lookup cap has a limit but clears at the next KST midnight. Use details.used for the budget and Retry-After for everything that clears.

Test webhook throttle

POST /v1/eagle-eye/watches/{watch_id}/test-webhook is limited to 5 fires per minute per watch, counted over a rolling 60-second window. Exceeding it returns:
No details object, and no Retry-After. The window is rolling, so capacity returns gradually as older fires age out — waiting a full minute after the last accepted fire is always sufficient. This is the only throttle enforced on /v1; the fair-use targets in Authentication are not enforced server-side.

Trial budget exhausted

Sandbox keys minted from a trial (self-serve) account carry a lifetime call budget — 1,000 calls by default. When it runs out, every subsequent request returns 429:
This is not a throttle, and backing off will not clear it. The budget is a lifetime total, not a per-minute or per-month window — it does not refill on its own. A client that treats 429 as “retry with exponential backoff” will retry forever. error.details.used being present is what distinguishes this from the transient 429s above, which do clear.
The API sends no Retry-After header on this response, because there is no time after which the request would succeed.

How to resolve it

Rotating an exhausted key does not reset its budget. Rotation deliberately carries over both the budget and the calls already used, so the new key is exhausted the moment it’s created — otherwise rotation would be an unlimited free-budget loop. To get a fresh budget you must create a new key, which is a different button.

Staying ahead of it

Budgeted keys carry the remaining count on every response, including successful ones:
X-Quota-Remaining is computed after the current request is counted. Log it, or alert when it drops below a threshold, rather than waiting for the first 429. Keys with no budget — every production key, plus any sandbox key whose budget has been lifted — send no quota headers at all. Absence means unlimited, not zero; a client that parses a missing header as 0 will lock itself out of a perfectly healthy production key.

Dealer pricing continuation

Dealer-inclusive vehicle cursors are signed and expire 24 hours after search creation. A malformed, expired, legacy or query/partner/environment-mismatched cursor returns 400 validation_error, details.field: "cursor", details.reason: "restart_required". Restart without a cursor; retain your own collected IDs for deduplication. Transient 503 responses may be retried with the same cursor while it remains valid. Price-sorted dealer searches that expand beyond four make/model branches return 400 validation_error with details.reason: "search_too_broad". Omit make for a single all-makes branch, narrow the selection, or search separately. The number of inventory pages is not capped by this rule. A price search that cannot advance within its source-call budget also returns 400 validation_error with details.reason=search_too_broad. Narrow the search or split the make/model selection; repeating the same cursor cannot resolve this limit. A mixed auction/dealer search with multiple make/model branches cannot continue, so it fails explicitly if it cannot establish its first page within the budget. 503 with details.reason=unsupported_source_configuration means price-sorted certified pricing is unavailable with the configured dealer sources. Contact support; retrying the same query does not change this configuration. Transient source failures retain retryable 503 behavior.