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://api.dubailandtravel.com/partner/v1/flights/search \
  -H 'Content-Type: application/json' \
  -d '{"slices":[{"origin":"BGW","destination":"DXB","date":"2026-12-12"}],"passengers":{"adults":1}}'

# 2. Confirm the price is still real — take data.price_confirmation_token
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  -X POST https://api.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://api.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-12"
            }
        }
    ],
    "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.

ForGet itThen
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.

Prices expire. Airline fares are held for minutes, not hours. Always call confirm-price immediately before ordering — an order whose offer has expired or whose price confirmation no longer matches is refused (for a flight TB-FLIGHT-1402, TB-BOOK-1701 or TB-FLIGHT-1406; for a room TB-HOTEL-1502), not silently repriced. We will never charge you more than the price you confirmed.
Everything here is in USD. Prices, balances, invoices and statements. You are buying wholesale from us and selling to your own customers — converting for them, at your rate, on your margin, is yours to do. A request naming another currency is refused rather than converted, so you never reconcile against a number you did not expect.

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

EnvironmentKey prefixReaches a supplierMoves money
Sandboxsk_test_…NoNo
Livesk_live_…YesYes

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 successful response echoes meta.environment so a support ticket answers its own first question.

Live keys need a static IP. Before we issue a live key, send us the static IP address of every server that will call the API. A live key refuses requests from any other address with TB-PARTNER-3004. Sandbox keys work from anywhere.

Endpoints

Base URL https://api.dubailandtravel.com/partner/v1.

Open an endpoint for what to send and what comes back.

GET /partner/language/{locale}

Scope —

No request body.

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

FieldTypeRequired
trip_typestring: one_way, round_trip, multi_city—
slicesarrayYes
slices[].originstringYes
slices[].destinationstringYes
slices[].datestringYes
passengersobjectYes
passengers.adultsintegerYes
passengers.childreninteger—
passengers.infantsinteger—
cabinstring: ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST—
preferencesobject—
preferences.non_stop_onlyboolean—
preferences.airlinesarray—
preferences.exclude_airlinesarray—
preferences.max_priceinteger—
currencystring—

Example

{
    "slices": [
        {
            "origin": "BGW",
            "destination": "DXB",
            "date": "2026-12-12"
        }
    ],
    "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. Returns every offer in the search, with no paging. Reading it again never costs another supplier call or consumes the search rate limit.

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

FieldTypeRequired
city_codestring—
hotel_idstring—
latitudenumber—
longitudenumber—
radius_kminteger—
check_instringYes
check_outstringYes
roomsinteger—
adultsinteger—
children_agesarray—
currencystring—
min_starsinteger—
board_typestring: room_only, breakfast, half_board, full_board, all_inclusive—

Example

{
    "city_code": "DXB",
    "check_in": "2026-12-12",
    "check_out": "2026-12-15",
    "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. Returns every rate in the search, with no paging. Reading it again never costs another supplier call or consumes the search rate limit.

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

FieldTypeRequired
origin_idstringYes
destination_idstringYes
pickup_atstringYes
return_atstring—
adultsintegerYes
childreninteger—
infantsinteger—
bagsinteger—
flight_numberstring—
currencystring—

Example

{
    "origin_id": "{{origin_id}}",
    "destination_id": "{{destination_id}}",
    "pickup_at": "2026-12-12 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. One item per order; add a room or a car afterwards with the hotels and transfers endpoints of the order. A 402 means the balance and credit limit together do not cover it and the account is not set to wait for approval; the body states the shortfall. A 409 `TB-PARTNER-3019` means the supplier did not say whether it created the order: the hold stays and the order must not be placed again. When the balance and credit limit do not cover the order, our team may still approve it: the answer is then a 409 `TB-PARTNER-3020` with status `pending_approval`, and the order must not be placed again. Nothing is charged meanwhile. Its outcome arrives as `order.confirmed`, or as `order.failed` (`TB-PARTNER-3001`) if it is rejected or not decided in time; you may cancel it while it waits.

Scope orders.create · Idempotency-Key header required

Request body

FieldTypeRequired
itemsarrayYes
items[].typestring: flight, hotel, transferYes
items[].offer_idstringYes
items[].price_confirmation_tokenstringYes
passengersarray—
passengers[].typestring: ADT, CHD, INF—
passengers[].first_namestringYes
passengers[].last_namestringYes
passengers[].date_of_birthstring—
passengers[].genderstring: M, F, X—
passengers[].nationalitystring—
passengers[].is_leadboolean—
passengers[].documentobject—
passengers[].document.typestring—
passengers[].document.numberstring—
passengers[].document.issuing_countrystring—
passengers[].document.expires_onstring—
passenger_countinteger—
guestsarray—
guests[].first_namestring—
guests[].last_namestring—
guests[].is_leadboolean—
special_requestsstring—
flight_numberstring—
bagsinteger—
estimated_arrival_timestring—
contactobjectYes
contact.emailstringYes
contact.phonestringYes
contact.namestring—
partner_referencestring—

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-12"
            }
        }
    ],
    "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",
            "travel_starts_at_local": "string",
            "travel_timezone": "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",
                "travel_starts_at_local": "string",
                "travel_timezone": "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",
            "travel_starts_at_local": "string",
            "travel_timezone": "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. An order still `pending_approval` was never charged: cancelling it closes it with no penalty and nothing to refund.

Scope orders.cancel · Idempotency-Key header required

Request body

FieldTypeRequired
reasonstring—

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",
            "travel_starts_at_local": "string",
            "travel_timezone": "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

FieldTypeRequired
offer_idstringYes
price_confirmation_tokenstringYes
special_requestsstring—
estimated_arrival_timestring—

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",
            "travel_starts_at_local": "string",
            "travel_timezone": "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

FieldTypeRequired
offer_idstringYes
flight_numberstring—
passengersinteger—
bagsinteger—

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",
            "travel_starts_at_local": "string",
            "travel_timezone": "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

`balances` is null on a sandbox key, which moves no money, rather than a zero that would read as "out of funds".

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. Live keys only: a sandbox key moves no money, so it has no statement and is refused with 403 `TB-PARTNER-3009`.

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": "…"
}
Branch on 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: 8 to 64 characters of letters, digits, ., _, : or -. A UUID fits. Keys are yours alone: they are scoped to your account and to sandbox or live.

Replaying a key returns the original response rather than repeating the work — including an error response, so a refused order stays refused. 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; after that the same key runs the request again. Reuse one with a different body and the call is refused.

Webhooks

An order's state can change after the request that created it: a ticket issues, an order that was under review is resolved, an order is cancelled. A webhook tells your server when that happens, so you do not have to poll your orders.

Webhooks are an add-on to your account, sold separately from bookings. Your account manager turns them on and registers your endpoint: an https address on the public internet, for one of your keys. You receive its signing secret once, when it is added or replaced. An endpoint on a sandbox key receives sandbox orders only, and one on a live key receives live orders only; the envelope's environment says which.

EventMeaning
order.confirmedThe supplier accepted the order. An order with more than one item can confirm partially: status is then partially_confirmed, and the order itself says which items were booked.
order.ticketedTicket numbers exist. This is the point at which the passenger can travel.
order.failedThe order was not booked: the supplier refused it, or it was over your credit limit and our team rejected it or did not decide in time (error_code TB-PARTNER-3001). Nothing was created and any credit hold has been released.
order.cancelledThe order was cancelled. Any refund is a credit on your statement; the cancel response itself carries the penalty applied. An order cancelled while waiting for approval was never charged.
order.needs_attentionThe order is ambiguous at the supplier and a human is resolving it. The credit hold is KEPT. Do not retry.

An endpoint receives the events you ask for, or every event — including any added later — if you ask for none in particular.

Every delivery is a POST of this envelope:

{
  "id": "…",                    // unique per delivery; the same on every retry of it
  "type": "order.confirmed",
  "environment": "live",        // or "sandbox"
  "sent_at": "2026-11-07T09:30:00+00:00",
  "data": {
    "order_reference": "TB-…",
    "status": "confirmed",
    "partner_reference": "…",   // when you sent one
    "total": { … },
    "airline_reference": "…",   // flights, once a PNR exists
    "ticket_numbers": [ … ],    // once tickets exist
    "attention_reason": "…",    // when the order is under review
    "occurred_at": "…"
  }
}

The payload is a nudge, not the order. Read the order back with GET /orders/{reference} for anything else, such as which items of a partially confirmed order were booked. A cancellation's penalty is in the response to POST /orders/{reference}/cancel, and any refund is a credit line on your statement.

Each delivery also carries the headers X-Event-Type, X-Delivery-Id and X-Attempt.

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 5xx, a timeout or a refused connection 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. After twenty failed attempts in a row the endpoint is switched off. Events that happen while it is off are not stored or sent later: once your account manager switches it back on, read back the orders you have open to catch up.

Deduplicate on 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.

Sandbox keys only. This console refuses anything that is not a 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 a passenger's or guest's surname to a trigger:

SurnameWhat happens
SANDBOXSOLDOUTThe supplier refuses the order. Nothing is created; on a live key the credit hold would be released.
SANDBOXPRICECHANGEThe price changed between pricing and ordering. The order is refused.
SANDBOXTIMEOUTThe supplier never answers. The order goes to manual review and is answered with TB-PARTNER-3019; on a live key the credit hold would be KEPT. Do not place it again.
SANDBOXNOTICKETFlights only: the order confirms with a PNR, but no ticket is issued automatically.
SANDBOXPENALTYCancelling this order costs a penalty instead of being free.
SANDBOXOVERLIMITThe order is over your credit limit and waits for our approval: answered with 409 TB-PARTNER-3020 and status pending_approval. Nobody approves a sandbox order: cancel it, or after 10 minutes it fails with order.failed (error_code TB-PARTNER-3001). Do not place it again.
Build against 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 — fetching your own results again 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.

Over your credit limit, an order waits instead of being refused. You get 409 TB-PARTNER-3020 and the order has status pending_approval: nothing is charged and no supplier is called while our team decides, for up to 10 minutes. Do not place it again. If it is approved you receive order.confirmed; if it is rejected or nobody decides in time, order.failed with error_code TB-PARTNER-3001. You can cancel it while it waits, at no cost. Try it in sandbox with the surname SANDBOXOVERLIMIT. If your account is set to refuse at once instead, you get 402 TB-PARTNER-3001 as before. A room or car added to an existing order is always refused with that 402 when it goes over the limit; it never waits.

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
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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 20 codes
CodeHTTPActionRetryMeaning
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
TB-PARTNER-3016 402 top_up No Not enough balance for this feature
TB-PARTNER-3017 409 none No This feature is already active
TB-PARTNER-3018 403 contact_support No Webhooks are not part of this account
TB-PARTNER-3019 409 wait No We are confirming this order with the supplier
TB-PARTNER-3020 409 wait No This order is waiting for our approval
Transfers 2 codes
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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
CodeHTTPActionRetryMeaning
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.