Skip to content

Errors

This API returns three different error shapes, depending on which layer rejected the request. A parser that assumes one shape will crash on the others.

Read this before writing your error handling

The difference is not cosmetic. Authentication failures have no timestamp. Validation failures have no success field at all. Write one helper that normalises all three, and use it everywhere.


Status codes

Status Meaning Typical cause
400 Bad Request Validation failure, invalid order or payment state, invalid return URL, a business rule failure, or invalid client credentials on /auth/token
401 Unauthorized Missing or invalid bearer token, revoked credential, or inactive company
403 Forbidden Your company does not own the resource
404 Not Found Route, package, order, traveller, or company not found
409 Conflict The resource is not in a state that allows this, or it changed underneath you. Some 409s are worth retrying — see Retries
429 Too Many Requests Rate limit exceeded. Wait Retry-After seconds — see Rate-limit errors
500 Internal Server Error Unexpected backend failure
405, 413, 415, 502, 503 Other Wrong method, body too large, wrong Content-Type, or a downstream service unavailable. Uses shape 2

The three shapes

1. Authentication errors

Rejected by the auth layer before reaching any endpoint. No timestamp field.

{
  "success": false,
  "message": "Missing Authorization header",
  "data": null
}

Returned for every 401, and for 429 rate-limit responses. See Authentication for the conditions that trigger a 401.

Rate-limit errors

A 429 uses this same shape, plus headers:

HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Retry-After-Seconds: 12
{
  "success": false,
  "message": "Rate limit exceeded. Please try again in 12 seconds.",
  "data": null
}

Successful responses also carry X-RateLimit-Limit and X-RateLimit-Remaining, so you can slow down before you hit the limit. Limits are listed in the overview.

2. Validation and business errors

The most common shape. Returned by endpoints for 400, 403, 404, 409, and 500. No success field — it uses status and error instead.

{
  "status": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "path": "/api/external/v2/reference/quote",
  "timestamp": "2026-07-07T03:30:00Z",
  "details": {
    "fieldErrors": {
      "dobs": "Exactly one date of birth is supported"
    }
  }
}
Field Type Description
status Integer HTTP status code, repeated in the body
error String Short error label — usually the status reason phrase, sometimes a category such as Traveller Validation Failed. Do not branch on it
message String Human-readable summary
path String The request path that failed
timestamp String ISO-8601 instant, UTC
details Object Present on validation failures and some other errors. See Validation errors

3. Document upload errors

POST /documents handles some upload failures itself and returns the success envelope with success: false.

{
  "success": false,
  "message": "Failed to upload file",
  "data": null,
  "timestamp": "2026-07-07T03:30:00Z"
}

This shape looks like shape 1 but does carry a timestamp. Do not use the presence of timestamp to tell error types apart.


Validation errors

A 400 from a validation failure carries details.fieldErrors — a map of field name to message. This is the one place where reading the message is useful, because it is per-field and actionable.

{
  "status": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "path": "/api/external/v2/reference/quote",
  "timestamp": "2026-07-07T03:30:00Z",
  "details": {
    "fieldErrors": {
      "dobs": "Exactly one date of birth is supported",
      "nationality": "Nationality is required"
    }
  }
}

Map fieldErrors keys onto your own form fields to show inline errors. Note details is absent on non-validation errors, so guard before reading it.

details varies with what failed:

Failure details content
JSON body validation fieldErrors: field → message (string)
Query or path parameter validation fieldErrors: field → array of messages; may also carry paramErrors and crossParameterErrors
Missing query parameter missingParameters
Missing multipart part (e.g. file on /documents) missingParts
POST /travellers batch totalErrors and errors[], each with requestIndex, errorType, field, message

Handle a fieldErrors value that is either a string or an array of strings.


Normalising the shapes

One helper, used everywhere:

/**
 * Normalises all three HGN error shapes into one object.
 * Never throws on an unexpected body.
 */
async function parseHgnError(res) {
  let body = null;
  try {
    body = await res.json();
  } catch {
    // Non-JSON body (proxy timeout, gateway HTML page, empty response)
    return { status: res.status, message: res.statusText, fieldErrors: {} };
  }

  return {
    status: res.status,
    // shape 2 uses `message`; shapes 1 and 3 also use `message`
    message: body?.message ?? res.statusText,
    // only present on validation failures
    fieldErrors: body?.details?.fieldErrors ?? {},
    raw: body,
  };
}

async function hgnFetch(path, options) {
  const res = await fetch(`${BASE_URL}${path}`, options);
  if (!res.ok) {
    const err = await parseHgnError(res);
    throw Object.assign(new Error(err.message), err);
  }
  return (await res.json()).data;
}
class HgnApiError(Exception):
    def __init__(self, status, message, field_errors=None, raw=None):
        super().__init__(f"[{status}] {message}")
        self.status = status
        self.message = message
        self.field_errors = field_errors or {}
        self.raw = raw


def parse_hgn_error(res):
    """Normalises all three HGN error shapes."""
    try:
        body = res.json()
    except ValueError:
        return HgnApiError(res.status_code, res.reason)

    return HgnApiError(
        status=res.status_code,
        message=body.get("message", res.reason),
        # `details` is absent on non-validation errors
        field_errors=(body.get("details") or {}).get("fieldErrors", {}),
        raw=body,
    )


def hgn_request(method, path, access_token, **kwargs):
    res = requests.request(
        method,
        f"{BASE_URL}{path}",
        headers={"Authorization": f"Bearer {access_token}"},
        timeout=15,
        **kwargs,
    )
    if not res.ok:
        raise parse_hgn_error(res)
    return res.json()["data"]

Handling guidance

Never branch on message

message is for humans and logs. It is not part of the contract and can change without a version bump. Branch on the HTTP status code, and on fields inside data.

The one exception is details.fieldErrors, which is keyed by field name and is intended for display.

Promo rejections are deliberately uniform

Every reason that makes a code unusable returns the same message, Invalid promo code. This is an anti-enumeration measure — see Promo Codes.

success: true is not "it worked"

Two places return 200/201 with success: true while the thing you asked for did not happen:

Case What to check
POST /orders with a promoCode promoApplyResult — a rejected code still creates the order
GET /payment-status paymentStatus — NOT_INITIATED is a 200, not an error

Retries

Status Retry? Notes
400, 403, 404 No The request is wrong. Retrying changes nothing
409 After re-reading Re-read the order and retry if it still makes sense. For /pay, a 409 for an Idempotency-Key still being processed clears after a short delay, and a promo removed at payment time means refresh and pay again
429 Yes Wait Retry-After seconds
401 Once Refresh the token and retry. If it fails again, the credential is the problem — stop
500 Yes, with backoff Use exponential backoff and a retry cap
Network timeout Carefully For /pay, reuse the same Idempotency-Key for the same order — it returns the same payment session. A new key starts a new payment attempt and cancels the unfinished one

Order creation is not idempotent

Only POST /orders/{orderRef}/pay takes an Idempotency-Key. POST /orders does not — a blind retry after a timeout can create a duplicate order.

On an ambiguous failure, reconcile with GET /travellers or your own stored references before retrying.


Next steps

  • Examples — a complete booking with error handling in place
  • Overview — response envelope and conventions