Every error response uses the same JSON envelope. Branch on code, not on the
message — messages are English-only (for your logs) and may be reworded; code
is stable and documented.
{
"code": "validation.required.field",
"message": "Field 'rate_pack_id' is required.",
"request_id": "req_8f3a1c9e2b7d40",
"details": { "field": "rate_pack_id" }
}
| Field | Notes |
|---|
code | Stable i18n code, form domain.subdomain.detail. Branch on this. |
message | English, for your logs. Never localized server-side. |
request_id | Quote this when contacting support — it correlates with Safariat logs. |
details | Optional structured context (offending field, expected value). |
HTTP status mapping
| Status | Meaning |
|---|
400 | Malformed request (syntactic validation). |
401 | API key missing, invalid, expired, or incorrect HMAC signature. |
403 | Authenticated but not authorized (missing scope, non-allowlisted IP). |
404 | Resource does not exist or is inaccessible for this partner. |
409 | Same Idempotency-Key reused with a different body. No action performed. |
422 | Syntactically correct but a business rule was violated. |
429 | Rate limit exceeded for the current key. |
500 | Internal error — Safariat is notified automatically. |
503 | Service temporarily unavailable. Retry later. |
Stable error codes
These codes are part of the contract and will not be renamed within v1.
| Code | Typical status | When |
|---|
auth.api_key.missing | 401 | No Authorization bearer sent. |
auth.api_key.invalid | 401 | Unknown, expired, or revoked key. |
auth.signature.invalid | 401 | X-Signature does not match (production writes). |
auth.timestamp.skew | 401 | X-Timestamp outside the ±5 min window. |
validation.required.field | 400 | A required field is missing — see details.field. |
adventure.not.found | 404 | Unknown adventure_slug. |
adventure.capacity.exhausted | 422 | No remaining capacity on the requested slot. |
booking.not.cancellable | 422 | Booking already cancelled/completed or past its cancellation window. |
payment.amount.mismatch | 422 | payment_confirmation amount differs from the quote. |
partner.credit.limit.exceeded | 403 | Partner credit ceiling reached. |
idempotency.key.conflict | 409 | Idempotency-Key reused with a different body — see Idempotency. |
rate_limit.exceeded | 429 | See Rate limiting. |
Unrecognized codes may be added over time (additive, never breaking). Treat an
unknown code as a generic failure of its HTTP status class.