Errors
The shape
Every failure returns application/problem+json, following
RFC 9457. The HTTP status tells you
the class of problem; code tells you exactly which one.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
X-Request-Id: req_4b81f0c9
{
"type": "https://skautik.com/docs/developers/errors#validation_failed",
"title": "Validation failed",
"status": 422,
"code": "validation_failed",
"detail": "One or more fields could not be accepted.",
"request_id": "req_4b81f0c9",
"errors": [
{
"field": "price",
"code": "out_of_range",
"detail": "Must be a positive integer in minor units."
},
{
"field": "address.country",
"code": "unknown_value",
"detail": "Expected an ISO 3166-1 alpha-2 code."
}
]
}The errors array appears only on 422. It names every problem at once rather
than the first, so a form can be corrected in one pass instead of one field per
round trip.
Branch on code rather than on the prose in title or detail. The code is
stable; the prose is written for a person and may be reworded.
Codes
| Code | Status | Meaning | What to do |
|---|---|---|---|
authentication_required | 401 | No key, or a key that is not valid. | Fix the header or the key. Retrying unchanged will never succeed. |
permission_denied | 403 | Valid key, but the scope or the record's ownership forbids this. | Use a write-scoped key, or stop trying to modify a record you did not publish. |
not_found | 404 | No such resource, or none your key can see. | Treat as absence. Not a transient failure. |
conflict | 409 | An Idempotency-Key was reused with a different body. | Generate a fresh key for a genuinely new creation. |
managed_by_import | 409 | The record belongs to an import source, which is authoritative for it. | Change it in the system that feeds the import, not here. |
precondition_failed | 412 | If-Match did not match the current ETag. | Re-read, reapply your change, retry once. |
payload_too_large | 413 | Upload exceeds the limit for its endpoint. | Resize before uploading. Do not retry the same bytes. |
unsupported_media_type | 415 | Content-Type is missing or not accepted. | Send application/json, or multipart/form-data for uploads. |
validation_failed | 422 | The body parsed but a field is unacceptable. | Read the errors array; each entry names a field and why it failed. |
rate_limited | 429 | Burst limit exceeded. The monthly volume does not refuse. | Wait for Retry-After, then retry with jitter. |
internal_error | 500 | A fault on our side. | Retry with exponential backoff. Quote the request_id if it persists. |
service_unavailable | 503 | Temporarily unable to serve, usually during a deploy. | Retry with backoff. Usually resolves in seconds. |
What to retry
The rule is simple, and getting it wrong is the most common way an integration turns a small outage into a large one.
- 4xx: do not retry, unless it is a 429. The request is wrong, and repeating it will stay wrong. The exceptions are 429, and 412 which you retry once after re-reading.
- 5xx: retry with exponential backoff and jitter, up to a bounded budget. Never in a tight loop: a service that is struggling recovers more slowly the harder you push it.
- Timeouts are ambiguous. A request that timed out may still have been applied. That is exactly what idempotency keys are for on writes.
Reporting a problem
Quote the request_id when you write to
support@skautik.com. With it we can find the exact
request; without it, the first thing we will ask for is a reproduction. Log the
header on every failed call so it is there when you need it.