Skip to main content

Error Responses

A request that does not succeed comes back in one of three different JSON bodies, and the endpoint decides which — not the status code. One of the three arrives at HTTP 200, so a client that branches on response.ok alone will read a rejection as a success.

None of the three includes a success field, and none repeats the HTTP status in the body.

1. The standard envelope​

Most /v2 and /x402/v2 endpoints return this when a request is rejected:

{
"code": "ERR_UNAUTHORIZED",
"message": "Invalid or expired API key",
"errorId": "d2cd7856",
"requestId": "027f86ef-f7da-4486-a914-e3f14aed8877"
}
FieldAlways presentNotes
codeyesA stable string you can branch on. See Error codes — not every code starts with ERR_.
messageyesHuman-readable. Wording is not stable; do not match on it.
detailsnoPresent on most 4xx. On a 5xx in production it is omitted deliberately, so do not require it.
errorIdyesShort id for this failure. Quote it in support requests.
requestIdyesUnique per request. Also returned as the x-request-id header.

When details is present it carries the field-level reason:

{
"code": "ERR_INVALID_PARAMETER",
"message": "Invalid parameters",
"details": [{ "field": "query.keywords", "message": "keywords is required" }],
"errorId": "9d832e9b",
"requestId": "715b9dc6-a0a3-4830-bf1e-bd518c7406c6"
}

2. The short shape​

Parts of Auto and the x402 surface reject inside the handler and return only this:

{ "error": "Unsupported exchange" }

There is no code, no errorId and no requestId. A client reading body.code unconditionally gets undefined and falls through whatever switch it is driving. An optional details may accompany error, and it is not always the same type — a string, an object, or an array of { field, message } — so check its type before indexing it.

Live example: GET /v2/auto/validate-symbol/gmx/BTC returns 400 with exactly that body, because gmx was removed as a supported venue in 2.7.3.

3. The validation result​

Auto returns EQL validation failures as a result object rather than as an error envelope:

{
"valid": false,
"errors": [
{
"code": "EQL_INVALID_JSON",
"message": "Expected object, received string",
"path": ""
}
],
"warnings": []
}

The same body arrives under two different statuses, which is the trap:

EndpointStatusWhy
POST /v2/auto/queries/validate200Checking a query is the endpoint's job, so "invalid" is a successful answer.
POST /v2/auto/queries422Creating an invalid query is a failed request.

So on /v2/auto/queries/validate you must read valid — response.ok is true either way. Note also that errors[].code is a different vocabulary from the envelope's code: EQL_INVALID_JSON, EQL_INVALID_ARG_VALUE and friends describe the query, not the request.

An element of errors or warnings is an object in every case we have observed, but the schema also permits a bare string — so check the type before reading .code off it, the same way you would with details.

Handling all three​

Branch on the HTTP status first, then look for whichever field the body actually has:

// Wrong — misses every short-shape rejection, misses the four codes that
// carry no ERR_ prefix, and treats an invalid query at 200 as a success.
if (body.code?.startsWith("ERR_")) {
handle(body.code);
}

// Right — the status is always there; the body refines it.
const first = body.errors?.[0];
const firstCode = typeof first === "string" ? first : first?.code;

if (!res.ok) {
handle(res.status, body.code ?? body.error ?? firstCode ?? "unknown");
} else if (body.valid === false) {
handleInvalidQuery(body.errors);
}

Error codes​

These are the codes the documented /v2 and /x402/v2 endpoints raise. New codes can appear without a breaking change, so treat an unrecognised code as the HTTP status implies rather than as a failure to parse.

CodeHTTPMeansNext step
ERR_UNAUTHORIZED401Missing, invalid or expired API key.Check the x-elfa-api-key header. See Authentication.
ERR_FORBIDDEN403The key is valid but not entitled to this endpoint.Check your plan against Pricing.
ERR_INSUFFICIENT_CREDITS402Not enough credits for the call. API-key surface only — see 402 on x402.Top up, or wait for the monthly reset.
ERR_BAD_REQUEST400The request was malformed.Read message; fix and resend.
ERR_INVALID_PARAMETER400A parameter is missing or invalid.Read details for the field.
ERR_NOT_FOUND404No route, or the resource does not exist.Check the path against the API reference.
ERR_RATE_LIMITED429Request rate exceeded.Back off and retry. See Rate Limits.
ERR_RATE_LIMIT_EXCEEDED429Request rate exceeded, raised by a different limiter.Same handling — branch on 429, not on which of the two codes you got.
ERR_SERVICE_UNAVAILABLE503A dependency the endpoint needs declined or could not serve the request.Retryable — see below.
EXTERNAL_API_ERRORupstream's statusAn upstream service returned an error, so this one is not always 5xx.Retry if the status is 5xx; otherwise fix the request.
EXTERNAL_API_REQUEST_FAILED500An upstream service did not respond at all.Retryable.
INTERNAL_SERVER_ERROR500Unhandled failure on our side.Retry once; if it persists, send us errorId and requestId.

A 422 on Auto does not appear here because it does not carry a code — it returns the validation result instead.

Two codes cover 429 and two more cover upstream failures. That is why the status, not the code, is the right thing to branch on — the code tells you why, the status tells you what to do.

402 means something else on x402​

On /x402/v2/* a 402 is not an error. It is the payment handshake: the body is an empty object {} and the quote is carried in the PAYMENT-REQUIRED response header, base64-encoded. A client that maps 402 to "insufficient credits" will misread its first quote as a billing failure and never look at the header. See x402 Payments.

ERR_INSUFFICIENT_CREDITS is the API-key surface only.

Which failures are worth retrying​

  • Retry with backoff: 429, 503, and 500 (INTERNAL_SERVER_ERROR, EXTERNAL_API_REQUEST_FAILED, or EXTERNAL_API_ERROR where the status is 5xx).
  • Do not retry unchanged: 400, 401, 402, 403, 404 and 422. The same request will fail the same way — change the request, the key, or the balance first.

A 503 in particular is not an empty result. Where an endpoint can return an empty payload at 200, it does so deliberately, and a 503 means the request could not be served at all — so retrying it is meaningful, whereas rendering it as "no data" is not.

Reporting a failure​

Every response carries an x-request-id header, including the short shape, so that is the one identifier you can always quote. Send it along with the errorId from the body when there is one, plus the endpoint and the time. Together they identify the exact request in our logs.


For questions about API errors, please contact our support team.