{"openapi":"3.1.0","info":{"title":"GoChatTravel API","description":"\nThe GoChatTravel API powers hotel, flight and car search, booking and cancellation.\n\n### Authentication\n\nEvery endpoint below expects two credentials on each request:\n\n| header | value |\n|---|---|\n| `X-App-Id` | the UUID of the client application calling the API |\n| `Authorization` | `Bearer <token>`, an RS256 JWT identifying the end user |\n\nA request missing or failing either one is answered `401`, and a known but disabled app id is\nanswered `403`. Both come back with an empty body: the status is the whole answer.\n\n### Conventions\n\n* Requests and responses are `application/json`, UTF-8 encoded.\n* Calendar dates use `YYYY-MM-DD`; timestamps follow RFC 3339.\n* Monetary values are decimals paired with an ISO 4217 currency code.\n* Languages are ISO 639-1 codes, countries ISO 3166-1 alpha-2.\n\n### Errors\n\n`4xx` responses signal a malformed or rejected request, `5xx` a temporary failure on our side.\nBoth carry a JSON body of the shape:\n\n```json\n{ \"error\": \"check_in must be before check_out\" }\n```\n\n### Trying it out\n\nChoose a server, open an operation in the sidebar and press **TRY** to send a live request\nstraight from this page.\n","license":{"name":"Proprietary","identifier":"LicenseRef-Proprietary"},"version":"1.0.0"},"servers":[{"url":"https://sandbox.gochattravel.com","description":"Sandbox"},{"url":"/","description":"Current host"}],"paths":{"/api/v1/airport/autocomplete":{"get":{"tags":["Airport"],"summary":"Suggest places to fly from and to","description":"\nResolves what a traveller is typing into somewhere a flight can depart from or arrive at.\nEvery suggestion carries `codes`, ready to send straight back as the `origin` or\n`destination` of a flight search — nothing needs resolving a second time.\n\n### What it searches\n\nThe 3,944 air locations a scheduled flight actually serves, of two kinds, told apart by\n`kind`:\n\n| `kind` | count | what it is |\n|---|---|---|\n| `airport` | 3,917 | one airport |\n| `city` | 27 | an IATA metro code standing for all the airports of one city |\n\nThe booking catalog holds 9,038 airports, but most of them cannot be flown from on a\nscheduled ticket: altiports, air bases, private fields and airports that have since closed.\nCourchevel Altiport is 2 km from the village and takes no scheduled traffic; offering it\nwould be offering a flight that does not exist. So a row is only offered when\n[OurAirports](https://ourairports.com/) lists it with scheduled service, which is also what\nkeeps Tegel and Schönefeld — closed, but still in the booking catalog — out of the answer.\nSeaplane bases and heliports are excluded whether or not they carry a code.\n\nA metro code is offered only where it means something no single airport does: it has to\ngroup more than one airport, and it must not simply be its own main airport's code. `LON`,\n`NYC`, `PAR`, `MOW` and `TYO` qualify; `MAD` does not, because `MAD` *is* Barajas, and\noffering \"Madrid, all airports\" beside it offers the same thing twice.\n\n### Matching\n\nThe term is matched against the airport's own name, its translations, its IATA and ICAO\ncodes, and every name of the city it serves — in every language the API supports, not just\nthe one being rendered, and in non-Latin scripts as readily as in Latin ones. `SVO`,\n`Sheremetyevo`, `Moscow` and the local spelling of either all reach the same airport. Case,\naccents and punctuation are ignored, so `dusseldorf` finds `Düsseldorf`.\n\n**Typing a code works and is ranked accordingly.** An exact IATA or ICAO code wins outright,\nwhich matters because 3,729 of the codes are also a three-letter prefix of some place name:\n`PAR` has to answer with Paris rather than with Paraná and its 11.6 million residents, and\n`LAX` with Los Angeles rather than with Laxmangarh. Where the collision is genuine — `SAN`\nis San Diego and also how `Santiago` and `Santa Catarina` start — both come back, the code\nfirst, and the ambiguity resolves itself on the next keystroke.\n\nMatching is by prefix. Terms shorter than two characters are rejected with `400`. A term\nthat matches no prefix falls back to fuzzy matching on names, so `Amstrdam` still returns\nSchiphol; codes are excluded from that fallback, because a typo in a three-letter code is\nanother valid code and guessing would be worse than returning nothing.\n\n### Somewhere with no airport of its own\n\nMost places people fly to have no airport: Ischgl, Positano, Courchevel, Zermatt. A term\nthat names one of the 176,691 places in the hotel catalog is answered with the airports one\nactually flies into, and `via` says which place was matched and how far away the airport is.\n\nWhich airports those are is not simply the nearest ones. Ischgl's four closest are a private\nairfield, a general-aviation field and two more of the same before Innsbruck, 85 km out; and\nCourchevel's two closest are altiports with no scheduled traffic at all. So how far an\nairport is worth reaching depends on its size — 250 km for a large one, 110 for a medium\none, 60 for a small one — and the ranking weighs size against distance rather than taking\nwhatever is closest. `Ischgl` answers with Innsbruck, `Courchevel` with Geneva and Lyon,\n`Bali` with Denpasar.\n\nDistance is great-circle, so a mountain range between the traveller and the airport is not\naccounted for: Turin is nearer Courchevel than Geneva by air and considerably further by\nroad.\n\nWhere a place has an airport of its own, its neighbours are left out — `Milan` returns\nMalpensa and Linate, not Turin and Genoa.\n\n### Localisation\n\n`language` only decides how suggestions are **labelled**. Where the catalog has no name for\nan airport in that language, `name` falls back to the airport's own name rather than to the\ncity's — otherwise every untranslated airport of one city comes back under the city's name,\nwith no way to tell them apart. `country_name` is always in English.\n\n### Using the result\n\n`codes` is the field to use: pass it as the `origin` or `destination` of a flight search. For\nan airport it holds one code, for a metro code all of its airports, best first. `label` is\nready to render as-is; build your own from `name`, `city_name` and `country_name` if you want\nit differently.\n\n`latitude`, `longitude` and `timezone` describe one airport, so they are `null` on a `city`\nsuggestion: a metro code stands for airports spread across a region and sits at no point of\nits own.\n","operationId":"autocompleteAirports","parameters":[{"name":"q","in":"query","description":"What the user has typed so far.","required":true,"schema":{"type":"string"}},{"name":"language","in":"query","description":"ISO 639-1 code deciding the language of `name`, `city_name` and `label`. Defaults\nto the `Accept-Language` header, then English.","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","description":"How many suggestions to return, 1–25.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"country","in":"query","description":"ISO 3166-1 alpha-2 code to restrict the search to one country.","required":false,"schema":{"type":["string","null"]}},{"name":"kind","in":"query","description":"`airport` or `city` to return only one kind. Both by default.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Matching air locations, best first. `results` is empty when nothing matched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AirportAutocompleteResponse"},"example":{"results":[{"airport_count":6,"city_name":"London","code":"LON","codes":["LHR","LGW","STN","LTN","SEN","LCY"],"country_code":"GB","country_name":"United Kingdom","icao_code":null,"kind":"city","label":"London (LON), United Kingdom","latitude":null,"longitude":null,"name":"London","timezone":null},{"airport_count":1,"city_name":"London","code":"LHR","codes":["LHR"],"country_code":"GB","country_name":"United Kingdom","icao_code":"EGLL","kind":"airport","label":"Heathrow Airport (LHR), London, United Kingdom","latitude":51.470311,"longitude":-0.458118,"name":"Heathrow Airport","timezone":"Europe/London"},{"airport_count":1,"city_name":"Innsbruck","code":"INN","codes":["INN"],"country_code":"AT","country_name":"Austria","icao_code":"LOWI","kind":"airport","label":"Innsbruck Airport (INN), Innsbruck, Austria","latitude":47.260188,"longitude":11.345793,"name":"Innsbruck Airport","timezone":"Europe/Vienna","via":{"distance_km":84.5,"geoname_id":2775220,"name":"Ischgl"}}]}}}},"400":{"description":"The term is shorter than two characters, or `kind` is neither `airport` nor `city`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"q must be at least 2 characters"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"500":{"description":"The airport catalog could not be queried.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Airport autocomplete failed"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/bookings":{"get":{"tags":["Bookings"],"summary":"Everything the traveller has booked","description":"\nEvery reservation the authenticated traveller has settled - hotels, flights, events and cars in\none list, newest first. Cancelled ones are included and say so in `status`; anything still in a cart,\nheld or half-paid, is not here and belongs to `GET /api/v1/cart`.\n\nEach entry is the same shape a cart item has, told apart by `type`, so one renderer draws both.\nOn top of that a booking carries `confirmation` - the airline's record locator, the hotel's\nconfirmation code, or the provider's booking id - and `booked_at`.\n\n`confirmation` is absent while the provider is still settling the reservation: a flight sits in\nthe airline's queue for a while before it is ticketed, and no locator exists until it is.\n","operationId":"listBookings","responses":{"200":{"description":"The traveller's reservations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingsResponse"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/bookings/{item_type}/{item_id}/cancel":{"post":{"tags":["Bookings"],"summary":"Cancel a booking","description":"\nCancels one reservation with its provider and answers with the booking as it now stands.\n\nQuote it first with `GET /api/v1/bookings/{item_type}/{item_id}/cancellation`: this endpoint does\nnot check that the traveller was told the price, and a non-refundable booking cancels just as\nreadily as a free one.\n\nA booking that is already cancelled answers `409` rather than being cancelled twice.\n","operationId":"cancelBooking","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight` or `car`","required":true,"schema":{"type":"string"}},{"name":"item_id","in":"path","description":"Booking id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The booking is cancelled"},"404":{"description":"No such booking"},"409":{"description":"It is not in a state that can be cancelled"},"502":{"description":"The provider refused the cancellation"}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/bookings/{item_type}/{item_id}/cancellation":{"get":{"tags":["Bookings"],"summary":"What cancelling a booking would cost","description":"\nRead this before offering to cancel anything: it says what the traveller gets back and what they\nforfeit, so the confirmation can quote real numbers rather than a general warning.\n\nA hotel is priced from the policy the rate was sold under - `free_until` is the moment free\ncancellation ends. A flight is priced by asking the airline, which is a live call and the only\nway to learn a fare's real penalty.\n\n`refundability` is `unknown` when the provider would not answer. That is not the same as free and\nnot the same as non-refundable: say so plainly rather than guessing, and let the traveller decide.\n","operationId":"getCancellationQuote","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight` or `car`","required":true,"schema":{"type":"string"}},{"name":"item_id","in":"path","description":"Booking id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"What cancelling costs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancellationQuote"}}}},"404":{"description":"No such booking"}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/car/locations/dropoff":{"post":{"tags":["Car"],"summary":"Find rental branches to return a car to","description":"\nLists the branches around a point where a car picked up at `pickup_location_ids` can be returned,\nnearest first. Only needed for a one-way rental; leave `dropoff_location_ids` empty in the car\nsearch to return the car where it was picked up.\n\nA car is returned to the vendor it was rented from, so only branches of the vendors behind\n`pickup_location_ids` are listed. The car search pairs each pickup branch with the first dropoff\nbranch of the same vendor in the order you send them, so keep this list's order, or send just the\nnearest branch per vendor.\n\n`search` takes the same fields and defaults as `POST /api/v1/car/locations/pickup`. An empty\n`locations` array means none of those vendors has a branch in range.\n","operationId":"searchCarDropoffLocations","requestBody":{"description":"The pickup branches already chosen and the point to return the car near.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarDropoffLocationSearchRequest"},"example":{"pickup_location_ids":["ZIPHXPHX","ZEPHXPHX"],"search":{"latitude":32.1161,"longitude":-110.941}}}},"required":true},"responses":{"200":{"description":"Branches of the pickup vendors in range, nearest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarLocationSearchResult"}}}},"400":{"description":"No pickup branch was sent, too many were, or the search point is out of range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"at least one pickup_location_id is required"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"500":{"description":"The lookup could not be run: the provider failed, or car search is not enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Car location search failed"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/car/locations/pickup":{"post":{"tags":["Car"],"summary":"Find rental branches to pick a car up from","description":"\nLists the rental branches around a point, nearest first. Each entry is one counter of one vendor,\nso an airport with six rental companies comes back as six entries.\n\nA car search runs on branch ids, not on coordinates: pick the branches you want from this list\nand send their `id`s as `pickup_location_ids` to `POST /api/v1/car/search`. Ids are opaque, so\nnever build or parse them yourself.\n\n### Defaults applied when a field is omitted\n\n| Field | Default |\n|---|---|\n| `radius_km` | `40` (kilometres), at most `100` |\n| `max_results` | `200`, at most `200` |\n\n### Choosing branches\n\nEvery branch sent to the car search is a separate availability call to the vendor, so searching\nall of them is slow. Send the nearest handful, and use `airport` to favour airport counters when\nthe traveller arrives by plane.\n\nA point with no branch in range is not an error: it answers `200` with an empty `locations` array.\n","operationId":"searchCarPickupLocations","requestBody":{"description":"The point to search around.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarLocationSearchRequest"},"example":{"latitude":33.4352,"longitude":-112.0101,"radius_km":20.0}}},"required":true},"responses":{"200":{"description":"Branches in range, nearest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarLocationSearchResult"},"example":{"locations":[{"address":{"city":"Phoenix","country":"US","postal_code":"85034","street":"1805 E Sky Harbor Circle S"},"airport":true,"distance_km":0.4,"geo":{"latitude":33.4352,"longitude":-112.0101},"id":"ZIPHXPHX","name":"Phoenix Sky Harbor Intl Airport","provider":"Sabre","vendor":{"code":"ZI","logo_url":null,"name":"Avis"}}]}}}},"400":{"description":"The coordinates, radius or result limit are out of range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"latitude must be between -90 and 90"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"500":{"description":"The lookup could not be run: the provider failed, or car search is not enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Car location search failed"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/car/offer/{offer_id}/select":{"post":{"tags":["Car"],"summary":"Put a shopped car offer in the cart","description":"\nHolds one rental for the authenticated traveller and puts it in their cart, where it waits until\nit is removed. Nothing is booked and nothing is charged by this call.\n\n### Which offers can be selected\n\nAny `offer_id` this traveller was given by `POST /api/v1/car/search`. Offers stay resolvable for\n**15 minutes**. A `404` means the offer is gone rather than that it never existed: re-run the\nsearch and select the fresh id. It is also a `404` when the offer belongs to somebody else - an id\nis only selectable by the traveller it was shopped for.\n\n### What comes back\n\nA cart item. Its `id` is a **booking id, not the offer id** - that is the handle the rest of the\nfunnel uses, and the offer id stops mattering the moment the item exists.\n\n### Selecting twice\n\nThe call is idempotent per rental. Selecting an offer for a car class, pickup and dropoff branch\nand rental period the cart already holds returns the existing item with `200` instead of a second\none with `201`, so a retried request cannot quietly duplicate it. Selections are serialised per\ntraveller, so two requests racing each other - a double click - settle the same way rather than\nboth inserting; one that arrives while another is still being processed answers `409`.\n\n### Which cart it lands in\n\nWithout `cart_id` the offer goes into the traveller's **current** cart, and one is opened for them\nif they have none. Send `cart_id` to put it in a particular cart instead.\n\nA `cart_id` that is not this traveller's answers `404`, like an offer that never existed. One that\nhas already started checking out answers `409`: a cart being paid for takes no new items.\n\nRead the whole cart back with `GET /api/v1/cart`, and list them all with `GET /api/v1/carts`.\n","operationId":"selectCarOffer","parameters":[{"name":"cart_id","in":"query","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"offer_id","in":"path","description":"`offer_id` of an entry in a car search's `offers`.","required":true,"schema":{"type":"string"},"example":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f"}],"responses":{"200":{"description":"The cart already held this rental; the existing item is returned untouched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarCartItem"}}}},"201":{"description":"The offer is now in the cart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarCartItem"},"example":{"created_at":"2026-09-01T10:15:00Z","currency":"USD","dropoff_at":"2026-11-08T10:00:00","dropoff_location_id":"ZIPHXPHX","id":"c3a9f1e2-7d45-4b8a-9e0c-5f2d1b6a8c34","pickup_at":"2026-11-05T10:00:00","pickup_location_id":"ZIPHXPHX","provider":"Sabre","rental_days":3,"status":"pending","total_amount":"187.41","updated_at":"2026-09-01T10:15:00Z","vehicle_code":"ECAR","vehicle_name":"Kia Rio or similar","vendor_code":"ZI","vendor_name":"Avis"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"404":{"description":"The offer has expired, was never shopped, or belongs to another traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Offer not found or expired"}}}},"409":{"description":"Another selection for this traveller is still being processed, or the named cart is checking out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Another selection for this traveller is in progress"}}}},"500":{"description":"The offer could not be put in the cart, or car search is not enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to select car offer"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/car/search":{"post":{"tags":["Car"],"summary":"Search car rental availability","description":"\nShops live availability for one rental at the branches you name and returns every bookable car,\ncheapest first.\n\n### Where the search runs\n\nCars are shopped per branch, so the search takes branch ids rather than a point. Get them from\n`POST /api/v1/car/locations/pickup` and send the ones to shop as `pickup_location_ids`.\n\nLeave `dropoff_location_ids` empty to return the car where it was picked up. For a one-way rental,\nsend branches from `POST /api/v1/car/locations/dropoff`: each pickup branch is paired with the\nfirst dropoff branch of the same vendor in the order sent, and a pickup branch whose vendor has\nno dropoff branch in the list is not shopped at all.\n\n### Times\n\n`pickup_at` and `dropoff_at` are local times at their branches, without an offset, as\n`YYYY-MM-DDTHH:MM:SS`. `dropoff_at` must be after `pickup_at`. `rental_days` on each offer is\nthe number of days the vendor bills, which can be one more than the calendar dates suggest when\nthe car goes back later in the day than it was picked up.\n\n### Currency\n\n`currency` asks for every offer to be priced in one ISO 4217 currency. A vendor that cannot\nconvert keeps its own, so read `pricing.currency` off each offer. Omitted, each vendor prices in\nits own currency.\n\n### Reading the response\n\n* `offers` holds one entry per car class per pickup and dropoff branch pair: when several rates\n  quote the same car, only the cheapest is kept.\n* `pricing.total` is the vendor's approximate total for the whole rental, in `pricing.currency`,\n  and `pricing.per_day` is that total spread evenly over `rental_days`. `drop_off_charge` is the\n  one-way fee, when the vendor quotes one separately.\n* `pricing.taxes` is the mandatory taxes and fees already inside `pricing.total`, and\n  `pricing.base_rate` is what is left of it. Both are `null` when the vendor quoted no estimate,\n  which is not the same as a tax-free rental - `pricing.total` is what the traveller pays either\n  way.\n* `vehicle.classification` decodes the four-letter ACRISS code (`ECAR`, `IFAR`, ...). A letter\n  outside the ACRISS tables decodes to `null` rather than failing the whole code.\n* `vehicle.make_model` is a representative model: vendors rent a class, not a car.\n* `mileage` is `\"Unlimited\"`, or `{ \"Limited\": { \"allowance\", \"unit\", \"extra_charge\" } }` with the\n  allowance for the rental and the charge per extra `unit`. `null` means the vendor did not say.\n* `offer_id` stays resolvable for **15 minutes**; after that the search has to be re-run.\n\n### Limits and failure modes\n\n* Every pickup branch is a separate call to its vendor, so a call can take several seconds.\n  Send at most 20 pickup branches, fewer for a faster answer, and allow a generous client timeout.\n* Results are **not** paginated.\n* A search that matches nothing is a success: it answers `200` with an empty `offers` array.\n  A `500` means the search itself could not be run, for example every vendor call failed, and\n  is worth retrying.\n","operationId":"searchCars","requestBody":{"description":"The branches to shop and the rental period.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarSearchRequest"},"examples":{"One way":{"summary":"Pick up at Phoenix airport, return in Tucson","description":"Each pickup branch is shopped against the dropoff branch of its own vendor.","value":{"dropoff_at":"2026-11-08T18:00:00","dropoff_location_ids":["ZITUSTUS","ZETUSTUS"],"pickup_at":"2026-11-05T10:00:00","pickup_location_ids":["ZIPHXPHX","ZEPHXPHX"]}},"Round trip":{"summary":"Return the car where it was picked up","value":{"currency":"USD","dropoff_at":"2026-11-08T10:00:00","dropoff_location_ids":[],"pickup_at":"2026-11-05T10:00:00","pickup_location_ids":["ZIPHXPHX","ZEPHXPHX","ZRPHXPHX"]}}}}},"required":true},"responses":{"200":{"description":"Bookable cars, cheapest first. `offers` is empty when nothing is available.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarSearchResult"},"example":{"offers":[{"dropoff_at":"2026-11-08T10:00:00","dropoff_location":{"address":{"city":"Phoenix","country":"US","postal_code":null,"street":null},"airport":true,"distance_km":null,"geo":null,"id":"ZIPHXPHX","name":"Phoenix Sky Harbor Intl Airport","provider":"Sabre","vendor":{"code":"ZI","logo_url":null,"name":"Avis"}},"mileage":"Unlimited","offer_id":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f","pickup_at":"2026-11-05T10:00:00","pickup_location":{"address":{"city":"Phoenix","country":"US","postal_code":null,"street":null},"airport":true,"distance_km":null,"geo":null,"id":"ZIPHXPHX","name":"Phoenix Sky Harbor Intl Airport","provider":"Sabre","vendor":{"code":"ZI","logo_url":null,"name":"Avis"}},"pricing":{"base_rate":"137.01","currency":"USD","drop_off_charge":null,"per_day":"62.47","taxes":"50.40","total":"187.41"},"provider":"Sabre","rate_code":"BEST","rental_days":3,"vehicle":{"bags_large":1,"bags_small":1,"classification":{"air_conditioning":true,"body_type":"TwoFourDoor","category":"Economy","code":"ECAR","drive":"Unspecified","fuel":"Unspecified","transmission":"Automatic"},"doors":"4","image_url":null,"make_model":"Kia Rio or similar","seats":5},"vendor":{"code":"ZI","logo_url":null,"name":"Avis"}}]}}}},"400":{"description":"No pickup branch was sent or too many were, the rental period is not a valid range or has passed, or the currency is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"dropoff_at must be after pickup_at"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"500":{"description":"Shopping could not be run: every vendor call failed, or car search is not enabled. An empty result is not an error; it comes back as `200` with no offers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Car search failed"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/cart":{"get":{"tags":["Cart"],"summary":"Read the traveller's current cart","description":"\nEverything the authenticated traveller is holding in their **current** cart but has not yet paid\nfor — hotels, flights, events and cars in one list.\n\nA traveller can keep several carts at once, one per trip they are putting together; this endpoint\nalways answers with the current one, and a traveller who has none gets a fresh empty cart opened\nfor them rather than an error. `GET /api/v1/carts` lists them all, and\n`GET /api/v1/carts/{cart_id}` reads any one of them in exactly this shape.\n\n### What is in it\n\nAn item enters the cart when an offer is selected, and leaves it when it is paid for, removed or\nabandoned. So a cart holds exactly its bookings in two states:\n\n| `status` | |\n|---|---|\n| `pending` | held, nothing charged |\n| `prepared` | checkout has started and a payment is outstanding |\n\nEverything else — booked, cancelled, abandoned — has left the cart and does not appear here.\n\n### The cart itself\n\nAlongside the items, the response carries the cart's own `id`, `status` and optional `title`, plus\n`is_current` — always `true` here, and the field that tells the other cart endpoints apart. A\ncart's `status` is `open` while it is being filled; it becomes `checking_out` once a payment is\noutstanding and takes no new offers until that settles.\n\n### Reading the items\n\n`items` is one list of four shapes, told apart by `type` (`hotel`, `flight`, `event`, `car`); the\nremaining fields of each entry are that shape's own. Every entry carries an `id`, and that `id` is\nthe **booking id** — the handle for removing or checking the item out, not the offer id it came\nfrom. Items are ordered oldest first.\n\n### Prices\n\n`total.by_currency` sums the cart per currency, one entry each, ordered by code. There is\ndeliberately no single grand total: a cart can hold a hotel priced in EUR beside a flight priced\nin USD, and adding those up would mean picking an exchange rate and quoting a number nobody will\nbe charged. Show the currencies side by side, or convert them yourself with a rate you are\nwilling to stand behind.\n\n### `unavailable_item_ids`\n\nItems whose checkout is known to be blocked: the provider reported the inventory gone, or a\npayment went through and the booking behind it failed and is awaiting manual review. They stay in\nthe cart — and stay counted in `total` — because dropping them silently would leave a traveller\nwondering where their selection went, and because a post-payment failure is precisely what someone\nneeds to see rather than have tidied away.\n\nRender these as unavailable and keep them out of any \"check out everything\" action. An id being\nabsent is not a promise: availability is only ever confirmed at checkout, so an item can still\nfail there.\n","operationId":"getCart","responses":{"200":{"description":"The cart, empty or not.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartResponseEnriched"},"example":{"id":"3f1a9c22-8d4e-4f70-9b21-5c8e7a0d1b34","is_current":true,"items":[{"checkin":"2026-11-05","checkout":"2026-11-08","created_at":"2026-09-01T10:15:00Z","currency":"USD","hotel_id":"nuitee:lp1f2a9","hotel_name":"Grand Luxury Hotel & Spa","id":"b7c1e2a4-5f60-4d31-9a8e-2c4b6d8f0a12","provider":"nuitee","room_types":"1 × Deluxe King","status":"pending","total_amount":"1284.00","type":"hotel","updated_at":"2026-09-01T10:15:00Z"},{"created_at":"2026-09-01T10:22:00Z","currency":"USD","id":"9d2f4c81-3ab7-4e05-8c19-6f7a0b3d5e21","owner_name":"British Airways","passengers_count":1,"slices":[],"status":"pending","total_amount":"350.00","type":"flight","updated_at":"2026-09-01T10:22:00Z"}],"status":"open","title":"Rome in May","total":{"by_currency":[{"amount":"1634.00","currency":"USD"}],"car_count":0,"event_count":0,"flight_count":1,"hotel_count":1,"item_count":2},"unavailable_item_ids":[]}}}},"500":{"description":"The cart could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to load cart"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/cart/items/{item_type}/{item_id}/travelers":{"get":{"tags":["Travelers"],"summary":"List travelers assigned to a cart item","operationId":"getItemTravelers","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight`, `event` or `car` - a car takes one driver","required":true,"schema":{"$ref":"#/components/schemas/CartItemType"}},{"name":"item_id","in":"path","description":"The booking id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Assigned travelers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartTravelersResponse"}}}},"404":{"description":"Item not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"put":{"tags":["Travelers"],"summary":"Assign travelers to a cart item","operationId":"assignItemTravelers","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight`, `event` or `car` - a car takes one driver","required":true,"schema":{"$ref":"#/components/schemas/CartItemType"}},{"name":"item_id","in":"path","description":"The booking id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignTravelersRequest"}}},"required":true},"responses":{"200":{"description":"Travelers assigned."},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"post":{"tags":["Travelers"],"summary":"Add a traveler to a cart item without saving them","description":"\nPuts a new traveler on the booking alone. Nothing is written to the caller's saved travelers, so the\ntraveler exists only for this reservation - the same state a saved traveler ends up in once their\nprofile is deleted. Edit them with `updateItemTraveler` and drop them with `removeItemTraveler`.\n\nUse `createCompanion` followed by `assignItemTravelers` instead when the traveler should be kept for\nlater trips.\n\nThe traveler still gets an id, returned as `companion_id`, which is what the other traveler\nendpoints address them by.\n","operationId":"createItemTraveler","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight`, `event` or `car` - a car takes one driver","required":true,"schema":{"$ref":"#/components/schemas/CartItemType"}},{"name":"item_id","in":"path","description":"The booking id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompanionRequest"}}},"required":true},"responses":{"201":{"description":"Traveler added to the booking."},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/cart/items/{item_type}/{item_id}/travelers/{companion_id}":{"delete":{"tags":["Travelers"],"summary":"Remove a traveler from a cart item","operationId":"removeItemTraveler","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight`, `event` or `car` - a car takes one driver","required":true,"schema":{"$ref":"#/components/schemas/CartItemType"}},{"name":"item_id","in":"path","description":"The booking id","required":true,"schema":{"type":"string"}},{"name":"companion_id","in":"path","description":"The traveler's companion id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Traveler removed."},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"patch":{"tags":["Travelers"],"summary":"Edit a traveler on a cart item","description":"\nRewrites one traveler's details on the booking itself, leaving the saved companion untouched. This\nis what edits a traveler whose saved profile was deleted: the booking keeps its own copy, so it can\nstill be corrected before checkout.\n\nFields follow the same omitted/null/set rules as `updateCompanion`. Fields the item cannot hold are\nignored - only flights carry a middle name, gender, nationality and travel document.\n","operationId":"updateItemTraveler","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight`, `event` or `car` - a car takes one driver","required":true,"schema":{"$ref":"#/components/schemas/CartItemType"}},{"name":"item_id","in":"path","description":"The booking id","required":true,"schema":{"type":"string"}},{"name":"companion_id","in":"path","description":"The traveler's companion id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCompanionRequest"}}},"required":true},"responses":{"200":{"description":"Traveler updated."},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/cart/{item_type}/{item_id}":{"delete":{"tags":["Cart"],"summary":"Remove cart item","description":"\nDrops one held item. Nothing is cancelled with the provider and no money moves, because a cart\nitem was never confirmed with them in the first place — this only releases our own hold.\n\nThe reservation record itself is not deleted: its status is set to `abandoned`, which takes it out\nof the cart while leaving the row in place for history.\n\n### Addressing the item\n\nBoth parts of the path come straight off the entry in `GET /api/v1/cart`: `item_type` is its\n`type`, `item_id` its `id`. The type is needed because each kind of booking is numbered\nindependently, so an id alone does not say what to look in. The cart the item belongs to is not\npart of the path — a booking id names exactly one item, whichever of the traveller's carts holds\nit.\n\nAn id belonging to another traveller answers `404`, exactly like one that never existed.\n\n### What comes back\n\nThe traveller's **current** cart as it now stands, in the same shape `GET /api/v1/cart` returns —\nso a UI can redraw from this response instead of fetching again. Note that when the removed item\nsat in some other cart, the body is still the current one; re-read that cart directly with\n`GET /api/v1/carts/{cart_id}` if it is the one on screen.\n\nRemoval is idempotent in effect but not in status: the item is gone after the first call, and a\nsecond call for the same id answers `404` because it is no longer in the cart.\n\n### When it refuses\n\nAn item whose payment went through but whose booking then failed is held for manual review and\nanswers `409`. It cannot be removed until that review resolves — it is the record of money that\nmoved, and dropping it would hide a case somebody has to settle. Those ids are exactly the ones\n`GET /api/v1/cart` reports in `unavailable_item_ids`.\n","operationId":"removeCartItem","parameters":[{"name":"item_type","in":"path","description":"`hotel`, `flight`, `event` or `car` - the item's `type`.","required":true,"schema":{"$ref":"#/components/schemas/CartItemType"},"example":"hotel"},{"name":"item_id","in":"path","description":"The item's `id`.","required":true,"schema":{"type":"string"},"example":"b7c1e2a4-5f60-4d31-9a8e-2c4b6d8f0a12"}],"responses":{"200":{"description":"Removed. The body is the remaining cart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartResponseEnriched"}}}},"400":{"description":"`item_type` is not one of `hotel`, `flight`, `event` or `car`, or `item_id` is not a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"item_type must be hotel, flight, event or car"}}}},"404":{"description":"No such item in this traveller's cart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Item not found in cart"}}}},"409":{"description":"The item is under post-payment review and cannot be removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Item is under review after a payment and cannot be removed"}}}},"500":{"description":"The item could not be removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to remove cart item"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/carts":{"get":{"tags":["Cart"],"summary":"List the traveller's carts","description":"\nEvery cart the authenticated traveller has, newest first — the one they are filling, the ones\nbeing paid for, and the ones already settled.\n\n### Summaries, not contents\n\nEach entry carries what the cart *is* — its id, title, status and totals — but not its items.\nFetch those from `GET /api/v1/carts/{cart_id}`, which returns the full item list with display\ndetails. Listing deliberately stops short of that: enriching every item of every cart would turn\none page load into dozens of lookups.\n\n### `status`\n\n| | |\n|---|---|\n| `open` | accepting offers |\n| `checking_out` | a payment is outstanding; the cart takes no new items until it settles |\n| `completed` | everything in it was booked |\n| `abandoned` | emptied, or nothing in it ever got booked |\n\n### `is_current`\n\nExactly one open cart is the **current** one: where a selected offer lands when\n`POST .../select` is called without a `cart_id`. It is the most recently created or explicitly\nchosen open cart, so starting a checkout hands the role to the next open cart on its own. Switch\nit deliberately with `PATCH /api/v1/carts/{cart_id}` and `{\"current\": true}`.\n\nA traveller with no open cart has none current, and the first selection makes one.\n","operationId":"listCarts","responses":{"200":{"description":"The traveller's carts, newest first. Empty when they have none.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CartSummary"}},"example":[{"created_at":"2026-09-01T10:15:00Z","id":"3f1a9c22-8d4e-4f70-9b21-5c8e7a0d1b34","is_current":true,"status":"open","title":"Rome in May","total":{"by_currency":[{"amount":"1634.00","currency":"USD"}],"car_count":0,"event_count":0,"flight_count":1,"hotel_count":1,"item_count":2},"updated_at":"2026-09-01T10:22:00Z"}]}}},"500":{"description":"The carts could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to load carts"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"post":{"tags":["Cart"],"summary":"Start a new cart","description":"\nOpens an empty cart for the authenticated traveller and makes it the current one, so the next\noffer selected without an explicit `cart_id` lands in it.\n\nCarts are not exclusive: a traveller can hold several at once and fill them in any order. Use\nthis when they are planning a second trip while the first is still unpaid, or when a set of\nselections belongs together and should be paid for as one.\n\nSend a `title` to name it. Untitled carts are fine — the field exists so a traveller can tell two\nof them apart, not because anything depends on it.\n","operationId":"createCart","requestBody":{"description":"The cart to open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCartRequest"},"example":{"title":"Rome in May"}}},"required":true},"responses":{"201":{"description":"The new cart, empty and current.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartSummary"},"example":{"created_at":"2026-09-01T10:15:00Z","id":"3f1a9c22-8d4e-4f70-9b21-5c8e7a0d1b34","is_current":true,"status":"open","title":"Rome in May","total":{"by_currency":[],"car_count":0,"event_count":0,"flight_count":0,"hotel_count":0,"item_count":0},"updated_at":"2026-09-01T10:15:00Z"}}}},"400":{"description":"The title is longer than 256 characters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"title must be at most 256 characters"}}}},"500":{"description":"The cart could not be created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to create the cart"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/carts/{cart_id}":{"get":{"tags":["Cart"],"summary":"Get cart by id","description":"\nOne cart with everything in it, in the same shape `GET /api/v1/cart` returns for the current one —\nso a UI can render either from the same code.\n\nA cart belonging to another traveller answers `404`, exactly like one that never existed.\n\nItems are ordered oldest first and each `id` is a **booking id**, the handle for removing or\nchecking that item out. See `GET /api/v1/cart` for how to read the items and\n`unavailable_item_ids`.\n","operationId":"getCartById","parameters":[{"name":"cart_id","in":"path","description":"The cart's `id`.","required":true,"schema":{"type":"string"},"example":"3f1a9c22-8d4e-4f70-9b21-5c8e7a0d1b34"}],"responses":{"200":{"description":"The cart, empty or not.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartResponseEnriched"}}}},"400":{"description":"`cart_id` is not a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"cart_id must be a UUID"}}}},"404":{"description":"No such cart for this traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Cart not found"}}}},"500":{"description":"The cart could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to load cart"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"delete":{"tags":["Cart"],"summary":"Abandon a cart","description":"\nReleases every hold in the cart and closes it. Nothing is cancelled with any provider and no money\nmoves — the items were never confirmed with them.\n\nThe cart row survives as `abandoned` rather than being deleted, so a traveller looking back at\n`GET /api/v1/carts` can still see what they had shortlisted and decided against.\n\n### When it refuses\n\nA cart that is `checking_out` answers `409`: a payment is outstanding, and dropping the items it\nis waiting on would strand it. Wait for that checkout to settle or fail first.\n\nAn item already charged and held for manual review is kept even when the rest of the cart goes —\nthe same rule `DELETE /api/v1/cart/{item_type}/{item_id}` applies. Such a cart stays open with\nthat item still in it.\n","operationId":"deleteCart","parameters":[{"name":"cart_id","in":"path","description":"The cart's `id`.","required":true,"schema":{"type":"string"},"example":"3f1a9c22-8d4e-4f70-9b21-5c8e7a0d1b34"}],"responses":{"200":{"description":"Abandoned. The body is the cart as it now stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartResponseEnriched"}}}},"400":{"description":"`cart_id` is not a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"cart_id must be a UUID"}}}},"404":{"description":"No such cart for this traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Cart not found"}}}},"409":{"description":"The cart is checking out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Cart is checking out and cannot be abandoned"}}}},"500":{"description":"The cart could not be abandoned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to abandon the cart"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"patch":{"tags":["Cart"],"summary":"Update a cart","description":"\nChanges what a cart *is* rather than what it holds. Both fields are optional and independent;\nsend either, both, or neither.\n\n### `title`\n\nA string renames the cart. An explicit `null` clears the name. Omitting the field leaves the\nexisting title untouched — that is the difference between `{\"title\": null}` and `{}`.\n\n### `current`\n\n`true` makes this the cart that selections land in when no `cart_id` is given. There is no\n`false`: a cart stops being current when another one takes over, or when it leaves `open`.\n\nOnly an open cart can be made current, so this answers `409` for one that is checking out or has\nsettled. Renaming such a cart is still allowed.\n","operationId":"updateCart","parameters":[{"name":"cart_id","in":"path","description":"The cart's `id`.","required":true,"schema":{"type":"string"},"example":"3f1a9c22-8d4e-4f70-9b21-5c8e7a0d1b34"}],"requestBody":{"description":"The fields to change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCartRequest"},"example":{"current":true,"title":"Rome in May"}}},"required":true},"responses":{"200":{"description":"The cart as it now stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartResponseEnriched"}}}},"400":{"description":"`cart_id` is not a UUID, or the title is too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"title must be at most 256 characters"}}}},"404":{"description":"No such cart for this traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Cart not found"}}}},"409":{"description":"`current` was requested for a cart that is no longer open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Cart is checking out and cannot be made current"}}}},"500":{"description":"The cart could not be updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to update the cart"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/carts/{cart_id}/processing":{"get":{"tags":["Cart Processing"],"summary":"Get cart processing state","description":"\nWhere the run stands, item by item. Composed from the cart and its bookings as they are right now,\nso it cannot disagree with them. Poll it to follow a run: it is the only way to learn that a paused\nrun's blocker is cleared or that the run has finished.\n\n`status` is `running` while it works, `paused` when it needs something, and `completed` or\n`cancelled` when it is over. A paused run names the item in `blocked_on` and what it wants in\n`blocked_by`:\n\n- `traveller_details` - passengers or guests are missing. Fill them in through the booking's own\n  endpoint, then resume.\n- `price_confirmation` - the provider repriced. Resume with `confirmed_price` set to the new total.\n- `hosted_payment` - the provider takes payment on its own page. Send the traveller to\n  `payment_url`; once they pay, the item shows as `booked` here on the next poll.\n- `payment_method` - an item ahead has to be charged and the run holds no card. Resume with one.\n\nAn item marked `post_payment_review` had a payment attempted whose outcome is not known. Do not\noffer to retry it and do not tell the traveller either that they were or were not charged.\n","operationId":"getCartProcessing","parameters":[{"name":"cart_id","in":"path","description":"Cart id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The run as it stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartProcessingSnapshot"}}}},"404":{"description":"No such cart, or it has never been processed"}},"security":[{"app_id":[],"bearer_auth":[]}]},"post":{"tags":["Cart Processing"],"summary":"Start cart processing","description":"\nStarts booking every reservation in the cart and answers immediately, before any of them has been\nbooked. Poll `GET /api/v1/carts/{cart_id}/processing` to follow it.\n\nThe payment method is used for the items whose provider accepts a card directly and is never\nstored. Items whose provider takes payment on its own page do not use it: the run pauses on them\nwith a `hosted_payment` URL for the traveller to open, and picks up again once that booking settles.\n\nThe run stops at the first item that fails or needs something, so a cart is never part-charged\nwithout the traveller hearing about it. Resume once you have answered what it asked for.\n","operationId":"startCartProcessing","parameters":[{"name":"cart_id","in":"path","description":"Cart id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartProcessingRequest"}}},"required":true},"responses":{"202":{"description":"The run has started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartProcessingSnapshot"}}}},"404":{"description":"No such cart"},"409":{"description":"The cart is empty, already checking out, or has settled"}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/carts/{cart_id}/processing/cancel":{"post":{"tags":["Cart Processing"],"summary":"Cancel cart processing","description":"\nStops the run at its next item. Anything already booked stays booked - there is nothing to undo -\nand the cart goes back to the traveller with whatever is left in it.\n","operationId":"cancelCartProcessing","parameters":[{"name":"cart_id","in":"path","description":"Cart id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelProcessingRequest"}}},"required":true},"responses":{"200":{"description":"The run is stopping","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartProcessingSnapshot"}}}},"404":{"description":"No such cart, or no run to cancel"},"409":{"description":"The run id is stale"}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/carts/{cart_id}/processing/resume":{"post":{"tags":["Cart Processing"],"summary":"Resume cart processing","description":"\nAnswers whatever the run stopped on and starts it again from that item.\n\nSend `payment_method` again whenever an item ahead still has to be charged: the card is dropped\nwhen a run pauses, and the run will pause right back with `payment_method` if it needs one it does\nnot have. Send `confirmed_price` to accept a repricing, or `skip_item` to abandon the blocking item\nand carry on with the rest.\n\nAlso takes over a run whose driver stopped reporting - a deployment mid-run, say. A run that is\nstill working answers `409`.\n","operationId":"resumeCartProcessing","parameters":[{"name":"cart_id","in":"path","description":"Cart id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResumeProcessingRequest"}}},"required":true},"responses":{"202":{"description":"The run has started again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartProcessingSnapshot"}}}},"404":{"description":"No such cart, or no run to resume"},"409":{"description":"The run is still working, or the run id is stale"}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/chat/messages":{"get":{"tags":["Chat"],"summary":"Read the conversation with the travel assistant","description":"\nThe traveller's conversation, oldest first - the same one they have in any messenger they have\ntalked to us on.\n\nPoll this after sending a message. `replying` is true while the assistant is working, and the\nmessages it produces appear here as they are stored. A message the traveller just sent is stored\ntogether with the answer to it, so a client that wants it on screen sooner has to show it\noptimistically.\n\n`since_ts` keeps a poll cheap: pass the `ts` of the last message you already have.\n\nAn `action` appears once, on the first read after the assistant searched something: it carries\nthe criteria it searched, for the client to open its own results on. It is cleared as it is read.\n","operationId":"getChatMessages","parameters":[{"name":"since_ts","in":"query","description":"Return only what was stored after this Unix epoch second. Omit it for the whole\nconversation.","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"The conversation, oldest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatHistoryResponse"},"example":{"messages":[{"role":"user","text":"Find me a hotel in Lisbon","ts":1757419200},{"role":"agent","text":"For which nights?","ts":1757419207}],"replying":false}}}},"500":{"description":"The conversation could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to load the conversation"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"post":{"tags":["Chat"],"summary":"Send a message to the travel assistant","description":"\nHands a message to the assistant and returns immediately.\n\nThe answer is not part of this response: the assistant works in the background, and both the\nmessage and everything it replies land in the conversation read with\n`GET /api/v1/chat/messages`. Poll that while `replying` is true.\n\nThe conversation is the traveller's, not this client's - the same history is what they see in\nany messenger they have talked to us on, and an answer that ends up somewhere else in the\nproduct (a search result, an offer in the cart) still belongs to it.\n","operationId":"sendChatMessage","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendChatMessageRequest"}}},"required":true},"responses":{"202":{"description":"The message was accepted and is being worked on."},"400":{"description":"The message is empty or too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"text must not be empty"}}}},"500":{"description":"The message could not be accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to accept the message"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"delete":{"tags":["Chat"],"summary":"Start the conversation over","description":"\nCloses the current conversation and opens an empty one.\n\nWhat the assistant remembers about the traveller - who they are, what they prefer, the trip they\nare planning - is not part of the conversation and survives this. So does everything already in\ntheir cart: this clears what was said, not what was done.\n\nThe conversation is the traveller's across every channel, so clearing it here clears the one they\nalso see in any messenger they have talked to us on.\n","operationId":"clearChatMessages","responses":{"204":{"description":"The conversation was cleared."},"500":{"description":"The conversation could not be cleared.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to clear the conversation"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/companions":{"get":{"tags":["Companion"],"summary":"List the traveller's saved companions","description":"\nEveryone the authenticated traveller has saved to travel with, oldest first.\n\nA companion is a reusable set of traveller details - the name, date of birth and, where it was\ncollected, the contact details and travel document. Saving them once means a booking can name a\ncompanion instead of asking for the same passport number on every trip.\n\n### What is filled in\n\nOnly `first_name`, `last_name` and `dob` are ever present. Everything else was optional when the\ncompanion was saved and is left out of the payload when it was never collected, so a companion\nwith no `document` cannot yet be used as a flight passenger - fill it in with\n`PATCH /api/v1/companions/{companion_id}` before checkout.\n","operationId":"listCompanions","responses":{"200":{"description":"The traveller's companions, oldest first. Empty when they have none.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CompanionResponse"}},"example":[{"created_at":"2026-09-01T10:15:00Z","dob":"1990-12-10","email":"ada@example.com","first_name":"Ada","id":"0b6f5a41-2c7d-4e18-9a03-77c2b1e4d905","last_name":"Lovelace","nationality":"GB","updated_at":"2026-09-01T10:15:00Z"}]}}},"500":{"description":"The companions could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to load companions"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"post":{"tags":["Companion"],"summary":"Save a companion","description":"\nSaves someone the authenticated traveller travels with, so their details need not be typed again\non the next booking.\n\n### What is required\n\n`first_name`, `last_name` and `dob` identify the person and are always required. Everything else\ncan be supplied now or filled in later with `PATCH /api/v1/companions/{companion_id}`; a blank\nstring counts as not supplied.\n\n### Before they can fly\n\nA flight passenger needs `gender`, `nationality` and a `document`. A companion saved without them\nis still perfectly usable for a hotel booking - only checkout of a flight will ask for the rest.\n\nCompanions are not deduplicated: saving the same person twice creates two of them.\n","operationId":"createCompanion","requestBody":{"description":"The companion to save.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompanionRequest"},"example":{"dob":"1990-12-10","document":{"document_expiry":"2032-04-18","document_issue_country":"GB","document_number":"X1234567","document_type":"passport"},"email":"ada@example.com","first_name":"Ada","gender":"female","last_name":"Lovelace","nationality":"GB","phone":"+1 415 555 0134"}}},"required":true},"responses":{"201":{"description":"The companion as saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanionResponse"}}}},"400":{"description":"A field did not pass validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"email is not a valid email address"}}}},"500":{"description":"The companion could not be saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to save the companion"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/companions/{companion_id}":{"delete":{"tags":["Companion"],"summary":"Forget a companion","description":"\nRemoves one companion of the authenticated traveller for good. Nothing else is touched: bookings\nalready made for that person keep the details they were made with, because those were copied onto\nthe booking when it was created rather than read back from here.\n\nDeleting a companion that is not the caller's, or one that never existed, answers `404` alike - a\nguessed id cannot confirm that someone else has it.\n","operationId":"deleteCompanion","parameters":[{"name":"companion_id","in":"path","description":"The companion's `id`.","required":true,"schema":{"type":"string"},"example":"0b6f5a41-2c7d-4e18-9a03-77c2b1e4d905"}],"responses":{"204":{"description":"Deleted. The response has no body."},"400":{"description":"`companion_id` is not a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"companion_id must be a UUID"}}}},"404":{"description":"No such companion for this traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Companion not found"}}}},"500":{"description":"The companion could not be deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to delete the companion"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]},"patch":{"tags":["Companion"],"summary":"Change a saved companion","description":"\nEdits one companion of the authenticated traveller. Every field is optional and independent, so a\nform that collects only a passport can send only the `document`.\n\n### Omitted, null, and set\n\n| sent | effect |\n|---|---|\n| field omitted | left exactly as stored |\n| field sent as `null` | cleared |\n| field sent with a value | replaced |\n\nThat is the difference between `{\"phone\": null}` and `{}`. `first_name`, `last_name` and `dob`\nidentify the person and cannot be cleared - send a value or leave them out.\n\nA blank string is read as `null`, so a form that submits its empty fields clears them rather than\nsaving whitespace.\n","operationId":"updateCompanion","parameters":[{"name":"companion_id","in":"path","description":"The companion's `id`.","required":true,"schema":{"type":"string"},"example":"0b6f5a41-2c7d-4e18-9a03-77c2b1e4d905"}],"requestBody":{"description":"The fields to change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCompanionRequest"},"example":{"document":{"document_expiry":"2032-04-18","document_issue_country":"GB","document_number":"X1234567","document_type":"passport"},"phone":"+1 415 555 0134"}}},"required":true},"responses":{"200":{"description":"The companion as it now stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanionResponse"}}}},"400":{"description":"`companion_id` is not a UUID, or a field did not pass validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"email is not a valid email address"}}}},"404":{"description":"No such companion for this traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Companion not found"}}}},"500":{"description":"The companion could not be updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to update the companion"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/flight/search":{"post":{"tags":["Flight"],"summary":"Search flight availability","description":"\nShops live availability for one itinerary and returns every priceable option, each with its full\nflight detail, all-in price and fare conditions.\n\n### Describing the trip\n\n`slices` is the trip, one entry per leg, in the order they are flown. The count is the trip type —\nthere is no separate field for it:\n\n| `slices` | trip |\n|---|---|\n| 1 | one-way |\n| 2 | return |\n| 3+ | multi-city |\n\nEvery leg is priced together as one ticket, which is not the same as buying them separately and\nusually cheaper. Legs must be ordered by `departure_date`.\n\n`origin` and `destination` are IATA codes — either one airport (`LHR`) or a metro code standing for\nall the airports of a city (`LON`, `NYC`, `PAR`). Metro codes shop every airport they cover at once,\nso use them where the traveller does not care which one they fly from.\n`GET /api/v1/airport/autocomplete` turns what a traveller types into exactly these codes; its\n`codes` field is what to send here.\n\nDates are local to the origin, and a date in the past is rejected rather than rolled forward.\n\n### Occupancy\n\n`passengers` has one entry per traveller, so three adults are three entries. `age` is what drives\nchild and infant fares and is required for both — an `infant` is a lap infant, a `child` occupies a\nseat. Adults need no age.\n\n### Defaults applied when a field is omitted\n\n`slices` and `passengers` are the only required fields. Everything else can be left out:\n\n| Field | Default |\n|---|---|\n| `cabin_class` | `economy` |\n| `max_connections` | connections are not capped |\n| `currency` | whatever currency each airline sells in |\n| `airline_filter` | every airline |\n\n### Narrowing the search\n\n* `cabin_class` shops one cabin for the whole trip. Where an airline cannot sell that cabin on a\n  leg it prices the nearest one it can, so read `slices[].segments[].cabin_class` off the result\n  rather than assuming the request was honoured on every flight.\n* `max_connections` is per leg. `0` means direct flights only.\n* `airline_filter` narrows the results to, or away from, named airlines. Both of its lists are\n  optional — send `include_only` to shop nothing else, `exclude` to drop a few carriers, and\n  `include_only` wins if you send both. Airlines are named by their **two-letter IATA code**\n  (`BA`, `AF`, `LH`, `F9`), never by name, and the code is matched against the marketing carrier —\n  the airline whose code is on the ticket, which on a codeshare is not the one operating the\n  flight.\n* `currency` prices every offer in one ISO 4217 currency. Omit it and each airline prices in its\n  own, which leaves the offers not comparable to each other.\n\nThere is no price, duration or baggage filter: shop first, then narrow what came back on\n`total_amount`, `slices[].duration` and `slices[].baggages`.\n\n### Reading the response\n\n* `search_id` plus an `offers[].id` is the handle for the rest of the funnel — pass both to\n  `POST /api/v1/flight/search/{search_id}/offer/{offer_id}/select` to move an offer into booking. The pair stays resolvable for\n  **15 minutes**; after that the search has to be re-run.\n* `total_amount` is the all-in price for every traveller and every leg, in `currency`.\n  `base_amount` is the fare before tax and `tax_amount` the part broken out as tax, when the\n  airline breaks it out at all.\n* `expires_at` is when the airline stops honouring the price — it is usually well inside the\n  15-minute window, and a booking attempted after it will be re-priced.\n* `slices[]` are the legs in the order requested, and `slices[].segments[]` the individual flights\n  within one. More than one segment means the leg connects; `stops[]` inside a segment is a\n  technical stop, where the aircraft lands under the same flight number and nobody changes planes.\n* `departing_at` and `arriving_at` are local times without an offset, so an arrival can read as\n  earlier than its departure on a westbound flight. Use `duration` (minutes) for elapsed time.\n* `marketing_carrier` is the code on the ticket, `operating_carrier` who actually flies it. They\n  differ on a codeshare, and it is the marketing carrier a traveller booked with.\n* `airports` resolves every IATA code appearing anywhere in the response to a name; a code missing\n  from it has no reference data, so render the bare code.\n* `conditions.refund_before_departure` / `change_before_departure` carry the fare rules. An\n  absent one means the airline told us nothing — which is not the same as \"not allowed\". The same\n  goes for `segments[].aircraft`.\n* `baggages` is what the fare includes per traveller on that leg. An absent `bag_type` means none\n  of that kind is included, so a leg with no `checked` entry is a hand-baggage-only fare.\n\n### Limits and failure modes\n\n* Results are **not** paginated and are returned in no particular order — sort them yourself.\n* Shopping fans out to every enabled provider and waits for all of them, so a call can take several\n  seconds. Allow a generous client timeout.\n* A search that matches nothing is a success: it answers `200` with an empty `offers` array.\n  A `500` means the search itself could not be run — every provider errored out, or none is\n  enabled — and is worth retrying.\n","operationId":"searchFlights","requestBody":{"description":"The legs to price, who is travelling and any filters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightSearchRequest"},"examples":{"Multi-city, one airline":{"summary":"Three legs on a single ticket","description":"`airline_filter.include_only` keeps the whole trip on one carrier, which is what makes the legs protected against each other.","value":{"airline_filter":{"include_only":["BA"]},"cabin_class":"business","passengers":[{"passenger_type":"adult"}],"slices":[{"departure_date":"2026-11-05","destination":"CDG","origin":"LHR"},{"departure_date":"2026-11-09","destination":"FCO","origin":"CDG"},{"departure_date":"2026-11-14","destination":"LHR","origin":"FCO"}]}},"One-way, one adult":{"summary":"Minimal request — required fields only","description":"Economy, unlimited connections, priced in whatever currency the airline sells in.","value":{"passengers":[{"passenger_type":"adult"}],"slices":[{"departure_date":"2026-11-05","destination":"JFK","origin":"LHR"}]}},"Return, family, direct only":{"summary":"Return trip with children, filtered","description":"Two adults and a child, direct flights in premium economy, priced in EUR.","value":{"cabin_class":"premium_economy","currency":"EUR","max_connections":0,"passengers":[{"passenger_type":"adult"},{"passenger_type":"adult"},{"age":9,"passenger_type":"child"}],"slices":[{"departure_date":"2026-11-05","departure_time_after":"08:00:00","destination":"JFK","origin":"LON"},{"departure_date":"2026-11-12","destination":"LON","origin":"JFK"}]}}}}},"required":true},"responses":{"200":{"description":"Priceable itineraries for the requested trip. `offers` is empty when nothing is available.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightSearchResultExt"},"example":{"airlines_available":["BA"],"airports":{"JFK":{"city_name":"New York","name":"John F. Kennedy International Airport"},"LHR":{"city_name":"London","name":"London Heathrow Airport"}},"offers":[{"base_amount":"300.00","conditions":{"change_before_departure":{"allowed":true,"penalty_amount":"50.00","penalty_currency":"USD"},"refund_before_departure":{"allowed":true,"penalty_amount":"100.00","penalty_currency":"USD"}},"currency":"USD","expires_at":"2026-11-01T10:00:00Z","id":"6d4b1c77-2f90-4a11-b0c8-7e5a9d3f1002","owner":{"iata_code":"BA","logo_url":"https://cdn.example.com/airlines/ba.png","name":"British Airways"},"passenger_identity_documents_required":false,"passengers":[{"age":null,"passenger_type":"adult"}],"slices":[{"baggages":[{"bag_type":"carry_on","quantity":1}],"conditions":{"advance_seat_selection":null,"priority_boarding":null,"priority_check_in":null},"destination":"JFK","duration":465,"fare_brand_name":"Economy","origin":"LHR","segments":[{"aircraft":{"iata_code":"777","name":"Boeing 777"},"arriving_at":"2026-11-05T13:45:00","cabin_class":"economy","departing_at":"2026-11-05T10:00:00","destination":"JFK","destination_terminal":"7","duration":465,"fare_basis_code":null,"marketing_carrier":{"iata_code":"BA","logo_url":null,"name":"British Airways"},"marketing_carrier_flight_number":"117","operating_carrier":{"iata_code":"BA","logo_url":null,"name":"British Airways"},"operating_carrier_flight_number":"117","origin":"LHR","origin_terminal":"5","stops":[]}]}],"tax_amount":"50.00","total_amount":"350.00"}],"search_id":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f"}}}},"400":{"description":"The request body is malformed, a leg is missing or misordered, a departure date has passed, or nobody is travelling.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"departure_date must not be in the past"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"500":{"description":"Shopping could not be run: every provider failed, or none is enabled. An empty result is not an error — it comes back as `200` with no offers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Flight search failed"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/flight/search/{search_id}/offer/{offer_id}/select":{"post":{"tags":["Flight"],"summary":"Put a shopped flight offer in the cart","description":"\nHolds one itinerary for the authenticated traveller and puts it in their cart, where it waits\nuntil it is checked out or removed. Nothing is booked and nothing is charged by this call.\n\n### Why both ids\n\nA flight offer exists only inside the search that produced it. Airlines hand out no retrievable id\nfor a shopped fare — the price and the itinerary are a snapshot of one response — so the offer is\nresolved out of that search rather than fetched on its own. Send the `search_id` from\n`POST /api/v1/flight/search` together with the `id` of the entry in its `offers` array.\n\nThis is the one place the flight funnel differs from the hotel one, where an offer id stands on\nits own.\n\nSearches are kept for **15 minutes**. After that both ids mean nothing and the call answers `404`:\nre-shop and select from the fresh result. It is also a `404` when the search belongs to somebody\nelse — a search is only selectable by the traveller it was run for.\n\n### What comes back\n\nA cart item. Its `id` is a **booking id, not the offer id** — that is the handle the rest of the\nfunnel uses, and the offer id stops mattering the moment the item exists.\n\n`status` is `pending`: held, unpaid and not ticketed. Selecting reserves nothing with the airline,\nand the fare is revalidated only at checkout — `expires_at` on the offer says how long the airline\nstood behind the price.\n\n### Selecting twice\n\nThe call is idempotent per itinerary. Selecting an offer whose flights and departure times the\ncart already holds returns the existing item with `200` instead of a second one with `201`, so a\nretried request cannot quietly duplicate a booking. Selections are serialised per traveller, so\ntwo requests racing each other — a double click — settle the same way rather than both inserting;\none that arrives while another is still being processed answers `409`.\n\nOffers that fly the same route at different times are two items.\n\n### Which cart it lands in\n\nWithout `cart_id` the offer goes into the traveller's **current** cart, and one is opened for them\nif they have none. Send `cart_id` to put it in a particular cart instead — that is how several\nselections are made to belong to one trip.\n\nA `cart_id` that is not this traveller's answers `404`, like an offer that never existed. One that\nhas already started checking out answers `409`: a cart being paid for takes no new items, so open\na new cart or wait for that payment to settle.\n\nRead the whole cart back with `GET /api/v1/cart`, list them all with `GET /api/v1/carts`, and drop\nan item with `DELETE /api/v1/cart/{item_type}/{item_id}`.\n","operationId":"selectFlightOffer","parameters":[{"name":"cart_id","in":"query","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"search_id","in":"path","description":"`search_id` of the flight search the offer came from.","required":true,"schema":{"type":"string"},"example":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f"},{"name":"offer_id","in":"path","description":"`id` of the entry in that search's `offers`.","required":true,"schema":{"type":"string"},"example":"6d4b1c77-2f90-4a11-b0c8-7e5a9d3f1002"}],"responses":{"200":{"description":"The cart already held this itinerary; the existing item is returned untouched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightCartItem"}}}},"201":{"description":"The offer is now in the cart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightCartItem"},"example":{"created_at":"2026-09-01T10:22:00Z","currency":"USD","id":"9d2f4c81-3ab7-4e05-8c19-6f7a0b3d5e21","owner_name":"British Airways","passengers_count":1,"slices":[{"baggages":[{"bag_type":"carry_on","quantity":1}],"conditions":{"advance_seat_selection":null,"priority_boarding":null,"priority_check_in":null},"destination":"JFK","duration":465,"fare_brand_name":"Economy","origin":"LHR","segments":[{"arriving_at":"2026-11-05T13:45:00","cabin_class":"economy","departing_at":"2026-11-05T10:00:00","destination":"JFK","duration":465,"marketing_carrier":{"iata_code":"BA","logo_url":null,"name":"British Airways"},"marketing_carrier_flight_number":"117","operating_carrier":{"iata_code":"BA","logo_url":null,"name":"British Airways"},"operating_carrier_flight_number":"117","origin":"LHR","stops":[]}]}],"status":"pending","total_amount":"350.00","updated_at":"2026-09-01T10:22:00Z"}}}},"404":{"description":"The search has expired, the offer is not in it, or the search belongs to another traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Offer not found or expired"}}}},"409":{"description":"Another selection for this traveller is still being processed, or the named cart is checking out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Another selection for this traveller is in progress"}}}},"500":{"description":"The offer could not be put in the cart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to select flight offer"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/hotel/offer/{offer_id}/select":{"post":{"tags":["Hotel"],"summary":"Put a shopped hotel offer in the cart","description":"\nHolds one offer for the authenticated traveller and puts it in their cart, where it waits until it\nis checked out or removed. Nothing is booked and nothing is charged by this call.\n\n### Which offers can be selected\n\nAny `offer_id` this traveller was given by `POST /api/v1/hotel/search` or\n`GET /api/v1/hotel/rates/{offer_id}` — the two return ids from the same pool, so a rate opened\nfrom a property listing is selectable exactly like the lead rate that surfaced it.\n\nOffers are search-scoped and expire with the search that produced them. A `404` means the offer is\ngone rather than that it never existed: re-shop and select the fresh id. It is also a `404` when\nthe offer belongs to somebody else — an id is only selectable by the traveller it was shopped for.\n\n### What comes back\n\nA cart item. Its `id` is a **booking id, not the offer id** — that is the handle the rest of the\nfunnel uses, and the offer id stops mattering the moment the item exists.\n\n`status` is `pending`: held, unpaid, not confirmed with the property. Selecting does not hold\ninventory at the provider either, so an offer can still be gone at checkout.\n\n### Selecting twice\n\nThe call is idempotent per stay. Selecting an offer for a property and date range the cart already\nholds returns the existing item with `200` instead of a second one with `201`, so a retried\nrequest cannot quietly duplicate a booking. Selections are serialised per traveller, so two\nrequests racing each other — a double click — settle the same way rather than both inserting; one\nthat arrives while another is still being processed answers `409`. Two genuinely different stays at the same property —\ndifferent dates — are two items.\n\n### Which cart it lands in\n\nWithout `cart_id` the offer goes into the traveller's **current** cart, and one is opened for them\nif they have none. Send `cart_id` to put it in a particular cart instead — that is how several\nselections are made to belong to one trip.\n\nA `cart_id` that is not this traveller's answers `404`, like an offer that never existed. One that\nhas already started checking out answers `409`: a cart being paid for takes no new items, so open\na new cart or wait for that payment to settle.\n\nRead the whole cart back with `GET /api/v1/cart`, and list them all with `GET /api/v1/carts`.\n","operationId":"selectHotelOffer","parameters":[{"name":"cart_id","in":"query","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"offer_id","in":"path","description":"Offer id from hotel search or hotel rates.","required":true,"schema":{"type":"string"},"example":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f"}],"responses":{"200":{"description":"The cart already held this stay; the existing item is returned untouched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelCartItem"}}}},"201":{"description":"The offer is now in the cart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelCartItem"},"example":{"checkin":"2026-11-05","checkout":"2026-11-08","created_at":"2026-09-01T10:15:00Z","currency":"USD","hotel_id":"nuitee:lp1f2a9","hotel_name":"Grand Luxury Hotel & Spa","id":"b7c1e2a4-5f60-4d31-9a8e-2c4b6d8f0a12","provider":"nuitee","room_types":"1 × Deluxe King","status":"pending","total_amount":"1284.00","updated_at":"2026-09-01T10:15:00Z"}}}},"404":{"description":"The offer has expired, was never shopped, or belongs to another traveller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Offer not found or expired"}}}},"409":{"description":"Another selection for this traveller is still being processed, or the named cart is checking out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Another selection for this traveller is in progress"}}}},"500":{"description":"The offer could not be put in the cart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to select hotel offer"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/hotel/rates/{offer_id}":{"get":{"tags":["Hotel"],"summary":"List every rate of a shopped property","description":"\nReturns the property behind an offer together with **all** of its bookable rates, not just the\nlead one that search returned.\n\n### When to call it\n\nSearch keeps its response small, and some providers answer it with a single rate per property.\nThose properties come back with `more_rates_available: true` — that flag is the signal to call\nthis endpoint, typically when the guest opens the property to compare rooms.\n\nProperties with `more_rates_available: false` already carried everything their provider has, and\nthis endpoint answers `404` for them. Calling it blindly for every property in a result set just\nbuys you a page of 404s.\n\n### Identifying the property\n\nThere is no property id in the path: pass any `offer_id` from that property's `offers` array and\nthe rates for the property behind it come back. The stay dates, occupancy, currency and guest\nnationality of the original search are carried by the offer, so nothing needs restating — which\nalso means you cannot change them here. To search a different stay, shop again.\n\nOffer ids are search-scoped and go stale as availability moves. A `404` on an id that worked\nminutes ago means the search behind it has expired; re-shop rather than retrying.\n\n### What comes back\n\nOne property, shaped exactly like a single entry of the `hotels` array from search: the same\n`hotel` object and a longer `offers` list. Pricing, cancellation rules, `flags` and\n`free_cancellation` all read the same way as they do there, so the same rendering code handles\nboth. `more_rates_available` is `false` in this response — this *is* the full list.\n\n### Language\n\n`?language=` sets the language of what we render ourselves: the `cancellation_policy_description`\nsummary and the normalised `amenity_keys`. Supported values are `de`, `en`, `es`, `fr`, `it`,\n`ru`, `uk` and `zh`; anything else falls back to English instead of failing.\n\nProperty and room texts are not re-translated here — they come from the provider in the language\nthe original search ran in, so pass `language` to the search itself if you need those localised.\n","operationId":"getHotelRates","parameters":[{"name":"offer_id","in":"path","description":"Any `offer_id` from the property's `offers` array in a search response.","required":true,"schema":{"type":"string"},"example":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f"},{"name":"language","in":"query","description":"Language of the text rendered on our side: one of `de`, `en`, `es`, `fr`, `it`, `ru`,\n`uk`, `zh`. Anything else, including an absent value, is served in English.","required":false,"schema":{"type":"string"},"example":"en"}],"responses":{"200":{"description":"The property with its complete list of bookable rates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelOffersExt"},"example":{"hotel":{"address":{"city":"New York","country":"United States","postal_code":"10001","street":"123 5th Avenue"},"amenities":["Free WiFi","Swimming Pool","Spa & Wellness Center"],"amenity_keys":["free_wifi","pool","spa"],"description":"Five-star property on 5th Avenue, a block from Madison Square Park.","geo_location":{"latitude":40.7127837,"longitude":-74.0059413},"id":"sabre:100234","images":["https://cdn.example.com/hotels/100234/1.jpg"],"name":"Grand Luxury Hotel & Spa","rating":"8.2","rating_cnt":300,"star_rating":5,"tz":"America/New_York"},"more_rates_available":false,"offers":[{"cancellation_policy":{"rules":[{"from":"2026-10-01T00:00:00-04:00","penalty":{"type":"free"}}],"timezone":"America/New_York"},"cancellation_policy_description":"Free cancellation until 3 Nov, 14:00","checkin":"2026-11-05","checkout":"2026-11-08","flags":1,"free_cancellation":true,"offer_id":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f","pricing":{"currency":"USD","taxes":[],"total":"1284.00"},"rooms":[{"adults":2,"area_sqft":344,"bedding":"1 king bed","cancellation_policy":{"rules":[{"from":"2026-10-01T00:00:00-04:00","penalty":{"type":"free"}}],"timezone":"America/New_York"},"capacity":2,"children_ages":null,"flags":1,"pricing":{"currency":"USD","taxes":[],"total":"1284.00"},"rate_plan":"Bed & Breakfast","room_amenities":["Free WiFi","Minibar"],"room_id":"DLX-K","room_images":[],"room_type":"Deluxe King","view":"Park view"}]},{"cancellation_policy":{"rules":[{"from":null,"penalty":{"type":"full"}}],"timezone":"America/New_York"},"cancellation_policy_description":"Non-refundable","checkin":"2026-11-05","checkout":"2026-11-08","flags":9,"free_cancellation":false,"offer_id":"b71c0e94-0f52-4a6d-9a2f-3d8e5c1b7a44","pricing":{"currency":"USD","taxes":[],"total":"1980.00"},"rooms":[{"adults":2,"area_sqft":592,"bedding":"1 king bed and 1 sofa bed","cancellation_policy":{"rules":[{"from":null,"penalty":{"type":"full"}}],"timezone":"America/New_York"},"capacity":3,"children_ages":null,"flags":9,"pricing":{"currency":"USD","taxes":[],"total":"1980.00"},"rate_plan":"All Inclusive, Non-refundable","room_amenities":["Free WiFi","Minibar","Lounge access"],"room_id":"EXE-STE","room_images":[],"room_type":"Executive Suite","view":null}]}]}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"404":{"description":"No offer with this id, or its provider has no rates beyond the ones search already returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"No additional rates for this offer"}}}},"500":{"description":"The provider could not be reached or answered with something unusable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Failed to load hotel rates"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/hotel/search":{"post":{"tags":["Hotel"],"summary":"Search hotel availability","description":"\nShops live availability for one stay and returns every matching property together with its\nbookable room offers, all-in pricing and cancellation terms.\n\n### Where the search runs\n\n`latitude` / `longitude` are the centre of the search, and only properties within `radius_km` of\nthat point are considered. Resolve the place in your own UI — a map pin, a geocoded search box —\nand send the point.\n\nThe one exception is `requested_hotel_name`: because a property name is not a coordinate, it is\ngeocoded, and the point it resolves to wins over the one you sent. Your coordinates remain the\nfallback for when that lookup finds nothing, so keep sending a sensible pair.\n\n### Defaults applied when a field is omitted\n\n| Field | Default |\n|---|---|\n| `radius_km` | `5` (kilometres) |\n| `guest_nationality` | `US` |\n| `currency` | `USD` |\n| `language` | `en` |\n\n`language` is fully supported for `de`, `en`, `es`, `fr`, `it`, `ru`, `uk` and `zh` — those\nlocalise both the provider's property texts and the strings we render ourselves (cancellation\nsummaries, amenity keys). Other ISO 639-1 codes are still passed to providers that accept them,\nso property texts may arrive localised while our own strings stay English. Nothing unsupported\nfails the request; it falls back to English.\n\n`guest_nationality` matters for pricing: many rates are nationality-restricted.\n\n### Occupancy\n\n`rooms` describes the physical rooms you need — one entry per room. Every returned offer is\npriced for the whole party, so a two-entry `rooms` array yields offers that already contain two\nrooms in `offers[].rooms[]`. `children_ages` must list one age per child; ages drive both\neligibility and price, and an empty array means no children.\n\n### Reading the response\n\n* `hotels[].offers[].offer_id` is the handle for the rest of the funnel. Pass it to\n  `GET /api/v1/hotel/rates/{offer_id}` for the property's full rate list and to\n  `POST /api/v1/hotel/offer/{offer_id}/select` to move the offer into booking. Offer ids are search-scoped —\n  re-shop instead of storing them long term.\n* `pricing.total` is the all-in amount for the entire stay and all requested rooms, in\n  `pricing.currency`. `taxes[]` breaks that down; check `included_in_price` on each entry to see\n  whether it is already inside `total` or payable at the property.\n* `suggested_selling_price` is only present when it is above `total`, i.e. when the rate leaves\n  a margin to display as a strike-through price.\n* `cancellation_policy.rules[]` is ordered by `from` in the property's `timezone`. `free_cancellation`\n  and the ready-to-render `cancellation_policy_description` are derived from those rules in the\n  requested language.\n* `flags` carries the board and lounge benefits — see *Board and benefits* below.\n* `amenity_keys` are the normalised, language-independent counterparts of the free-text\n  `amenities` list — match on those (`free_wifi`, `pool`, `gym`, …), not on the display strings.\n* `more_rates_available` marks properties whose provider returned only a lead rate. For those,\n  `GET /api/v1/hotel/rates/{offer_id}` returns the property with its full list of offers,\n  shaped like one entry of `hotels`; for the rest that call answers `404`, because search already\n  carried everything.\n\n### Filtering\n\n`hotel_star_ratings` matches half steps, so `4` also returns 4.5-star properties.\n\n`amenities` is an AND: a property has to offer every amenity you list. Providers that can filter\non their side do so; for the rest the property's own facility list is matched instead, so the\nfilter bites either way. The accepted values are enumerated on the field itself — they are\ncase-sensitive identifiers such as `FreeWiFi` or `SwimmingPool`, not free text.\n\nBoth filters narrow real inventory, so an over-constrained search comes back empty rather than\nmerely short.\n\nBoard and refundability are not request filters: shop first, then narrow what came back using\n`offers[].flags` and `offers[].free_cancellation`, both of which are returned for exactly that.\n\n### Board and benefits\n\n`offers[].flags` is a bitmask. Each room carries its own `flags` too, which matter when an offer\ncovers several rooms on different rate plans.\n\n| Bit | Value | Meaning |\n|---|---|---|\n| 0 | `1` | Breakfast |\n| 1 | `2` | Lunch |\n| 2 | `4` | Dinner |\n| 3 | `8` | All inclusive |\n| 4 | `16` | Lounge access |\n\nThree things to know before you filter on it:\n\n* **Test bits, never equality.** Benefits combine: a rate with breakfast and lounge access is\n  `17`, so `flags === 1` silently drops it.\n* **All-inclusive does not set the meal bits.** It is mapped to `8` on its own, so a \"breakfast\n  included\" filter has to accept `1` *or* `8` — otherwise the most generous rates disappear from\n  your results.\n* **`null` is not `0`.** `0` means the rate explicitly includes no meals (room only); `null` means\n  the provider told us nothing about board. Treat `null` as unknown, not as \"no breakfast\".\n\n```js\nconst BREAKFAST = 1, ALL_INCLUSIVE = 8;\nconst withBreakfast = result.hotels\n  .flatMap((h) => h.offers)\n  .filter((offer) => (offer.flags ?? 0) & (BREAKFAST | ALL_INCLUSIVE));\n```\n\nRefundable rates work the same way, minus the arithmetic: keep the offers whose\n`free_cancellation` is `true`, and show `cancellation_policy_description` for the rendered terms.\n\n### Limits and failure modes\n\n* Results are capped per provider and are **not** paginated.\n  Tighten `radius_km` or the filters to bring the most relevant properties into that window.\n* Shopping fans out to every enabled provider and waits for all of them, so a call can take\n  several seconds. Allow a generous client timeout.\n* A search that matches nothing is a success: it answers `200` with an empty `hotels` array.\n  A `500` means the search itself could not be run — every provider errored out, or none is\n  enabled — and is worth retrying.\n","operationId":"searchHotels","requestBody":{"description":"Stay dates, occupancy and optional filters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelSearchRequest"},"examples":{"Couple, one room":{"summary":"Minimal request — required fields only","description":"Everything else falls back to the documented defaults: 5 km radius, US nationality, USD, English.","value":{"check_in":"2026-11-05","check_out":"2026-11-08","latitude":40.7075,"longitude":-74.0113,"rooms":[{"adults":2,"children":0,"children_ages":[]}]}},"Family, two rooms, filtered":{"summary":"Two rooms with children, filters and localisation","description":"Four-star-and-up properties with a pool and free WiFi, priced in EUR and described in German.","value":{"amenities":["SwimmingPool","FreeWiFi"],"check_in":"2026-11-05","check_out":"2026-11-08","currency":"EUR","guest_nationality":"DE","hotel_star_ratings":[4,5],"language":"de","latitude":41.3874,"longitude":2.1686,"radius_km":3.0,"rooms":[{"adults":2,"children":2,"children_ages":[5,9]},{"adults":1,"children":0,"children_ages":[]}]}},"One named property":{"summary":"Availability for a specific hotel","description":"`requested_hotel_name` is geocoded and matched leniently against provider inventory, which narrows the search onto that property.","value":{"check_in":"2026-11-05","check_out":"2026-11-06","latitude":48.8566,"longitude":2.3522,"radius_km":1.0,"requested_hotel_name":"Hotel Lutetia","rooms":[{"adults":1,"children":0,"children_ages":[]}]}}}}},"required":true},"responses":{"200":{"description":"Matching properties with their bookable offers. `hotels` is empty when nothing is available for the requested stay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelSearchResultExt"},"example":{"hotels":[{"hotel":{"address":{"city":"New York","country":"United States","postal_code":"10001","street":"123 5th Avenue"},"amenities":["Free WiFi","Swimming Pool","Spa & Wellness Center","Fitness Center"],"amenity_keys":["free_wifi","pool","spa","gym"],"description":"Five-star property on 5th Avenue, a block from Madison Square Park.","geo_location":{"latitude":40.7127837,"longitude":-74.0059413},"id":"nuitee:lp1f2a9","images":["https://cdn.example.com/hotels/lp1f2a9/1.jpg"],"name":"Grand Luxury Hotel & Spa","rating":"8.2","rating_cnt":300,"star_rating":5,"tz":"America/New_York"},"more_rates_available":true,"offers":[{"cancellation_policy":{"rules":[{"from":"2026-10-01T00:00:00-04:00","penalty":{"type":"free"}},{"from":"2026-11-03T14:00:00-05:00","penalty":{"currency":"USD","type":"amount","value":"428.00"}},{"from":"2026-11-05T00:00:00-05:00","penalty":{"type":"full"}}],"timezone":"America/New_York"},"cancellation_policy_description":"Free cancellation until 3 Nov, 14:00","checkin":"2026-11-05","checkout":"2026-11-08","flags":1,"free_cancellation":true,"offer_id":"0f8e6a3c-6b1e-4f2a-9f1d-1c2b3a4d5e6f","pricing":{"currency":"USD","suggested_selling_price":"1420.00","taxes":[{"amount":"112.35","description":"Sales tax","included_in_price":true,"mandatory":true,"percentage":"8.875","tax_type":"VAT"},{"amount":"24.00","description":"City tax, payable at the property","included_in_price":false,"mandatory":true,"percentage":null,"tax_type":"CITY_TAX"}],"total":"1284.00"},"rooms":[{"adults":2,"area_sqft":344,"bedding":"1 king bed","cancellation_policy":{"rules":[{"from":"2026-10-01T00:00:00-04:00","penalty":{"type":"free"}}],"timezone":"America/New_York"},"capacity":2,"children_ages":null,"description":"32 m² room with a king bed and park view.","flags":1,"pricing":{"currency":"USD","taxes":[],"total":"1284.00"},"rate_plan":"Bed & Breakfast","room_amenities":["Free WiFi","Minibar","Safe"],"room_id":"DLX-K","room_images":["https://cdn.example.com/hotels/lp1f2a9/rooms/deluxe-king.jpg"],"room_type":"Deluxe King","view":"Park view"}]},{"cancellation_policy":{"rules":[{"from":null,"penalty":{"type":"full"}}],"timezone":"America/New_York"},"cancellation_policy_description":"Non-refundable","checkin":"2026-11-05","checkout":"2026-11-08","flags":0,"free_cancellation":false,"offer_id":"6d4b1c77-2f90-4a11-b0c8-7e5a9d3f1002","pricing":{"currency":"USD","taxes":[],"total":"1098.00"},"rooms":[{"adults":2,"area_sqft":344,"bedding":"1 king bed","cancellation_policy":{"rules":[{"from":null,"penalty":{"type":"full"}}],"timezone":"America/New_York"},"capacity":2,"children_ages":null,"flags":0,"pricing":{"currency":"USD","taxes":[],"total":"1098.00"},"rate_plan":"Room Only, Non-refundable","room_amenities":["Free WiFi","Minibar","Safe"],"room_id":"DLX-K","room_images":[],"room_type":"Deluxe King","view":null}]}]}]}}}},"400":{"description":"The request body is malformed, the stay dates are not a valid range, no room was requested, or the coordinates are out of range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"check_in must be before check_out"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"500":{"description":"Shopping could not be run: every provider failed, or none is enabled. An empty result is not an error — it comes back as `200` with no hotels.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Hotel search failed"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}},"/api/v1/place/autocomplete":{"get":{"tags":["Place"],"summary":"Suggest places to search hotels in","description":"\nResolves what a traveller is typing into places they can shop — towns, islands and regions.\nEach suggestion carries a ready-to-use hotel search: pass `latitude`, `longitude` and\n`search_radius_km` straight into `POST /api/v1/hotel/search`.\n\n### What it searches\n\nAround 177,000 places from the [GeoNames](https://www.geonames.org/) catalog, of three\nkinds, told apart by `kind`:\n\n| `kind` | count | what it is |\n|---|---|---|\n| `city` | 170,860 | every populated place with 1000+ inhabitants |\n| `region` | 3,493 | regions and first-order divisions — Tuscany, Algarve, Bavaria |\n| `island` | 2,338 | islands people book by name — Santorini, Bali, Crete, Malta |\n\nWhole countries are deliberately absent as suggestions: a country is not a point a search can\nrun from, and offering one would mean thousands of properties scattered across it.\n\nTyping one still works, though — it is how people ask for \"somewhere in there\". A term that\nmatches a country's name, in any of the eight languages, is answered with that country's\nleading cities: `USA` gives New York, Los Angeles and Washington; `Мальдивы` gives Malé.\nThose rank below anything matching what was typed by name, so `par` still opens with Paris\nrather than Asunción, Paraguay.\n\nThe population threshold is deliberately low, because resorts are where hotel inventory is\ndensest and residents are scarcest: Ischgl has 1233 people and Courchevel 1700. Ranking,\nnot omission, is what keeps the small ones out of the way.\n\n### The point and the radius\n\nWhere to search is not where a place's centre is. GeoNames puts Bali's centre in the\nmountains, 42 km from Denpasar and 52 km from Benoa, so searching that point with any modest\nradius misses the entire resort south.\n\nSo both values are derived from where the place's population actually sits: `latitude` and\n`longitude` are the population-weighted centre of the towns inside it, and\n`search_radius_km` reaches 90% of those people. For a city the point is the city itself and\nthe radius follows its size.\n\n| place | `search_radius_km` |\n|---|---|\n| Ischgl (1233 residents) | 5.0 |\n| Paris | 19.9 |\n| Santorini | 9.0 |\n| Bali | 46.5 |\n| Crete | 83.4 |\n\nThe radius is a recommendation, not a constraint — narrow it for a tighter search, widen it\nto shop a whole area. It is capped at 150 km. Use `kind` to filter out kinds your UI does\nnot want.\n\nOne place is offered once. Where an island and the town on it share a name only the town is\noffered, because that is the half with the hotels — `Mykonos`, `Rhodes` and `Ibiza` all\nresolve to the town; and where the catalog holds a destination as both an island and the\nprovince over it (`Sicily` and `Sicilia`, `Bali` and `Provinsi Bali`), the two are folded\ninto one suggestion.\n\n### Matching\n\nThe term is matched against the city's own name, its ASCII spelling, its translations into\nevery language the API supports and its common transliterations. That is independent of\n`language`: `Munich`, `München`, `Мюнхен`, `Munchen` and `慕尼黑` all resolve to the same\ncity, whatever language you are rendering in. Case, accents and punctuation are ignored, so\n`val d isere` finds `Val-d'Isère`.\n\nMatching is by prefix, which is what makes it usable a keystroke at a time. Terms shorter\nthan three characters are rejected with `400`: one or two letters match too much of the\ncatalog to rank meaningfully. A term that matches no prefix at all falls back to fuzzy\nmatching, so a typo (`Amstrdam`) still returns something rather than nothing.\n\nResults are ordered by how well the name matched and how prominent the place is, so a\none-word term lands on the place the traveller almost certainly meant — `par` gives Paris\nbefore Paramaribo, and before the Cornish village actually called Par. Where a city and a\nregion share a name, the city comes first.\n\n### Airports\n\nWith `include=airport`, airports join the same ranking, for searching around the one a\ntraveller lands at. They are the ones a scheduled flight serves, the same set\n`GET /api/v1/airport/autocomplete` offers, and they come back with `kind` set to `airport`, an\n`iata_code`, a `search_radius_km` of 10, and neither `id` nor `population`.\n\nThey are reached by their names, by their IATA and ICAO codes and by the name of the city they\nserve, but always rank below the city itself: `paris` gives Paris, then Charles de Gaulle and\nOrly; `cdg` gives Charles de Gaulle; `ber` gives Berlin before Berlin Brandenburg. Metro codes\nsuch as `LON` are not offered, because the city they stand for already is.\n\n### Localisation\n\n`language` only decides how suggestions are **labelled**. Where the catalog has a\ntranslation, `name` and `label` come back in that language; where it does not — true for\nmost small places — they fall back to the local spelling. `region` and `country_name` are\nalways in English.\n\n### Using the result\n\n`label` is ready to render as-is (`Paris, Île-de-France, France`); build your own from\n`name`, `region` and `country_name` if you want it differently. `id` is the GeoNames id and\nis stable, so it is safe to store as a user's saved destination. `iata_city_code` is present\nfor the ~5800 settlements that have one, and never for an island or a region.\n","operationId":"autocompletePlaces","parameters":[{"name":"q","in":"query","description":"What the user has typed so far.","required":true,"schema":{"type":"string"}},{"name":"language","in":"query","description":"ISO 639-1 code deciding the language of `name` and `label`. Defaults to the\n`Accept-Language` header, then English.","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","description":"How many suggestions to return, 1–25.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"country","in":"query","description":"ISO 3166-1 alpha-2 code to restrict the search to one country.","required":false,"schema":{"type":["string","null"]}},{"name":"include","in":"query","description":"`airport` to rank airports in with the places. Places only by default.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Matching places, best first. `results` is empty when nothing matched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaceAutocompleteResponse"},"example":{"results":[{"country_code":"FR","country_name":"France","iata_city_code":"PAR","iata_code":null,"id":2988507,"kind":"city","label":"Paris, Île-de-France, France","latitude":48.85341,"longitude":2.3488,"name":"Paris","population":2138551,"region":"Île-de-France","search_radius_km":19.9,"timezone":"Europe/Paris"},{"country_code":"SR","country_name":"Suriname","iata_city_code":"PBM","iata_code":null,"id":3703358,"kind":"city","label":"Paramaribo, Paramaribo District, Suriname","latitude":5.86638,"longitude":-55.16682,"name":"Paramaribo","population":223757,"region":"Paramaribo District","search_radius_km":15.9,"timezone":"America/Paramaribo"},{"country_code":"ID","country_name":"Indonesia","iata_city_code":null,"iata_code":null,"id":1650534,"kind":"island","label":"Bali, Indonesia","latitude":-8.52,"longitude":115.18,"name":"Bali","population":4225384,"region":null,"search_radius_km":46.5,"timezone":"Asia/Makassar"}]}}}},"400":{"description":"The term is shorter than three characters, or `include` is not `airport`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"q must be at least 3 characters"}}}},"401":{"description":"The `X-App-Id` header is missing or malformed, or the bearer token is missing, invalid or expired. The response carries no body."},"403":{"description":"The app id is known but disabled. The response carries no body."},"500":{"description":"The place catalog could not be queried.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":"Place autocomplete failed"}}}}},"security":[{"app_id":[],"bearer_auth":[]}]}}},"components":{"schemas":{"Address":{"type":"object","description":"Postal address of a property.","required":["city","country"],"properties":{"city":{"type":"string","example":"New York"},"country":{"type":"string","example":"United States"},"postal_code":{"type":["string","null"],"example":"10001"},"street":{"type":["string","null"],"example":"123 5th Avenue"}}},"Aircraft":{"type":"object","required":["name","iata_code"],"properties":{"iata_code":{"type":"string","example":"777"},"name":{"type":"string","example":"Boeing 777"}}},"Airline":{"type":"object","description":"An airline, resolved against our reference data where we have it.","required":["iata_code","name"],"properties":{"iata_code":{"type":"string","example":"BA"},"logo_url":{"type":["string","null"]},"name":{"type":"string","description":"Falls back to the IATA code when the carrier is not in our reference data.","example":"British Airways"}}},"AirlineFilter":{"type":"object","description":"Airlines to keep or drop, given as two-letter IATA airline codes — `BA`, `AF`, `LH` — and\nmatched against the marketing carrier, the code on the ticket. Both lists are optional; send\nwhichever one you need. `include_only` wins when both are filled.","properties":{"exclude":{"type":"array","items":{"type":"string"},"description":"Optional. Leave these carriers out. Ignored while `include_only` is non-empty.","example":["F9","NK"]},"include_only":{"type":"array","items":{"type":"string"},"description":"Optional. Shop these carriers only. Omitted or empty means every airline.","example":["BA","AF"]}}},"AirportAutocompleteResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/AirportSuggestion"}}}},"AirportDisplayInfo":{"type":"object","description":"Airport display info for enriching IATA codes with readable names.","required":["name","city_name"],"properties":{"city_name":{"type":"string"},"country_code":{"type":["string","null"],"description":"ISO country the airport sits in, which is what makes an itinerary domestic or not."},"name":{"type":"string"}}},"AirportInfo":{"type":"object","description":"What to call an IATA code.","required":["name"],"properties":{"city_name":{"type":["string","null"],"description":"The city it serves, when known — usually the more recognisable of the two."},"name":{"type":"string","description":"Airport name as the reference data has it, e.g. `John F. Kennedy Intl`."}}},"AirportSuggestion":{"type":"object","description":"One autocomplete result: somewhere a flight can depart from or arrive at.","required":["code","kind","codes","name","label","country_code","country_name","airport_count"],"properties":{"airport_count":{"type":"integer","format":"int32","description":"How many airports `codes` holds.","example":1},"city_name":{"type":["string","null"],"description":"The city served, in the requested language.","example":"London"},"code":{"type":"string","description":"IATA code. For a `city` this is the metro code — `LON`, `NYC`, `PAR`.","example":"LHR"},"codes":{"type":"array","items":{"type":"string"},"description":"What to send as `origin` or `destination`. One code for an airport, its whole\nmetro area for a city, best first.","example":["LHR"]},"country_code":{"type":"string","description":"ISO 3166-1 alpha-2.","example":"GB"},"country_name":{"type":"string","example":"United Kingdom"},"icao_code":{"type":["string","null"],"example":"EGLL"},"kind":{"type":"string","description":"`airport`, or `city` for a metro code standing for several airports at once.","example":"airport"},"label":{"type":"string","description":"Ready to render in a dropdown.","example":"Heathrow Airport (LHR), London, United Kingdom"},"latitude":{"type":["number","null"],"format":"double","example":51.4706},"longitude":{"type":["number","null"],"format":"double","example":-0.461941},"name":{"type":"string","description":"The airport's name in the requested language, falling back to its own name rather\nthan to the city's, so an airport is never labelled with the name of its city.","example":"Heathrow Airport"},"timezone":{"type":["string","null"],"description":"IANA time zone.","example":"Europe/London"},"via":{"$ref":"#/components/schemas/ViaPlace","description":"Set when the term matched a place rather than the airport: the traveller typed\n`Ischgl` and this is Innsbruck, 85 km away. Absent for a direct match."}}},"ApiError":{"type":"object","description":"Error payload returned with every non-2xx response of the public API.","required":["error"],"properties":{"error":{"type":"string","description":"Human readable reason the request was rejected or could not be served.","example":"check_in must be before check_out"}},"example":{"error":"check_in must be before check_out"}},"AssignTravelersRequest":{"type":"object","required":["companion_ids"],"properties":{"companion_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"lead_companion_id":{"type":["string","null"],"format":"uuid"}}},"BagType":{"type":"string","enum":["carry_on","checked"]},"Baggage":{"type":"object","description":"Baggage of one kind included per traveller.","required":["bag_type","quantity"],"properties":{"bag_type":{"$ref":"#/components/schemas/BagType"},"quantity":{"type":"integer","format":"int64"}}},"BookingEntry":{"allOf":[{"$ref":"#/components/schemas/CartItemEnriched"},{"type":"object","required":["booked_at"],"properties":{"booked_at":{"type":"string","format":"date-time"},"confirmation":{"type":["string","null"],"description":"Airline record locator, hotel confirmation code, or the provider's own booking id -\nwhichever this provider issues. Absent while the provider is still settling it."}}}],"description":"One settled reservation: the item exactly as the cart publishes it, plus what only a booking\nhas - the reference the traveller quotes at the desk, and when it was made."},"BookingsResponse":{"type":"object","required":["bookings"],"properties":{"bookings":{"type":"array","items":{"$ref":"#/components/schemas/BookingEntry"},"description":"Newest first."}}},"CabinClass":{"type":"string","description":"Cabin a fare is sold in.","enum":["economy","premium_economy","business","first"]},"CancelProcessingRequest":{"type":"object","properties":{"run_id":{"type":["string","null"],"format":"uuid"}}},"CancellationPenaltySchema":{"type":"object","description":"Schema-only representation of CancellationPenalty for OpenAPI docs.\nThe actual enum uses internal tagging (`{\"type\": \"free\"}`, `{\"type\": \"amount\", ...}`).","required":["type"],"properties":{"currency":{"type":["string","null"],"description":"Currency code (only when type = \"amount\")"},"type":{"type":"string","description":"One of: \"free\", \"full\", \"amount\""},"value":{"type":["string","null"],"description":"Penalty amount (only when type = \"amount\")"}},"example":{"type":"free"}},"CancellationPolicy":{"type":"object","description":"When an offer can still be cancelled, and at what cost.","required":["timezone","rules"],"properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/CancellationRule"},"description":"Rules ordered by `from`; the one in force is the last whose `from` has passed."},"timezone":{"type":"string","description":"IANA timezone every rule boundary is expressed in.","example":"America/New_York"}}},"CancellationQuote":{"type":"object","description":"What cancelling costs, in one shape for every kind of booking.","required":["refundability","cancellable","lines"],"properties":{"cancellable":{"type":"boolean","description":"Whether this booking can be cancelled here at all."},"currency":{"type":["string","null"]},"free_until":{"type":["string","null"],"format":"date-time","description":"Cancel before this and nothing is charged."},"lines":{"type":"array","items":{"type":"string"},"description":"The policy in the traveller's own words, one line per rule."},"note":{"type":["string","null"],"description":"Why it cannot be cancelled, when it cannot."},"penalty_amount":{"type":["string","null"]},"refund_amount":{"type":["string","null"]},"refundability":{"type":"string","description":"`full`, `partial`, `none`, or `unknown` when the provider would not say."}}},"CancellationRule":{"type":"object","description":"What cancelling costs from a point in time onwards.","required":["penalty"],"properties":{"from":{"type":["string","null"],"format":"date-time","description":"RFC 3339 instant this rule takes effect. `null` means it applies from the moment of booking."},"penalty":{"$ref":"#/components/schemas/CancellationPenaltySchema","description":"Charge incurred while this rule is in force."}}},"CarBodyType":{"type":"string","enum":["TwoThreeDoor","TwoFourDoor","FourFiveDoor","Wagon","PassengerVan","Limousine","Sport","Convertible","Suv","OpenAirAllTerrain","Special","PickupRegularCab","PickupExtendedCab","SpecialOffer","Coupe","Monospace","RecreationalVehicle","MotorHome","TwoWheelVehicle","Roadster","Crossover","CommercialVan"]},"CarCartItem":{"type":"object","description":"A car rental in the cart.","required":["id","provider","vendor_code","vehicle_code","vehicle_name","pickup_location_id","dropoff_location_id","pickup_at","dropoff_at","rental_days","total_amount","currency","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"currency":{"type":"string","example":"USD"},"dropoff_at":{"type":"string","format":"date-time","description":"Local time at the dropoff branch."},"dropoff_location_id":{"type":"string"},"id":{"type":"string","format":"uuid","description":"Handle for this item - what removing it takes. Not the offer id."},"payment_policy":{"type":["string","null"],"description":"How the rental is paid for, once the rate has been re-priced.","enum":["guarantee","deposit",null]},"pickup_at":{"type":"string","format":"date-time","description":"Local time at the pickup branch."},"pickup_location_id":{"type":"string"},"provider":{"type":"string"},"rental_days":{"type":"integer","format":"int32"},"status":{"$ref":"#/components/schemas/ReservationStatus"},"total_amount":{"type":"string","example":"187.41"},"updated_at":{"type":"string","format":"date-time"},"vehicle_code":{"type":"string","description":"ACRISS code of the car class.","example":"ECAR"},"vehicle_name":{"type":"string","description":"Representative model, or the ACRISS code when the vendor names none.","example":"Kia Rio or similar"},"vendor_code":{"type":"string","example":"ZI"},"vendor_name":{"type":["string","null"],"example":"Avis"}}},"CarCartItemEnriched":{"type":"object","description":"Enriched car cart item with the offer's vendor, vehicle and branches.","required":["id","provider","vendor","vehicle_code","vehicle_name","pickup_at","dropoff_at","rental_days","total_amount","currency","status","travelers","created_at","updated_at","young_driver"],"properties":{"base_amount":{"type":["string","null"],"description":"Rental cost before the mandatory taxes and fees."},"created_at":{"type":"string","format":"date-time"},"currency":{"type":"string"},"drop_off_charge":{"type":["string","null"],"description":"One-way fee, when the vendor quotes it separately."},"dropoff_at":{"type":"string","format":"date-time"},"dropoff_location":{"$ref":"#/components/schemas/CarLocation"},"id":{"type":"string","format":"uuid"},"mileage":{"oneOf":[{"type":"string","enum":["Unlimited"]},{"type":"object","required":["Limited"],"properties":{"Limited":{"type":"object","required":["allowance","unit"],"properties":{"allowance":{"type":"integer","format":"int32","minimum":0},"extra_charge":{"type":["string","null"]},"unit":{"$ref":"#/components/schemas/DistanceUnit"}}}}}]},"payment_policy":{"type":["string","null"],"description":"How the rental is paid for, once the rate has been re-priced.","enum":["guarantee","deposit",null]},"pickup_at":{"type":"string","format":"date-time"},"pickup_location":{"$ref":"#/components/schemas/CarLocation"},"provider":{"type":"string"},"rental_days":{"type":"integer","format":"int32"},"status":{"$ref":"#/components/schemas/ReservationStatus"},"tax_amount":{"type":["string","null"],"description":"Mandatory taxes and fees already inside `total_amount`."},"total_amount":{"type":"string"},"travelers":{"type":"array","items":{"$ref":"#/components/schemas/CartTraveler"},"description":"The driver, as a single-entry list so every item type reads the same."},"travelers_required":{"type":["integer","null"],"minimum":0},"updated_at":{"type":"string","format":"date-time"},"vehicle":{"$ref":"#/components/schemas/CarVehicle"},"vehicle_code":{"type":"string"},"vehicle_name":{"type":"string"},"vendor":{"$ref":"#/components/schemas/CarVendor"},"young_driver":{"type":"boolean","description":"The driver is younger than 25 at pickup, so the rental company may refuse the rental or\ncharge a young-driver fee."}}},"CarCategory":{"type":"string","enum":["Mini","MiniElite","Economy","EconomyElite","Compact","CompactElite","Intermediate","IntermediateElite","Standard","StandardElite","Fullsize","FullsizeElite","Premium","PremiumElite","Luxury","LuxuryElite","Oversize","Special"]},"CarDropoffLocationSearchRequest":{"type":"object","required":["pickup_location_ids","search"],"properties":{"pickup_location_ids":{"type":"array","items":{"type":"string"},"description":"Locations already chosen for pickup; dropoff branches are limited to the same vendors."},"search":{"$ref":"#/components/schemas/CarLocationSearchRequest"}}},"CarLocation":{"type":"object","description":"A single rental branch of one vendor.","required":["id","provider","vendor","name","address","airport"],"properties":{"address":{"$ref":"#/components/schemas/Address"},"airport":{"type":"boolean"},"distance_km":{"type":["number","null"],"format":"double","description":"Distance from the search point; only set for location search results."},"geo":{"$ref":"#/components/schemas/GeoLocation"},"id":{"type":"string","description":"Provider-issued identifier, opaque to callers; pass it back into car searches.","example":"ZIPHXPHX"},"name":{"type":"string","example":"Phoenix Sky Harbor Intl Airport"},"provider":{"type":"string"},"vendor":{"$ref":"#/components/schemas/CarVendor"}}},"CarLocationSearchRequest":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","format":"double","example":33.4352},"longitude":{"type":"number","format":"double","example":-112.0101},"max_results":{"type":["integer","null"],"format":"int32","minimum":0},"radius_km":{"type":["number","null"],"format":"double"}}},"CarLocationSearchResult":{"type":"object","required":["locations"],"properties":{"locations":{"type":"array","items":{"$ref":"#/components/schemas/CarLocation"},"description":"Nearest first. Empty when no branch is within the radius."}}},"CarOffer":{"type":"object","required":["offer_id","provider","vendor","vehicle","pickup_location","dropoff_location","pickup_at","dropoff_at","rental_days","pricing"],"properties":{"dropoff_at":{"type":"string","format":"date-time"},"dropoff_location":{"$ref":"#/components/schemas/CarLocation"},"ext":{"type":["object","null"],"description":"Provider-specific data needed to price-check and book this offer."},"mileage":{"oneOf":[{"type":"string","enum":["Unlimited"]},{"type":"object","required":["Limited"],"properties":{"Limited":{"type":"object","required":["allowance","unit"],"properties":{"allowance":{"type":"integer","format":"int32","minimum":0},"extra_charge":{"type":["string","null"]},"unit":{"$ref":"#/components/schemas/DistanceUnit"}}}}}]},"offer_id":{"type":"string","format":"uuid"},"pickup_at":{"type":"string","format":"date-time"},"pickup_location":{"$ref":"#/components/schemas/CarLocation"},"pricing":{"$ref":"#/components/schemas/CarPricing"},"provider":{"type":"string"},"rate_code":{"type":["string","null"]},"rental_days":{"type":"integer","format":"int32","minimum":0},"vehicle":{"$ref":"#/components/schemas/CarVehicle"},"vendor":{"$ref":"#/components/schemas/CarVendor"}}},"CarPaymentPolicy":{"type":"string","description":"How the rental is paid for. A guaranteed rental is paid at the counter; the card only holds it.","enum":["guarantee","deposit"]},"CarPricing":{"type":"object","required":["total","per_day","currency"],"properties":{"base_rate":{"type":["string","null"],"description":"Rental cost before the mandatory taxes and fees.","example":"504.65"},"currency":{"type":"string","example":"USD"},"drop_off_charge":{"type":["string","null"]},"per_day":{"type":"string"},"taxes":{"type":["string","null"],"description":"Mandatory taxes and fees already inside `total`. `None` when the vendor quoted none.","example":"168.22"},"total":{"type":"string","description":"Approximate total for the whole rental, as quoted by the vendor.","example":"672.87"}}},"CarSearchAction":{"type":"object","description":"The rental the assistant shopped for.","required":["pickup","pickup_at","dropoff_at"],"properties":{"dropoff":{"$ref":"#/components/schemas/CarSearchPlace","description":"Absent when the car goes back where it was picked up."},"dropoff_at":{"type":"string","format":"date-time","example":"2026-11-08T10:00:00"},"pickup":{"$ref":"#/components/schemas/CarSearchPlace"},"pickup_at":{"type":"string","format":"date-time","description":"Branch-local.","example":"2026-11-05T10:00:00"}}},"CarSearchPlace":{"type":"object","description":"A place the traveller named, as the portal's car search form takes it.","required":["name","latitude","longitude"],"properties":{"latitude":{"type":"number","format":"double"},"longitude":{"type":"number","format":"double"},"name":{"type":"string","example":"Los Angeles International Airport"}}},"CarSearchRequest":{"type":"object","required":["pickup_location_ids","dropoff_location_ids","pickup_at","dropoff_at"],"properties":{"currency":{"type":["string","null"],"example":"USD"},"dropoff_at":{"type":"string","format":"date-time","description":"Local time at the dropoff location."},"dropoff_location_ids":{"type":"array","items":{"type":"string"},"description":"Empty means the car is returned where it was picked up."},"pickup_at":{"type":"string","format":"date-time","description":"Local time at the pickup location."},"pickup_location_ids":{"type":"array","items":{"type":"string"}}}},"CarSearchResult":{"type":"object","required":["offers"],"properties":{"offers":{"type":"array","items":{"$ref":"#/components/schemas/CarOffer"},"description":"Cheapest first. Empty when nothing is available."}}},"CarVehicle":{"type":"object","required":["classification"],"properties":{"bags_large":{"type":["integer","null"],"format":"int32","minimum":0},"bags_small":{"type":["integer","null"],"format":"int32","minimum":0},"classification":{"$ref":"#/components/schemas/VehicleClassification"},"doors":{"type":["string","null"],"example":"2-4"},"image_url":{"type":["string","null"]},"make_model":{"type":["string","null"],"example":"Toyota Corolla or similar"},"seats":{"type":["integer","null"],"format":"int32","minimum":0}}},"CarVendor":{"type":"object","required":["code"],"properties":{"code":{"type":"string","example":"ZI"},"logo_url":{"type":["string","null"]},"name":{"type":["string","null"],"example":"Avis"}}},"CartItemEnriched":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/HotelCartItemEnriched"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["hotel"]}}}],"title":"hotel"},{"allOf":[{"$ref":"#/components/schemas/FlightCartItemEnriched"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["flight"]}}}],"title":"flight"},{"allOf":[{"$ref":"#/components/schemas/EventCartItemEnriched"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["event"]}}}],"title":"event"},{"allOf":[{"$ref":"#/components/schemas/CarCartItemEnriched"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["car"]}}}],"title":"car"}],"description":"Enriched cart item - wraps every item type with a type discriminator.","discriminator":{"propertyName":"type","mapping":{"car":"#/components/schemas/CarCartItemEnriched","event":"#/components/schemas/EventCartItemEnriched","flight":"#/components/schemas/FlightCartItemEnriched","hotel":"#/components/schemas/HotelCartItemEnriched"}}},"CartItemType":{"type":"string","enum":["hotel","event","flight","car"]},"CartProcessingItem":{"allOf":[{"$ref":"#/components/schemas/ItemState"},{"type":"object","required":["item_id","item_type","name"],"properties":{"item_id":{"type":"string","format":"uuid"},"item_type":{"$ref":"#/components/schemas/CartItemType"},"name":{"type":"string"}}}]},"CartProcessingSnapshot":{"type":"object","description":"What `GET /api/v1/carts/{cart_id}/processing` answers with.","required":["cart_id","run_id","status","cart_status","items","started_at","updated_at"],"properties":{"blocked_by":{"oneOf":[{"type":"object","description":"The provider repriced the item. Resume with `confirmed_price` set to `new` to accept it.","required":["old","new","currency","type"],"properties":{"currency":{"type":"string"},"new":{"type":"string"},"old":{"type":"string"},"type":{"type":"string","enum":["price_confirmation"]}}},{"type":"object","description":"Passenger or guest details are missing. Fill them in through the booking's own endpoint,\nthen resume.","required":["missing","type"],"properties":{"missing":{"type":"array","items":{"type":"string"}},"type":{"type":"string","enum":["traveller_details"]}}},{"type":"object","description":"The provider takes payment on its own page. Open this, pay, then poll again.","required":["payment_url","type"],"properties":{"payment_url":{"type":"string"},"type":{"type":"string","enum":["hosted_payment"]}}},{"type":"object","description":"The next item is charged from a card and the run is holding none.","required":["type"],"properties":{"type":{"type":"string","enum":["payment_method"]}}}],"description":"What the run needs before it can go on. The only place a payment URL is ever handed out."},"blocked_on":{"type":["string","null"],"format":"uuid"},"cart_id":{"type":"string","format":"uuid"},"cart_status":{"$ref":"#/components/schemas/CartStatus"},"items":{"type":"array","items":{"$ref":"#/components/schemas/CartProcessingItem"}},"run_id":{"type":"string","format":"uuid"},"started_at":{"type":"string","format":"date-time"},"status":{"$ref":"#/components/schemas/CartProcessingStatus"},"updated_at":{"type":"string","format":"date-time"}}},"CartProcessingStatus":{"type":"string","description":"Where a cart's processing run stands.\n\nOnly a `Running` run touched recently holds the cart frozen - a paused one may be waiting on a\ntraveller who never returns, and must not wedge their cart.","enum":["running","paused","completed","cancelled"]},"CartResponseEnriched":{"type":"object","description":"Enriched cart response with display-friendly item details.","required":["id","status","is_current","items","total","unavailable_item_ids"],"properties":{"id":{"type":"string","format":"uuid"},"is_current":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/CartItemEnriched"},"description":"Held items, oldest first. Empty when nothing is in the cart."},"status":{"$ref":"#/components/schemas/CartStatus"},"title":{"type":["string","null"]},"total":{"$ref":"#/components/schemas/CartTotal","description":"Counts and per-currency sums of `items`."},"unavailable_item_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Ids of items that cannot be checked out."}}},"CartStatus":{"type":"string","description":"Where a cart stands. Only `Open` accepts new offers.","enum":["open","checking_out","completed","abandoned"]},"CartSummary":{"type":"object","description":"One cart without its contents - what a listing shows.","required":["id","status","is_current","total","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"is_current":{"type":"boolean","description":"Whether this is the cart a selection lands in when none is named."},"status":{"$ref":"#/components/schemas/CartStatus"},"title":{"type":["string","null"],"description":"What the traveller called this cart, if they named it."},"total":{"$ref":"#/components/schemas/CartTotal","description":"Counts and per-currency sums of what it holds."},"updated_at":{"type":"string","format":"date-time"}}},"CartTotal":{"type":"object","description":"What the cart adds up to.","required":["item_count","hotel_count","event_count","flight_count","car_count","by_currency"],"properties":{"by_currency":{"type":"array","items":{"$ref":"#/components/schemas/CurrencyTotal"},"description":"One entry per currency the cart holds, ordered by code. There is deliberately no single\ngrand total: a cart can hold a hotel priced in EUR beside a flight priced in USD, and\nadding those up would mean picking an exchange rate and quoting a number nobody will\nactually be charged."},"car_count":{"type":"integer","minimum":0},"event_count":{"type":"integer","minimum":0},"flight_count":{"type":"integer","minimum":0},"hotel_count":{"type":"integer","minimum":0},"item_count":{"type":"integer","minimum":0}}},"CartTraveler":{"type":"object","description":"One traveller as the booking holds them — a snapshot of the saved profile, not a reference.","required":["companion_id","first_name","last_name","dob","role","is_complete","missing_fields"],"properties":{"companion_id":{"type":"string","format":"uuid"},"dob":{"type":"string","format":"date"},"document":{"$ref":"#/components/schemas/PassengerDocument"},"email":{"type":["string","null"]},"first_name":{"type":"string"},"gender":{"type":["string","null"],"enum":["male","female",null]},"is_complete":{"type":"boolean"},"last_name":{"type":"string"},"middle_name":{"type":["string","null"]},"missing_fields":{"type":"array","items":{"type":"string"}},"nationality":{"type":["string","null"]},"phone":{"type":["string","null"]},"role":{"$ref":"#/components/schemas/TravelerRole"}}},"CartTravelersResponse":{"type":"object","required":["travelers","assigned_count"],"properties":{"assigned_count":{"type":"integer","minimum":0},"document_required":{"type":["boolean","null"],"description":"Flights only: whether a passport has to be on file before this item can be booked."},"required_count":{"type":["integer","null"],"minimum":0},"route_type":{"type":["string","null"],"description":"Flights only, so the UI can say why a passport is asked for.","enum":["domestic","international",null]},"travelers":{"type":"array","items":{"$ref":"#/components/schemas/CartTraveler"}}}},"ChatHistoryResponse":{"type":"object","description":"The conversation, plus whether an answer is still on its way.","required":["messages","replying"],"properties":{"action":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/HotelSearchAction"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["hotel_search"]}}}],"title":"hotel_search"},{"allOf":[{"$ref":"#/components/schemas/FlightSearchAction"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["flight_search"]}}}],"title":"flight_search"},{"allOf":[{"$ref":"#/components/schemas/CarSearchAction"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["car_search"]}}}],"title":"car_search"}],"description":"Present once, on the first read after the assistant searched something: what the client\nshould open. Read it and it is gone, so a reload does not navigate again.","discriminator":{"propertyName":"type","mapping":{"car_search":"#/components/schemas/CarSearchAction","flight_search":"#/components/schemas/FlightSearchAction","hotel_search":"#/components/schemas/HotelSearchAction"}}},"messages":{"type":"array","items":{"$ref":"#/components/schemas/ChatMessageResponse"},"description":"Oldest first."},"replying":{"type":"boolean","description":"True while the assistant is working on a message, which is what a client shows a typing\nindicator on."}}},"ChatMessageResponse":{"type":"object","description":"One turn of the conversation.","required":["ts","role","text"],"properties":{"role":{"type":"string","description":"`user` for the traveller, `agent` for us.","example":"agent"},"text":{"type":"string"},"ts":{"type":"integer","format":"int64","description":"When the message was stored, in Unix epoch seconds.","example":1757419200}}},"CompanionResponse":{"type":"object","description":"A saved traveller companion. The owner is the authenticated traveller, so it is not repeated\nin the payload.","required":["id","first_name","last_name","dob","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time","example":"2026-11-05T10:00:00Z"},"dob":{"type":"string","format":"date"},"document":{"$ref":"#/components/schemas/PassengerDocument","description":"The travel document, needed before this companion can fly."},"email":{"type":["string","null"]},"first_name":{"type":"string"},"gender":{"type":["string","null"],"enum":["male","female",null]},"id":{"type":"string","format":"uuid"},"last_name":{"type":"string"},"middle_name":{"type":["string","null"]},"nationality":{"type":["string","null"],"description":"ISO 3166-1 alpha-2 country of citizenship."},"phone":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time","example":"2026-11-05T10:00:00Z"}}},"Conditions":{"type":"object","description":"Perks the fare includes on this leg. `null` means the airline said nothing.","properties":{"advance_seat_selection":{"type":["boolean","null"]},"priority_boarding":{"type":["boolean","null"]},"priority_check_in":{"type":["boolean","null"]}}},"CreateCartRequest":{"type":"object","properties":{"title":{"type":["string","null"],"description":"What to call it, e.g. `Rome in May`. Optional - a cart works unnamed.","example":"Rome in May"}}},"CreateCompanionRequest":{"type":"object","required":["first_name","last_name","dob"],"properties":{"dob":{"type":"string","format":"date","example":"1990-12-10"},"document":{"$ref":"#/components/schemas/PassengerDocument"},"email":{"type":["string","null"],"example":"ada@example.com"},"first_name":{"type":"string","example":"Ada"},"gender":{"type":["string","null"],"enum":["male","female",null]},"last_name":{"type":"string","example":"Lovelace"},"middle_name":{"type":["string","null"]},"nationality":{"type":["string","null"],"description":"ISO 3166-1 alpha-2 country of citizenship.","example":"GB"},"phone":{"type":["string","null"],"example":"+1 415 555 0134"}}},"CurrencyTotal":{"type":"object","description":"What the cart comes to in one currency.","required":["currency","amount"],"properties":{"amount":{"type":"string","description":"Sum of every item priced in `currency`.","example":"1284.00"},"currency":{"type":"string","description":"ISO 4217 code.","example":"USD"}}},"DistanceUnit":{"type":"string","enum":["Km","Mi"]},"DocumentType":{"type":"string","enum":["passport","id_card"]},"DriveType":{"type":"string","enum":["Unspecified","FourWheel","AllWheel"]},"EventCartItemEnriched":{"type":"object","description":"Enriched event cart item with display-friendly fields.","required":["id","event_name","provider","event_start","tz","quantity","ticket_price","total_amount","currency","status","travelers","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"currency":{"type":"string"},"event_end":{"type":["string","null"],"format":"date-time","description":"Event end time if available."},"event_images":{"type":["array","null"],"items":{"type":"string"},"description":"Event images for display."},"event_name":{"type":"string"},"event_start":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"provider":{"type":"string"},"quantity":{"type":"integer","format":"int32"},"status":{"$ref":"#/components/schemas/ReservationStatus"},"ticket_price":{"type":"string"},"total_amount":{"type":"string"},"travelers":{"type":"array","items":{"$ref":"#/components/schemas/CartTraveler"},"description":"The ticket recipient, as a single-entry list so every item type reads the same."},"travelers_required":{"type":["integer","null"],"minimum":0},"tz":{"type":"string","description":"IANA timezone."},"updated_at":{"type":"string","format":"date-time"},"venue_address":{"type":["string","null"],"description":"Venue address for display."},"venue_name":{"type":["string","null"],"description":"Venue name for display."}}},"FlightCartItem":{"type":"object","description":"A held flight itinerary.","required":["id","provider","owner_name","slices","passengers_count","total_amount","currency","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"currency":{"type":"string"},"id":{"type":"string","format":"uuid","description":"Handle for this item — what removing it or checking it out takes. Not the offer id."},"owner_name":{"type":"string","description":"Airline owning the ticket."},"passengers_count":{"type":"integer","format":"int32"},"provider":{"type":"string","description":"Distribution channel the fare was sourced through."},"slices":{"type":"array","items":{"$ref":"#/components/schemas/OfferSlice"},"description":"The legs, in the order they are flown."},"status":{"$ref":"#/components/schemas/ReservationStatus"},"total_amount":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"FlightCartItemEnriched":{"type":"object","description":"Enriched flight cart item with display-friendly fields loaded from booking.","required":["id","provider","owner_name","slices","passengers_count","total_amount","currency","status","travelers","document_required","route_type","created_at","updated_at"],"properties":{"airports":{"type":["object","null"],"description":"Map of airport code to display info (name, city).","additionalProperties":{"$ref":"#/components/schemas/AirportDisplayInfo"},"propertyNames":{"type":"string"}},"base_amount":{"type":["string","null"],"description":"Base fare amount before taxes."},"cabin_class":{"type":["string","null"],"description":"Cabin class from first segment (economy, business, etc)."},"created_at":{"type":"string","format":"date-time"},"currency":{"type":"string"},"document_required":{"type":"boolean","description":"Whether a passport has to be on file before this flight can be booked."},"id":{"type":"string","format":"uuid"},"owner_iata_code":{"type":["string","null"],"description":"IATA code of the owning airline."},"owner_logo_url":{"type":["string","null"],"description":"Logo URL of the owning airline."},"owner_name":{"type":"string","description":"Airline owning the ticket."},"passengers_count":{"type":"integer","format":"int32"},"provider":{"type":"string","description":"Distribution channel the fare was sourced through."},"route_type":{"$ref":"#/components/schemas/RouteType"},"slices":{"type":"array","items":{"$ref":"#/components/schemas/OfferSlice"},"description":"The legs, in the order they are flown."},"status":{"$ref":"#/components/schemas/ReservationStatus"},"tax_amount":{"type":["string","null"],"description":"Tax amount."},"total_amount":{"type":"string"},"travelers":{"type":"array","items":{"$ref":"#/components/schemas/CartTraveler"},"description":"Passengers already on the reservation."},"travelers_required":{"type":["integer","null"],"description":"Seats sold, which is how many passengers the booking needs.","minimum":0},"updated_at":{"type":"string","format":"date-time"}}},"FlightSearchAction":{"type":"object","description":"The trip the assistant shopped for.","required":["origin","destination","departure_date","adults","children_ages"],"properties":{"adults":{"type":"integer","format":"int32","minimum":0},"children_ages":{"type":"array","items":{"type":"integer","format":"int32","minimum":0}},"departure_date":{"type":"string","format":"date"},"destination":{"type":"string","example":"LIS"},"origin":{"type":"string","description":"IATA code.","example":"CDG"},"return_date":{"type":["string","null"],"format":"date","description":"Absent on a one way."}}},"FlightSearchPassenger":{"type":"object","description":"One traveller.","required":["passenger_type"],"properties":{"age":{"type":["integer","null"],"format":"int32","description":"Age at departure. Required for `child` and `infant`, where the fare depends on it, and\nomitted for `adult`.","example":9},"passenger_type":{"$ref":"#/components/schemas/PassengerType"}}},"FlightSearchRequest":{"type":"object","description":"Everything needed to shop one itinerary.","required":["slices","passengers"],"properties":{"airline_filter":{"$ref":"#/components/schemas/AirlineFilter","description":"Optional. Restricts which airlines may appear in the results, by IATA airline code. Omit it\nto shop every airline."},"cabin_class":{"type":["string","null"],"description":"Optional, `economy` when omitted. A leg the airline cannot sell in that cabin comes back in\nthe nearest one it can, so read the cabin off each returned segment.","enum":["economy","premium_economy","business","first",null]},"currency":{"type":["string","null"],"description":"Optional. ISO 4217 code to price every offer in. Omit it and each airline prices in its own\ncurrency, which leaves the offers not comparable to each other.","example":"USD"},"max_connections":{"type":["integer","null"],"format":"int32","description":"Optional. Most connections allowed on any one leg — `0` returns direct flights only.\nConnections are not capped when omitted.","example":1,"minimum":0},"passengers":{"type":"array","items":{"$ref":"#/components/schemas/FlightSearchPassenger"},"description":"One entry per traveller — three adults are three entries, not one with a count."},"slices":{"type":"array","items":{"$ref":"#/components/schemas/FlightSearchSlice"},"description":"The legs to price together, in the order they are flown: one entry is a one-way, two a\nreturn, three or more a multi-city trip. All of them are priced as a single ticket."}}},"FlightSearchResultExt":{"type":"object","description":"Search results: the offers shopped for one itinerary, ready to render.","required":["search_id","offers","airports","airlines_available"],"properties":{"airlines_available":{"type":"array","items":{"type":"string"},"description":"IATA codes of the airlines owning the offers, sorted. Ready to build a carrier filter from.","example":["BA"]},"airports":{"type":"object","description":"Every IATA code appearing anywhere in `offers`, resolved to what to call it. Codes missing\nfrom the map have no reference data — render the bare code.","additionalProperties":{"$ref":"#/components/schemas/AirportInfo"},"propertyNames":{"type":"string"}},"offers":{"type":"array","items":{"$ref":"#/components/schemas/OfferExt"},"description":"Priceable itineraries, unsorted and unpaginated. Empty when nothing is available."},"search_id":{"type":"string","description":"Handle for the search as a whole. With an `offers[].id` it is what\n`POST /api/v1/flight/search/{search_id}/offer/{offer_id}/select` takes to move an offer into booking."}}},"FlightSearchSlice":{"type":"object","description":"One leg of the trip.","required":["origin","destination","departure_date"],"properties":{"arrival_time_before":{"type":["string","null"],"description":"Optional. Latest local arrival time at `destination`, `HH:MM:SS`."},"departure_date":{"type":"string","format":"date","description":"Departure day in the origin's local time, `YYYY-MM-DD`.","example":"2026-11-05"},"departure_time_after":{"type":["string","null"],"description":"Optional. Earliest local departure time on this leg, `HH:MM:SS`. Omit for the whole day."},"departure_time_before":{"type":["string","null"],"description":"Optional. Latest local departure time on this leg, `HH:MM:SS`."},"destination":{"type":"string","description":"IATA airport or metro code to arrive at.","example":"JFK"},"origin":{"type":"string","description":"IATA code to depart from — an airport (`LHR`) or a metro code standing for every airport of\na city (`LON`). `GET /api/v1/airport/autocomplete` turns what a traveller types into these.","example":"LHR"}}},"FuelType":{"type":"string","enum":["Unspecified","Diesel","Hybrid","Electric","Lpg","Hydrogen","MultiFuel","Petrol","Ethanol"]},"Gender":{"type":"string","enum":["male","female"]},"GeoLocation":{"type":"object","description":"WGS 84 coordinates.","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","format":"double","example":40.7127837},"longitude":{"type":"number","format":"double","example":-74.0059413}}},"HotelCartItem":{"type":"object","description":"A held hotel stay.","required":["id","hotel_name","hotel_id","provider","checkin","checkout","total_amount","currency","status","created_at","updated_at"],"properties":{"checkin":{"type":"string","format":"date","description":"First night of the stay."},"checkout":{"type":"string","format":"date","description":"Departure day, exclusive."},"created_at":{"type":"string","format":"date-time","description":"When the offer was selected."},"currency":{"type":"string","example":"USD"},"hotel_id":{"type":"string","description":"The property's id at its provider, the same one hotel search returned."},"hotel_name":{"type":"string"},"id":{"type":"string","format":"uuid","description":"Handle for this item — what removing it or checking it out takes. Not the offer id."},"provider":{"type":"string"},"room_types":{"type":["string","null"],"description":"Ready-to-render summary of the rooms held, e.g. `2 × Deluxe King`."},"status":{"$ref":"#/components/schemas/ReservationStatus"},"total_amount":{"type":"string","description":"All-in price of the whole stay, in `currency`.","example":"1284.00"},"updated_at":{"type":"string","format":"date-time"}}},"HotelCartItemEnriched":{"type":"object","description":"Enriched hotel cart item with display-friendly fields loaded from booking.","required":["id","hotel_name","hotel_id","provider","checkin","checkout","total_amount","currency","status","travelers","created_at","updated_at"],"properties":{"address":{"$ref":"#/components/schemas/Address","description":"Hotel address for display."},"base_amount":{"type":["string","null"],"description":"Base price before taxes."},"board_type":{"type":["string","null"],"description":"Board type (breakfast, half board, etc)."},"cancellation_policy_description":{"type":["string","null"],"description":"Cancellation policy description."},"checkin":{"type":"string","format":"date"},"checkout":{"type":"string","format":"date"},"created_at":{"type":"string","format":"date-time"},"currency":{"type":"string"},"free_cancellation":{"type":["boolean","null"],"description":"Free cancellation available."},"geo":{"$ref":"#/components/schemas/GeoLocation","description":"Where the property is, when the provider placed it."},"hotel_id":{"type":"string"},"hotel_images":{"type":["array","null"],"items":{"type":"string"},"description":"Hotel images for display."},"hotel_name":{"type":"string"},"id":{"type":"string","format":"uuid"},"payment_policy":{"oneOf":[{"type":"object","description":"The guest pays nothing upfront. The card is held as a guarantee against no-show or late\ncancellation; the property collects the full amount at check-in or check-out.","required":["type"],"properties":{"type":{"type":"string","enum":["pay_at_property"]}}},{"type":"object","description":"The property charges a deposit before arrival. `amount` is present once the rate has been\nprice-checked; at search time it may be `None` (the deposit is known to be required but\nthe exact sum has not been confirmed yet).","required":["type"],"properties":{"amount":{"type":["string","null"],"example":"150.00"},"type":{"type":"string","enum":["deposit"]}}},{"type":"object","description":"The full stay price is charged to the card at booking time.","required":["type"],"properties":{"type":{"type":"string","enum":["prepay"]}}}],"description":"How the property collects payment (deposit, pay-at-property, prepay)."},"provider":{"type":"string"},"room_description":{"type":["string","null"],"description":"Detailed room description."},"room_types":{"type":["string","null"]},"star_rating":{"type":["integer","null"],"format":"int32","description":"Hotel star rating (1-5).","minimum":0},"status":{"$ref":"#/components/schemas/ReservationStatus"},"tax_amount":{"type":["string","null"],"description":"Tax amount."},"total_amount":{"type":"string"},"travelers":{"type":"array","items":{"$ref":"#/components/schemas/CartTraveler"},"description":"Guests already on the reservation."},"travelers_required":{"type":["integer","null"],"description":"How many the room sleeps.","minimum":0},"updated_at":{"type":"string","format":"date-time"}}},"HotelInfo":{"type":"object","description":"A property as returned by search.","required":["id","name","address","geo_location","star_rating","amenities","images"],"properties":{"address":{"$ref":"#/components/schemas/Address"},"amenities":{"type":"array","items":{"type":"string"},"description":"Free-text amenity labels in the requested language. For matching, use `amenity_keys`."},"description":{"type":"string","description":"Property description in the requested language; may be empty."},"geo_location":{"$ref":"#/components/schemas/GeoLocation"},"id":{"type":"string","description":"Provider-scoped property id. Stable within a provider, not across providers."},"images":{"type":"array","items":{"type":"string"},"description":"Property photo URLs, best first."},"name":{"type":"string","description":"Property name as the provider spells it.","example":"Grand Luxury Hotel & Spa"},"rating":{"type":["string","null"],"description":"rating from travellers reviews 0..10"},"rating_cnt":{"type":["integer","null"],"format":"int64","description":"number of ratings from travelers"},"star_rating":{"type":"integer","format":"int32","description":"Official star classification, `0` when the provider gives none.","example":5,"minimum":0},"tz":{"type":["string","null"],"description":"IANA timezone of the property, e.g. `America/New_York`.","example":"America/New_York"}}},"HotelInfoExt":{"allOf":[{"$ref":"#/components/schemas/HotelInfo"},{"type":"object","required":["amenity_keys"],"properties":{"amenity_keys":{"type":"array","items":{"type":"string"},"description":"Language-independent counterparts of the free-text `amenities` list (`free_wifi`, `pool`,\n`gym`, …). Match on these rather than on the display strings."}}}],"description":"Property details plus normalised amenity keys. Serialised flat."},"HotelOffersExt":{"type":"object","description":"One property and the offers shopped for it.","required":["hotel","offers","more_rates_available"],"properties":{"hotel":{"$ref":"#/components/schemas/HotelInfoExt","description":"The property itself."},"more_rates_available":{"type":"boolean","description":"Whether this property has rates beyond the ones listed here. Some providers answer a search\nwith a single lead rate per property and keep the full rate list behind a second call — this\nflag marks those properties, so a UI knows when \"see all rooms\" is worth offering.\n\n`true`: `GET /api/v1/hotel/rates/{offer_id}`, with any `offer_id` from this property's\n`offers`, returns the property again with its complete list of offers, shaped exactly like\none entry of `hotels`. `false`: everything the provider has is already here, and that call\nanswers `404`."},"offers":{"type":"array","items":{"$ref":"#/components/schemas/RoomOffersExt"},"description":"Bookable offers for the requested stay and occupancy."}}},"HotelSearchAction":{"type":"object","description":"The stay the assistant shopped for.","required":["location","latitude","longitude","check_in","check_out","adults","children_ages"],"properties":{"adults":{"type":"integer","format":"int32","minimum":0},"check_in":{"type":"string","format":"date","example":"2026-11-01"},"check_out":{"type":"string","format":"date","example":"2026-11-03"},"children_ages":{"type":"array","items":{"type":"integer","format":"int32","minimum":0},"description":"One age per child, empty when there are none."},"latitude":{"type":"number","format":"double"},"location":{"type":"string","description":"The place as the traveller named it, for the destination field.","example":"Paris"},"longitude":{"type":"number","format":"double"},"radius_km":{"type":["number","null"],"format":"double"}}},"HotelSearchRequest":{"type":"object","description":"Everything needed to shop one hotel stay.","required":["latitude","longitude","check_in","check_out","rooms"],"properties":{"amenities":{"type":["array","null"],"items":{"type":"string","enum":["AirConditioning","Balcony","Bar","Bath","Beach","Bed","CoffeeTea","Concierge","Desk","Elevator","FitnessCenter","FreeParking","FreeWiFi","Fridge","Hairdryer","Iron","Kitchen","Lounge","Minibar","Parking","PetsAllowed","Phone","Restaurant","Safe","Shower","Shuttle","Spa","SwimmingPool","TV","Toilet","View","WiFi"]},"description":"Keep only properties offering all of these amenities. Matched provider-side where the\nprovider supports it and against property facilities otherwise, so it narrows results\neither way.\n\nValues are case-sensitive, spelled exactly as listed: `AirConditioning`, `Balcony`, `Bar`,\n`Bath`, `Beach`, `Bed`, `CoffeeTea`, `Concierge`, `Desk`, `Elevator`, `FitnessCenter`,\n`FreeParking`, `FreeWiFi`, `Fridge`, `Hairdryer`, `Iron`, `Kitchen`, `Lounge`, `Minibar`,\n`Parking`, `PetsAllowed`, `Phone`, `Restaurant`, `Safe`, `Shower`, `Shuttle`, `Spa`,\n`SwimmingPool`, `TV`, `Toilet`, `View`, `WiFi`.","example":["FreeWiFi","SwimmingPool"]},"check_in":{"type":"string","format":"date","description":"First night of the stay, `YYYY-MM-DD`.","example":"2026-11-05"},"check_out":{"type":"string","format":"date","description":"Departure day, exclusive — the night before it is the last one booked. Must be after\n`check_in`.","example":"2026-11-08"},"currency":{"type":["string","null"],"description":"ISO 4217 code every price in the response is expressed in. Defaults to `USD`.","example":"USD"},"guest_nationality":{"type":["string","null"],"description":"ISO 3166-1 alpha-2 country of the lead guest. Many rates are nationality-restricted, so\nthis changes both price and availability. Defaults to `US`.","example":"US"},"hotel_star_ratings":{"type":["array","null"],"items":{"type":"integer","format":"int32"},"description":"Star classes to keep. Half steps are included, so `4` also matches 4.5-star properties.","example":[4,5]},"language":{"type":["string","null"],"description":"Language of the response: hotel and room texts from the provider, plus the cancellation\nsummary and amenity keys rendered on our side.\n\nFully supported: `de`, `en`, `es`, `fr`, `it`, `ru`, `uk`, `zh`. Other ISO 639-1 codes are\nstill forwarded to providers that accept them, so property texts may come back localised\nwhile our own strings stay English. Anything unsupported falls back to English rather than\nfailing the request. Defaults to `en`.","example":"en"},"latitude":{"type":"number","format":"double","description":"Centre of the search. Resolve the place in your own UI and send the point.","example":40.7075},"longitude":{"type":"number","format":"double","description":"Centre of the search. Resolve the place in your own UI and send the point.","example":-74.0113},"radius_km":{"type":["number","null"],"format":"double","description":"Search radius around the resolved centre, in kilometres. Defaults to `5`.","example":5.0},"requested_hotel_name":{"type":["string","null"],"description":"Narrows the search onto a single property. The name is geocoded — the point it resolves to\ntakes precedence over `latitude` / `longitude` — and matched leniently against provider\ninventory."},"rooms":{"type":"array","items":{"$ref":"#/components/schemas/RoomOccupancy"},"description":"One entry per room required. Returned offers are priced for the whole party, so a\ntwo-entry list yields offers that already contain two rooms."}}},"HotelSearchResultExt":{"type":"object","description":"Search results: every matching property with its bookable offers.","required":["hotels"],"properties":{"hotels":{"type":"array","items":{"$ref":"#/components/schemas/HotelOffersExt"},"description":"Matching properties, unsorted and unpaginated."}}},"ItemState":{"oneOf":[{"type":"object","required":["state"],"properties":{"state":{"type":"string","enum":["queued"]}}},{"type":"object","required":["state"],"properties":{"state":{"type":"string","enum":["prepared"]}}},{"type":"object","required":["state"],"properties":{"state":{"type":"string","enum":["booked"]}}},{"type":"object","required":["required","state"],"properties":{"required":{"$ref":"#/components/schemas/RequiredInput"},"state":{"type":"string","enum":["awaiting_input"]}}},{"type":"object","required":["reason_slug","message","state"],"properties":{"message":{"type":"string"},"reason_slug":{"type":"string"},"state":{"type":"string","enum":["failed"]}}},{"type":"object","required":["state"],"properties":{"state":{"type":"string","enum":["skipped"]}}},{"type":"object","description":"A charge was attempted and its outcome is unknown. Never retried by anything.","required":["state"],"properties":{"state":{"type":"string","enum":["post_payment_review"]}}}],"description":"Where one item stands. Derived from its booking row and the run's record of why it stopped."},"Mileage":{"oneOf":[{"type":"string","enum":["Unlimited"]},{"type":"object","required":["Limited"],"properties":{"Limited":{"type":"object","required":["allowance","unit"],"properties":{"allowance":{"type":"integer","format":"int32","minimum":0},"extra_charge":{"type":["string","null"]},"unit":{"$ref":"#/components/schemas/DistanceUnit"}}}}}]},"OfferConditions":{"type":"object","description":"What the fare rules allow before departure. An absent entry means the airline said nothing,\nwhich is not the same as \"not allowed\".","properties":{"change_before_departure":{"$ref":"#/components/schemas/Refund","description":"Moving the ticket to another flight."},"refund_before_departure":{"$ref":"#/components/schemas/Refund","description":"Handing the ticket back for a refund."}}},"OfferExt":{"type":"object","description":"One priceable itinerary: every leg, for the whole party, on one ticket.","required":["id","provider","currency","base_amount","total_amount","slices","passengers","passenger_identity_documents_required","owner","expires_at","conditions"],"properties":{"base_amount":{"type":"string","description":"Fare before taxes, for the whole party.","example":"300.00"},"conditions":{"$ref":"#/components/schemas/OfferConditions"},"currency":{"type":"string","description":"ISO 4217 code of every amount on this offer.","example":"USD"},"expires_at":{"type":"string","format":"date-time","description":"When the airline stops honouring this price. Past it, re-shop."},"id":{"type":"string","description":"Handle for this offer inside its search."},"owner":{"$ref":"#/components/schemas/Airline","description":"Airline owning the ticket — whose fare rules apply, not necessarily who flies it."},"passenger_identity_documents_required":{"type":"boolean","description":"Passport or ID details have to be collected before this offer can be booked."},"passengers":{"type":"array","items":{"$ref":"#/components/schemas/OfferPassenger"},"description":"The travellers this price covers, one entry each."},"provider":{"type":"string","description":"Distribution channel the fare was sourced through."},"slices":{"type":"array","items":{"$ref":"#/components/schemas/OfferSlice"},"description":"The legs, in the order they were requested."},"tax_amount":{"type":["string","null"],"description":"The part of `total_amount` that is tax, when the airline breaks it out.","example":"50.00"},"total_amount":{"type":"string","description":"All-in price for every traveller and every leg — the amount to display.","example":"350.00"}}},"OfferPassenger":{"type":"object","description":"One traveller this offer is priced for.","required":["passenger_type"],"properties":{"age":{"type":["integer","null"],"format":"int64"},"passenger_type":{"$ref":"#/components/schemas/PassengerType"}}},"OfferSlice":{"type":"object","description":"One leg as sold: the flights that make it up, end to end.","required":["origin","destination","segments","conditions","baggages"],"properties":{"baggages":{"type":"array","items":{"$ref":"#/components/schemas/Baggage"},"description":"Baggage the fare includes on this leg, per traveller. An absent `bag_type` means the fare\ncarries none of that kind."},"conditions":{"$ref":"#/components/schemas/Conditions"},"destination":{"type":"string","description":"destination airport iata code","example":"JFK"},"duration":{"type":["integer","null"],"format":"int32","description":"Gate to gate, in minutes, layovers included.","example":465},"fare_brand_name":{"type":["string","null"],"description":"Branded fare this leg is priced at, e.g. `ECONOMY BASIC`. Two offers can be the same flights\nat different brands, and this is what tells them apart.","example":"Economy"},"origin":{"type":"string","description":"origin airport iata code","example":"LHR"},"segments":{"type":"array","items":{"$ref":"#/components/schemas/OfferSliceSegment"},"description":"The flights flown in order. More than one means the leg connects."}}},"OfferSliceSegment":{"type":"object","description":"One flight: a single flight number from one airport to the next.","required":["origin","destination","stops","operating_carrier","marketing_carrier_flight_number","marketing_carrier","departing_at","arriving_at","cabin_class"],"properties":{"aircraft":{"$ref":"#/components/schemas/Aircraft","description":"Aircraft scheduled to fly it, when the airline names one."},"arriving_at":{"type":"string","format":"date-time","description":"Local time at `destination`, without an offset — so it can read as earlier than\n`departing_at` on a westbound flight.","example":"2026-11-05T17:45:00"},"cabin_class":{"$ref":"#/components/schemas/CabinClass","description":"The cabin this segment is actually sold in, which can differ from the one requested."},"departing_at":{"type":"string","format":"date-time","description":"Local time at `origin`, without an offset.","example":"2026-11-05T10:00:00"},"destination":{"type":"string","example":"JFK"},"destination_terminal":{"type":["string","null"]},"duration":{"type":["integer","null"],"format":"int32","description":"Scheduled flying time, in minutes.","example":465},"fare_basis_code":{"type":["string","null"],"description":"The fare's own code in the airline's tariff, for support to quote back."},"marketing_carrier":{"$ref":"#/components/schemas/Airline","description":"The airline the flight is sold under — the code on the ticket."},"marketing_carrier_flight_number":{"type":"string"},"operating_carrier":{"$ref":"#/components/schemas/Airline","description":"The airline actually flying it, which on a codeshare differs from the marketing carrier."},"operating_carrier_flight_number":{"type":["string","null"]},"origin":{"type":"string","example":"LHR"},"origin_terminal":{"type":["string","null"]},"stops":{"type":"array","items":{"$ref":"#/components/schemas/Stop"},"description":"Technical stops on the way. Usually empty."}}},"PassengerDocument":{"type":"object","required":["document_type","document_number","document_expiry","document_issue_country"],"properties":{"document_expiry":{"type":"string","format":"date","example":"2032-04-18"},"document_issue_country":{"type":"string","description":"ISO 3166-1 alpha-2 country that issued the document.","example":"US"},"document_number":{"type":"string","example":"X1234567"},"document_type":{"$ref":"#/components/schemas/DocumentType"}}},"PassengerType":{"type":"string","description":"Fare category a traveller is priced under. `infant` means an infant on an adult's lap.","enum":["adult","child","infant"]},"PaymentMethodRequest":{"type":"object","description":"Card details, straight from the checkout form. Used for one run and never stored; a run that\npauses drops them, which is why a resume carries them again.\n\nThis shape is how the sandbox takes payment: one rate-limited test card\n(`4242 4242 4242 4242`), so an integration can be driven end to end before a\npayment provider is wired up. In production, payment is an adapter onto the\npartner's own rails — their PSP, gateway, tokenisation, or a provider's\nhosted payment page — and the shape a partner runs determines their PCI\nscope.","required":["number","expiry_month","expiry_year","card_type_code","security_code","holder_given_name","holder_surname"],"properties":{"card_type_code":{"type":"string","description":"Two-letter scheme code: `VI`, `CA`, `AX`, `DS`, `DC`, `JC`."},"expiry_month":{"type":"string","description":"Two digits, `01`..`12`."},"expiry_year":{"type":"string","description":"Two digits, the last two of the year."},"holder_given_name":{"type":"string"},"holder_surname":{"type":"string"},"number":{"type":"string","description":"Card number, digits only."},"security_code":{"type":"string"}}},"PaymentPolicy":{"oneOf":[{"type":"object","description":"The guest pays nothing upfront. The card is held as a guarantee against no-show or late\ncancellation; the property collects the full amount at check-in or check-out.","required":["type"],"properties":{"type":{"type":"string","enum":["pay_at_property"]}}},{"type":"object","description":"The property charges a deposit before arrival. `amount` is present once the rate has been\nprice-checked; at search time it may be `None` (the deposit is known to be required but\nthe exact sum has not been confirmed yet).","required":["type"],"properties":{"amount":{"type":["string","null"],"example":"150.00"},"type":{"type":"string","enum":["deposit"]}}},{"type":"object","description":"The full stay price is charged to the card at booking time.","required":["type"],"properties":{"type":{"type":"string","enum":["prepay"]}}}],"description":"How the property collects payment from the guest.\n\nProviders read this from the rate's guarantee / prepay indicators and surface it on both\n`HotelOffer` (stored in Redis) and `RoomOffers` (returned to the frontend). The booking\nservice uses it to pick the correct `paymentPolicy` value when calling the provider's\ncreate-booking API."},"PlaceAutocompleteResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/PlaceSuggestion"}}}},"PlaceSuggestion":{"type":"object","description":"One autocomplete result, carrying everything `POST /api/v1/hotel/search` needs.","required":["name","kind","country_code","country_name","label","latitude","longitude","search_radius_km"],"properties":{"country_code":{"type":"string","description":"ISO 3166-1 alpha-2.","example":"FR"},"country_name":{"type":"string","example":"France"},"iata_city_code":{"type":["string","null"],"description":"IATA city code, where one exists — for the providers that shop by code rather than\nby coordinates. Only settlements carry one.","example":"PAR"},"iata_code":{"type":["string","null"],"description":"Set only on an airport."},"id":{"type":["integer","null"],"format":"int64","description":"GeoNames id. Stable across releases of the catalog, so it is safe to store. Absent\nfor an airport, which `iata_code` identifies instead.","example":2988507},"kind":{"type":"string","description":"`city`, `island` or `region`, and `airport` when asked for. Islands and regions cover\nground rather than sitting at a point, so they carry a wider search radius.","example":"city"},"label":{"type":"string","description":"Ready to render in a dropdown: the place, its region and its country.","example":"Paris, Île-de-France, France"},"latitude":{"type":"number","format":"double","description":"Where to search. Feed straight into a hotel search. For a city this is the city; for\nan island or a region it is where that place's population actually sits, which is\nnot its geometric centre.","example":48.85341},"longitude":{"type":"number","format":"double","example":2.3488},"name":{"type":"string","description":"The place's name in the requested language, falling back to its local name.","example":"Paris"},"population":{"type":["integer","null"],"format":"int64","description":"Absent for an airport.","example":2138551},"region":{"type":["string","null"],"description":"First-level administrative division — state, region, province.","example":"Île-de-France"},"search_radius_km":{"type":"number","format":"float","description":"The radius, in kilometres, that a hotel search around this point should use. Derived\nfrom how far the place's population spreads: about 5 km for a village, 20 for a big\ncity, 46 for Bali, 83 for Crete.","example":19.9},"timezone":{"type":["string","null"],"description":"IANA time zone.","example":"Europe/Paris"}}},"PricingDetails":{"type":"object","description":"What the stay costs and how that amount breaks down.","required":["total","currency","taxes"],"properties":{"currency":{"type":"string","description":"ISO 4217 code of every amount in this object.","example":"USD"},"suggested_selling_price":{"type":["string","null"],"description":"Display price to strike through. Present only when it exceeds `total`."},"taxes":{"type":"array","items":{"$ref":"#/components/schemas/Tax"},"description":"Taxes and fees making up part of `total`, or payable on top of it — see\n`included_in_price` on each entry."},"total":{"type":"string","description":"All-in amount for everything this offer covers, in `currency`.","example":"1284.00"}}},"Refund":{"type":"object","description":"Whether the airline permits it, and what it charges.","required":["allowed"],"properties":{"allowed":{"type":"boolean"},"penalty_amount":{"type":["string","null"],"description":"Fee charged for it. `null` when the airline quotes no fixed amount up front.","example":"100.00"},"penalty_currency":{"type":["string","null"],"description":"ISO 4217 code of `penalty_amount`.","example":"USD"}}},"RequiredInput":{"oneOf":[{"type":"object","description":"The provider repriced the item. Resume with `confirmed_price` set to `new` to accept it.","required":["old","new","currency","type"],"properties":{"currency":{"type":"string"},"new":{"type":"string"},"old":{"type":"string"},"type":{"type":"string","enum":["price_confirmation"]}}},{"type":"object","description":"Passenger or guest details are missing. Fill them in through the booking's own endpoint,\nthen resume.","required":["missing","type"],"properties":{"missing":{"type":"array","items":{"type":"string"}},"type":{"type":"string","enum":["traveller_details"]}}},{"type":"object","description":"The provider takes payment on its own page. Open this, pay, then poll again.","required":["payment_url","type"],"properties":{"payment_url":{"type":"string"},"type":{"type":"string","enum":["hosted_payment"]}}},{"type":"object","description":"The next item is charged from a card and the run is holding none.","required":["type"],"properties":{"type":{"type":"string","enum":["payment_method"]}}}],"description":"What the run needs before it can go on. The only place a payment URL is ever handed out."},"ReservationStatus":{"type":"string","description":"Where a booking stands. Only `pending` and `prepared` are in the cart; the rest have left it.","enum":["pending","prepared","booked","cancelled","abandoned"]},"ResumeProcessingRequest":{"type":"object","properties":{"confirmed_price":{"type":["string","null"],"description":"Accepts the new total a `price_confirmation` pause reported."},"payment_method":{"$ref":"#/components/schemas/PaymentMethodRequest","description":"Required whenever an item ahead still has to be charged - the card never survives a pause."},"run_id":{"type":["string","null"],"format":"uuid","description":"The run being resumed, from a previous snapshot. Rejected when the run has moved on."},"skip_item":{"type":"boolean","description":"Abandons the blocking item and carries on with the rest of the cart."}}},"RoomOccupancy":{"type":"object","description":"One physical room and the party sleeping in it.","required":["adults","children","children_ages"],"properties":{"adults":{"type":"integer","format":"int32","description":"Adults sharing this room; at least one.","example":2,"minimum":1},"children":{"type":"integer","format":"int32","description":"Children sharing this room. Rates are derived from `children_ages`, so send one age there\nfor every child counted here.","example":0,"minimum":0},"children_ages":{"type":"array","items":{"type":"integer","format":"int32","minimum":0},"description":"Age at check-in of each child in this room, one entry per child. Empty when there are none.","example":[]}}},"RoomOffer":{"type":"object","description":"One room within an offer.","required":["room_type","rate_plan","pricing","cancellation_policy","room_amenities","room_images","capacity","adults","bedding"],"properties":{"adults":{"type":"integer","format":"int32","description":"adults number comes from search request","minimum":0},"area_sqft":{"type":["integer","null"],"format":"int32","description":"Room size in square feet, when published.","minimum":0},"bedding":{"type":"string","description":"Bed configuration as described by the provider, e.g. `1 king bed`.","example":"1 king bed"},"cancellation_policy":{"$ref":"#/components/schemas/CancellationPolicy"},"capacity":{"type":"integer","format":"int32","description":"Maximum number of guests the room sleeps.","example":2,"minimum":0},"children_ages":{"type":["array","null"],"items":{"type":"integer","format":"int32","minimum":0},"description":"childrean ages comes from search request"},"description":{"type":["string","null"]},"flags":{"type":["integer","null"],"format":"int32","description":"Bitmask of board and lounge benefits for this room, read the same way as the offer's own\n`flags`: `1` breakfast, `2` lunch, `4` dinner, `8` all-inclusive, `16` lounge access."},"pricing":{"$ref":"#/components/schemas/PricingDetails"},"rate_plan":{"type":"string","description":"Commercial terms attached to the room, e.g. `Bed & Breakfast`.","example":"Bed & Breakfast"},"room_amenities":{"type":"array","items":{"type":"string"},"description":"Free-text amenity labels for the room itself."},"room_id":{"type":["string","null"],"description":"Provider room identifier. Rooms sharing an id and view are the same product, which is how\nidentical rooms are grouped for display."},"room_images":{"type":"array","items":{"type":"string"},"description":"Photo URLs of this room."},"room_type":{"type":"string","description":"Room name as sold, e.g. `Deluxe King`.","example":"Deluxe King"},"view":{"type":["string","null"],"description":"View the room faces, when published."}}},"RoomOffers":{"type":"object","description":"One bookable offer: every room of the requested party, priced together.","required":["offer_id","checkin","checkout","rooms","pricing","cancellation_policy"],"properties":{"cancellation_policy":{"$ref":"#/components/schemas/CancellationPolicy","description":"Cancellation rules, ordered by `from` in the property's timezone."},"checkin":{"type":"string","format":"date","description":"First night of the stay."},"checkout":{"type":"string","format":"date","description":"Departure day, exclusive."},"flags":{"type":["integer","null"],"format":"int32","description":"Bitmask of board and lounge benefits: `1` breakfast, `2` lunch, `4` dinner,\n`8` all-inclusive, `16` lounge access. Test single bits — all-inclusive does not set the\nindividual meal bits, and lounge access combines with them. `0` means room only; `null`\nmeans the provider said nothing about board, which is not the same as \"no meals\".","example":1},"offer_id":{"type":"string","format":"uuid","description":"Handle for the rest of the funnel — hotel rates and offer selection. Search-scoped: re-shop\nrather than storing it long term."},"payment_policy":{"oneOf":[{"type":"object","description":"The guest pays nothing upfront. The card is held as a guarantee against no-show or late\ncancellation; the property collects the full amount at check-in or check-out.","required":["type"],"properties":{"type":{"type":"string","enum":["pay_at_property"]}}},{"type":"object","description":"The property charges a deposit before arrival. `amount` is present once the rate has been\nprice-checked; at search time it may be `None` (the deposit is known to be required but\nthe exact sum has not been confirmed yet).","required":["type"],"properties":{"amount":{"type":["string","null"],"example":"150.00"},"type":{"type":"string","enum":["deposit"]}}},{"type":"object","description":"The full stay price is charged to the card at booking time.","required":["type"],"properties":{"type":{"type":"string","enum":["prepay"]}}}],"description":"How the property collects payment. `None` means the provider did not specify."},"pricing":{"$ref":"#/components/schemas/PricingDetails","description":"All-in price of the whole stay for all rooms."},"rooms":{"type":"array","items":{"$ref":"#/components/schemas/RoomOffer"},"description":"The rooms this offer covers, one entry per requested room."}}},"RoomOffersExt":{"allOf":[{"$ref":"#/components/schemas/RoomOffers"},{"type":"object","required":["free_cancellation"],"properties":{"cancellation_policy_description":{"type":["string","null"],"description":"Ready-to-render summary of `cancellation_policy`, in the requested language."},"free_cancellation":{"type":"boolean","description":"The offer still has a free-cancellation window open."}}}],"description":"A bookable offer plus cancellation fields derived for display. Serialised flat."},"RouteType":{"type":"string","description":"Whether an itinerary crosses a border, which decides if a passport is required.","enum":["domestic","international"]},"SendChatMessageRequest":{"type":"object","description":"What the traveller typed into the web chat.","required":["text"],"properties":{"text":{"type":"string","description":"The message, as typed. Trailing whitespace is trimmed and an empty message is rejected.","example":"Find me a hotel in Lisbon for the first week of May"}}},"StartProcessingRequest":{"type":"object","required":["payment_method"],"properties":{"language":{"type":["string","null"],"description":"Language for anything the traveller is sent while the run works. Defaults to `en`.","example":"en"},"payment_method":{"$ref":"#/components/schemas/PaymentMethodRequest"},"use_referral_points":{"type":["boolean","null"],"description":"Whether to spend the traveller's referral credit on items whose provider redeems it."}}},"Stop":{"type":"object","description":"A technical stop: the aircraft lands but the flight number does not change, so it is not a\nconnection and nobody changes planes.","required":["arriving_at","departing_at","airport"],"properties":{"airport":{"type":"string","description":"IATA code of the airport stopped at."},"arriving_at":{"type":"string","format":"date-time"},"departing_at":{"type":"string","format":"date-time"},"duration":{"type":["integer","null"],"format":"int32","description":"Time on the ground, in minutes."}}},"Tax":{"type":"object","description":"A single tax or fee line.","required":["tax_type","amount","mandatory","description","included_in_price"],"properties":{"amount":{"type":"string","description":"Charge in the offer's currency.","example":"112.35"},"description":{"type":"string","description":"Human readable label; may be empty, in which case fall back to a generic \"Tax & Fee\"."},"included_in_price":{"type":"boolean","description":"`true` when the charge is already inside `total`, `false` when it is collected at the\nproperty on top of it."},"mandatory":{"type":"boolean","description":"The guest cannot opt out of this charge."},"percentage":{"type":["string","null"],"description":"Rate the charge was computed at, when the provider expresses it as a percentage."},"tax_type":{"type":"string","description":"Provider's classification of the charge, e.g. `VAT` or `CITY_TAX`.","example":"VAT"}}},"Transmission":{"type":"string","enum":["Manual","Automatic"]},"TravelerRole":{"type":"string","description":"What a traveller is to their booking. `Guest` and `Passenger` are the same standing as each\nother, named as hotels and airlines name them.","enum":["lead","guest","passenger"]},"UpdateCartRequest":{"type":"object","properties":{"current":{"type":["boolean","null"],"description":"Set to `true` to make this the traveller's current cart."},"title":{"type":["string","null"],"description":"New title. Send `null` to clear it; omit the field to leave it alone.","example":"Rome in May"}}},"UpdateCompanionRequest":{"type":"object","description":"Every field is optional: omitting one leaves it as stored. The fields that can hold nothing\nalso accept an explicit `null`, which clears them.","properties":{"dob":{"type":["string","null"],"format":"date","example":"1990-12-10"},"document":{"$ref":"#/components/schemas/PassengerDocument"},"email":{"type":["string","null"],"example":"ada@example.com"},"first_name":{"type":["string","null"],"example":"Ada"},"gender":{"type":["string","null"],"enum":["male","female",null]},"last_name":{"type":["string","null"],"example":"Lovelace"},"middle_name":{"type":["string","null"]},"nationality":{"type":["string","null"],"example":"GB"},"phone":{"type":["string","null"],"example":"+1 415 555 0134"}}},"VehicleClassification":{"type":"object","description":"Decoded 4-letter ACRISS (SIPP) vehicle code, e.g. `ECAR` = Economy, 2/4 door, automatic, AC.\nLetters outside the ACRISS tables decode to `None` rather than failing the whole code.","required":["code"],"properties":{"air_conditioning":{"type":["boolean","null"]},"body_type":{"type":["string","null"],"enum":["TwoThreeDoor","TwoFourDoor","FourFiveDoor","Wagon","PassengerVan","Limousine","Sport","Convertible","Suv","OpenAirAllTerrain","Special","PickupRegularCab","PickupExtendedCab","SpecialOffer","Coupe","Monospace","RecreationalVehicle","MotorHome","TwoWheelVehicle","Roadster","Crossover","CommercialVan",null]},"category":{"type":["string","null"],"enum":["Mini","MiniElite","Economy","EconomyElite","Compact","CompactElite","Intermediate","IntermediateElite","Standard","StandardElite","Fullsize","FullsizeElite","Premium","PremiumElite","Luxury","LuxuryElite","Oversize","Special",null]},"code":{"type":"string","example":"ECAR"},"drive":{"type":["string","null"],"enum":["Unspecified","FourWheel","AllWheel",null]},"fuel":{"type":["string","null"],"enum":["Unspecified","Diesel","Hybrid","Electric","Lpg","Hydrogen","MultiFuel","Petrol","Ethanol",null]},"transmission":{"type":["string","null"],"enum":["Manual","Automatic",null]}}},"ViaPlace":{"type":"object","description":"The place a suggestion was reached through, when it was not matched directly.","required":["geoname_id","name","distance_km"],"properties":{"distance_km":{"type":"number","format":"float","description":"Great-circle kilometres from the place to the airport.","example":84.5},"geoname_id":{"type":"integer","format":"int64","description":"GeoNames id, the same one `/api/v1/place/autocomplete` returns.","example":2775220},"name":{"type":"string","example":"Ischgl"}}},"WebChatAction":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/HotelSearchAction"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["hotel_search"]}}}],"title":"hotel_search"},{"allOf":[{"$ref":"#/components/schemas/FlightSearchAction"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["flight_search"]}}}],"title":"flight_search"},{"allOf":[{"$ref":"#/components/schemas/CarSearchAction"},{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["car_search"]}}}],"title":"car_search"}],"description":"What a web client should put on screen after the assistant searched something for it.\n\nA messenger gets a link to a results page. A browser already sitting on the portal is told what\nwas searched instead and runs it again there: offers listed under the portal's own session are\nthe ones it can select into the cart.","discriminator":{"propertyName":"type","mapping":{"car_search":"#/components/schemas/CarSearchAction","flight_search":"#/components/schemas/FlightSearchAction","hotel_search":"#/components/schemas/HotelSearchAction"}}}},"securitySchemes":{"app_id":{"type":"apiKey","in":"header","name":"X-App-Id","description":"UUID of the client application, issued when your integration is registered."},"bearer_auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"RS256 JWT identifying the end user, sent as `Authorization: Bearer <token>`."}}},"tags":[{"name":"Place","description":"Place lookup for choosing where to search"},{"name":"Airport","description":"Airport and city lookup for choosing where to fly"},{"name":"Hotel","description":"Hotel search, rates, booking and cancellation"},{"name":"Flight","description":"Flight search, booking and cancellation"},{"name":"Car","description":"Car rental branch lookup, availability search and selection"},{"name":"Cart","description":"Selected offers held for checkout"},{"name":"Cart Processing","description":"Booking every reservation in a cart"},{"name":"Bookings","description":"Reservations the traveller has settled"},{"name":"Chat","description":"Conversation with the travel assistant"},{"name":"Companion","description":"Saved travellers to book for"},{"name":"Travelers","description":"Assignment of saved travellers to cart items"}]}