Errors
Every non-2xx response is a problem+json document. Predictable, machine-readable.
The Tourical API uses standard HTTP status codes. Every non-2xx response carries a Content-Type: application/problem+json body following RFC 7807.
Problem document shape
{
"type": "https://developers.tourical.com/errors/scope-required",
"title": "Missing required scope",
"status": 403,
"detail": "This endpoint requires the `trips:write` scope.",
"required_scope": "trips:write"
}type, title and status are always present; detail almost always is. Some problems add members of their own — required_scope above, errors on a validation failure, next on the booking hand-off.
Branch on type, not on title or detail. type is the stable machine identifier: an absolute URL ending in the problem's slug, which never changes once shipped. title and detail are human-facing prose and may be reworded.
Field-level validation errors add an errors map keyed by field path:
{
"type": "https://developers.tourical.com/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields failed validation.",
"errors": {
"url": ["URL must use https:"],
"events": ["Unknown event type. Allowed: booking.confirmed, ..."]
}
}Status codes
| Status | When |
|---|---|
200 | Success on GET / PATCH / DELETE. |
201 | Resource created (POST). |
202 | Accepted — queued, not yet done (redelivering a webhook event). |
401 | Token missing, malformed, invalid, revoked, or expired. All four are type .../errors/unauthorized; detail says which. |
403 | Authenticated but not allowed: the token lacks the scope, the plan lacks API access, or a per-plan quota is full. Three distinct type values — see the catalog below. |
404 | Resource not found or hidden by tenant scoping. We don't distinguish — it would leak resource existence. |
409 | Conflict — an idempotency key reused with a different body, or a state clash such as redelivering a delivery that is still in flight. |
422 | The request body failed validation — bad JSON, a wrong Content-Type, an oversized body, or a field that failed its schema. See the errors map. |
429 | Rate limited. See Rate limits. Retry-After tells you how long to wait. |
500 | We got it wrong. The Tourical-Request-Id header gives you something to put in a support ticket. |
501 | The route exists and accepted the call shape, but the action is not implemented (only POST /v1/bookings). |
There is no 400 and no 402 on /v1/*: a malformed body is a 422, a missing credential is a 401, and the plan gate is a 403.
Tracing a request
Every response carries a Tourical-Request-Id header. Send it back to us in any bug report and we can correlate to the exact request in our logs.
You can also send your own request id by setting the Tourical-Request-Id header on the request — we'll echo it back instead of generating one. Useful when you already trace requests in your own system.
curl https://app.tourical.com/api/v1/trips \
-H "Authorization: Bearer tk_live_..." \
-H "Tourical-Request-Id: $(uuidgen)"Retry guidance
| Status | Safe to retry? |
|---|---|
429 | Yes — honor Retry-After. Exponential backoff if you hit it twice. |
500 / 503 | Yes — exponential backoff with jitter, capped at 60s. |
4xx (other) | No. Fix the request first. |
For write endpoints (POST, PATCH, DELETE), a 5xx response means the server may or may not have completed the action. Send an Idempotency-Key on the first attempt and reuse it on every retry: 5xx responses are never stored against the key, so the retry re-executes, while a write that did land is replayed instead of repeated.
Problem type catalog
Every type below is prefixed with https://developers.tourical.com/errors/.
type slug | Status | Meaning and extra members |
|---|---|---|
unauthorized | 401 | Missing, malformed, unknown, revoked or expired token. detail distinguishes; the response carries WWW-Authenticate: Bearer. |
scope-required | 403 | The token lacks the scope this endpoint needs. required_scope names it. |
plan-required | 403 | The tenant's subscription has no API access. required_plans lists the plans that do; current_plan is the tenant's. |
forbidden | 403 | Allowed to authenticate, not allowed to do this. A per-plan quota adds code, e.g. webhook_quota_exceeded when the webhook endpoint cap is full. |
not-found | 404 | No such resource, or it belongs to another tenant. |
conflict | 409 | State clash — e.g. redelivering a delivery that is still in flight. May add code. |
idempotency-key-reuse | 409 | This Idempotency-Key was already used with a different request body. |
idempotency-in-progress | 409 | A request with this key is still running. Back off and retry with the same key. |
validation-failed | 422 | Bad JSON, a non-JSON Content-Type, a body over 256 KB, or a field that failed its schema. errors maps field path to messages. |
rate-limited | 429 | The burst (60/s) or sustained (600/min) bucket is exhausted. Retry-After is set; a pre-authentication IP block also sets X-RateLimit-Scope: ip. |
internal | 500 | Something we did not anticipate. Quote Tourical-Request-Id. |
booking-create-not-supported | 501 | POST /v1/bookings only. next.checkoutUrl is where to send the traveller. |

