Skip to content

Reference

Errors

One shape for every failure, whatever caused it, and a code that tells you what to do next.

What went wrong at a hotel's own system never reaches you in its own words — and neither does which system it was. Every failure is translated into the set below before the response leaves Gateway Stream, so you write one error handler rather than one per source. The detail is kept in Gateway Stream's logs against the requestId.

The Error Shape

Every error looks like this
{
  "error": {
    "code": "rate_key_expired",
    "message": "rateKey 'rk_01JD8Q5T6H2V8ZCN4PQ1RMB7XE' is unknown or has expired — search again",
    "requestId": "req_01JD8Q9ZB3H5F2PA"
  }
}
error.code
string required
A stable machine-readable code. Branch on this, never on the message.
error.message
string required
A human-readable explanation. Wording may change; do not parse it.
error.requestId
string optional
Identifies this exact request in Gateway Stream’s logs. Log it, and quote it when you ask about a failure.
error.details
object optional
Extra context, shaped per code — the expected and actual price on price_changed, a retry delay on rate_limited, the booking id on supplier_error.

Status Codes and What They Mean

400 validation_error
The request body or query is malformed. details says which field. Fix the request — retrying it unchanged will fail identically.
401 unauthorized
Missing x-api-key, or a key that is unknown, inactive, or not present in the environment its prefix points at.
404 not_found
No such resource for this key. Another client’s booking is indistinguishable from one that never existed, deliberately.
409 conflict
The request collided with the current state — most often a price_changed booking, or cancelling something already cancelled.
409 rate_unavailable
The rate is gone: sold out, stop-sold or withdrawn since it was quoted. Search again.
410 rate_key_expired
The rate key is unknown, expired, or already spent. Rate keys live about 30 minutes and are single-use. Search again.
424 distributor_unavailable
The source behind this request has been switched off for the operation you attempted. Search again. Existing bookings can still be cancelled.
429 rate_limited
Too many requests for this key. details.retryAfterSeconds says how long to wait.
500 internal_error
Something broke at Gateway Stream. Safe to retry once after a pause; report it with the requestId if it persists.
501 not_implemented
This operation is not available for this booking.
502 supplier_error
A call to the hotel’s own system failed or was refused. Distinct from 424: this was an attempt that failed, not a source already known to be off. The message never names it; details.bookingId is set when a booking record was kept.
502 cancel_outcome_unknown
A cancel reached the supplier and came back without a definite answer. The booking is deliberately left in CANCEL_REQUESTED — confirm rather than resend.
503 mode_unavailable
This deployment has no configuration for the environment your key points at — typically a live key against a sandbox-only deployment.

When a Hotel's System Fails

A booking or a cancel can fail at the hotel's own reservation system. You get the same shape as any error, with a generic message and — for a booking — the id of the record that was kept of the attempt.

502 supplier_error
{
  "error": {
    "code": "supplier_error",
    "message": "The provider could not complete this request. Quote the requestId if you contact support.",
    "requestId": "req_01JD8QB9K4M7P2VC",
    "details": { "bookingId": "0b5e1a92-6c3f-4a7d-8e21-9b4f0d3c5a18" }
  }
}

What Is Safe to Retry

  • Never retried unchanged: 400, 401, 404, 501. The answer will not change.
  • Retry after fixing the cause: 409 and 410 mean search again and present the new price before trying once more. Nothing was booked.
  • Retry after a delay: 429 after details.retryAfterSeconds; 500 and 503 with backoff.
  • Do not retry — reconcile: a 202 UNKNOWN booking, or a 502 cancel_outcome_unknown. Both mean the supplier's state is genuinely unknown, and a second attempt is how a duplicate reservation gets created.

Failures That Are Not Errors

Search and property listing ask several sources at once, and one of them failing does not fail the call. You get a 200 with whatever arrived, and meta.complete reads false. Treat that as “there may be more”, not as an empty result — Request conventions covers this.

A Handler to Start From

Normalising errors once
async function gws(path, init) {
  const res = await fetch('https://api.gatewaystream.com/api/v1' + path, {
    ...init,
    headers: { 'x-api-key': process.env.GWS_API_KEY, ...init?.headers }
  });

  if (res.ok) return res.json();

  const body = await res.json().catch(() => null);
  const err = body?.error ?? { code: 'unknown', message: res.statusText };

  // requestId is what Gateway Stream traces a specific failure by — always log it.
  throw Object.assign(new Error(err.message), {
    status: res.status,
    code: err.code,
    requestId: err.requestId,
    details: err.details
  });
}

Branch on code rather than on status where the two differ: two distinct 409s and two distinct 502s need different responses from your application, and only the code tells them apart.