Error Handling¶
All error responses follow the JSON:API error format, providing structured information about what went wrong.
Error Response Structure¶
{
"errors": [
{
"status": "400",
"detail": "This field is required.",
"source": {"pointer": "/data/attributes/email"},
"code": "required"
}
]
}
| Field | Always present? | Description |
|---|---|---|
status |
Yes | HTTP status code as a string |
detail |
Yes | Specific explanation of what went wrong. Human-readable prose — the wording may change, so do not match on it |
source.pointer |
Yes | JSON pointer to the request field that caused the error; /data when the error is not field-specific |
code |
Yes | Stable, machine-readable identifier for the error — the field to branch on in code |
title |
No | Short, human-readable summary, on some errors only |
meta |
No | Extra context specific to the error, such as the channel or a request id |
Branch on status and code, never on title
Whether title is present depends on where the error came from, which is an
internal detail you cannot predict from the outside: errors raised by the
API framework — authentication, permissions, parsing, validation, throttling
— carry no title, while errors Beyond constructs itself (channel failures,
gateway problems, some customization rejections) do. Treat it as a display
nicety.
code is the field to rely on. Every error carries one, so you can switch
on it without a fallback. Codes documented on this page are stable; treat an
unfamiliar one as generic for its status rather than failing on it, since
new codes can appear as errors become more specific over time.
Common Error Codes¶
400 -- Bad Request¶
Malformed requests or request-body validation failures detected while parsing the JSON:API document and validating request attributes. This includes both structural issues and semantic or value validation errors raised while handling the request body.
A request can also be rejected before it reaches an endpoint at all -- a body
larger than the API accepts, a form with too many fields, or an unrecognized
Host header. Those carry code: "invalid" and source.pointer of /data,
because no single field is at fault.
{
"errors": [
{
"status": "400",
"detail": "This field is required.",
"source": {"pointer": "/data/attributes/email"},
"code": "required"
}
]
}
When more than one request field is invalid, the response may include multiple error objects. For example:
{
"errors": [
{
"status": "400",
"detail": "Ensure this value is greater than or equal to 1.",
"source": {"pointer": "/data/attributes/extra-guest-fee"},
"code": "min_value"
},
{
"status": "400",
"detail": "Ensure this value is greater than or equal to 1.",
"source": {"pointer": "/data/attributes/extra-guest-threshold"},
"code": "min_value"
}
]
}
Invalid sort and filter parameters are also reported as 400, with
source.pointer set to /data because the problem is in the query string
rather than the request body:
{
"errors": [
{
"status": "400",
"detail": "invalid sort parameter: -base-price",
"source": {"pointer": "/data"},
"code": "invalid"
}
]
}
Token request errors: When requesting a token at /o/token/, you may
receive an error if user_id references a user that doesn't exist or isn't
owned by your application, credential_id does not belong to the provided
user_id, user_id is provided with app-level scopes such as user:write, or
user-level scopes are requested without user_id when your application
requires user-scoped tokens. These return a standard OAuth2 error response, not
JSON:API format, with "error": "invalid_scope".
Request ID Header¶
You may send an optional X-Request-ID header on any Public API request to make
log correlation easier when debugging with Beyond support.
- If you send a value made up of letters, digits, and the separators
-,_,., or:(up to 128 characters — this covers UUIDs, ULIDs, hex strings, and prefixed ids likereq_…), the API uses that exact value. - If you omit the header, or send it blank or whitespace-only, the API generates a UUID for the request.
- If you send a value with characters outside that set or longer than 128 characters, the API does not reject the request — it falls back to a server-generated UUID for that request. Your original value is not echoed back.
- Every response except the
/healthzfast-path includes the effectiveX-Request-IDheader so you can log or report the exact value the server used.
401 -- Unauthorized¶
Missing or invalid authentication token.
{
"errors": [
{
"status": "401",
"detail": "Authentication credentials were not provided.",
"source": {"pointer": "/data"},
"code": "not_authenticated"
}
]
}
Tip
If you receive a 401, check that your access token has not expired. Tokens are valid for 1 hour. See Token Lifetimes.
403 -- Forbidden¶
The token is valid but lacks the required scope for this operation, or the token is scoped to a different user.
{
"errors": [
{
"status": "403",
"detail": "You do not have permission to perform this action.",
"source": {"pointer": "/data"},
"code": "permission_denied"
}
]
}
Common causes:
- Missing scope: The token doesn't include the required scope for this endpoint.
- User-scoped token restriction: A user-scoped token attempted an app-level operation (e.g., creating or deleting a user).
- Cross-user access: A user-scoped token attempted to access resources belonging to a different user.
- Source IP not allowed: Your application has an IP allowlist configured and the request origin IP is not in that allowlist (applies to both
/o/* OAuth2 endpoints and/api/v1/*resource endpoints).
404 -- Not Found¶
The requested resource does not exist or is not accessible to your application.
A path that is not an endpoint at all -- a typo in the URL, or a resource id in
a segment that expects a number -- returns the same shape with the same
not_found code. Only detail distinguishes it from a resource you are not
allowed to see, so read the URL rather than branching on the wording.
{
"errors": [
{
"status": "404",
"detail": "Listing 12345 not found or not owned by this application",
"source": {"pointer": "/data"},
"code": "not_found"
}
]
}
405 -- Method Not Allowed¶
The endpoint exists but does not support that HTTP method — for example a
DELETE on a resource that is read-only over the API.
{
"errors": [
{
"status": "405",
"detail": "Method \"DELETE\" not allowed.",
"source": {"pointer": "/data"},
"code": "method_not_allowed"
}
]
}
409 -- Conflict¶
Three different situations produce a 409, and they call for opposite responses —
one is a permanent defect in your request, the other two are temporary server
state. Tell them apart with code.
data.type does not match the endpoint's resource type. A JSON:API document
error, raised before any validation of your attributes runs — so a write can fail
this way even though every value in it is correct.
{
"errors": [
{
"status": "409",
"detail": "The resource object's type (listing) is not the type that constitute the collection represented by the endpoint (base-price-customizations).",
"source": {"pointer": "/data"},
"code": "error"
}
]
}
The detail names the type the endpoint expected — use it to correct the
payload. Resource types are plural and dasherized and do not always match
the URL path segment, so read the expected type from the endpoint's page or the
Swagger UI rather than deriving it from the path. See
Resource types. Retrying the same document will
never succeed.
A refresh is already running for the account (code: "sync_in_progress").
Returned when you ask a managed account to
refresh its listings while a previous refresh is still in flight.
{
"errors": [
{
"status": "409",
"detail": "A listings refresh is already in progress for this account. Please retry shortly.",
"source": {"pointer": "/data"},
"code": "sync_in_progress",
"title": "Conflict",
"meta": {"request_id": "0f1c1b2e-..."}
}
]
}
This one is retryable: wait for the running refresh to finish rather than issuing another. Nothing about your request needs to change.
A concurrent operation on the same account blocked this request
(code: "concurrent_modification"). Writes to the same
managed account are applied one at a time.
A request that cannot take its turn before the server's wait bound — because
another create for the same account or a credentials refresh is still running —
is rejected having changed nothing.
{
"errors": [
{
"status": "409",
"detail": "Another operation on this account is in progress and this request timed out waiting for it, so it was not applied. Please retry shortly.",
"source": {"pointer": "/data"},
"code": "concurrent_modification",
"title": "Conflict",
"meta": {"request_id": "0f1c1b2e-..."}
}
]
}
This one is retryable too: the request itself is fine, it just arrived while the account was busy. Wait a moment and resend — the retry proceeds once the concurrent operation finishes. Issuing one write at a time per account avoids the wait entirely.
422 -- Unprocessable Entity¶
The JSON:API document is structurally valid and request-body validation has already passed, but the operation is still rejected during downstream domain or integration processing. This includes channel-specific validation or authentication failures that are not simple request-body field errors.
{
"errors": [
{
"status": "422",
"detail": "Channel 'hostaway': Invalid client credentials",
"source": {"pointer": "/data/attributes/credentials"},
"code": "invalid_credentials",
"title": "Invalid Credentials",
"meta": {"channel": "hostaway"}
}
]
}
Channel errors are the main place title and meta are populated: title
summarizes the channel's complaint and meta.channel names the channel that
raised it. The machine-readable identifier is the top-level code.
415 -- Unsupported Media Type¶
The request body's Content-Type is not one the API accepts. Send
application/vnd.api+json (or application/json) on every request with a body.
{
"errors": [
{
"status": "415",
"detail": "Unsupported media type \"text/plain\" in request.",
"source": {"pointer": "/data"},
"code": "unsupported_media_type"
}
]
}
429 -- Too Many Requests¶
Rate limit exceeded, or the account's monthly usage allocation is exhausted.
The two are distinguished by code — see
Rate Limiting for the full
comparison and for how to handle each.
500 -- Internal Server Error¶
An unexpected server error. Your request reached Beyond and failed for a reason
that is not your request's fault, so nothing about it needs changing before you
retry. code is always error, and meta.request_id repeats the response's
X-Request-ID -- that is the value to quote to support.
{
"errors": [
{
"status": "500",
"detail": "An unexpected error occurred and your request could not be completed. Please retry shortly; if the problem persists, contact support and quote the request id.",
"source": {"pointer": "/data"},
"code": "error",
"title": "Internal Server Error",
"meta": {"request_id": "0f1c1b2e-..."}
}
]
}
Retry with backoff; if it persists, contact support@beyondpricing.com and quote the request id.
502 -- Bad Gateway¶
A service Beyond depends on to answer your request failed. Your request was well-formed — nothing about it needs changing before you retry.
Two cases produce it. A channel or PMS failure during a write, such as
connecting a managed account — meta.channel names the channel and
meta.request_id is the value to quote to support:
{
"errors": [
{
"status": "502",
"detail": "The 'hostaway' channel returned an unexpected error and your request could not be completed. Please retry shortly; if the problem persists, contact support and quote the request id.",
"source": {"pointer": "/data"},
"code": "bad_gateway",
"title": "Channel Unavailable",
"meta": {"channel": "hostaway", "request_id": "0f1c1b2e-..."}
}
]
}
Or the webhook-events service being unavailable when reading webhook events:
{
"errors": [
{
"status": "502",
"detail": "The webhook events service is temporarily unavailable.",
"source": {"pointer": "/data"},
"code": "error"
}
]
}
Retry with exponential backoff. A 502 is transient by definition, but a
persistent one is worth reporting with the request id rather than retried
indefinitely.
Best Practices¶
- Check the
statusfield to determine the error category - Branch on
code, nottitleordetail:codeis the stable machine-readable identifier.titleis absent from most errors anddetailis prose whose wording can change - Use
source.pointerto identify which request field caused validation errors. It is always present, so no presence check is needed;/datameans the error is not tied to a single field - Expect multiple error objects when more than one request attribute is invalid
- Log the full error response along with the response's
X-Request-ID— that is the value support needs to trace the request - Implement retry logic with exponential backoff for 429 and 5xx errors
- Do not retry 400, 401, 403, 404, 405, 415, or 422 errors without changing
the request. The exceptions among the 4xx are a
409withcode: "sync_in_progress"orcode: "concurrent_modification", both of which clear on their own