Concepts
Errors and retries
One envelope for every error, a fixed code-to-status table, and explicit rules for when a retry is safe and when it is billed.
The error envelope
{
"error": {
"code": "invalid_request", // one of the codes below (contract)
"message": "The execute body does not match the schema.",
"hint"?: "Inspect the capability for its body_schema.",
"retryable": false, // true only for the four retryable codes
"violations"?: [ { "path": "/num", "message": "Expected integer to be less or equal to 25" } ],
"requestId"?: "req-1" // present on errors produced by the API itself
}
}code, retryable and the HTTP status are contract. Messages and hints are for humans and logs; do not branch on them.
Codes
| code | status | retryable | meaning |
|---|---|---|---|
| invalid_request | 400 | no | Body or params failed validation; `violations[]` names each field (JSON pointer path, or "" for the body). Also adapter-level rules, unpaged `more`, unknown record actions, bodies over 256 KB, non-JSON bodies. |
| unauthorized | 401 | no | Missing, unknown or revoked key or token. |
| credits_exhausted | 402 | no | Monthly allowance reached; wait for `balance.period_end` or upgrade. |
| capability_disabled | 403 | no | Switched off for this account in the dashboard. |
| capability_unknown | 404 | no | No such capability id. |
| record_unknown | 404 | no | Malformed or purged `search_id` / `record_id`. |
| not_found | 404 | no | No such route. |
| session_expired | 410 | no | Session older than 24 h since last use; execute again. |
| rate_limited | 429 | yes | Either the 60/min limiter (has `x-ratelimit-*` headers) or the one-call-per-account admission (`Retry-After: 1`). |
| internal_error | 500 | yes | Unexpected failure; carries `requestId` for support. |
| provider_error | 502 | yes | The source failed or answered unusably; message is prefixed with the provider name. |
| sharpen_no_reviewer | 503 | no | No reviewer of another model family answered. |
| capability_unavailable | 503 | no | The vendor key is missing in this deployment (`available: false` in the catalog). |
| provider_timeout | 504 | yes | The search or detail deadline was hit. |
| service_unavailable | 503 | yes | The service is in maintenance or recovery, or a durable record could not be confirmed; nothing was applied. Honour `Retry-After`. |
Retry rules
- Retry blindly only when
retryableis true. Forrate_limitedwait forRetry-After(or the limiter'sx-ratelimit-reset) and repeat the same request; it charged nothing. - Execute retries are billable. There is no idempotency key. A retry is a new call: if the first attempt completed and settled server-side (for example your client gave up before the response arrived) and the retry also completes, both are charged. If the first attempt was refused or failed, it charged 0 and only the retry is charged. While the first attempt is still running, the retry is refused by admission with 429, which prevents concurrent duplicate spend.
- No error proves that a mutating call did nothing. A
provider_timeoutis produced by a server-side deadline that races with the adapter and the session write, so a timed-outmoreorupdatemay still have advanced or replaced the session; and one variant of429 rate_limited("the request admission has expired") is returned after the provider work ran. Failed calls charge 0, but the session state after a failure is not guaranteed. - Consequence: do not auto-repeat execute,
moreorupdate. The common 429 variant ("Another request is already running for this account", withRetry-After: 1) is refused before any work and is safe to repeat; the API does not distinguish the two variants by code, so a generic client must treat every 429 on a mutating call as "wait, re-check, decide". Before issuing anothermore, re-check the session'sresult_countand record ids from the last response you did receive. - Record actions and reads are the safe case: a completed record action is cached, so a repeat is normally served at 0 credits with
cached: true; a failed one charged 0. Repeating them on 429, provider failures or a lost response is reasonable, with a bounded attempt count.
If exactly-once execution matters to you, keep the
search_id of every successful execute and check your own ledger before retrying; the API does not accept an Idempotency-Key today. This is a recorded product decision, see stability and versioning.Validation details
- Defaults are applied server-side (
num,sort,units…), so an omitted optional field behaves exactly as its documented default. - Scalars are coerced (
"5"→5). This is lenient, not something to rely on; send the documented types. - Unknown keys are rejected: every request schema has
additionalProperties: false, and the violation path names the key. - Rules a schema cannot express (price ranges, passenger counts, selector exclusivity) are listed per capability under "Rules the schema cannot express" and return
invalid_requestwith a plain message and usually noviolations[].