API reference
Pull API
Endpoints for partners reading from Staylah — handshake, mapping, availability, bookings.
All under /api/connectivity/v1.
Handshake#
GET /ping#
Scope: none. A key issued before commercial terms are agreed holds no scopes at all and must still be able to prove it works.
{
"success": true,
"data": {
"pong": true,
"environment": "sandbox",
"server_time": "2026-08-29T09:14:22+00:00"
},
"request_id": "…"
}
server_time is our clock. If you are implementing signing, diff it against
yours — a stale-request rejection is otherwise indistinguishable from a bad
secret at your end.
GET /whoami#
Scope: none. This is the endpoint that answers "why am I getting a 403".
{
"success": true,
"data": {
"partner": { "code": "dida", "name": "Dida Travel" },
"key": {
"key_id": "slk_test_9f2c1ab34d5e6f7089ab12cd",
"name": "Dida sandbox",
"environment": "sandbox",
"requires_signature": false,
"expires_at": null,
"rate_limit_per_minute": 300
},
"scopes": ["ari.read", "content.read", "booking.quote", "booking.write"],
"mapped_properties": 12
}
}
No secret material is ever returned. mapped_properties is a count, not a
list — the identifiers you may use are your own mapped codes, which
/mapping/hotels serves.
Two numbers here explain nearly every 403: the scopes you actually hold, and
whether mapped_properties is 0.
Mapping#
Scope: content.read
GET /mapping/hotels#
Every property mapped and switched on for you. Driven by active
property_distribution_configs, so a property the commercial team pauses
disappears from your list automatically — there is no second switch for anyone
to forget.
Status:
501— pendingchannel_property_mappings(partner hotel code catalogue).
GET /mapping/hotels/{hotelCode}#
One property's room types and rate plans in your codes, with the occupancy limits and board type each rate plan implies. The occupancy bounds are there so you can avoid sending searches that can never match.
Status:
501— pendingchannel_rate_mappings(room type and rate plan code catalogue).
Availability#
Scope: ari.read
Both are POST, not GET: the occupancy array is nested and repeated per room,
which a query string cannot express without inventing an encoding both sides
have to agree on.
POST /availability/search#
Which of your mapped properties have anything sellable for a stay. Deliberately lightweight — no per-night breakdown, no policies — so you can call it across a wide property set and cache the result.
| Field | Rules |
|---|---|
hotel_codes | required, array, 1–200 |
hotel_codes.* | required, string, ≤120 — your codes |
check_in | required, Y-m-d, today or later |
check_out | required, Y-m-d, strictly after check_in |
rooms | required, integer, 1–30 |
occupancy | required, array, ≥1 |
occupancy.*.adults | required, integer, 1–20 |
occupancy.*.children | optional, integer, 0–10 |
occupancy.*.child_ages | optional, array |
occupancy.*.child_ages.* | integer, 0–17 |
nationality | optional, ISO-2 |
currency | optional, ISO-4217 |
Notes:
check_outis strictly aftercheck_in. A same-day checkout is a day-use booking — a different product on a different endpoint.- Send
child_ageswhenever children are present. Child pricing runs against each property'smax_child_agethresholds, and a guessed age is how a booking gets repriced at check-in. nationalitydrives nationality-restricted rate plans. It is a pricing input, not a guest attribute for reporting.
{
"hotel_codes": ["DIDA-KL-001", "DIDA-KL-002"],
"check_in": "2026-09-14",
"check_out": "2026-09-17",
"rooms": 1,
"occupancy": [{ "adults": 2, "children": 1, "child_ages": [7] }],
"nationality": "SG",
"currency": "SGD"
}
Status:
501— pendingAriReadService(canonical availability read).
POST /availability/rates#
The full picture for one property: per-night rate and inventory per room type and rate plan, restrictions, and the cancellation policy that applies.
One property per call on purpose — this response is large, and thirty hotels batched into one request produces a payload neither side can usefully time out on or retry.
| Field | Rules |
|---|---|
hotel_code | required, string, ≤120 |
check_in | required, Y-m-d, today or later |
check_out | required, Y-m-d, after check_in |
rooms | required, integer, 1–30 |
occupancy | required, array, ≥1 (same shape as search) |
room_type_codes | optional, array ≤100 of strings ≤120 |
rate_plan_codes | optional, array ≤100 of strings ≤120 |
nationality | optional, ISO-2 |
currency | optional, ISO-4217 |
The two code arrays narrow the response to codes you already know about.
Status:
501— pendingAriReadService(per-night rates, restrictions, cancellation policy).
Bookings#
Three things in this contract are load-bearing:
partner_referenceis required and is the idempotency key. A retriedPOSTwith the same reference returns the original booking — it never creates a second one.quote_tokencarries the price we offered. Present it, or accept that the live price wins.- Inventory is decremented atomically as part of creating the booking, not
checked-then-written. Two partners hitting the last room in the same second
both pass a check; only one passes an
UPDATE … WHERE remaining >= n.
POST /bookings/quote#
Scope: booking.quote
Prices an exact stay and returns a short-lived token. Optional in the flow, but strongly recommended: it is the only way to show a price and then book that same price.
| Field | Rules |
|---|---|
hotel_code | required, string, ≤120 |
room_type_code | required, string, ≤120 |
rate_plan_code | required, string, ≤120 |
check_in | required, Y-m-d, today or later |
check_out | required, Y-m-d, after check_in |
rooms | required, integer, 1–30 |
occupancy | required, array ≥1 (adults 1–20; children 0–10; child_ages.* 0–17) |
nationality | optional, ISO-2 |
currency | optional, ISO-4217 |
Status:
501— pendingAriReadService+ quote token issuing.
POST /bookings#
Scopes: booking.write and booking.quote
Creates a confirmed reservation. Idempotent on partner_reference; a repeat
returns the existing booking with 200 and a flag rather than 201.
| Field | Rules |
|---|---|
partner_reference | required, string ≤120 — your booking id, unique per partner, the idempotency key |
quote_token | optional, string ≤512 |
hotel_code | required, string ≤120 |
room_type_code | required, string ≤120 |
rate_plan_code | required, string ≤120 |
check_in | required, Y-m-d, today or later |
check_out | required, Y-m-d, after check_in |
rooms | required, integer 1–30 |
guests | required, array ≥1 |
guests.*.room_number | required, integer ≥1 |
guests.*.first_name | required, string ≤100 |
guests.*.last_name | required, string ≤100 |
guests.*.is_adult | required, boolean |
guests.*.age | optional, integer 0–120 |
guests.*.nationality | optional, ISO-2 |
guests.*.is_lead | optional, boolean |
contact | required, object |
contact.first_name | required, string ≤100 |
contact.last_name | optional, string ≤100 |
contact.email | required, valid email ≤255 |
contact.phone | optional, string ≤50 |
total_amount | required, numeric ≥0 |
currency | required, ISO-4217 |
special_requests | optional, string ≤2000 |
arrival_time | optional, H:i |
Notes:
guestsis a per-room list keyed byroom_number, so a two-room booking with different occupants per room is representable. A flat guest list is not, and collapses occupant counts into a single total.total_amountis what you believe you owe, in the currency you believe. It is checked against our figure, not trusted. A mismatch is a rejected booking, not a booking at your number.
{
"partner_reference": "DIDA-8827361",
"quote_token": "qt_…",
"hotel_code": "DIDA-KL-001",
"room_type_code": "DLX-KING",
"rate_plan_code": "BAR-BB",
"check_in": "2026-09-14",
"check_out": "2026-09-17",
"rooms": 1,
"guests": [
{ "room_number": 1, "first_name": "Ada", "last_name": "Lim",
"is_adult": true, "is_lead": true, "nationality": "SG" }
],
"contact": { "first_name": "Ada", "last_name": "Lim",
"email": "ada@example.com", "phone": "+6591234567" },
"total_amount": 642.00,
"currency": "SGD"
}
Status:
501— pendingConnectivityBookingService(idempotent create with atomic inventory hold).
GET /bookings/{reference}#
Scope: booking.read
Current state of a booking by your own reference. Scoped to the calling partner — you can never read another partner's booking, even with a correct reference.
reference must match [A-Za-z0-9._-]{1,120}. It is your reference, so it may
carry characters Laravel's default segment pattern would swallow; the pattern is
deliberately conservative rather than open, because a slash in a reference would
match a different route entirely.
Status:
501— pendingConnectivityBookingService(partner-scoped reservation read).
POST /bookings/{reference}/cancel#
Scope: booking.cancel
Cancels and returns the penalty actually applied.
| Field | Rules |
|---|---|
reason | optional, string ≤500 |
expected_penalty_amount | optional, numeric ≥0 |
currency | optional, ISO-4217 |
expected_penalty_amount lets you refuse a cancellation that would cost more
than you expect, instead of discovering the penalty afterwards.
The penalty is computed against the policy in force at booking time, not the policy in force today — a property changing its terms must not change what an existing guest owes.
Status:
501— pendingConnectivityBookingService+BookingPenaltyService.
