Errors
The TattooBooking API returns errors as RFC 9457problem+json — a stable, well-defined schema that's easier to consume than ad-hoc error envelopes.
Error envelope
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://developers.tattoobooking.com/errors/validation_failed",
"title": "Validation failed",
"status": 400,
"detail": "body/email must match format \"email\"",
"code": "validation_failed",
"instance": "/v1/clients",
"request_id": "req-a1b2c3"
}type— absolute URI identifying the error classtitle— short human-readable summarystatus— HTTP status code (matches the response status)detail— human-readable explanation specific to this occurrencecode— machine-readable error code (the one you switch on)instance— the request path that produced the error (no query string)request_id— present on every error response. Quote it when contacting support about a specific failure. If you send anX-Request-IDheader, we use your value, so it matches your own logs.
The type URI is an identifier, not a link: it names the error class and is stable, but there is no page behind each one. This page is the reference for every code.
Some errors include extra fields. For example, a 403 fromrequireScope includes required_scopes and granted_scopes so you know what to fix.
Status code reference
| Status | Meaning | Action |
|---|---|---|
| 400 | Validation failed, or a body we couldn't parse | Fix the request; not retryable. See errors. |
| 401 | Auth missing/invalid | Check the bearer token. |
| 403 | Forbidden — scope, plan, or IP | Read code + the extra fields. |
| 404 | Not found | Cross-tenant resources also return 404. |
| 409 | Conflict (duplicate, racing update) | Reconcile; not retryable. |
| 415 | Unsupported content type | Send Content-Type: application/json. |
| 422 | Idempotency-Key reused with a different body | Use a new key for a new operation. |
| 429 | Rate limited | Wait and retry — see Retry-After header. |
| 500 | Server error | Retry with exponential backoff. |
| 503 | Service unavailable (Redis, etc.) | Retry. |
Common codes
The code field is the stable contract — switch on this in code, not on title or detail:
missing_api_key,invalid_api_key,revoked_api_key,expired_api_key,inactive_api_key,ip_not_allowedinvalid_access_token,revoked_access_token,expired_access_token,revoked_grant,app_suspendedinsufficient_scope— auth ok, but the key/grant lacks the required scopeplan_limit_reached— the tenant's plan caps this resource (also:feature_not_available)validation_failed,validation_error,invalid_cursor,invalid_id— some endpoints report simple 400 field problems asvalidation_error; treat both validation codes the same waymalformed_json— the request body wasn't valid JSON, or was empty while declaring a JSON content typeunsupported_media_type— sendContent-Type: application/jsoninternal_error— something failed on our side. Retry with backoff, and quote therequest_idif it persists.rate_limit_exceeded— seeRetry-Afterduplicate_email— POST /v1/clients with an email that already exists; the body includesexisting_idso you can reconcileconflict— the update conflicts with the resource's current state (409). Example: changing an appointment's status away fromCANCELLED/NO_SHOWwhile a charged cancellation fee exists — refund the fee first. Not retryable without changing state.resource_not_found— the id doesn't exist in your account, or the URL matches no/v1route at all (a typo in the path returns this same problem+json shape, not a generic 404 page)
Validation errors
Validation failures return 400 with detail explaining which field failed and why. Every failed field is also listed individually in the top-level errors array, so you never have to parse the prose.
{
"type": "https://developers.tattoobooking.com/errors/validation_failed",
"title": "Validation failed",
"status": 400,
"detail": "body/budget_range_min must be >= 0",
"code": "validation_failed",
"request_id": "req-a1b2c3",
"errors": [
{ "location": "body", "path": "preferred_communication",
"message": "must be equal to one of the allowed values" },
{ "location": "body", "path": "budget_range_min", "message": "must be >= 0" }
]
}location— which part of the request failed:body,querystring,paramsorheaderspath— dotted path to the field, relative tolocation. Omitted when the failure is about the object as a whole.message— why that field was rejected
Unknown fields are ignored, not rejected. Sending a query parameter or body property the endpoint doesn't define is not an error — it's dropped and the request proceeds. Don't rely on a 400 to catch a misspelled field name; check the response instead.