DubaiLand Travel API
Flight and hotel inventory for businesses. Search, book and ticket through one API, settled against a credit balance — no supplier contract, no card processing, no PCI scope of your own.
Quickstart
Three calls from nothing to a confirmed order. Every example works against sandbox.
# 1. Search — take an offer id from data.offers
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
-X POST https://staging.dubailandtravel.com/partner/v1/flights/search \
-H 'Content-Type: application/json' \
-d '{"slices":[{"origin":"BGW","destination":"DXB","date":"2026-12-02"}],"passengers":{"adults":1}}'
# 2. Confirm the price is still real — take data.price_confirmation_token
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
-X POST https://staging.dubailandtravel.com/partner/v1/flights/offers/$OFFER_ID/confirm-price
# 3. Order it, with the offer id and the token
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
-X POST https://staging.dubailandtravel.com/partner/v1/orders \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d @order.json
order.json:
{
"items": [
{
"type": "flight",
"offer_id": "{{offer_id}}",
"price_confirmation_token": "{{price_confirmation_token}}"
}
],
"passengers": [
{
"type": "ADT",
"first_name": "Dana",
"last_name": "Aziz",
"date_of_birth": "1990-04-02",
"gender": "F",
"nationality": "IQ",
"is_lead": true,
"document": {
"type": "passport",
"number": "A1234567",
"issuing_country": "IQ",
"expires_on": "2031-11-02"
}
}
],
"contact": {
"email": "traveller@example.com",
"phone": "+9647701234567"
},
"partner_reference": "your-own-id"
}
Skip the reading
Everything on this page, in the form your tools want. All four are generated from the running API, so they cannot describe an endpoint that does not exist.
| For | Get it | Then |
|---|---|---|
| Postman, Insomnia, Bruno | Postman collection | Import it, set client_id and client_secret on the collection, send. Every request is pre-filled. |
| Code generators and SDKs | OpenAPI 3.1 (JSON) | Feed it to openapi-generator, or to your editor for autocomplete. |
| Claude, ChatGPT, Cursor, Copilot | Reference (Markdown) | Paste it into the chat, or give the assistant this URL, and ask it to write your integration. |
| Claude Code and other agents | AI skill (SKILL.md) | Save it as .claude/skills/dubailand-travel-api/SKILL.md in your project. The agent then knows this API whenever it is relevant. |
AI tools that look for it will also find /llms.txt.
confirm-price immediately before ordering — an order
against a stale offer is refused with TB-HOTEL-1502, not silently
repriced. We will never charge you more than the price you confirmed.
Authentication
HTTP Basic: your client id as the username, your secret as the password. A bearer
token of the form client_id:secret works identically if your HTTP
stack makes Basic awkward.
Authorization: Basic {base64(client_id:secret)}
The secret is shown once, when the key is issued, and is stored hashed — we cannot read it back to you. Lost secrets are replaced by issuing a new key, and both work until you revoke the old one, so a rotation needs no downtime.
Sandbox and live
| Environment | Key prefix | Reaches a supplier | Moves money |
|---|---|---|---|
| Sandbox | sk_test_… | No | No |
| Live | sk_live_… | Yes | Yes |
The environment is a property of the key, not a header or a base URL. There is no
flag to forget: a sandbox key cannot reach real inventory even if you point it at
production, and every response echoes meta.environment so a support
ticket answers its own first question.
Endpoints
Base URL https://staging.dubailandtravel.com/partner/v1.
Open an endpoint for what to send and what comes back.
POST /flights/search Search flights
Returns offers already priced for the calling partner. A supplier failing degrades the search rather than failing it.
Scope flights.search
Request body
| Field | Type | Required |
|---|---|---|
| trip_type | string: one_way, round_trip, multi_city | — |
| slices | array | Yes |
| slices[].origin | string | Yes |
| slices[].destination | string | Yes |
| slices[].date | string | Yes |
| passengers | object | Yes |
| passengers.adults | integer | Yes |
| passengers.children | integer | — |
| passengers.infants | integer | — |
| cabin | string: ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST | — |
| preferences | object | — |
| preferences.non_stop_only | boolean | — |
| preferences.airlines | array | — |
| preferences.exclude_airlines | array | — |
| preferences.max_price | integer | — |
| currency | string | — |
Example
{
"slices": [
{
"origin": "BGW",
"destination": "DXB",
"date": "2026-12-02"
}
],
"passengers": {
"adults": 1
}
}
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offers": [
{
"id": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"base": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"taxes": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"validating_carrier": "string",
"cabin": "string",
"fare_type": "string",
"slices": [
"string"
],
"passengers": [
"string"
],
"conditions": {
"refundable": true,
"changeable": true,
"seats_remaining": 0,
"last_ticketing_date": "string"
},
"baggage": {},
"expires_at": "string"
}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"search_id": "string",
"count": 0,
"cache_hit": "string",
"degraded": true
}
}
GET /flights/search/{searchId}/results Read a held search
Cheap — the offers were computed once. Paging or filtering a result set a partner already has must never cost another supplier call, and must never consume their search rate limit either.
Scope flights.results
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offers": [
{
"id": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"base": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"taxes": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"validating_carrier": "string",
"cabin": "string",
"fare_type": "string",
"slices": [
"string"
],
"passengers": [
"string"
],
"conditions": {
"refundable": true,
"changeable": true,
"seats_remaining": 0,
"last_ticketing_date": "string"
},
"baggage": {},
"expires_at": "string"
}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"search_id": "string",
"count": 0
}
}
POST /flights/offers/{offerId}/confirm-price Confirm a price
Mandatory before an order. Airfare moves between search and booking often enough that ordering from a held price is a chargeback waiting to happen — and in B2B it would be the partner absorbing it, which is worse. Returns a token bound to the offer and its final amount. Ordering without one is refused.
Scope flights.price
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offer": {
"id": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"base": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"taxes": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"validating_carrier": "string",
"cabin": "string",
"fare_type": "string",
"slices": [
"string"
],
"passengers": [
"string"
],
"conditions": {
"refundable": true,
"changeable": true,
"seats_remaining": 0,
"last_ticketing_date": "string"
},
"baggage": {},
"expires_at": "string"
},
"price_changed": true,
"previous_total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"price_confirmation_token": "string"
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string"
}
}
POST /hotels/search Search hotels
Returns rates already priced for the calling partner. A supplier failing degrades the search rather than failing it.
Scope hotels.search
Request body
| Field | Type | Required |
|---|---|---|
| city_code | string | — |
| hotel_id | string | — |
| latitude | number | — |
| longitude | number | — |
| radius_km | integer | — |
| check_in | string | Yes |
| check_out | string | Yes |
| rooms | integer | — |
| adults | integer | — |
| children_ages | array | — |
| currency | string | — |
| min_stars | integer | — |
| board_type | string: room_only, breakfast, half_board, full_board, all_inclusive | — |
Example
{
"city_code": "DXB",
"check_in": "2026-12-02",
"check_out": "2026-12-05",
"rooms": 1,
"adults": 2
}
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offers": [
{
"id": "string",
"property": {
"id": "string",
"name": "string",
"star_rating": 0,
"city_code": "string",
"address": "string",
"latitude": 0,
"longitude": 0,
"amenities": [
"string"
],
"images": [
"string"
]
},
"room": {
"type_code": "string",
"description": {},
"board_type": "string",
"rate_plan": "string"
},
"stay": {
"check_in": "string",
"check_out": "string",
"nights": 0,
"rooms": 0,
"adults": 0,
"children_ages": [
"integer"
]
},
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"taxes_and_fees": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"per_night": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"payment_policy": "string",
"collected_by_property": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"payable_now": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"cancellation": {
"refundable": true,
"free_until": "string",
"penalty": {
"amount": "integer",
"currency": "string",
"decimals": "integer",
"formatted": "string"
},
"tiers": [
{}
],
"description": "string"
},
"expires_at": "string"
}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"search_id": "string",
"count": "string",
"cache_hit": "string",
"expires_at": "string",
"degraded": true
}
}
GET /hotels/search/{searchId}/results Read a held search
Cheap — the rates were computed once. Paging or filtering a result set a partner already holds must never cost another supplier call, and must never consume their search rate limit either.
Scope hotels.results
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offers": [
{
"id": "string",
"property": {
"id": "string",
"name": "string",
"star_rating": 0,
"city_code": "string",
"address": "string",
"latitude": 0,
"longitude": 0,
"amenities": [
"string"
],
"images": [
"string"
]
},
"room": {
"type_code": "string",
"description": {},
"board_type": "string",
"rate_plan": "string"
},
"stay": {
"check_in": "string",
"check_out": "string",
"nights": 0,
"rooms": 0,
"adults": 0,
"children_ages": [
"integer"
]
},
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"taxes_and_fees": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"per_night": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"payment_policy": "string",
"collected_by_property": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"payable_now": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"cancellation": {
"refundable": true,
"free_until": "string",
"penalty": {
"amount": "integer",
"currency": "string",
"decimals": "integer",
"formatted": "string"
},
"tiers": [
{}
],
"description": "string"
},
"expires_at": "string"
}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"search_id": "string",
"count": 0
}
}
POST /hotels/offers/{offerId}/confirm-price Confirm a rate
Mandatory before an order, and for more than the price: a cancellation policy that changed between search and booking is a promise we would otherwise make and not keep — and in B2B it is the partner who would have made it to their customer. Returns a token bound to the offer and its final amount. Ordering without one is refused.
Scope hotels.price
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offer": {
"id": "string",
"property": {
"id": "string",
"name": "string",
"star_rating": 0,
"city_code": "string",
"address": "string",
"latitude": 0,
"longitude": 0,
"amenities": [
"string"
],
"images": [
"string"
]
},
"room": {
"type_code": "string",
"description": {},
"board_type": "string",
"rate_plan": "string"
},
"stay": {
"check_in": "string",
"check_out": "string",
"nights": 0,
"rooms": 0,
"adults": 0,
"children_ages": [
0
]
},
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"taxes_and_fees": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"per_night": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"payment_policy": "string",
"collected_by_property": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"payable_now": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"cancellation": {
"refundable": true,
"free_until": "string",
"penalty": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"tiers": [
{
"from": "string",
"amount_minor": "integer"
}
],
"description": "string"
},
"expires_at": "string"
},
"price_changed": true,
"previous_total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"policy_changed": true,
"price_confirmation_token": "string"
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string"
}
}
POST /transfers/search Quote a transfer
Returns every vehicle an operator will put on the journey, already priced for the calling partner and already filtered to those that can actually carry the party.
Scope transfers.search
Request body
| Field | Type | Required |
|---|---|---|
| origin_id | string | Yes |
| destination_id | string | Yes |
| pickup_at | string | Yes |
| return_at | string | — |
| adults | integer | Yes |
| children | integer | — |
| infants | integer | — |
| bags | integer | — |
| flight_number | string | — |
| currency | string | — |
Example
{
"origin_id": "{{origin_id}}",
"destination_id": "{{destination_id}}",
"pickup_at": "2026-12-02 14:30",
"adults": 2
}
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offers": [
{
"id": "string",
"journey": {
"origin": {
"id": "string",
"name": "string",
"type": "string",
"address": "string",
"city_name": "string",
"country_code": "string",
"iata_code": "string",
"latitude": "number",
"longitude": "number"
},
"destination": {
"id": "string",
"name": "string",
"type": "string",
"address": "string",
"city_name": "string",
"country_code": "string",
"iata_code": "string",
"latitude": "number",
"longitude": "number"
},
"pickup_at": "string",
"pickup_timezone": "string",
"return_pickup_at": "string",
"duration_minutes": 0,
"distance_km": 0
},
"vehicle": {
"class": "string",
"description": {},
"max_passengers": 0,
"max_bags": 0,
"inclusions": [
"string"
]
},
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"payment_policy": "string",
"collected_by_driver": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"payable_now": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"cancellation": {
"refundable": true,
"free_until": "string",
"penalty": {
"amount": "integer",
"currency": "string",
"decimals": "integer",
"formatted": "string"
},
"description": "string"
},
"expires_at": "string"
}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"search_id": "string",
"count": "string",
"cache_hit": "string",
"expires_at": "string",
"degraded": true,
"unserved": "string"
}
}
GET /transfers/search/{searchId}/results Read a held quote
Cheap — the quotes were computed once. Re-reading a result set a partner already holds must never cost another operator call, and must never consume their search rate limit either.
Scope transfers.results
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"offers": [
{
"id": "string",
"journey": {
"origin": {
"id": "string",
"name": "string",
"type": "string",
"address": "string",
"city_name": "string",
"country_code": "string",
"iata_code": "string",
"latitude": "number",
"longitude": "number"
},
"destination": {
"id": "string",
"name": "string",
"type": "string",
"address": "string",
"city_name": "string",
"country_code": "string",
"iata_code": "string",
"latitude": "number",
"longitude": "number"
},
"pickup_at": "string",
"pickup_timezone": "string",
"return_pickup_at": "string",
"duration_minutes": 0,
"distance_km": 0
},
"vehicle": {
"class": "string",
"description": {},
"max_passengers": 0,
"max_bags": 0,
"inclusions": [
"string"
]
},
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"currency": "string",
"payment_policy": "string",
"collected_by_driver": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"payable_now": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"cancellation": {
"refundable": true,
"free_until": "string",
"penalty": {
"amount": "integer",
"currency": "string",
"decimals": "integer",
"formatted": "string"
},
"description": "string"
},
"expires_at": "string"
}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"search_id": "string",
"count": 0
}
}
GET /transfers/locations Places we can be driven to
Transfers are quoted between identifiers from an operator's catalogue, not between addresses — so without this endpoint a partner has nothing valid to put in a search. It is part of the product rather than a convenience.
Scope transfers.locations
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"locations": [
{}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"count": 0
}
}
POST /orders Place an order
Requires an `Idempotency-Key`. Draws the total from the partner's balance, creates the order with the supplier, and returns the confirmation. A refusal at the supplier releases the hold immediately — the partner is never out of pocket for a booking that did not happen. A 402 means the balance and credit limit together do not cover it, and the body states the shortfall.
Scope orders.create · Idempotency-Key header required
Request body
| Field | Type | Required |
|---|---|---|
| items | array | Yes |
| items[].type | string: flight, hotel, transfer | Yes |
| items[].offer_id | string | Yes |
| items[].price_confirmation_token | string | Yes |
| passengers | array | — |
| passengers[].type | string: ADT, CHD, INF | — |
| passengers[].first_name | string | Yes |
| passengers[].last_name | string | Yes |
| passengers[].date_of_birth | string | — |
| passengers[].gender | string: M, F, X | — |
| passengers[].nationality | string | — |
| passengers[].is_lead | boolean | — |
| passengers[].document | object | — |
| passengers[].document.type | string | — |
| passengers[].document.number | string | — |
| passengers[].document.issuing_country | string | — |
| passengers[].document.expires_on | string | — |
| guests | array | — |
| guests[].first_name | string | — |
| guests[].last_name | string | — |
| guests[].is_lead | boolean | — |
| special_requests | string | — |
| flight_number | string | — |
| bags | integer | — |
| estimated_arrival_time | string | — |
| contact | object | Yes |
| contact.email | string | Yes |
| contact.phone | string | Yes |
| contact.name | string | — |
| partner_reference | string | — |
Example
{
"items": [
{
"type": "flight",
"offer_id": "{{offer_id}}",
"price_confirmation_token": "{{price_confirmation_token}}"
}
],
"passengers": [
{
"type": "ADT",
"first_name": "Dana",
"last_name": "Aziz",
"date_of_birth": "1990-04-02",
"gender": "F",
"nationality": "IQ",
"is_lead": true,
"document": {
"type": "passport",
"number": "A1234567",
"issuing_country": "IQ",
"expires_on": "2031-11-02"
}
}
],
"contact": {
"email": "traveller@example.com",
"phone": "+9647701234567"
},
"partner_reference": "your-own-id"
}
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"order": {
"reference": "string",
"partner_reference": "string",
"status": "string",
"type": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"charged_to_balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"contact": {
"email": "string",
"phone": "string",
"name": "string"
},
"passengers": [
{
"first_name": "string",
"last_name": "string",
"type": "string",
"document_last4": "string"
}
],
"items": [
{
"id": 0,
"type": "string",
"status": "string",
"total": {
"amount": "integer",
"currency": "string"
},
"hotel": "string",
"confirmation_number": "string",
"check_in": "string",
"check_out": "string"
}
],
"travel_starts_at": "string",
"created_at": "string",
"ticketing_deadline_at": "string"
}
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
}
}
}
GET /orders List orders
Scope orders.read
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"orders": [
{
"reference": "string",
"partner_reference": "string",
"status": "string",
"type": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"charged_to_balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"contact": {
"email": "string",
"phone": "string",
"name": "string"
},
"passengers": [
{
"first_name": "string",
"last_name": "string",
"type": "string",
"document_last4": "string"
}
],
"items": [
{
"id": "integer",
"type": "string",
"status": "string",
"total": {},
"hotel": "string",
"confirmation_number": "string",
"check_in": "string",
"check_out": "string"
}
],
"travel_starts_at": "string",
"created_at": "string",
"ticketing_deadline_at": "string"
}
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"page": 0,
"per_page": 0,
"total": 0
}
}
GET /orders/{reference} Read an order
Scope orders.read
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"order": {
"reference": "string",
"partner_reference": "string",
"status": "string",
"type": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"charged_to_balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"contact": {
"email": "string",
"phone": "string",
"name": "string"
},
"passengers": [
{
"first_name": "string",
"last_name": "string",
"type": "string",
"document_last4": "string"
}
],
"items": [
{
"id": 0,
"type": "string",
"status": "string",
"total": {
"amount": "integer",
"currency": "string"
},
"hotel": "string",
"confirmation_number": "string",
"check_in": "string",
"check_out": "string"
}
],
"travel_starts_at": "string",
"created_at": "string",
"ticketing_deadline_at": "string"
}
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string"
}
}
POST /orders/{reference}/cancel Cancel an order
Returns the penalty the supplier actually charged and credits the remainder back to the partner's balance. On a prepaid model a refund is a ledger entry rather than a gateway call that can fail, which is the single biggest operational advantage of settling this way.
Scope orders.cancel · Idempotency-Key header required
Request body
| Field | Type | Required |
|---|---|---|
| reason | string | — |
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"order": {
"reference": "string",
"partner_reference": "string",
"status": "string",
"type": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"charged_to_balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"contact": {
"email": "string",
"phone": "string",
"name": "string"
},
"passengers": [
{
"first_name": "string",
"last_name": "string",
"type": "string",
"document_last4": "string"
}
],
"items": [
{
"id": 0,
"type": "string",
"status": "string",
"total": {
"amount": "integer",
"currency": "string"
},
"hotel": "string",
"confirmation_number": "string",
"check_in": "string",
"check_out": "string"
}
],
"travel_starts_at": "string",
"created_at": "string",
"ticketing_deadline_at": "string"
},
"refunded_to_balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"penalty": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
}
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
}
}
}
POST /orders/{reference}/hotels Add a hotel to an order
Requires an `Idempotency-Key`: this changes what the partner owes, and a retry must not book the room twice. The room is reserved and confirmed on its own. A property refusing leaves the flight exactly as it was — that is the whole reason items carry independent lifecycles — and comes back as a 409 on the room rather than as a failure of the order.
Scope orders.add_hotel · Idempotency-Key header required
Request body
| Field | Type | Required |
|---|---|---|
| offer_id | string | Yes |
| price_confirmation_token | string | Yes |
| special_requests | string | — |
| estimated_arrival_time | string | — |
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"order": {
"reference": "string",
"partner_reference": "string",
"status": "string",
"type": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"charged_to_balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"contact": {
"email": "string",
"phone": "string",
"name": "string"
},
"passengers": [
{
"first_name": "string",
"last_name": "string",
"type": "string",
"document_last4": "string"
}
],
"items": [
{
"id": 0,
"type": "string",
"status": "string",
"total": {
"amount": "integer",
"currency": "string"
},
"hotel": "string",
"confirmation_number": "string",
"check_in": "string",
"check_out": "string"
}
],
"travel_starts_at": "string",
"created_at": "string",
"ticketing_deadline_at": "string"
}
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
}
}
}
POST /orders/{reference}/transfers Add a transfer to an order
Requires an `Idempotency-Key`: this changes what the partner owes, and a retry must not put a second car on the road. The car is booked on its own. An operator refusing leaves the flight exactly as it was and comes back as a 410 on the transfer rather than a failure of the order.
Scope orders.add_transfer · Idempotency-Key header required
Request body
| Field | Type | Required |
|---|---|---|
| offer_id | string | Yes |
| flight_number | string | — |
| passengers | integer | — |
| bags | integer | — |
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"order": {
"reference": "string",
"partner_reference": "string",
"status": "string",
"type": "string",
"total": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"charged_to_balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
},
"contact": {
"email": "string",
"phone": "string",
"name": "string"
},
"passengers": [
{
"first_name": "string",
"last_name": "string",
"type": "string",
"document_last4": "string"
}
],
"items": [
{
"id": 0,
"type": "string",
"status": "string",
"total": {
"amount": "integer",
"currency": "string"
},
"hotel": "string",
"confirmation_number": "string",
"check_in": "string",
"check_out": "string"
}
],
"travel_starts_at": "string",
"created_at": "string",
"ticketing_deadline_at": "string"
}
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"balance": {
"amount": 0,
"currency": "string",
"decimals": 0,
"formatted": "string"
}
}
}
GET /account The account at a glance
Scope account.read
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"partner": {
"name": "string",
"status": "string",
"account_id": "string"
},
"credential": {
"label": "string",
"environment": "string",
"scopes": [
"string"
],
"expires_at": "string"
},
"services": [
"string"
],
"usage_this_period": {
"period": "string",
"calls": 0,
"remaining": 0
},
"balances": [
"string"
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string"
}
}
GET /account/usage Usage for a closed or current period
Scope account.usage
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"period": "string",
"calls": 0,
"billable_calls": 0,
"overage_calls": 0,
"errors": 0,
"p95_duration_ms": 0
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string",
"billable": true
}
}
GET /account/statement Every movement on the account in a period, with a running balance
The running balance is what makes this reconcilable line by line rather than only in total — a partner whose closing figure disagrees with ours needs to find WHERE, not merely THAT.
Scope account.statement
No request body.
Response
The shape of a successful reply. Values are placeholders.
{
"data": {
"period": "string",
"currency": "string",
"opening_balance": {},
"closing_balance": {},
"credits": {},
"debits": {},
"entries": [
"string"
]
},
"meta": {
"request_id": "string",
"timestamp": "string",
"environment": "string"
}
}
Generated from the running API: the fields, the scope each key is checked against and the response shapes are read from the code, not written here by hand.
Responses
Two shapes, and only two. Success:
{
"data": { … },
"meta": { "request_id": "…", "timestamp": "…", "environment": "sandbox" }
}
Failure, as an RFC 9457 problem document:
{
"type": "https://…/errors/TB-PARTNER-3001",
"title": "Insufficient balance",
"detail": "This booking exceeds the available balance and credit limit on this account.",
"status": 402,
"code": "TB-PARTNER-3001",
"action": "top_up",
"retryable": false,
"request_id": "…"
}
code and action, never on the message.
Messages are localised into Arabic, English and Kurdish Sorani and will be reworded.
Codes are permanent. action tells your integration what to do without
it having to know what every code means.
Idempotency
Every endpoint that books or moves money requires an Idempotency-Key
header — a value you generate, unique per logical operation.
Replaying a key returns the original response rather than repeating the work. This is what makes a network timeout safe to retry: without it, a request that timed out after we created a booking would create a second one when you tried again, and you would be charged for both.
Keys are held for 24 hours. Reuse one with a different body and the call is refused.
Webhooks
An order's state changes long after the request that created it — a ticket issues, a carrier moves a flight, a reconciler resolves an ambiguous order. Subscribe rather than poll.
| Event | Meaning |
|---|---|
| order.confirmed | The supplier accepted the order. Some multi-item orders confirm partially; the payload says which. |
| order.ticketed | Ticket numbers exist. This is the point at which the passenger can travel. |
| order.failed | The supplier refused the order. Nothing was created and any credit hold has been released. |
| order.cancelled | The order was cancelled. The payload carries the penalty that was actually applied. |
| order.needs_attention | The order is ambiguous at the supplier and a human is resolving it. The credit hold is KEPT. Do not retry. |
Subscriptions accept wildcards — order.* follows the family without
you enumerating events we have not invented yet. Registering no filter means
everything.
Verifying the signature
Every delivery carries X-Signature: HMAC-SHA256 of the
raw request body using your endpoint secret. Verify it before you
trust a payload — a webhook URL is otherwise an unauthenticated endpoint anyone
who learns it can post fabricated order confirmations to.
// Raw bytes, not the parsed JSON: re-encoding changes key order and
// unicode escaping, and the signature will not match.
$expected = hash_hmac('sha256', $request->getContent(), $endpointSecret);
if (! hash_equals($expected, $request->header('X-Signature'))) {
abort(400);
}
Retries
A non-2xx is retried with backoff at 10 seconds, 1 minute, 10 minutes and 1 hour.
A 4xx is not retried — an unchanged body will be
rejected identically. An endpoint that fails twenty times consecutively is
disabled; deliveries are still recorded and can be replayed once you fix it.
id. We retry, so the same event can
arrive twice. Treat the envelope's id as seen-or-not and you will
never act on one twice.
Try it
Make a real call from this page. Your credentials stay in this browser tab — the request goes straight to the endpoint from here, and nothing is stored or proxied.
sk_test_ key. A live key typed into a page is a live key in your browser
history, and sandbox answers every question you have at this stage.
The response appears here.
Sandbox scenarios
You can reach the happy path by accident. What you cannot exercise without help is the handful of ways an order fails — and those are exactly the paths your support team will live in. Set the lead passenger's surname to a trigger:
| Surname | What happens |
|---|---|
| SANDBOXSOLDOUT | Supplier refuses the order. Nothing is created and the credit hold is released. |
| SANDBOXPRICECHANGE | The fare changed between pricing and ordering. The order is refused. |
| SANDBOXTIMEOUT | The supplier never responds. The order goes to manual review and the credit hold is KEPT — do not retry. |
| SANDBOXNOTICKET | Order confirms with a PNR, but no ticket is issued automatically. |
| SANDBOXSCHEDULECHANGE | Order confirms, then the carrier changes the departure time. |
| SANDBOXPENALTY | Cancelling this order costs a penalty instead of being free. |
SANDBOXTIMEOUT before you go live.
It is the only outcome where the money is neither spent nor free, and an
integration that retries blindly on it will double-book a real passenger.
Rate limits
Requests are rate limited per key, and search carries its own tighter limit because every miss costs a supplier call. Reading back a held result set does not touch it — paging through your own results is not a new search.
A 429 carries Retry-After; honour it rather than
tightening your retry loop. If your traffic needs a higher limit, ask your account
manager.
Balance and statements
Orders settle against a USD credit balance you top up in advance by bank transfer — there is no card in the booking path. A booking reserves before it commits and releases if the supplier refuses, so you are never out of pocket for an order that never existed.
GET /account returns your balance and your usage.
GET /account/statement returns every movement for a month with a
running balance, so a disagreement can be traced to a line rather than a total.
Versioning
partner/v1 is the contract. Adding a field, an endpoint or an error code does
not move it — so parse leniently: ignore fields you do not recognise rather
than failing on them. Anything that would break a working integration gets a new version and
an overlap, announced in advance.
Error catalogue
Every failure this API can return, grouped by what it is about, with the status it returns and the action your integration should take. Copy shown in English; the same codes carry Arabic and Kurdish Sorani.
Requests 12 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-CORE-1001 | 400 | correct_input | No | We could not read that request |
| TB-CORE-1002 | 422 | correct_input | No | Some details need correcting |
| TB-CORE-1003 | 404 | none | No | Not found |
| TB-CORE-1004 | 403 | none | No | Not allowed |
| TB-CORE-1005 | 429 | wait | Yes | Too many requests |
| TB-CORE-1007 | 426 | update_app | No | Please update the app |
| TB-CORE-1008 | 400 | correct_input | No | Missing request identifier |
| TB-CORE-1009 | 409 | none | No | This request was already submitted |
| TB-CORE-1010 | 409 | wait | Yes | Still working on that |
| TB-CORE-1011 | 400 | correct_input | No | Currency not available |
| TB-CORE-1012 | 400 | correct_input | No | Language not available |
| TB-CORE-1013 | 405 | none | No | Not supported |
Authentication 6 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-AUTH-1102 | 401 | reauthenticate | No | Your session has expired |
| TB-AUTH-1105 | 403 | contact_support | No | This account is suspended |
| TB-AUTH-1106 | 400 | correct_input | Yes | That code is incorrect |
| TB-AUTH-1107 | 410 | retry | No | That code has expired |
| TB-AUTH-1108 | 429 | wait | Yes | Too many codes requested |
| TB-AUTH-1114 | 401 | reauthenticate | No | Security check failed |
Search 5 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-SRCH-1301 | 422 | correct_input | No | Choose a departure date in the future |
| TB-SRCH-1302 | 422 | correct_input | No | Check your return date |
| TB-SRCH-1303 | 422 | correct_input | No | Choose two different places |
| TB-SRCH-1306 | 422 | correct_input | No | Too many passengers |
| TB-SRCH-1307 | 422 | correct_input | No | Each infant needs an adult |
Flights 4 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-FLIGHT-1401 | 410 | restart_search | No | This fare is no longer available |
| TB-FLIGHT-1402 | 410 | restart_search | No | This offer has expired |
| TB-FLIGHT-1403 | 409 | none | No | The price for this flight has changed |
| TB-FLIGHT-1406 | 422 | none | No | Please confirm the price first |
Hotels 6 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-HOTEL-1501 | 410 | restart_search | No | This room is no longer available |
| TB-HOTEL-1502 | 409 | restart_search | No | The rate has changed |
| TB-HOTEL-1503 | 409 | none | No | This property cannot confirm the booking |
| TB-HOTEL-1504 | 422 | correct_input | No | Check your dates |
| TB-HOTEL-1505 | 422 | correct_input | No | Too many guests for these rooms |
| TB-HOTEL-1507 | 422 | correct_input | No | The room needs a name |
Account and balance 15 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-PARTNER-3001 | 402 | top_up | No | Insufficient balance |
| TB-PARTNER-3002 | 403 | contact_support | No | Account suspended |
| TB-PARTNER-3003 | 409 | none | No | This top-up has already been processed |
| TB-PARTNER-3004 | 401 | check_credentials | No | Invalid API credentials |
| TB-PARTNER-3005 | 403 | check_scopes | No | This key is not permitted to do that |
| TB-PARTNER-3006 | 429 | back_off | Yes | Rate limit exceeded |
| TB-PARTNER-3007 | 402 | upgrade_plan | No | Monthly quota exhausted |
| TB-PARTNER-3008 | 403 | upgrade_plan | No | Product not included in this plan |
| TB-PARTNER-3009 | 403 | use_live_key | No | Sandbox keys cannot do that |
| TB-PARTNER-3010 | 402 | top_up | No | No balance in this currency |
| TB-PARTNER-3011 | 422 | correct_input | No | This API settles in USD |
| TB-PARTNER-3012 | 403 | contact_support | No | This service is not active on your account |
| TB-PARTNER-3013 | 429 | back_off | Yes | Daily limit reached |
| TB-PARTNER-3014 | 422 | correct_input | No | This order exceeds your per-booking limit |
| TB-PARTNER-3015 | 409 | contact_support | No | No permitted supplier can fulfil this |
Transfers 2 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-TRANSFER-1601 | 410 | restart_search | No | This transfer is no longer available |
| TB-TRANSFER-1602 | 422 | correct_input | No | Too many passengers or bags |
Orders 10 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-BOOK-1701 | 410 | restart_search | No | Your booking hold has expired |
| TB-BOOK-1704 | 409 | contact_support | No | This booking cannot be changed right now |
| TB-BOOK-1706 | 422 | contact_support | No | This fare cannot be cancelled |
| TB-BOOK-1708 | 202 | wait | No | Your booking needs a quick review |
| TB-BOOK-1709 | 409 | wait | Yes | This booking is being updated |
| TB-BOOK-1712 | 409 | none | No | A booking needs at least one item |
| TB-BOOK-1713 | 409 | restart_search | No | This booking can no longer be changed |
| TB-BOOK-1714 | 409 | restart_search | No | That price is in a different currency |
| TB-BOOK-1716 | 409 | contact_support | No | This booking needs a human to issue the ticket |
| TB-BOOK-1715 | 409 | restart_search | No | The supplier could not confirm this order |
Travellers 9 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-PAX-1801 | 422 | correct_input | No | Name must match the travel document |
| TB-PAX-1802 | 422 | correct_input | No | Use Latin characters for names |
| TB-PAX-1803 | 422 | correct_input | No | This passport expires before travel |
| TB-PAX-1804 | 422 | correct_input | No | Passport expires soon after travel |
| TB-PAX-1805 | 422 | correct_input | No | Date of birth does not match the passenger type |
| TB-PAX-1806 | 422 | correct_input | No | Check the infant age |
| TB-PAX-1807 | 422 | correct_input | No | Passenger count does not match the fare |
| TB-PAX-1808 | 422 | correct_input | No | Check the document number |
| TB-PAX-1809 | 422 | correct_input | No | Contact details needed |
Suppliers 8 codes
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-PROV-3001 | 503 | retry | Yes | This airline's system is unavailable |
| TB-PROV-3002 | 504 | wait | Yes | The airline is taking longer than usual |
| TB-PROV-3003 | 502 | contact_support | Yes | Something went wrong on our side |
| TB-PROV-3004 | 429 | wait | Yes | We're very busy right now |
| TB-PROV-3005 | 500 | contact_support | No | Something went wrong on our side |
| TB-PROV-3006 | 409 | correct_input | No | The airline did not accept these details |
| TB-PROV-3007 | 500 | contact_support | No | Something went wrong on our side |
| TB-PROV-3008 | 503 | wait | Yes | This option is temporarily unavailable |
System 1 code
| Code | HTTP | Action | Retry | Meaning |
|---|---|---|---|---|
| TB-SYS-5001 | 500 | contact_support | Yes | Something went wrong on our side |
Machine-readable contract: openapi.json. Generate a client with openapi-generator, or mock the whole API offline with npx @stoplight/prism-cli mock openapi.json.