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.
Step 1: Search
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:
- An item already cancelled reports the penalty realised at cancellation, frozen on the item. The remaining policy rules no longer apply to it.
- For a live item,
refundability: non_refundablecharges the full booked amount of the item, excluding exclusive accommodation tax, from the moment of booking. This outranksscheduleand is not gated by any entry'sfrom. - Otherwise the
scheduleentry in force applies: the latest one whosefromhas passed. - 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
- Idempotency: the mandatory key on book.
- Currencies, occupancy & allocations: the search request and allocation model in depth.
- Errors: the complete status-code reference.