Staylah

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.

json
{
  "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".

json
{
  "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 — pending channel_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 — pending channel_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.

FieldRules
hotel_codesrequired, array, 1–200
hotel_codes.*required, string, ≤120 — your codes
check_inrequired, Y-m-d, today or later
check_outrequired, Y-m-d, strictly after check_in
roomsrequired, integer, 1–30
occupancyrequired, array, ≥1
occupancy.*.adultsrequired, integer, 1–20
occupancy.*.childrenoptional, integer, 0–10
occupancy.*.child_agesoptional, array
occupancy.*.child_ages.*integer, 0–17
nationalityoptional, ISO-2
currencyoptional, ISO-4217

Notes:

  • check_out is strictly after check_in. A same-day checkout is a day-use booking — a different product on a different endpoint.
  • Send child_ages whenever children are present. Child pricing runs against each property's max_child_age thresholds, and a guessed age is how a booking gets repriced at check-in.
  • nationality drives nationality-restricted rate plans. It is a pricing input, not a guest attribute for reporting.
json
{
  "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 — pending AriReadService (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.

FieldRules
hotel_coderequired, string, ≤120
check_inrequired, Y-m-d, today or later
check_outrequired, Y-m-d, after check_in
roomsrequired, integer, 1–30
occupancyrequired, array, ≥1 (same shape as search)
room_type_codesoptional, array ≤100 of strings ≤120
rate_plan_codesoptional, array ≤100 of strings ≤120
nationalityoptional, ISO-2
currencyoptional, ISO-4217

The two code arrays narrow the response to codes you already know about.

Status: 501 — pending AriReadService (per-night rates, restrictions, cancellation policy).


Bookings#

Three things in this contract are load-bearing:

  1. partner_reference is required and is the idempotency key. A retried POST with the same reference returns the original booking — it never creates a second one.
  2. quote_token carries the price we offered. Present it, or accept that the live price wins.
  3. 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.

FieldRules
hotel_coderequired, string, ≤120
room_type_coderequired, string, ≤120
rate_plan_coderequired, string, ≤120
check_inrequired, Y-m-d, today or later
check_outrequired, Y-m-d, after check_in
roomsrequired, integer, 1–30
occupancyrequired, array ≥1 (adults 1–20; children 0–10; child_ages.* 0–17)
nationalityoptional, ISO-2
currencyoptional, ISO-4217

Status: 501 — pending AriReadService + 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.

FieldRules
partner_referencerequired, string ≤120 — your booking id, unique per partner, the idempotency key
quote_tokenoptional, string ≤512
hotel_coderequired, string ≤120
room_type_coderequired, string ≤120
rate_plan_coderequired, string ≤120
check_inrequired, Y-m-d, today or later
check_outrequired, Y-m-d, after check_in
roomsrequired, integer 1–30
guestsrequired, array ≥1
guests.*.room_numberrequired, integer ≥1
guests.*.first_namerequired, string ≤100
guests.*.last_namerequired, string ≤100
guests.*.is_adultrequired, boolean
guests.*.ageoptional, integer 0–120
guests.*.nationalityoptional, ISO-2
guests.*.is_leadoptional, boolean
contactrequired, object
contact.first_namerequired, string ≤100
contact.last_nameoptional, string ≤100
contact.emailrequired, valid email ≤255
contact.phoneoptional, string ≤50
total_amountrequired, numeric ≥0
currencyrequired, ISO-4217
special_requestsoptional, string ≤2000
arrival_timeoptional, H:i

Notes:

  • guests is a per-room list keyed by room_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_amount is 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.
json
{
  "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 — pending ConnectivityBookingService (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 — pending ConnectivityBookingService (partner-scoped reservation read).

POST /bookings/{reference}/cancel#

Scope: booking.cancel

Cancels and returns the penalty actually applied.

FieldRules
reasonoptional, string ≤500
expected_penalty_amountoptional, numeric ≥0
currencyoptional, 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 — pending ConnectivityBookingService + BookingPenaltyService.