Skip to content

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 like req_…), 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 /healthz fast-path includes the effective X-Request-ID header 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 status field to determine the error category
  • Branch on code, not title or detail: code is the stable machine-readable identifier. title is absent from most errors and detail is prose whose wording can change
  • Use source.pointer to identify which request field caused validation errors. It is always present, so no presence check is needed; /data means 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 409 with code: "sync_in_progress" or code: "concurrent_modification", both of which clear on their own