Errors

Every failure tells you what it was and what to do.

Four fields, a stable vocabulary of types and a request identifier in the body as well as the headers. Branch on the type, never on the message.

The envelope

Every non-2xx response has the same shape. type is stable and safe to branch on. message is written for a developer and may change. The other two fields appear when they are meaningful.

Error response
{
  "error": {
    "type": "insufficient_scope",
    "message": "This key does not carry the wakes:read scope.",
    "param": "scopes",
    "wake_id": "wk_0c41f2f4",
    "docs": "https://flintwake.com/docs/errors/"
  }
}

Every type

The full vocabulary. If you see a type that is not on this list, it is a bug and we want to hear about it.

Every error type, its HTTP status and the expected client recovery.
TypeStatusWhat your client should do
invalid_request400Fix the request shape. The param field names the offending key.
authentication_required401Send an Authorization header with a live key.
key_revoked401Issue a replacement key. Revocation is not reversible.
insufficient_scope403Reissue the key with the scope named in the message.
resource_missing404Check the identifier and the environment. Identifiers are environment-scoped.
conflict409Re-read the resource and retry with the current version.
unprocessable422The shape is valid but the values are not. See param.
rate_limit_exceeded429Back off for retry_after seconds. Do not retry immediately.
internal_error500Retry once with backoff, then report the wake_id to support.
upstream_timeout504Your origin did not answer in time. Retry is safe for idempotent calls.

Retrying safely

Retry 429, 500 and 504. Do not retry 4xx validation errors — the request will fail identically and you are spending your own quota to prove it.

A retry that behaves
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function call(path, attempt = 0) {
  const response = await fetch(path, { headers });
  if (response.ok) return response.json();

  const payload = await response.json();
  if (!RETRYABLE.has(response.status) || attempt >= 4) {
    throw new ApiError(payload.error.type, payload.error);
  }

  const retryAfter = Number(response.headers.get("retry-after") ?? 0);
  const backoff = retryAfter * 1000 || 2 ** attempt * 250;
  const jitter = Math.random() * 120;
  await sleep(backoff + jitter);
  return call(path, attempt + 1);
}
Use the idempotency header.

Send x-flintwake-idempotency-key on every write. A retried request with the same key returns the original response instead of performing the action twice.

Rate limits

A 429 always carries retry_after in the body and retry-after in the headers. Honour it. Retrying immediately makes the window longer for everyone on your account.

429 response
HTTP/1.1 429 Too Many Requests
retry-after: 12
x-ratelimit-limit: 600
x-ratelimit-remaining: 0
x-ratelimit-reset: 12

{
  "error": {
    "type": "rate_limit_exceeded",
    "message": "Consumer quota of 600 req/min exhausted.",
    "retry_after": 12
  }
}

Reporting a problem

Every error carries a wake_id. Quote it and we can read the exact exchange without asking you for a timestamp, a timezone or a screenshot. That is the entire support protocol.