Skip to main content
Every error response — auth failures, validation problems, rate limits, internal hiccups — follows the same shape. There is no field that’s only present on some errors and not others.

The envelope

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded: 60 requests per minute on the free tier.",
    "request_id": "req_01H...",
    "docs_url": "https://docs.elestrals.com/errors/rate_limit_exceeded"
  }
}
  • code — one of the codes in the table below. Treat this as the machine-readable identifier; key your client logic off it.
  • message — a human-readable explanation. Phrasing may evolve; don’t string-match on it.
  • request_id — quote this in support tickets. Logged on our side; lets us find your exact request.
  • docs_url — deep link to the troubleshooting page for this code.
Successful responses use a different envelope (data + pagination + meta); see Pagination for that shape.

All error codes

CodeHTTPMeaning
bad_request400The request was malformed: invalid query params, key in the wrong place, missing required field.
invalid_cursor400Cursor is malformed, expired, or from a different path/filter combination.
unauthorized401Missing, malformed, revoked, or unknown API key.
forbidden403The key is valid but isn’t allowed to use this endpoint (e.g., a Free key hitting a Partner-only bulk endpoint).
not_found404The requested resource doesn’t exist. Check the ID format and try again.
rate_limit_exceeded429Per-minute burst limit hit. Respect Retry-After.
quota_exceeded429Monthly quota exhausted. Reset is the 1st of next month, UTC.
internal_error500Something broke on our side. The request was not retried.
service_unavailable503Upstream dependency timed out. Retry with backoff.
The set of codes is closed — you can write an exhaustive switch over them and trust we won’t add a new code without a version bump.

Handling errors

The minimum responsible client treats codes in three buckets:
  • 4xx that you fix in code. bad_request, invalid_cursor, unauthorized, forbidden, not_found. Don’t retry — your code or config is wrong, and retrying with the same input gets the same answer.
  • 4xx that you fix by waiting. rate_limit_exceeded, quota_exceeded. Respect Retry-After. See Rate limits for backoff strategy.
  • 5xx that you fix by retrying. internal_error, service_unavailable. Retry with exponential backoff and a cap (e.g., 5 attempts, then surface the failure).
async function call(path, key) {
  const res = await fetch(`https://api.elestrals.com${path}`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  if (res.ok) return res.json();

  const { error } = await res.json();
  switch (error.code) {
    case 'rate_limit_exceeded':
    case 'quota_exceeded':
      throw new RetryableError(error, Number(res.headers.get('Retry-After') ?? 1));
    case 'internal_error':
    case 'service_unavailable':
      throw new RetryableError(error);
    default:
      throw new PermanentError(error);
  }
}

When to involve support

Open a ticket when:
  • A 5xx recurs across multiple request_ids within a few minutes.
  • A 4xx doesn’t match what the API Reference describes for the endpoint.
  • A not_found is returned for a resource you can see exists in another endpoint’s response.
Always include the request_id from at least one failing call. Without it, support gets to triangulate from a heap of haystacks.