Skip to main content

Error Handling

REST and GraphQL use different error formats:

  • REST endpoints follow RFC 9457 problem details and return the relevant non-2xx HTTP status.
  • GraphQL query/validation/runtime errors are returned in GraphQL's native data + errors response shape (typically HTTP 200 OK).

REST error response format​

REST error responses follow this structure:

{
"type": "https://problems.tourfold.com/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields failed validation",
"errors": [
{
"type": "https://problems.tourfold.com/field-size-invalid",
"title": "Field size invalid",
"detail": "The 'title' field exceeds the maximum length of 20 characters",
"pointer": "#/title",
"data": {
"max_length": 20
}
},
{
"type": "https://problems.tourfold.com/invalid-email",
"title": "Invalid email format",
"detail": "The email address format is invalid",
"pointer": "#/driver/email"
}
],
"data": {
"error_count": 2
}
}

Response fields​

Top-Level Fields​

FieldTypeRequiredDescriptionExample
typestringYesMachine-readable error type URI"https://problems.tourfold.com/validation-failed"
titlestringYesHuman-readable error summary"Validation failed"
statusintegerYesHTTP status code422
detailstringYesDetailed error description"One or more fields failed validation"
errorsarrayNoField-level error details(See below)
dataobjectNoAdditional context data{"timestamp": "2024-01-15T10:30:00Z"}

Error Array Fields​

Each item in the errors array contains:

FieldTypeRequiredDescriptionExample
typestringNoMachine-readable field error type"https://problems.tourfold.com/field-required"
detailstringYesField-specific error description"The 'name' field is required"
pointerstringNoJSON pointer to the field"#/name"
dataobjectNoField-specific additional data{"provided_value": "invalid-email"}

HTTP status codes​

The status code tells you where a request went wrong; the type and errors[] tell you what.

StatusMeaningWhen
400 Bad RequestThe request could not be parsed.Malformed JSON, a truncated body, a syntactically invalid request — the server never received a well-formed request to act on. Problem type invalid-input.
402 Payment RequiredThe workspace is billing-locked.Use an exempt billing-recovery operation to resolve the billing condition, then retry. Problem type tenant-billing-locked.
422 Unprocessable ContentThe request was well-formed but rejected.Any validation failure on a request we could read: a field with the wrong type or shape (e.g. 1.5 for an integer field, a string where a number is expected), a missing required field, a value that violates a constraint (length, range, format), or a business rule that rejects the request. Problem type validation-failed, with errors[] pinpointing each offending field via pointer.
401 UnauthorizedThe request carried no usable identity.No Authorization header at all, or a token that is expired, malformed, or issued by an untrusted issuer. Problem type authentication-required; data.reason is token_missing, token_expired, or token_invalid, and a WWW-Authenticate header accompanies the response. Only token_expired warrants an automatic refresh-and-retry.
403 ForbiddenThe caller is authenticated but not permitted.The identity is known but lacks a required grant, is not the owner/author of the resource, or the workspace's plan excludes the capability. Problem type access-denied (or a narrower feature-namespaced type); data.reason names the cause and data.required_grants lists the satisfying grants when the denial is grant-based. Retrying with the same identity does not help.
404 Not FoundThe addressed resource does not exist.
405 Method Not AllowedThe path exists, but not for the requested HTTP method.Use a method listed in the Allow response header. Problem type method-not-allowed; data.method contains the rejected method and data.supported lists supported methods.
406 Not AcceptableThe endpoint cannot produce a representation accepted by the client.Send an Accept header supported by the operation. Problem type not-acceptable.
409 ConflictThe request conflicts with current state — e.g. an optimistic-locking lock_version mismatch (resource-conflict).
410 GoneA known resource's underlying data is permanently unavailable.Do not retry without changing the request or restoring the resource. The exact feature-specific type explains what is gone.
413 Content Too LargeThe request exceeds an operation's documented payload limit.Reduce the request size. Problem type payload-too-large.
415 Unsupported Media TypeThe request body uses a media type the endpoint does not consume.Send a Content-Type declared by the operation, normally application/json. Problem type unsupported-media-type; data.content_type contains the rejected value and data.supported lists accepted media types.
429 Too Many RequestsThe client has been rate limited.
503 Service UnavailableA required upstream service is temporarily unavailable.Retry according to the operation-specific guidance.

Rule of thumb — 400 vs 422: if we could not turn your bytes into a request, it is a 400; if we understood the request but will not act on it, it is a 422. A wrong-typed or missing field is always a 422 with a pointer (not a 400) — only a body we cannot parse at all is a 400. The same rule applies to query and path parameters: a value we cannot coerce to the expected type is a 422, keyed by the parameter name.

Auth error envelope (401 / 403)​

authentication-required 401s and authorization-denial 403s share one envelope: alongside the usual type / title / status / detail, the problem document carries a machine-readable data.reason so clients never have to parse detail prose to decide what to do. Treat data as optional and check for presence before dereferencing it.

401 — authentication-required. data.reason is exactly one of:

  • token_missing — no credentials were presented. Authenticate.
  • token_expired — a well-formed token past its expiry. Refresh the access token and retry once.
  • token_invalid — malformed token, bad signature, or untrusted issuer. Do not blind-retry.

The response also carries a WWW-Authenticate header: with RFC 6750 parameters (Bearer error="invalid_token", error_description="...") when a token was presented and rejected, or a bare Bearer when none was sent. login-failed is the only other problem type that maps to 401; it covers a rejected credential login rather than an unusable bearer token.

403 — access-denied. data.reason is exactly one of:

  • missing_grant — the caller lacks a grant the operation requires.
  • not_owner / not_author / not_member — the caller's relationship to the resource is wrong.
  • plan_restricted — the workspace's plan or feature flags do not include the capability.
  • tenant_scope — the resource belongs to a different workspace than the caller's.
  • resource_locked — the resource's state forbids the action for everyone.

data.required_grants lists the colon-delimited grants that would have satisfied the check, e.g. ["users:update"]. It appears only when the denial is grant-based and the required grants are statically known, so clients must treat it as optional. A feature may also return a narrower namespaced type in place of access-denied — for example https://problems.tourfold.com/comments/cannot-edit-others — while keeping the same status and the same data.reason.

A type-specific 403 can define its own reason vocabulary. In particular, a deactivated workspace returns https://problems.tourfold.com/tenant-disabled with data.reason = TENANT_INACTIVE. Match type before interpreting type-specific data; the closed list above applies to authorization-denial problems.

See REST authentication for the full payloads, the WWW-Authenticate details, and the token-refresh rules.

Errors in OpenAPI operations​

The OpenAPI description combines protocol-level responses with errors that are specific to an operation:

  • Secured operations document 401. Operations returning a representation document 406; operations with a request body document 400 and 415.
  • 405 is documented here instead of on every operation because it describes choosing a method that is not an operation for the path.
  • Contextual errors such as 403, 404, 409, 422, 429, and 503 appear only on operations that can actually produce them. Their descriptions explain the relevant condition and, where stable, name the problem type URI.
  • 429 and 503 are not universal defaults. If an operation does not implement rate limiting or depend on a translated unavailable upstream, those responses are intentionally absent.

Every error response also carries x-tourfold-problem-types, an array containing the stable top-level type URI values expected for that operation and status. The human-readable response description cites those same URIs. The extension intentionally covers only the top-level problem; validation sub-problems in errors[] use the standard vocabulary in the error-types reference or feature-specific types described by the operation.

The list is a curated public contract, not an exhaustive exception dump. It includes expected, actionable failures that callers can handle. An unexpected server failure can still return an unrecognized problem type even when an operation does not advertise a generic 500; clients must therefore preserve a fallback path for unknown types. Adding a new type is additive, while changing the meaning or status of an existing type requires a new API version.

Clients should branch on the HTTP status and stable type URI, not on mutable human-readable detail text.

Error type system​

All error type URIs are dereferenceable. Open any problem URI in a browser to view its documentation. The URIs point to the error types catalog.

Examples:

  • https://problems.tourfold.com/validation-failed - Common validation errors
  • https://problems.tourfold.com/not-found - Resource not found
  • https://problems.tourfold.com/tours/tour-ended - Tour-specific errors
  • https://problems.tourfold.com/cases/case-already-assigned - Case-specific errors

Try it​

Paste a problem URI into your browser for instant docs:

Complete catalog​

For all error types and descriptions, see the error types reference.

JSON Pointer syntax​

We use JSON Pointer syntax to identify specific fields:

  • #/name - Root level field
  • #/contact/email - Nested object field
  • #/drivers/0/email - Array element field
  • #/settings/notifications/0/channels/1 - Deep nested array element

Internationalization support​

While our API responses are in English, the structured error format allows applications to provide localized error messages. Frontends should use the type URI as a translation key.

GraphQL error responses​

  • HTTP status is usually 200 OK; query/validation/runtime failures are reported in the errors array.
  • GraphQL errors do not use the RFC 9457 problem envelope (type, title, detail, status).
  • Transport-level failures (for example auth) may still return non-200 HTTP statuses before GraphQL execution.

GraphQL response shape​

FieldTypeDescription
dataobject, null, or absentQuery result. Partial when a nullable field failed; null when a non-null root field failed; absent entirely when the document failed to parse or validate, because nothing executed.
errorsarrayList of GraphQL errors

Each entry in errors typically contains:

FieldTypeDescription
messagestringHuman-readable GraphQL error message
locationsarraySource positions (line, column) in the GraphQL document
patharray or nullResolver path (often null for validation errors)

GraphQL example​

{
"data": null,
"errors": [
{
"message": "Validation error (WrongType@[product]) : argument 'order_by[0].price' with value 'EnumValue{name='invalid_direction'}' is not a valid 'SortDirection' - Literal value not in allowable values for enum 'SortDirection' - 'EnumValue{name='invalid_direction'}'",
"locations": [{ "line": 1, "column": 11 }],
"path": null
},
{
"message": "Validation error (FieldUndefined@[vending_machine/cpu_processor]) : Field 'cpu_processor' in type 'vending_machine' is undefined",
"locations": [{ "line": 5, "column": 9 }],
"path": null
},
{
"message": "Validation error (FieldUndefined@[vending_machine/manufacturer/founding_year]) : Field 'founding_year' in type 'manufacturer' is undefined",
"locations": [{ "line": 8, "column": 13 }],
"path": null
}
]
}

Next steps​