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[]:
| Field | Wire |
|---|---|
code | ERR_CODE_* |
type | ERR_TYPE_* |
description | Human-readable text (often the provider message). Generic labels: read additionalData.description. |
connectionCode | Present when a connection was involved; omitted on search-wide refusals and on quote/book/cancel throws |
additionalData | Optional 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_request | Correct the payload (dates, occupancy, required fields). |
restart_availability | New search; do not reuse the option id. |
requote_new_price | Compare optionQuote.price to what you showed; do not invent PRICE_CHANGED. |
verify_booking_then_restart | List by clientReference before booking again. |
backoff_and_retry | Wait, 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.