Tratok Hospitality Sign in
On this page
  1. Ways to connect
  2. Going live
  3. Keys and scopes
  4. Requests and answers
  5. Error codes
  6. Listings
  7. Hotels
  8. Activities
  9. Restaurants
  10. Bookings
  11. Webhooks
  12. Sandbox bookings
  13. Settlement
  14. Certification
  15. OpenTravel XML
  16. OCTO for activities

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.

https://hospitality.tratok.net/api/v2https://hospitality.tratok.net/api/ota/

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

  1. 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.
  2. Pass the checklist. Each check ticks itself from what your system does in the sandbox. See Certification.
  3. Ask to go live. Tratok reviews the sandbox results, usually within two working days, and answers by email.
  4. 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.

contentscope
GET /listings
ariscope
Rates, availability, stay rules, slots and seatings: /hotels/ari, /activities/availability, /restaurants/availability and 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-Limit and X-RateLimit-Remaining; over the limit you get 429 with Retry-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 Allow header 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. details carries current_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. details lists each as {index, field, issue}, up to 100.
429 rate_limitederror
Over the per-minute limit. Wait Retry-After seconds.
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.

GET/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

POST/hotels/ari

Per 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}}
GET/hotels/ari?room_id=812&from=2026-11-01&to=2026-11-30

Reads 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

POST/activities/availability

A 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:

POST/activities/{id}/mode
{"availability_mode": "open"}
GET/activities/availability?activity_id=311&from=2026-11-01&to=2026-11-30

Each slot reads back with capacity, booked, remaining, closed and price.

Restaurants: seatings and covers

POST/restaurants/availability

A 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.

GET/bookings?since=2026-11-01&limit=50

Every 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}}
statustext
confirmed, cancelled, completed or no_show.
typetext
property, activity or restaurant. check_out and nights are for properties; time is the slot or seating for the others.
priceobject
gross is what the guest paid, commission is Tratok's, net_payable is yours. booked_to_balance turns true once the amount is in your payable balance.
payment.statustext
paid, refunded, partially_refunded, cancelled, or at_venue for 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.
GET/bookings/{id}
POST/bookings/{id}/acknowledge

Tell 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"}
Read the feed every one to five minutes, or when a webhook arrives. Treat the feed as the record: a webhook can arrive late or twice, the feed cannot.

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.

POST/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.

POST/sandbox/bookings/{id}/modify

Moves it one day later and adds a guest, giving it a new version.

POST/sandbox/bookings/{id}/cancel

The same three actions are on the Connectivity page.

Settlement

GET/settlement/statement?from=2026-11-01&to=2026-11-30

One 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.

GET/settlement/balance

What 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.

GET/payouts

Your 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.

GET/certification
authrequired
Any successful call with a sandbox key.
listingsrequired
GET /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 422 answer: send an impossible value on purpose and read details.
idempotencyrequired
A write repeated with the same Idempotency-Key, answered from the store.
feedrequired
GET /bookings at least twice, the second time with next_cursor.
ack_new, ack_change, ack_cancelrequired
Acknowledge a test booking when new, after modify, and after cancel.
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.

POSThttps://hospitality.tratok.net/api/ota/
Credentials
POS/Source/RequestorID with MessagePassword = your API key, or the X-Api-Key header.
HotelCode
The Tratok property id.
InvTypeCode, RoomTypeCode
The Tratok room id.
RatePlanCode
STD. Tratok sells one rate per room (BAR or no code are accepted too).

Messages

OTA_PingRQanswer OTA_PingRS
Echoes EchoData.
OTA_HotelAvailNotifRQanswer OTA_HotelAvailNotifRS
BookingLimit (rooms left), RestrictionStatus (Master = stop sell, Arrival, Departure; Open or Close) and LengthsOfStay (SetMinLOS, SetMaxLOS, SetForwardMinStay, SetForwardMaxStay, RemoveMinLOS, RemoveMaxLOS; Time 0 removes the rule).
OTA_HotelRateAmountNotifRQanswer OTA_HotelRateAmountNotifRS
Prices from BaseByGuestAmt: AmountAfterTax, or AmountBeforeTax. 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 HotelCode that your system has not yet confirmed at their latest version, with ResStatus Book, Modify or Cancel. The money sits in TPA_Extensions/TratokSettlement.
OTA_NotifReportRQanswer OTA_NotifReportRS
Confirms delivery: UniqueID ID is the Tratok reservation, and HotelReservationID 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