Overview
Errors
Every response carries ok; every error names itself in snake_case. Read the code, not the prose.
Error — application/json
{
"ok": false,
"error": "validation_error",
"details": [
{ "path": ["events", 0, "event_name"], "message": "Required" }
]
}Success responses are {"ok": true, …}; failures are {"ok": false, "error": "<code>"}, sometimes with message, hint, details, or issues giving specifics. Match on error, never on the human text.
Codes you'll actually meet
| Status | error | Meaning |
|---|---|---|
| 400 | validation_error / invalid_body | The body failed schema validation. validation_error carries details; the Links API's invalid_body carries issues; the error-ingest endpoint answers a bare invalid_body. |
| 400 | invalid_range | A malformed or inverted date range; parameter names the culprit. |
| 400 | empty_patch | A PATCH with nothing to change. |
| 401 | missing x-api-key / unauthorized | No key, or a key Fictura doesn't recognize. |
| 401 | missing_key / invalid_key | The Links API's equivalents for the Bearer key. |
| 403 | forbidden | Authenticated, but not a member of the app in x-app-id. |
| 404 | not_found | The resource doesn't exist — or isn't yours, which is deliberately the same answer. |
| 409 | slug_taken | A link slug already in use. |
| 400 / 422 | invalid_config / invalid_credentials / provider_error | Semantically invalid config (400 on the web-checkout save, 422 on paywalls and onboarding), or an upstream provider rejected what you gave it — message carries the provider's reason. |
| 429 | rate_limited | Over a rate limit. Back off. |
| 503 | encryption_not_configured | The server can't store credentials; hint says what's missing. |
| 500 | internal_error | Ours, not yours. |
One deliberate 200
An unknown user on the entitlements endpoint is not an error — it answers 200 with status: "none", so one code path handles known and unknown users alike.