Skip to main content

HTTP status vs business errors

Connect Hotels uses two layers (plus a rare parse 400). Do not treat every failure as an HTTP 4xx, and do not invent codes such as PRICE_CHANGED, NO_AVAILABILITY, RATE_LIMIT_EXCEEDED, or INTERNAL_SERVER_ERROR on errors[].code.

Full enum and description catalog: Error codes. Retry policy: Error handling. Booking lifecycle enums: Status codes (BOOK_STATUS_TYPE_*, not HTTP).

Layer 1 — Gateway (HTTP is the signal)

Auth and account rate limits are enforced before the aggregator. Typical statuses: 401, 403, 429, 5xx. Read Retry-After and X-RateLimit-* on 429. The JSON body is not aggregator errors[]. We do not document a made-up { "error": { "code": "RATE_LIMIT_EXCEEDED" } } envelope.

Layer 1b — Unparseable JSON (HTTP 400, not ERR_CODE_*)

Book rejects a non-array input.rooms / input.remarks, or input.clientReference longer than 20 characters, before Connect Hotels business validation:

{
"code": "invalid_argument",
"message": "input.clientReference must be at most 20 characters (got 24)"
}

Stay dates and missing connectionCodes are not this path. They are HTTP 200 + errors[].

Layer 2 — Connect Hotels (HTTP 200 + errors[])

Search, quote, book, detail, list, and cancel usually return HTTP 200. Business and provider failures are in errors[]:

FieldWire
codeERR_CODE_*
typeERR_TYPE_*
descriptionHuman-readable text (often the provider message). Generic labels: read additionalData.description.
connectionCodePresent when a connection was involved; omitted on search-wide refusals and on quote/book/cancel throws
additionalDataOptional string map (retryable, recommended_action, exception, description, quote policy keys)

Empty options with empty errors means no inventory from those connections — not a special NO_AVAILABILITY code.

Partial search: one connection can fail while others return options. Inspect errors and tracing.accessSpans.

Examples from the wire

Stay dates rejected (no provider call)

HTTP 200. Aggregator client error; connectionCode omitted. Same shape for missing dates and inverted stay.

{
"errors": [
{
"code": "ERR_CODE_BAD_REQUEST",
"type": "ERR_TYPE_CLIENT",
"description": "criteria.checkIn is in the past (more than one day before today UTC); the search was not forwarded to providers",
"additionalData": {
"retryable": "false",
"recommended_action": "fix_request"
}
}
]
}

Missing connectionCodes

HTTP 200. No hint. Search when connectionCodes is null:

{
"errors": [
{
"code": "ERR_CODE_CONNECTION_DATA_MISSING",
"type": "ERR_TYPE_CLIENT",
"description": "settings or connectionCodes is missing"
}
]
}

Quote, book, and cancel use a different sentence: settings is nil or connectionCodes is empty: exactly one connectionCode is required.

Provider returned HTML instead of the expected payload

HTTP 200. Parse failure after a bad upstream body. When the integration maps it, type is often ERR_TYPE_SERVICE. A search failure the aggregator could not classify is ERR_CODE_UNKNOWN / ERR_TYPE_UNKNOWN.

{
"code": "ERR_CODE_UNKNOWN",
"type": "ERR_TYPE_SERVICE",
"description": "No unmarshaller for element \"html\" (line 1, col 1)",
"connectionCode": "testb-conn-4865"
}

Generic proto description — real cause in additionalData

HTTP 200. Hints (recommended_action) match top-level description only. This example has no allotment hint until Insufficient allotment is also on description.

{
"code": "ERR_CODE_BAD_RESPONSE",
"type": "ERR_TYPE_PROVIDER",
"description": "Response contains errors.",
"connectionCode": "testb-conn-1876",
"additionalData": {
"description": "Insufficient allotment",
"exception": "ProductException"
}
}

One connection down, others still return

HTTP 200. Upstream non-200 maps to ERR_TYPE_PROVIDER / ERR_CODE_BAD_RESPONSE. No hint. Description starts with unexpected status code: received HTTP.

{
"errors": [
{
"code": "ERR_CODE_BAD_RESPONSE",
"type": "ERR_TYPE_PROVIDER",
"description": "unexpected status code: received HTTP 502 from https://integration.example/api/search. Response body: …",
"connectionCode": "testb-conn-1876"
}
]
}

Timeout (no hint)

HTTP 200. Same codes as 502. Internal stats bucket this as timeout from the text — that bucket is not a JSON field.

{
"code": "ERR_CODE_BAD_RESPONSE",
"type": "ERR_TYPE_PROVIDER",
"description": "connection testb-conn-1876 did not respond within 2.5s",
"connectionCode": "testb-conn-1876"
}

Use options from connections that completed. Do not retry book/cancel blindly.

Circuit breaker open (no hint)

HTTP 200. Connection is shed so it does not stall the fan-out.

{
"code": "ERR_CODE_BAD_RESPONSE",
"type": "ERR_TYPE_PROVIDER",
"description": "circuit breaker open for provider example connection testb-conn-1876",
"connectionCode": "testb-conn-1876"
}

Allotment / sold out (follow the hint)

HTTP 200. code/type stay ERR_CODE_* / ERR_TYPE_*. The hint is added only when top-level description contains a known phrase. Do not retry the same optionRefId.

{
"code": "ERR_CODE_BAD_RESPONSE",
"type": "ERR_TYPE_PROVIDER",
"description": "Insufficient allotment",
"connectionCode": "testb-conn-1876",
"additionalData": {
"retryable": "false",
"recommended_action": "restart_availability"
}
}

Provider rate-limit text can look the same HTTP-wise (still 200 + errors[]) with recommended_action = backoff_and_retry. That is not the same as Kong 429.

Quote blocked by DIRECT_ONLY (virtual card required)

HTTP 200. The option is stripped from the response. Resend with paymentMode=MERCHANT and a paymentCard, or pick another rate.

{
"errors": [
{
"code": "ERR_CODE_MISSING_FIELDS",
"type": "ERR_TYPE_CLIENT",
"description": "This rate requires a Virtual Credit Card (acceptVCard=true) and the current paymentMode is DIRECT_ONLY. Resend with additionalData.paymentMode=MERCHANT and a paymentCard, or pick a different rate.",
"connectionCode": "testb-conn-1876",
"additionalData": {
"connectionCode": "testb-conn-1876",
"paymentMode": "DIRECT_ONLY",
"acceptVCard": "true",
"optionRefId": "opt-123"
}
}
]
}

Stay date rewritten (warning, not an error)

HTTP 200. Search proceeds. WARN_CODE_NONE is the proto zero — read warning_type.

{
"warnings": [
{
"code": "WARN_CODE_NONE",
"description": "Stay dates are civil dates; time-of-day was ignored",
"additionalData": {
"warning_type": "STAY_DATE_NORMALIZED",
"severity": "INFO",
"field": "checkIn",
"received": "2026-11-02T22:00:00.000Z",
"used": "2026-11-02"
}
}
]
}

Content API client error (HTTP 400)

Catalog validation uses HTTP status, with the same ERR_CODE_* strings:

{
"errors": [
{
"code": "ERR_CODE_BAD_REQUEST",
"type": "ERR_TYPE_CLIENT",
"description": "since \"yesterday\" is not a valid RFC 3339 instant"
}
]
}

What to do

Hint (recommended_action)Action
fix_requestCorrect the payload (dates, occupancy, required fields).
restart_availabilityNew search; do not reuse the option id.
requote_new_priceCompare optionQuote.price to what you showed; do not invent PRICE_CHANGED.
verify_booking_then_restartList by clientReference before booking again.
backoff_and_retryWait, then retry that call (safe when the provider rejected before processing).
(missing)Use type + description. Timeout / circuit breaker / 502: that connection failed; others may be fine. Generic description: read additionalData.description.

Book and cancel are not retried by the platform.

Buyer booking webhooks are not available. For BOOK_STATUS_TYPE_ON_REQUEST, poll booking detail.