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": {
"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.
| Type | Status | What your client should do |
|---|---|---|
invalid_request | 400 | Fix the request shape. The param field names the offending key. |
authentication_required | 401 | Send an Authorization header with a live key. |
key_revoked | 401 | Issue a replacement key. Revocation is not reversible. |
insufficient_scope | 403 | Reissue the key with the scope named in the message. |
resource_missing | 404 | Check the identifier and the environment. Identifiers are environment-scoped. |
conflict | 409 | Re-read the resource and retry with the current version. |
unprocessable | 422 | The shape is valid but the values are not. See param. |
rate_limit_exceeded | 429 | Back off for retry_after seconds. Do not retry immediately. |
internal_error | 500 | Retry once with backoff, then report the wake_id to support. |
upstream_timeout | 504 | Your 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.
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);
}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.
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.