Connectivity API reference
Connect your own system to Tratok: a property management system or channel manager for hotels, a reservation system for tours and experiences, or booking software for a restaurant. Your system sends prices, availability and rules; Tratok sends every booking back. Tratok collects the guest's payment and pays you your share, so every booking carries its money: the gross price, Tratok's commission and the net payable to you.
Ways to connect
API v1 (api.php, on the API Documentation page) keeps working. New integrations should use v2: it has a sandbox, safe retries, a bookings feed that never skips a change, and every vertical.
Going live
- Create a sandbox key on the portal's Connectivity page. Sandbox keys start with
tk_test_and read and write a separate sandbox: nothing guests see changes. - Pass the checklist. Each check ticks itself from what your system does in the sandbox. See Certification.
- Ask to go live. Tratok reviews the sandbox results, usually within two working days, and answers by email.
- Create a live key (
tk_live_). Your prices, availability and rules then reach guests, and real bookings reach your system. A live key works only while the approval stands.
Keys and scopes
Send the key in the X-Api-Key header, or as Authorization: Bearer tk_.... Keys never go in the URL. Tratok stores only a fingerprint of each key, so a key is shown once when you create it. Up to 10 keys can be active at a time; revoking one on the Connectivity page refuses it at once.
contentscopeGET /listingsariscope- Rates, availability, stay rules, slots and seatings:
/hotels/ari,/activities/availability,/restaurants/availabilityand the mode switches. bookingsscope- The bookings feed, single bookings, acknowledgements and sandbox test bookings.
settlementscope- Statement, balance and payouts.
GET /me and GET /certification work with any key.
curl https://hospitality.tratok.net/api/v2/me \
-H "X-Api-Key: tk_test_..." If your server cannot rewrite URLs, the same endpoints answer at /api/v2/index.php/hotels/ari and /api/v2/index.php?path=/hotels/ari.
Requests and answers
Requests and answers are JSON in UTF-8. A success is {"data": ..., "meta": ...}; meta appears only where there is paging. An error is {"error": {"code", "message", "details"}}.
{"error": {"code": "validation_failed", "message": "2 fields are not valid.",
"details": [{"index": 0, "field": "min_stay", "issue": "must be a whole number from 1 to 365"},
{"index": 3, "field": "room_id", "issue": "is not one of your rooms"}]}} Every write lands completely or not at all
A write is checked in full first and then applied in one transaction, so a failed request never leaves half a calendar behind. Fields an item leaves out keep their current values.
Limits
- A request body may be up to 2 MB, with up to 500 items, covering up to 100,000 room-nights or slots.
- Dates are
YYYY-MM-DD, up to two years ahead. Writes may touch dates from yesterday (UTC) onwards, so a venue west of UTC can still change its today; earlier dates are skipped and counted in the answer. - Times are
"HH:MM", 24-hour, in the venue's local time. Weekday filters use["mon","tue","wed","thu","fri","sat","sun"]. - Each key may make 300 requests a minute. Every answer carries
X-RateLimit-LimitandX-RateLimit-Remaining; over the limit you get429withRetry-After: 30.
Retry safely with Idempotency-Key
Send an Idempotency-Key header (1 to 80 letters, digits or - _ : .) with any POST. Tratok keeps the first answer for 24 hours. A retry with the same key and the same body gets that answer back with Idempotent-Replay: true instead of being applied twice. The same key with a different body is refused with 409 idempotency_conflict. Server errors are not stored, so a retry after a 500 runs again.
Headers on every answer
X-Request-Id (quote it if you contact support), X-Tratok-Mode (sandbox or live) and X-Tratok-Api-Version (2.0).
Error codes
400 invalid_jsonerror- The body is empty or not a JSON object.
400 invalid_idempotency_keyerror- The Idempotency-Key has characters that are not allowed or is too long.
401 unauthorizederror- No key, an unknown key, or a revoked key.
403 forbidden_scopeerror- The key lacks the scope this endpoint needs.
403 live_not_approvederror- A live key was used before go-live approval, or after it was withdrawn.
403 sandbox_onlyerror- Test bookings need a sandbox key.
404 not_founderror- No such endpoint, or the listing, room or booking is not yours.
405 method_not_allowederror- Wrong HTTP method; the
Allowheader lists the right ones. 409 idempotency_conflicterror- That Idempotency-Key was already used for a different request.
409 idempotency_in_progresserror- The first request with this key is still running. Retry in a moment.
409 version_mismatcherror- The booking changed since the version you acknowledged.
detailscarriescurrent_version. 409 already_cancellederror- The test booking is already cancelled.
413 payload_too_largeerror- The body is over 2 MB. Split the update.
422 validation_failederror- One or more fields are not valid.
detailslists each as{index, field, issue}, up to 100. 429 rate_limitederror- Over the per-minute limit. Wait
Retry-Afterseconds. 500 server_errorerror- Something failed on Tratok's side. Nothing was changed; retry.
Listings
Map your own room, product and table codes to Tratok ids once, then use the ids everywhere else.
/listings?type=property|activity|restaurant{"data": [
{"type": "property", "id": 77, "name": "Al Seef Heritage Hotel", "city": "Dubai", "country": "United Arab Emirates",
"approved": true, "paused": false,
"rooms": [{"id": 812, "name": "Deluxe King", "currency": "AED", "default_price": 210.0, "rooms": 6, "max_guests": 2, "paused": false}]},
{"type": "activity", "id": 311, "name": "Sunrise Kayak at Dubai Marina", "currency": "USD", "default_price": 45.0,
"max_group": 12, "availability_mode": "managed", "approved": true, "paused": false},
{"type": "restaurant", "id": 54, "name": "Bait Al Bahar", "max_covers": 80, "availability_mode": "open",
"approved": true, "paused": false,
"packages": [{"id": 9, "name": "Chef's tasting menu", "price": 320.0, "currency": "AED", "max_guests": 8, "active": true}]}
]}Hotels: rates, rooms left and stay rules
/hotels/ariPer room, per night. Send a date range and the values that apply to every night in it.
{"updates": [
{"room_id": 812, "from": "2026-11-01", "to": "2027-10-31", "price": 210.00, "available": 6},
{"room_id": 812, "from": "2026-12-24", "to": "2027-01-02", "price": 340.00, "min_stay": 3},
{"room_id": 812, "from": "2026-11-01", "to": "2027-10-31", "days": ["fri"], "closed_to_arrival": true}
]} room_idinteger, required- A room id from
/listings. from, todate, required- The first and last night, inclusive.
dayslist, optional- Only these weekdays within the range.
pricenumber- Price per night in the room's currency. It cannot be null; leave it out to keep the current price.
currencytext, optional- If sent, it must match the room's currency. Tratok does not convert.
availableinteger 0 to 9999- Rooms left to sell that night. 0 closes the night.
min_stay, max_stayinteger 1 to 365, or null- Nights, judged on the arrival date. null removes the rule.
closed_to_arrivaltrue or false- Guests may not check in on that date.
closed_to_departuretrue or false- Guests may not check out on that date.
stop_selltrue or false- No stay may include that night.
Each item must carry at least one of price, available or a rule. The answer counts what was written:
{"data": {"items": 3, "room_nights": 418, "skipped_past_nights": 0}} /hotels/ari?room_id=812&from=2026-11-01&to=2026-11-30Reads back what Tratok sells, night by night (up to 366 days per read). price_is_default and available_is_default are true where no value was sent and the room's own price or room count applies.
Activities: start times and places
/activities/availabilityA slot is one start time on one date with a capacity in participants. Leave out times for an activity that runs all day.
{"updates": [
{"activity_id": 311, "from": "2026-11-01", "to": "2027-10-31", "times": ["06:30", "16:00"], "capacity": 12},
{"activity_id": 311, "from": "2026-12-20", "to": "2027-01-05", "times": ["06:30"], "price": 55.00},
{"activity_id": 311, "from": "2026-11-14", "to": "2026-11-14", "times": ["16:00"], "closed": true}
]} activity_idinteger, required- An activity id from
/listings. from, to, daysdates- As for hotels.
timeslist of "HH:MM"- Start times, up to 96 a day. Leave out for a whole-day slot.
capacityinteger 0 to 9999- Participants in total for the slot. Needed the first time a slot is sent.
pricenumber or null- Per person, in the activity's currency, for these slots only. null goes back to the activity's own price.
closedtrue or false- Stops sales for the slot and keeps it.
removetrue- Deletes slots nobody has booked; slots with bookings are closed instead and counted in
closed_instead_of_removed.
Lowering a capacity below what is already booked keeps the existing bookings; the answer carries a warning and no new bookings are taken for that slot.
Open and managed
An activity starts open: guests on Tratok pick any date. Your first live push makes it managed (the answer lists it in now_managed): from then on guests can book only the slots you sent, while places remain. To switch back:
/activities/{id}/mode{"availability_mode": "open"} /activities/availability?activity_id=311&from=2026-11-01&to=2026-11-30Each slot reads back with capacity, booked, remaining, closed and price.
Restaurants: seatings and covers
/restaurants/availabilityA seating is one time on one date with a capacity in covers, for any booking or for one package (a set menu or experience).
{"updates": [
{"restaurant_id": 54, "from": "2026-11-01", "to": "2027-04-30", "times": ["18:30", "21:00"], "capacity": 40},
{"restaurant_id": 54, "package_id": 9, "from": "2026-11-01", "to": "2027-04-30", "days": ["thu", "fri"], "times": ["20:00"], "capacity": 8},
{"restaurant_id": 54, "from": "2026-12-31", "to": "2026-12-31", "times": ["18:30", "21:00"], "closed": true}
]} restaurant_idinteger, required- A restaurant id from
/listings. package_idinteger, optional- 0 (the default) for table bookings, or one of the restaurant's package ids.
timeslist of "HH:MM", required- Seating times.
capacity, closed, remove- As for activities, in covers.
Restaurants also start open and become managed with the first live push; POST /restaurants/{id}/mode switches back. Read with GET /restaurants/availability?restaurant_id=54&from=&to=&package_id=.
Bookings
Only bookings a guest has paid for reach you. Unpaid holds and expired payments never appear.
/bookings?since=2026-11-01&limit=50Every booking that changed, oldest change first. Start with since (default: 30 days ago), then pass meta.next_cursor back as cursor on every later call. The cursor never skips a change. A booking appears again each time it changes, with a new version. limit is 1 to 200 (default 50); meta.has_more says whether to call again straight away. Changes from the last three seconds wait for the next call.
{"data": [
{"id": 48213, "reference": "TR-7D9D37BB", "version": "2026-11-02 14:05:09",
"status": "confirmed", "type": "property",
"listing": {"id": 77, "name": "Al Seef Heritage Hotel"},
"room": {"id": 812, "name": "Deluxe King"}, "package": null,
"check_in": "2026-12-27", "check_out": "2026-12-30", "nights": 3, "time": null, "guests": 2,
"guest": {"name": "Omar Saleh", "email": "[email protected]", "phone": "+971500000000"},
"price": {"currency": "AED", "gross": 1020.0, "commission": 0.0, "net_payable": 1020.0, "booked_to_balance": true},
"payment": {"collected_by": "tratok", "status": "paid"},
"created_at": "2026-11-02T14:04:51Z", "confirmed_at": "2026-11-02T14:05:09Z", "cancelled_at": null,
"acknowledged": null}
],
"meta": {"next_cursor": "MjAyNi0xMS0wMiAxNDowNTowOXw0ODIxMw", "has_more": false}} statustextconfirmed,cancelled,completedorno_show.typetextproperty,activityorrestaurant.check_outandnightsare for properties;timeis the slot or seating for the others.priceobjectgrossis what the guest paid,commissionis Tratok's,net_payableis yours.booked_to_balanceturns true once the amount is in your payable balance.payment.statustextpaid,refunded,partially_refunded,cancelled, orat_venuefor bookings with nothing to pay online.acknowledgedobject or null- The version your system last acknowledged, your reference, and
current: whether that is the latest version.
/bookings/{id}/bookings/{id}/acknowledgeTell Tratok your system holds this version of the booking, with your own reference. Send the version you stored: if the booking changed since, the answer is 409 version_mismatch with the current version, so a change is never lost between fetch and acknowledge.
{"provider_reference": "PMS-48213", "version": "2026-11-02 14:05:09"} Webhooks
Add an address on the portal's Webhooks page and pick events: booking.created, booking.updated, booking.confirmed, booking.cancelled, payment.processed, payment.failed, review.created. Tratok POSTs JSON every few minutes:
POST https://your-system.example.com/tratok
X-Tratok-Event: booking.created
X-Tratok-Signature: 9f2c... (HMAC-SHA256 of the raw body with your webhook secret, hex)
{"event": "booking.created", "timestamp": "2026-11-02T14:06:12+00:00", "data": {"id": 48213, "status": "confirmed", ...}} Check the signature against the raw body before trusting it, then fetch GET /bookings/{data.id} for the booking object documented above. An answer other than 2xx is retried up to five times; after 20 failures in a row the webhook is paused. Sandbox test bookings send the same events with "sandbox": true, and also carry the booking object in data.booking.
Sandbox test bookings
Sandbox keys only. Each call also sends the matching webhook, so you can test the whole loop.
/sandbox/bookings{"listing_type": "property", "listing_id": 77, "room_id": 812} Makes a confirmed booking 30 days ahead for "Test Guest", 2 guests (room_id is optional). It appears in the sandbox bookings feed.
/sandbox/bookings/{id}/modifyMoves it one day later and adds a guest, giving it a new version.
/sandbox/bookings/{id}/cancelThe same three actions are on the Connectivity page.
Settlement
/settlement/statement?from=2026-11-01&to=2026-11-30One line per sale, cancellation or venue payment: date, kind, booking_id, reference, listing, currency, gross, commission, net. Totals are per currency and never added across currencies. The default range is the current month to date.
/settlement/balanceWhat you can request as a payout now (available_usd_approx, converted to USD at the platform rate) and what is already requested (pending_payouts_usd). Request payouts on the portal's Payments page.
/payoutsYour payout requests: amount, currency, method, status, reference, requested_at, processed_at.
Certification
Before a live key works, your system shows in the sandbox that it handles the cases that cause real problems later: a full year of data, errors, retries, and bookings that change after they were made. Every check ticks itself; there is no form to fill in.
/certificationauthrequired- Any successful call with a sandbox key.
listingsrequiredGET /listings.yearrequired- At least 365 dates loaded for one room, activity or restaurant.
ruleshotels- A minimum or maximum stay, closed to arrival or departure, or stop sell on a room.
errorsrequired- At least one
422answer: send an impossible value on purpose and readdetails. idempotencyrequired- A write repeated with the same Idempotency-Key, answered from the store.
feedrequiredGET /bookingsat least twice, the second time withnext_cursor.ack_new, ack_change, ack_cancelrequired- Acknowledge a test booking when new, after
modify, and aftercancel. webhookoptional- A sandbox webhook delivered to your address.
The answer carries status (in_progress, requested, approved or rejected), ready_to_request and each check with ok. Ask to go live on the Connectivity page.
OpenTravel XML for hotel channel managers
If your channel manager already sends OpenTravel (OTA 2003B) messages to other channels, it can send the same messages to Tratok. They use the same keys, sandbox, checks, limits and checklist as the JSON API.
https://hospitality.tratok.net/api/ota/CredentialsPOS/Source/RequestorIDwithMessagePassword= your API key, or theX-Api-Keyheader.HotelCode- The Tratok property id.
InvTypeCode, RoomTypeCode- The Tratok room id.
RatePlanCodeSTD. Tratok sells one rate per room (BARor no code are accepted too).
Messages
OTA_PingRQanswer OTA_PingRS- Echoes
EchoData. OTA_HotelAvailNotifRQanswer OTA_HotelAvailNotifRSBookingLimit(rooms left),RestrictionStatus(Master = stop sell, Arrival, Departure; Open or Close) andLengthsOfStay(SetMinLOS, SetMaxLOS, SetForwardMinStay, SetForwardMaxStay, RemoveMinLOS, RemoveMaxLOS; Time 0 removes the rule).OTA_HotelRateAmountNotifRQanswer OTA_HotelRateAmountNotifRS- Prices from
BaseByGuestAmt:AmountAfterTax, orAmountBeforeTax. With several, the one for the most guests is the room price. OTA_HotelInvCountNotifRQanswer OTA_HotelInvCountNotifRS- Rooms left from
InvCount CountType="2". OTA_ReadRQanswer OTA_ResRetrieveRS- Reservations for
HotelReadRequest HotelCodethat your system has not yet confirmed at their latest version, withResStatusBook, Modify or Cancel. The money sits inTPA_Extensions/TratokSettlement. OTA_NotifReportRQanswer OTA_NotifReportRS- Confirms delivery:
UniqueID IDis the Tratok reservation, andHotelReservationID ResID_Type="10"carries your confirmation number. Confirmed reservations leave the ReadRQ list until they change again.
<OTA_HotelAvailNotifRQ xmlns="http://www.opentravel.org/OTA/2003/05" EchoToken="a1b2" Version="1.0">
<POS><Source><RequestorID Type="22" ID="YourChannelManager" MessagePassword="tk_test_..."/></Source></POS>
<AvailStatusMessages HotelCode="77">
<AvailStatusMessage BookingLimit="4">
<StatusApplicationControl Start="2026-12-24" End="2027-01-02" InvTypeCode="812" RatePlanCode="STD"/>
<LengthsOfStay><LengthOfStay MinMaxMessageType="SetMinLOS" Time="3"/></LengthsOfStay>
</AvailStatusMessage>
</AvailStatusMessages>
</OTA_HotelAvailNotifRQ> Errors come back in the message's own Errors element with OpenTravel codes: 15 (dates), 61 (currency), 245 (reservation), 249 (rate plan), 320 (invalid value), 321 (required field missing), 392 (hotel code), 402 (room type), 448 (system error, retry), 450 (message not supported), 497 (authorisation). Like the JSON API, a message with any error changes nothing. Documents with DOCTYPE or ENTITY declarations are refused.
OCTO for activities
Many tour and activity reservation systems speak OCTO, an open standard for selling experiences. With OCTO, Tratok connects to your system; you build nothing. On the portal's Connectivity page, enter the name of your system, its OCTO address (https) and an API key it issued for Tratok, then link each Tratok activity to a product, option and unit (the per-person unit, usually Adult).
What Tratok does
- Availability, every 15 minutes, 60 days ahead. Each start time becomes a slot; places are your vacancies; sold-out or unavailable times are closed. When your unit's retail price is in the activity's currency, it becomes the slot price. The activity becomes managed, so guests can book only these times.
- Bookings, within minutes. Each paid Tratok booking is reserved in your system under Tratok's own id (a retry can never book twice), then confirmed with the guest's name, email, phone and the Tratok reference as
resellerReference. Your supplier reference and voucher are kept with the booking. - Cancellations. A booking cancelled on Tratok is cancelled in your system.
- Failures. A send that fails is retried on each run, up to five times, and then shows as "Failed: needs a person" on the Connectivity page.
Only bookings made after an activity is linked are sent. Pause stops all calls to your system; Disconnect also removes the links. Your key is stored encrypted and never shown again.
Calls Tratok makes
GET {your OCTO address}/products
POST {your OCTO address}/availability {"productId", "optionId", "localDateStart", "localDateEnd", "units"}
POST {your OCTO address}/bookings {"uuid", "productId", "optionId", "availabilityId", "unitItems", "notes"}
GET {your OCTO address}/bookings/{uuid}
POST {your OCTO address}/bookings/{uuid}/confirm {"contact", "resellerReference"}
POST {your OCTO address}/bookings/{uuid}/cancel {"reason"}
Authorization: Bearer {your key}
Octo-Capabilities: octo/pricing