Adrasis™

The booking flow

The full booking lifecycle end to end: content, search, prebook, book, poll for terminal status, and the two-phase cancel.

A complete booking moves through a fixed lifecycle:

content  →  search  →  prebook  →  book  →  (poll bookings/{id})  →  cancel
  • content: your locally cached catalogue (see Property content).
  • search: exact top-1 comparison across properties, or a whole-party winner and complete room product lists for one property.
  • prebook: revalidate the exact slots sealed by the chosen selection_token, then pin price + cancellation policy behind a short-lived token.
  • book: finalize the reservation idempotently.
  • poll: read the booking back to resolve any non-terminal status.
  • cancel: quote the penalty, then cancel.

All booking endpoints require the distribution:booking scope, except the three accommodation search routes, which require distribution:read. The product-vertical endpoints (transfer / tour / flight / trip / package) require distribution:booking throughout, including their search steps.

The accommodation lifecycle uses search → prebook → book. The other product verticals use the same sequence, idempotency, polling, and two-phase cancel on their own path prefixes:

  • Transfers: POST /api/v1/transfer/{search,prebook,book}
  • Tours: POST /api/v1/tour/{search,prebook,book}
  • Flights: POST /api/v1/flight/{search,prebook,book}

Bookings of any kind can then be grouped into one itinerary. See Trip bundling.

For push notification of committed lifecycle transitions, configure booking webhooks. Webhooks are account-owned and carry the immutable originating profile public ID for attribution.

Use bulk search to compare properties:

POST /api/v1/properties/availability

property_ids contains 1..200 unique IDs. The response preserves request order and returns exactly the best zero-or-one allocation for each property. With max_offers_per_property (1..250, or "all" for every offer) each result also lists its cheapest offers in offers, and every payment option of an offer carries its own selection_token for this flow (Offers per property). Add include with cancellation_quote, selectable_quantity or both to get each slot's cancellation terms and each allocation's rooms left in the same answer (Cancellation terms and rooms left). Bulk does not accept include_alternatives, max_alternatives, max_offers, or limit.

curl -s -X POST \
  https://ari.console.adrasis.com/api/v1/properties/availability \
  -H 'Authorization: Bearer eyJ...' \
  -H 'Content-Type: application/json' \
  -d '{
    "property_ids": ["72259"],
    "checkin": "2026-06-10",
    "checkout": "2026-06-13",
    "occupancy": [{ "adults": 2, "child_ages": [7] }],
    "allocation_strategy": "flexible",
    "currency_code": "TRY",
    "country_code": "TR"
  }'

allocation_strategy is required. strict preserves 1..9 ordered room slots; flexible requires one aggregate party that can be composed across up to nine rooms. There is no auto value. Currency, occupancy, and ranking are covered in Currencies, occupancy & allocations.

{
  "currency_code": "TRY",
  "request_time": "2026-06-02T12:00:00Z",
  "selection_token_expires_at": "2026-06-02T12:15:00Z",
  "results": [
    {
      "property_id": "72259",
      "allocations": [
        {
          "allocation_id": "alc_5f3a9c1d8e7b4a2f6c0d9e8b7a6f5c4d",
          "selection_token": "sel1.active.<opaque>",
          "rooms": 1,
          "strategy": "single-room",
          "grand_total": "9000.00",
          "grand_total_before_promotion": "9000.00",
          "currency_code": "TRY",
          "slots": [
            {
              "room_public_id": "r3Tz9Lm0",
              "rate_plan_public_id": "rp_bar",
              "currency_code": "TRY",
              "meal_plan_code": "BB",
              "stay_total": "9000.00",
              "status": "AVAILABLE"
            }
          ]
        }
      ]
    }
  ]
}

In a slot, room_public_id and rate_plan_public_id together name what is offered. The rate plan is a sale condition shared by the rooms it is sold in, so the same rate_plan_public_id appears for every room that sells it and the pair is the product you prebook and book.

allocations[0] has the lowest exact accommodation amount, with room count and stable ordering for otherwise equal results. Its grand_total also includes exclusive tax collected with the booking; this can affect which allocation has the lowest payable total. See Taxes in the payable total. Per-property failures, including property, FX, work-limit, timeout, and server failures, stay inline on results[i].error_code.

A property that returns no allocation because of the party itself carries an unavailable_reason instead of an error_code. GUEST_AGE_NOT_ACCEPTED means a requested child is younger than the youngest age the property accepts. Read min_guest_age from Property content to show that rule before a search, and see Why a property returns no allocation for the full list.

For SEARCH_TIMEOUT, follow the timeout-only retry contract: keep completed rows, reuse the first response's request_time, and retry the remaining timed-out property IDs. If a retry makes no progress, partition that remaining ordered set into smaller batches. Set request_time on the initial bulk request when possible; this also preserves one evaluation instant if the first attempt returns top-level 503 SEARCH_TIMEOUT with no rows.

On an initial availability request, a client-supplied request_time may be at most 30 days behind or 60 seconds ahead of the server clock; both boundaries are inclusive. Outside the inclusive [server_time - 30 days, server_time + 60 seconds] window the whole request fails with top-level 400 INVALID_REQUEST and field=request_time; no per-property row is returned. A valid place-availability cursor keeps its fixed first-page instant usable until the cursor expires, even if the server clock crosses the initial lower boundary during the walk.

Use detail only when a traveller needs choices for one property:

POST /api/v1/properties/{property_public_id}/offers/search

The detail body contains the same stay and occupancy context but no property_ids. It returns best_allocation, the bulk winner for the same context, and room_offers, every sellable room and rate-plan pair for each requested occupancy. Strict requests keep one list per slot in request order. Flexible requests keep one list for the aggregate party in one room, which may be empty while the best allocation spans several rooms. There is no count cap or pagination.

Optional include: ["cancellation_quote"] attaches current terms to each slot. Every detail slot also carries one nights[] pricing row per stay night. Use best_allocation.selection_token to book the whole party, or pass the chosen one-room tokens as selection_tokens to build your own room selection. allocation_id remains stable identity for display and correlation, but it is not booking authority.

Search a whole place

To search a country or a destination such as Antalya instead of a list of properties, use place search:

POST /api/v1/places/availability

place_id is the Adrasis place id of a place from GET /places, or a country's ISO code from GET /countries ("TR" searches Turkey). Each result has the bulk shape: the best zero-or-one allocation per property. currency_code and the traveller's country_code are required, as are the stay, the occupancy and allocation_strategy. limit (1..100, default 100) sets the page size.

Place search runs in one of two modes, chosen by the request.

Catalogue order Sorted or filtered
Chosen by Neither sort nor filters sort, filters, or both
Pricing Only the page you ask for Every property in the place, once, then kept for the quote window
Order Catalogue order The sort you chose; price_asc when you send filters alone
Totals None page.matched and page.unavailable on every page
has_more More catalogue candidates, not more properties with an allocation More rows in the sorted set

In catalogue order a page can hold properties with no allocation, so walk until has_more is false and keep the rows that have one.

In sorted or filtered mode the first page prices the whole place, so it takes longer than later pages. Moving to the next page, changing the sort, or changing the filters reads the kept set and does not price again. A place with more than 10,000 visible properties, or more than 1,000 still requiring a price after unavailable properties are ruled out, answers 400 PLACE_TOO_BROAD. The second count is not a guarantee that every remaining property will return an offer. Add property-level filters such as stars, property_type_codes or place_ids to bring the search under both limits, or search a smaller place. Price, board and refundability filters do not reduce these limits because they are applied after pricing.

sort accepts price_asc, price_desc, star_desc, star_asc and guest_score_desc, and distance_asc for a search around a point (see Search around a point). Unrated properties follow rated ones in both star orders, and unscored properties follow scored ones for the guest score order. Ties are broken by price, then in a stable order.

filters combines groups; values inside one group are alternatives. Each list takes at most 20 values, and an unknown code or a sub-place outside the searched place is 400 INVALID_REQUEST naming the field.

Filter Meaning
stars Star ratings from 0 to 5; 0 selects unrated properties
property_type_codes, theme_codes Codes from the reference lists
amenity_codes An amenity group code (amenity.group.*) or a single amenity code
place_ids Adrasis place ids of sub-places inside the searched place
bounds A map viewport (min_latitude, min_longitude, max_latitude, max_longitude) of at most 5 degrees per side
min_guest_score Guest score on the 10-point scale; unscored properties do not match
price_min, price_max Whole-stay price in the requested currency
board_codes Meal plan codes. A property matches when one of its room products has one, and the row shows the cheapest matching offer
refundable_only Keeps properties with a refundable offer in their full room list, and shows the cheapest refundable one

page.matched is the number of available properties in the whole place after filters, the same on every page, so it gives the result count for a heading. page.unavailable counts the properties without availability or that could not be priced. Unless a price, board or refundability filter is set, they follow the matches, and each carries its unavailable_reason when the party is the cause. The response echoes the applied sort.

Send include_facets: true to add facets to the first page: counts of available properties per filter option (stars, property_types, themes, amenities, places, guest_scores, boards, refundable, and the shown price range). A group's counts apply every other group's selection and ignore its own, so each count says what selecting that option would return. A group lists its options busiest first, then by label in the request language, except stars (5 down to 0) and guest_scores (9 down to 6).

curl -s -X POST \
  https://ari.console.adrasis.com/api/v1/places/availability \
  -H 'Authorization: Bearer eyJ...' \
  -H 'Content-Type: application/json' \
  -d '{
    "place_id": "87178",
    "checkin": "2026-06-10",
    "checkout": "2026-06-13",
    "occupancy": [{ "adults": 2, "child_ages": [7] }],
    "allocation_strategy": "flexible",
    "currency_code": "EUR",
    "country_code": "DE",
    "sort": "price_asc",
    "filters": { "stars": [4, 5], "board_codes": ["BB"], "refundable_only": true },
    "include_facets": true,
    "limit": 20
  }'

Continue with page.next_cursor until has_more is false. The cursor alone is enough: send { "cursor": "<next_cursor>" }. It carries the stay, the filters and the evaluation instant, so request_time can be omitted on every page. Any field you repeat must equal the first request, otherwise the answer is 400 INVALID_CURSOR. The kept set expires with the quote window; a continuation after that answers 409 SEARCH_EXPIRED, and you search again from the first page. 409 CATALOG_VERSION_CHANGED means the catalogue changed during the walk, with the same remedy. Both codes are in Errors.

Search around a point

To find hotels near an airport, a landmark or a point on a map, send near instead of place_id. Name the point either by latitude and longitude, or by the Adrasis place id of any place that has a point, an airport or a landmark included. radius_km is required, from 0.1 to 50.

curl -s -X POST \
  https://ari.console.adrasis.com/api/v1/places/availability \
  -H 'Authorization: Bearer eyJ...' \
  -H 'Content-Type: application/json' \
  -d '{
    "near": { "place_id": "84117", "radius_km": 15 },
    "checkin": "2026-06-10",
    "checkout": "2026-06-13",
    "occupancy": [{ "adults": 2, "child_ages": [] }],
    "allocation_strategy": "flexible",
    "currency_code": "EUR",
    "country_code": "DE",
    "limit": 20
  }'

The candidates are the properties you can sell that lie within radius_km of the point, measured along the surface of the earth. Everything else works as in a sorted place search: the first page prices every property in the radius once and keeps the set for the quote window, filters and include_facets apply, and page.matched and page.unavailable count the result. Each row carries distance_km, rounded to one decimal.

Order distance_asc, nearest first, is the default. Ties are broken by price, then in a stable order. Properties without availability follow the matches, nearest first. You can send another sort; rows still carry distance_km.
place in the response The place that supplied the point, or null when you sent coordinates
Facets As in a place search, except facets.places, which is empty: a radius has no districts of its own to count.
Size The same 10,000-property and 1,000-property limits apply inside the radius. Exceeding either answers 400 PLACE_TOO_BROAD with field=near.radius_km. Reduce the radius or add property-level filters.

Send exactly one of place_id and near; both or neither is 400 INVALID_REQUEST, and so is distance_asc without near. A near.place_id that is unknown answers 404 PLACE_NOT_FOUND, and one without a point, a country for one, answers 400 PLACE_TYPE_UNSUPPORTED, both with field=near.place_id. The cursor binds the point and the radius: repeating a different near on a continuation answers 400 INVALID_CURSOR, and the cursor alone is enough.

Step 2: Prebook

POST /api/v1/prebook

Revalidates the selected allocation against current availability and mints a short-lived token that pins price and cancellation policy for the subsequent book call. The server authenticates the slots and price-affecting context in selection_token and revalidates the selected offer. Inventory is not held at this step. Availability is checked again at book time, and a lost race surfaces as 410 on /book. Prebook takes no Idempotency-Key; each retry mints a fresh token.

curl -s -X POST \
  https://ari.console.adrasis.com/api/v1/prebook \
  -H 'Authorization: Bearer eyJ...' \
  -H 'Content-Type: application/json' \
  -d '{
    "selection_token": "sel1.active.<opaque>",
    "quoted": { "grand_total": "9000.00", "currency_code": "TRY" }
  }'

quoted is optional. When supplied, every value must equal the visible quote sealed in the proof before current repricing begins. Omitting it does not disable quote verification. Search context is carried only by selection_token or selection_tokens; additional request fields are rejected.

For a custom selection, send selection_tokens instead of selection_token. The list takes one to nine one-room proofs from room_offers, all for the same property, stay and request context. Repeating a token means one additional physical room of that product. Each room keeps its own plan and cancellation terms in one booking. quoted.grand_total, when provided, is the sum of the selected unit grand_total values, including their exclusive taxes and repeats.

curl -sS -X POST \
  https://ari.console.adrasis.com/api/v1/prebook \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "selection_tokens": ["sel1.active.<breakfast-room>", "sel1.active.<room-only>"],
    "quoted": { "grand_total": "18000.00", "currency_code": "TRY" }
  }'

Products can share the same nightly inventory. Prebook checks the aggregate quantity and book consumes it atomically. Combining pay-at-property and prepaid rooms returns 422 MIXED_SETTLEMENT on selection_tokens. Book, idempotent replay and cancellation use the normal booking endpoints.

A value that does not match returns 400 with code QUOTED_MISMATCH and field naming the member at fault, quoted.grand_total or quoted.currency_code. The proof itself is untouched by this, so the fix is to correct the quoted values or drop the block, not to re-search.

Send the grand_total the search response published. It includes exclusive tax collected on top of the room prices. The room totals alone leave that tax out; do not reconstruct the quote from them or add tax to grand_total again. Prebook returns the payable offer.grand_total and, when tax applies, the tax-only tax_total_inclusive and tax_total_exclusive breakdowns.

{
  "match_status": "MATCHED",
  "prebook_token": "<PREBOOK_TOKEN>",
  "prebook_expires_at": "2026-06-02T12:30:00Z",
  "offer": {
    "allocation_id": "alc_5f3a9c1d8e7b4a2f6c0d9e8b7a6f5c4d",
    "property_public_id": "72259",
    "allocation_strategy": "single-room",
    "allocation_rooms": 1,
    "grand_total": "9000.00",
    "grand_total_before_promotion": "9000.00",
    "currency_code": "TRY",
    "checkin_date": "2026-06-10",
    "checkout_date": "2026-06-13",
    "nights": 3,
    "adults_total": 2,
    "children_total": 1,
    "infants_total": 0,
    "status": "AVAILABLE",
    "is_instant_confirm": true,
    "rtb_confirm_by_mins": 0,
    "cancellation_policy": {
      "is_non_refundable": false,
      "non_refundable_after": "2026-06-08T00:00:00Z",
      "currency_code": "TRY",
      "finalized": true,
      "tiers": [{ "from": "2026-06-08T00:00:00Z", "fee": "4500.00", "currency_code": "TRY" }]
    },
    "rooms": [{
      "slot_index": 0,
      "room_public_id": "r3Tz9Lm0",
      "rate_plan_public_id": "rp_bar",
      "occupancy_adults": 2,
      "children": 1,
      "child_ages": [7],
      "infants": 0,
      "room_total": "9000.00",
      "cancellation_policy": {
        "is_non_refundable": false,
        "non_refundable_after": "2026-06-08T00:00:00Z",
        "currency_code": "TRY",
        "finalized": true,
        "tiers": [{ "from": "2026-06-08T00:00:00Z", "fee": "4500.00", "currency_code": "TRY" }]
      }
    }]
  },
  "cancellation_policy": {
    "is_non_refundable": false,
    "non_refundable_after": "2026-06-08T00:00:00Z",
    "currency_code": "TRY",
    "finalized": true,
    "tiers": [{ "from": "2026-06-08T00:00:00Z", "fee": "4500.00", "currency_code": "TRY" }]
  },
  "warnings": [],
  "trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e"
}

The custom selection returns one booking quote with both room products:

{
  "match_status": "MATCHED",
  "prebook_token": "<PREBOOK_TOKEN>",
  "prebook_expires_at": "2026-06-02T12:30:00Z",
  "offer": {
    "allocation_id": "alc_00000000000000000000000000000001",
    "property_public_id": "72259",
    "allocation_strategy": "multi-room-split",
    "allocation_rooms": 2,
    "grand_total": "18000.00",
    "grand_total_before_promotion": "18000.00",
    "currency_code": "TRY",
    "checkin_date": "2026-06-10",
    "checkout_date": "2026-06-13",
    "nights": 3,
    "adults_total": 4,
    "children_total": 2,
    "infants_total": 0,
    "status": "AVAILABLE",
    "is_instant_confirm": true,
    "rtb_confirm_by_mins": 0,
    "cancellation_policy": {
      "is_non_refundable": false,
      "non_refundable_after": "2026-06-08T00:00:00Z",
      "currency_code": "TRY",
      "finalized": true,
      "tiers": [
        {
          "from": "2026-06-08T00:00:00Z",
          "fee": "9000.00",
          "currency_code": "TRY"
        }
      ]
    },
    "rooms": [
      {
        "slot_index": 0,
        "room_public_id": "r3Tz9Lm0",
        "rate_plan_public_id": "rp_bar",
        "occupancy_adults": 2,
        "children": 1,
        "child_ages": [
          7
        ],
        "infants": 0,
        "room_total": "9000.00",
        "cancellation_policy": {
          "is_non_refundable": false,
          "non_refundable_after": "2026-06-08T00:00:00Z",
          "currency_code": "TRY",
          "finalized": true,
          "tiers": [
            {
              "from": "2026-06-08T00:00:00Z",
              "fee": "4500.00",
              "currency_code": "TRY"
            }
          ]
        }
      },
      {
        "slot_index": 1,
        "room_public_id": "5bR4mTp8",
        "rate_plan_public_id": "6kR4mTp8",
        "occupancy_adults": 2,
        "children": 1,
        "child_ages": [
          7
        ],
        "infants": 0,
        "room_total": "9000.00",
        "cancellation_policy": {
          "is_non_refundable": false,
          "non_refundable_after": "2026-06-08T00:00:00Z",
          "currency_code": "TRY",
          "finalized": true,
          "tiers": [
            {
              "from": "2026-06-08T00:00:00Z",
              "fee": "4500.00",
              "currency_code": "TRY"
            }
          ]
        },
        "meal_plan_code": "RO"
      }
    ]
  },
  "cancellation_policy": {
    "is_non_refundable": false,
    "non_refundable_after": "2026-06-08T00:00:00Z",
    "currency_code": "TRY",
    "finalized": true,
    "tiers": [
      {
        "from": "2026-06-08T00:00:00Z",
        "fee": "9000.00",
        "currency_code": "TRY"
      }
    ]
  },
  "warnings": [],
  "trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e"
}

match_status

match_status compares the revalidated allocation with the allocation returned by search. Inspect it before booking:

match_status Meaning What to do
MATCHED The allocation is still available at the quoted price and currency. Proceed to /book with the returned token.
PRICE_CHANGED The stay total moved (either direction). Re-confirm the new price with the end traveller, then /book.
SOLD_OUT The offer is no longer available. Delivered as an HTTP 410 error response (code SOLD_OUT), not as a 200 body. Search again.

The cancellation_policy block is the finalized policy (finalized: true). These exact terms will be snapshotted onto the booking. The token (prebook_token) is valid for 30 minutes (prebook_expires_at); booking after it expires returns a 409 with code PREBOOK_EXPIRED.

The token is single-use: it is a quote for one stay. Once it has opened a booking, presenting it again returns a 409 with code PREBOOK_TOKEN_ALREADY_USED, and a different Idempotency-Key does not change that. To sell the same room twice, call prebook twice. Each call returns its own token, and each token can be consumed by a separate /book call. For a request that timed out, repeat it with the same Idempotency-Key. The retry returns the original booking rather than a conflict.

Treat the token as a credential rather than as data: it is opaque, its format is not part of this contract, and it carries the pinned offer. Pass it back to book unchanged, keep it server-side, and do not write it to a log in full.

A selection proof is accepted only while the property is still open to the profile that searched. When the seller has closed the property to that profile since the search, prebook returns 409 SELECTION_RESELECT_REQUIRED with field=selection_token: search again and prebook from the new results.

Guest age at prebook

Prebook reprices the sealed slots against the property's current guest age rules. When a child in the sealed party is younger than the youngest age the property now accepts, for example because the property raised its minimum guest age after your search, prebook answers 422 with code GUEST_AGE_NOT_ACCEPTED and retryable: false. It is not SOLD_OUT: no inventory change makes the party acceptable. Searching again with the same party returns the property as unavailable with the same unavailable_reason, so offer the traveller another property or a party the property accepts. Package prebook answers the same way when its accommodation component no longer accepts a child of the party.

The rule is checked at prebook. The prebook_token pins the offer as it was prebooked, so book does not check the guest age rule again while the token is valid.

Step 3: Book

POST /api/v1/book

Creates the booking from the prebook token under its accepted price and cancellation terms. The Idempotency-Key header is mandatory. See Idempotency for the full semantics.

How the stay is settled

You are the merchant of record: you collect from your guest, and this API collects no money. Every stay you book is invoiced to your account under your payment terms with the seller and confirmed, and you settle with the seller on account, monthly or weekly as your terms say. That holds whatever the rate's own terms for the seller's guests are, a rate the seller's own guests pay at the property included: the guest of a stay you sold pays you, never the hotel. An account that does not settle on account with the seller cannot book; prebook and /book answer 422 PAYMENT_INTENT_NOT_SUPPORTED before any token is issued or inventory held. Ask the seller to record payment terms for your account.

payment_intent_kind on the request body is optional, and omitting it is the normal case. It may confirm how the stay settles, never change it: INVOICE is accepted, and any other value returns 422 PAYMENT_INTENT_NOT_SUPPORTED, with field set to payment_intent_kind when the value is PAY_AT_PROPERTY. The request writes nothing and holds no inventory.

Each search offer exposes policy_card.payment_intent_kind: 1 is prepaid, 2 is pay at the property, 3 is deposit plus balance, 4 is virtual card and 5 is invoice. It says how the seller's own guests pay for the rate, not how you do, and it is always resolved: a rate plan without its own preference reports the property's default.

curl -s -X POST \
  https://ari.console.adrasis.com/api/v1/book \
  -H 'Authorization: Bearer eyJ...' \
  -H 'Idempotency-Key: 3b2a1c0d-5e6f-4a8b-9c0d-1e2f3a4b5c6d' \
  -H 'Content-Type: application/json' \
  -d '{
    "prebook_token": "<PREBOOK_TOKEN>",
    "contact": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "[email protected]"
    },
    "guests": [[
      { "kind": "ADULT", "first_name": "Ada", "last_name": "Lovelace", "is_lead": true }
    ]],
    "agency_reference": "your-ref-12345"
  }'

guests is the rooming list: one per-room list, aligned with the prebooked allocation's slots. Send [] to book without one, since contact is the identity of record. A supplied list has to describe the rooms the token froze, so its length must equal the room count, each room must name at least its lead traveller, and a room cannot name more people than it was priced for. Anything else returns 400 with field naming the room at fault. agency_reference is your own booking or order reference. It is an exact, case-sensitive value of 1..128 Unicode characters, without surrounding whitespace or control characters. It is permanently unique inside your authenticated partner account, across all of its profiles, and is echoed on every response that carries the booking envelope. A duplicate returns 409 AGENCY_REFERENCE_CONFLICT. On success you receive 201 Created:

{
  "booking": {
    "public_id": "bk_8H2nQ4pZ",
    "booking_number": 100482,
    "status": "CONFIRMED",
    "stay_total": "9000.00",
    "tax_total_exclusive": "0.00",
    "tax_total_payable_at_property": "0.00",
    "currency_code": "TRY",
    "checkin_date": "2026-06-10",
    "checkout_date": "2026-06-13",
    "nights": 3,
    "agency_reference": "your-ref-12345",
    "cancellation_policy_snapshot": {
      "is_non_refundable": false,
      "non_refundable_after": "2026-06-08T00:00:00Z",
      "currency_code": "TRY",
      "finalized": true,
      "tiers": [
        {
          "from": "2026-06-08T00:00:00Z",
          "fee": "4500.00",
          "currency_code": "TRY"
        }
      ]
    },
    "rooms": [
      {
        "room_public_id": "r3Tz9Lm0",
        "rate_plan_public_id": "rp_bar",
        "occupancy_adults": 2,
        "children": 1,
        "infants": 0,
        "child_ages": [
          7
        ],
        "room_total_amount": "9000.00"
      }
    ],
    "guests": [
      [
        {
          "kind": "ADULT",
          "first_name": "Ada",
          "last_name": "Lovelace",
          "is_lead": true
        }
      ]
    ],
    "items": [
      {
        "public_id": "bki_2mF7qR4x",
        "product_type": 1,
        "sequence": 0,
        "is_mandatory": true,
        "status": "CONFIRMED",
        "currency_code": "TRY",
        "selling_rate_amount": "9000.00",
        "service_start_date": "2026-06-10",
        "service_end_date": "2026-06-13",
        "realized_penalty_amount": "0.00"
      }
    ],
    "active_item_count": 1,
    "active_selling_rate_total_amount": "9000.00",
    "realized_penalty_total_amount": "0.00",
    "contact": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "[email protected]"
    }
  },
  "trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e"
}

rooms repeats, for each booked room, the room and the rate plan it was booked under. Two rooms of one booking can carry the same rate_plan_public_id, because a rate plan is shared by the rooms it is sold in.

Status codes on book

Code Meaning
201 Booking finalized synchronously.
200 Idempotent replay of an earlier finalized booking.
202 Booking finalized synchronously; Prefer: respond-async requested a response carrying poll_url.

Prefer: respond-async returns a poll_url for the booking created by this request. Poll step 4 whenever booking.status is still resolving. The partner-observable statuses that mean "not settled yet" are FINALIZED (sold and held, awaiting payment confirmation), RTB_PENDING (a request-to-book rate awaiting the property's acceptance), and SUSPENDED. Full cancellation returns its outcome directly; do not poll for PENDING_CANCEL.

The API request timeout is 50 seconds. A timeout can return an API error or close the connection without a response. If the client sees a transport timeout, an ambiguous connection failure, or 503 BOOKING_TIMEOUT, resend the exact request body with the exact same Idempotency-Key. The idempotent replay returns the one stored booking and cannot create a second booking.

Step 4: Read and reconcile bookings

Exact partner-reference lookup

GET /api/v1/bookings?agency_reference=your-ref-12345

The authenticated partner account comes from the credential; it is not a query parameter. The lookup is an exact, case-sensitive match across that account. A miss is 200 OK with items: [], never a cross-account result. Collection rows intentionally contain no contact or guest PII and report the immutable profile that originated the booking:

{
  "items": [{
    "public_id": "bk_8H2nQ4pZ",
    "profile_public_id": "profile000000001",
    "booking_number": 100482,
    "agency_reference": "your-ref-12345",
    "status": "CONFIRMED",
    "product_type": 1,
    "property_public_id": "72259",
    "checkin_date": "2026-06-10",
    "checkout_date": "2026-06-13",
    "created_at": "2026-06-02T12:28:14Z",
    "updated_at": "2026-06-02T12:28:15Z"
  }],
  "next_cursor": null,
  "has_more": false,
  "upper_watermark": null,
  "retry_after_seconds": null,
  "trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e"
}

Add profile_public_id=<16-character-profile-public-id> only when you need to narrow reconciliation to one originating profile. This is a reporting filter, not catalogue selection: it can match bookings from a profile that was later disabled. An unknown or cross-account value returns 404 PARTNER_PROFILE_NOT_FOUND; a temporary profile lookup failure returns retryable 503 PARTNER_PROFILE_UNAVAILABLE.

Created, stay-date, and delta lookup

Use complete half-open windows, each no longer than 31 days:

GET /api/v1/bookings?created_from=2026-06-01T00:00:00Z&created_to=2026-07-01T00:00:00Z
GET /api/v1/bookings?checkin_from=2026-06-01&checkin_to=2026-07-01
GET /api/v1/bookings?updated_since=2026-06-01T00:00:00Z

product_type may narrow any lookup (1 accommodation, 2 transfer, 3 tour, 4 flight, 5 package). limit defaults to 50 and is capped at 200. When has_more is true, repeat every original filter and pass next_cursor as cursor; the cursor is opaque and must not be decoded or changed. An invalid or obsolete cursor returns 400 INVALID_REQUEST with field=cursor. Restart the same lookup from its first page.

Delta mode reads [updated_since, upper_watermark) with a 60-second safety lag. Complete every cursor page, then persist upper_watermark as the next run's updated_since. If the interval cannot advance safely yet, the API returns no items and retry_after_seconds: 60.

Read one booking

GET /api/v1/bookings/{public_id}

Reads the persisted booking by its public_id. Use it to verify a booking settled after /book returns, and to resolve any booking left in RTB_PENDING or FINALIZED. There is no partner-facing confirm/decline action. The host decides through Console or the AI Operator; polling reports the result. Acceptance of a request moves it directly from RTB_PENDING to CONFIRMED. A booking.rtb.accepted webhook carries that confirmed status; do not wait for a second booking.confirmed event. Payment-dependent Console bookings can remain FINALIZED until settlement confirms them.

curl -s \
  https://ari.console.adrasis.com/api/v1/bookings/bk_8H2nQ4pZ \
  -H 'Authorization: Bearer eyJ...'
{
  "booking": {
    "public_id": "bk_8H2nQ4pZ",
    "booking_number": 100482,
    "status": "CONFIRMED",
    "stay_total": "9000.00",
    "tax_total_exclusive": "0.00",
    "tax_total_payable_at_property": "0.00",
    "currency_code": "TRY",
    "checkin_date": "2026-06-10",
    "checkout_date": "2026-06-13",
    "nights": 3,
    "agency_reference": "your-ref-12345",
    "cancellation_policy_snapshot": {
      "is_non_refundable": false,
      "non_refundable_after": "2026-06-08T00:00:00Z",
      "currency_code": "TRY",
      "finalized": true,
      "tiers": [
        {
          "from": "2026-06-08T00:00:00Z",
          "fee": "4500.00",
          "currency_code": "TRY"
        }
      ]
    },
    "rooms": [
      {
        "room_public_id": "r3Tz9Lm0",
        "rate_plan_public_id": "rp_bar",
        "occupancy_adults": 2,
        "children": 1,
        "infants": 0,
        "child_ages": [
          7
        ],
        "room_total_amount": "9000.00"
      }
    ],
    "guests": [
      [
        {
          "kind": "ADULT",
          "first_name": "Ada",
          "last_name": "Lovelace",
          "is_lead": true
        }
      ]
    ],
    "items": [
      {
        "public_id": "bki_2mF7qR4x",
        "product_type": 1,
        "sequence": 0,
        "is_mandatory": true,
        "status": "CONFIRMED",
        "currency_code": "TRY",
        "selling_rate_amount": "9000.00",
        "service_start_date": "2026-06-10",
        "service_end_date": "2026-06-13",
        "realized_penalty_amount": "0.00"
      }
    ],
    "active_item_count": 1,
    "active_selling_rate_total_amount": "9000.00",
    "realized_penalty_total_amount": "0.00",
    "contact": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "[email protected]"
    }
  },
  "trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e"
}

The booking envelope is wrapped under a booking key, exactly as /book returns it. It contains the same rooms and guests blocks, so a poll reports which room and which traveller the booking holds. Poll with a backoff until booking.status leaves the resolving set, that is until it reads CONFIRMED or MODIFIED (the booking is live) or a terminal value such as CANCELLED, RTB_DECLINED or EXPIRED.

Treat CONFIRMED and MODIFIED as equivalent when reconciling. MODIFIED means the booking was changed at some point, not that it is any less confirmed.

This GET response carries an ETag header. Keep it if you may amend the booking; the /book response carries one too, so a fresh booking can be amended without polling first.

Optional: Amend contact details

PATCH /api/v1/bookings/{public_id}

This endpoint is deliberately narrow. It accepts contact_first_name, contact_last_name, contact_email, contact_phone, and special_requests. It cannot change dates, rooms, rates, occupancy, products, inventory, or any price.

To change the dates, the room, the rate or the guests, cancel the booking (Step 5) and book the new stay from a fresh search. A patch that names any other field, such as checkin, checkout, room_public_id, rate_plan_public_id, occupancy or guests, returns 400 INVALID_REQUEST with that field in error.field, even alongside a contact field, and leaves the booking and its ETag unchanged.

A successful amendment never changes status. Because the endpoint touches contact details and nothing else, there is no lifecycle transition to reflect: a CONFIRMED booking stays CONFIRMED, and one still resolving in RTB_PENDING still awaits the host decision, and FINALIZED still awaits payment confirmation. What changes is the contact block, the updated_at timestamp, and the ETag.

Send the latest ETag in If-Match: the one returned by /book, or by GET /bookings/{public_id} / a previous PATCH if you fetched since. Send it back exactly as you received it. If the value you hold begins with W/, that prefix was added by a proxy re-encoding the response and identifies the same booking version, so leave it in place rather than stripping it:

curl -s -X PATCH \
  https://ari.console.adrasis.com/api/v1/bookings/bk_8H2nQ4pZ \
  -H 'Authorization: Bearer eyJ...' \
  -H 'If-Match: "booking-4ff317b4460e8f13c6c3bbdc0246d9a7"' \
  -H 'Content-Type: application/json' \
  -d '{
    "contact_phone": "+90 555 000 00 00",
    "special_requests": "Late arrival after 23:00"
  }'

The response is the canonical booking envelope and includes its new ETag. The values you patched appear in the envelope's contact block on this response and every later GET /bookings/{public_id}. Use them to verify the amendment and compare fields during reconciliation. All five patchable fields round-trip: contact_first_name and contact_last_name read back as contact.first_name and contact.last_name, the other three keep their names.

Missing If-Match returns 428 PRECONDITION_REQUIRED; a stale ETag returns 412 BOOKING_VERSION_CONFLICT. Fetch the booking again before retrying. A booking in a terminal status, such as a cancelled one, cannot be amended and returns 409 BOOKING_NOT_MODIFIABLE. An identical patch sent with the current ETag is a no-op and creates no duplicate timeline or outbound event.

Step 5: Cancel (two-phase)

Cancellation is two-phase: quote the penalty first, then commit the cancel.

5a. Cancellation quote

This example quotes cancellation on 9 June, after the 8 June penalty boundary in the prebook and booking snapshots. Half of the 9,000 TRY stay is charged.

POST /api/v1/bookings/{public_id}/cancellation-quote

Computes the penalty a cancellation would realise, with no side effects. The fee is derived from the frozen cancellation policy snapshot using the same rule the cancel path applies.

curl -s -X POST \
  https://ari.console.adrasis.com/api/v1/bookings/bk_8H2nQ4pZ/cancellation-quote \
  -H 'Authorization: Bearer eyJ...'
{
  "fee": "4500.00",
  "currency_code": "TRY",
  "cancellable": true,
  "cancellation_deadline": "2026-06-08T00:00:00Z",
  "refundability": "refundable",
  "schedule": [
    {
      "from": "2026-06-08T00:00:00Z",
      "fee": "4500.00",
      "currency_code": "TRY"
    }
  ],
  "trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e",
  "items": [
    {
      "item_public_id": "bki_2mF7qR4x",
      "penalty": "4500.00",
      "cancellable": true,
      "selling_rate_amount": "9000.00"
    }
  ],
  "penalty_total": "4500.00",
  "refund_due": "4500.00",
  "selected_selling_rate_total": "9000.00"
}

cancellable reports whether the booking is in a status that can be cancelled.

The item penalty resolves in a fixed order. A client reproducing it has to apply the same order:

  1. An item already cancelled reports the penalty realised at cancellation, frozen on the item. The remaining policy rules no longer apply to it.
  2. For a live item, refundability: non_refundable charges the full booked amount of the item, excluding exclusive accommodation tax, from the moment of booking. This outranks schedule and is not gated by any entry's from.
  3. Otherwise the schedule entry in force applies: the latest one whose from has passed.
  4. Otherwise the item's penalty is "0.00" at the currency scale because no penalty has started.

The top-level fee includes only items that can still be cancelled and mirrors penalty_total. It is therefore "0.00" when nothing selected can transition, including when the booking is already cancelled. An already-cancelled item's items[].penalty, refundability, and schedule still describe the realised penalty and the policy frozen at booking time; they do not make the item cancellable again. Check cancellable and item state before applying the live item policy rules.

For cancellable accommodation items, refund_due includes the exclusive tax collected with those stays, in addition to their selling rate less the penalty. selected_selling_rate_total excludes that tax. Even a non-refundable stay returns its exclusive tax: a 240.00 room price with 2.18 collected tax has a 240.00 penalty and 2.18 refund. Read the returned refund_due rather than subtracting fee from selected_selling_rate_total yourself.

cancellation_deadline answers a narrower question than its name suggests: the first instant a charge applies, which is the instant up to which cancelling is free. It is null when no charge ever applies, which is a stated answer rather than a missing field. A policy with no free window charges from the booking, so it states the instant of the booking, already past. A booking whose every room is non-refundable and that has no tier counted from the booking states the instant the offer was held at prebook, which can be up to 30 minutes before the booking. For a booking of several rooms it is the first instant any room's charge applies. It does not imply anything about what a cancellation costs right now. That is fee.

The booking's schedule is counted again at /book: a window counted from the booking starts when the booking is made, so the schedule can differ by minutes from the one prebook showed. Bookings made before this release return cancellation_deadline with its old meaning: for example an old non-refundable booking returns null, where a new one returns the instant of the booking or, with no tier counted from the booking, the instant of the hold.

5b. Cancel

POST /api/v1/bookings/{public_id}/cancel

Cancels the booking under its accepted cancellation terms and applies the requested notification options. The body is optional; an empty body is a valid cancel. An Idempotency-Key header is optional here and mandatory only when cancelling a package booking. See Idempotency.

curl -s -X POST \
  https://ari.console.adrasis.com/api/v1/bookings/bk_8H2nQ4pZ/cancel \
  -H 'Authorization: Bearer eyJ...' \
  -H 'Content-Type: application/json' \
  -d '{
    "reason": "guest requested cancellation",
    "notify_guest": true,
    "notify_channel": true
  }'
{
  "booking": {
    "public_id": "bk_8H2nQ4pZ",
    "booking_number": 100482,
    "status": "CANCELLED",
    "stay_total": "9000.00",
    "tax_total_exclusive": "0.00",
    "tax_total_payable_at_property": "0.00",
    "currency_code": "TRY",
    "checkin_date": "2026-06-10",
    "checkout_date": "2026-06-13",
    "nights": 3,
    "agency_reference": "your-ref-12345",
    "cancellation_policy_snapshot": {
      "is_non_refundable": false,
      "non_refundable_after": "2026-06-08T00:00:00Z",
      "currency_code": "TRY",
      "finalized": true,
      "tiers": [
        {
          "from": "2026-06-08T00:00:00Z",
          "fee": "4500.00",
          "currency_code": "TRY"
        }
      ]
    },
    "rooms": [
      {
        "room_public_id": "r3Tz9Lm0",
        "rate_plan_public_id": "rp_bar",
        "occupancy_adults": 2,
        "children": 1,
        "infants": 0,
        "child_ages": [
          7
        ],
        "room_total_amount": "9000.00"
      }
    ],
    "guests": [
      [
        {
          "kind": "ADULT",
          "first_name": "Ada",
          "last_name": "Lovelace",
          "is_lead": true
        }
      ]
    ],
    "items": [
      {
        "public_id": "bki_2mF7qR4x",
        "product_type": 1,
        "sequence": 0,
        "is_mandatory": true,
        "status": "CANCELLED",
        "currency_code": "TRY",
        "selling_rate_amount": "9000.00",
        "service_start_date": "2026-06-10",
        "service_end_date": "2026-06-13",
        "realized_penalty_amount": "4500.00",
        "cancelled_at": "2026-06-09T08:10:00Z"
      }
    ],
    "active_item_count": 0,
    "active_selling_rate_total_amount": "0.00",
    "realized_penalty_total_amount": "4500.00",
    "contact": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "[email protected]"
    }
  },
  "item_results": [
    {
      "item_public_id": "bki_2mF7qR4x",
      "outcome": "CANCELLED",
      "penalty_amount": "4500.00",
      "realized_at": "2026-06-09T08:10:00Z"
    }
  ],
  "penalty_total": "4500.00",
  "refund_due": "4500.00",
  "trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e"
}

Treat a full cancellation as successful only when HTTP is 200 and booking.status is CANCELLED. For an item subset, inspect every item_results[].outcome: CANCELLED and ALREADY_CANCELLED are successful; NOT_CANCELLABLE is not. A live remainder leaves the aggregate MODIFIED. Do not wait or poll for PENDING_CANCEL; read the cancellation outcome in the response.

Cancel is idempotent: a booking that is already CANCELLED returns 200 with the existing booking envelope and performs no further writes. A booking in a non-cancellable status or prohibited by its frozen policy returns 409 with code BOOKING_NOT_CANCELLABLE and writes nothing.

Next steps