TattooBookingDevelopers

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 class
  • title — short human-readable summary
  • status — HTTP status code (matches the response status)
  • detail — human-readable explanation specific to this occurrence
  • code — 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 an X-Request-ID header, 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

StatusMeaningAction
400Validation failed, or a body we couldn't parseFix the request; not retryable. See errors.
401Auth missing/invalidCheck the bearer token.
403Forbidden — scope, plan, or IPRead code + the extra fields.
404Not foundCross-tenant resources also return 404.
409Conflict (duplicate, racing update)Reconcile; not retryable.
415Unsupported content typeSend Content-Type: application/json.
422Idempotency-Key reused with a different bodyUse a new key for a new operation.
429Rate limitedWait and retry — see Retry-After header.
500Server errorRetry with exponential backoff.
503Service 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_allowed
  • invalid_access_token, revoked_access_token, expired_access_token, revoked_grant, app_suspended
  • insufficient_scope — auth ok, but the key/grant lacks the required scope
  • plan_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 as validation_error; treat both validation codes the same way
  • malformed_json — the request body wasn't valid JSON, or was empty while declaring a JSON content type
  • unsupported_media_type — send Content-Type: application/json
  • internal_error — something failed on our side. Retry with backoff, and quote the request_id if it persists.
  • rate_limit_exceeded — see Retry-After
  • duplicate_email — POST /v1/clients with an email that already exists; the body includes existing_id so you can reconcile
  • conflict — the update conflicts with the resource's current state (409). Example: changing an appointment's status away from CANCELLED/NO_SHOW while 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 /v1 route 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, params or headers
  • path — dotted path to the field, relative to location. 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.