Getting started
The API is a plain REST API over HTTPS that returns JSON, authenticated with a single API key you generate yourself once your access is approved. The examples below use {BASE_URL} as a placeholder — your base URL is shown next to your key in the developer dashboard.
- 1
Apply
Tell us about your platform. We review every request by hand, usually within 1–2 business days.
- 2
Generate your key
Once approved, create your API key in the developer dashboard and register the IP addresses of the servers that will call the API. The key is shown only once.
- 3
Call the API
Send the key as an X-API-Key header on every request. That is the whole handshake.
Authentication
Send your key in the X-API-Key header on every request. There is no OAuth flow, no token exchange and no expiry — the key stays valid until you regenerate or revoke it.
curl -X GET '{BASE_URL}/b2b/api/apiv2/b2c/tours/search?term=cappadocia&page_size=5' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Accept: application/json'const res = await fetch(
'{BASE_URL}/b2b/api/apiv2/b2c/tours/search',
{
method: 'POST',
headers: {
'X-API-Key': process.env.TSB_API_KEY,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ term: 'cappadocia', page_size: 5 }),
},
)
const data = await res.json()IP allowlist
Partner endpoints accept requests only from server IP addresses you have registered. Once your access is approved, add the public IP addresses your servers use for outgoing requests in the developer dashboard — up to 3 addresses by default.
- Register single IPv4 or IPv6 addresses. Ranges, private or reserved addresses and addresses owned by Cloudflare are refused.
- Use the address your server sends requests from, not your website's IP. If your website is behind Cloudflare, that address belongs to Cloudflare and cannot be registered.
- If your server has IPv6, requests may leave over IPv6. Register that address as well, or force your HTTP client to use IPv4. Run the commands below on the server itself to see both addresses.
- Changes take effect within about a minute.
curl -4 https://api.ipify.org
curl -6 https://api64.ipify.orgWhen a request is blocked
Requests from an IP that is not registered are stopped before they reach the API: instead of a JSON error you receive HTTP 403 with an HTML challenge page titled "Just a moment...". Until you register an IP your key itself is not IP-restricted, but requests from your servers will get this page. If you see it, check which IP your server actually uses and register that address.
As soon as at least one IP is registered, your key is accepted only from those addresses. A request with your key that reaches the API from any other IP is refused with 403 and the code ip_not_allowed; the body includes the IP we saw. A leaked key is useless outside your servers.
{
"message": "This API key is not allowed from your IP address. Register it under Allowed server IPs in your developer dashboard.",
"code": "ip_not_allowed",
"ip": "203.0.113.10"
}Rate limits & quotas
Two independent limits apply, both counted per partner — never shared with anyone else.
240 requests per minute
A burst limit. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining so you can pace yourself.
20000 requests per day
The default daily quota on approval. It resets at midnight UTC, and we raise it on request — tell us your expected volume and we will match it.
Exceeding either limit returns 429. Your current usage, broken down per endpoint, is visible in your developer dashboard.
Content rules
New tours are held back for 30 days. A tour added to our catalogue does not appear in partner search results, tour detail, or price and availability responses until it is 30 days old. Single-tour requests answer 404 with code tour_embargoed and the date the tour becomes available. This gives the tour time to be indexed on our own site first, so search engines see the original source. Everything older than that is fully available to you. If you are also one of our suppliers and your account is linked to your supplier company, your own tours are exempt: they are available through the API immediately, with no waiting period.
Pricing, dates and content are revalidated daily. Cache responses if you like, but refresh prices and availability at least once a day — and always re-check availability immediately before creating a booking.
Tour search & content
Search the catalogue and pull full tour content. These are the endpoints most integrations start with.
/b2b/api/apiv2/b2c/tours/searchSearch tours
Full catalogue search with faceted filters. Send filters as query parameters (GET) or as a JSON body (POST) — POST is recommended once you use array filters.
| Parameter | Type | Description |
|---|---|---|
term | string | Free-text search across tour titles and content. |
destination | string | Destination slug to scope the search to. |
category | string | Single category slug. |
categories | string[] | Several category slugs at once. |
activities | string[] | Activity slugs (see the activities endpoint). |
languages | string[] | Guiding language codes. |
price | [min, max] | Price range as a two-item array, in EUR. |
duration | [min, max] | Duration range in days (defaults 1–28). |
rating | number | Minimum review rating. |
start_location | integer | Pickup location id. |
end_location | integer | Drop-off location id. |
transports | string[] | Transport type slugs. |
accommodations | string[] | Accommodation type ids. |
physical_ratings | integer[] | Physical difficulty rating ids. |
ids | integer[] | Restrict the result to specific tour ids. |
supplier_ids | integer[] | Only tours run by these suppliers. To list your own tours as a supplier, use the Your supplier tours endpoint instead. |
order | string | Sort field. Defaults to created_at. |
page | integer | Page number, 1-based. |
page_size | integer | Results per page. Defaults to 10. |
Results respect the content embargo — see Content rules below.
/b2b/api/apiv2/b2c/tours/detail/{slug}Tour detail
The complete tour record: itinerary, inclusions, images, departure rules and review summary. {slug} is the tour slug returned by search.
An unavailable, deleted, or embargoed tour returns 404. A partner cannot infer its state from that response.
/b2b/api/apiv1/b2c/quicksearch/toursQuick tour search
Type-ahead search for search boxes. Same engine as the full search but tuned for short, fast queries.
| Parameter | Type | Description |
|---|---|---|
term | string | What the user has typed so far. |
/b2b/api/supplier/toursYour supplier tours
If you are also one of our suppliers, this returns your own tours. No supplier ID is needed: your API key identifies your company. Accepts the same parameters and returns the same format as Search tours. Your own tours are available as soon as they are published, with no waiting period.
| Parameter | Type | Description |
|---|---|---|
term | string | Optional free-text search within your tours. |
page | integer | Page number, 1-based. |
page_size | integer | Results per page, up to 250. Defaults to 10. |
Returns 403 if your API key is not linked to a supplier account. Contact us to have it linked.
Catalogue synchronization
Import complete public tour content and current retail departure schedules through two paginated change feeds.
For the first scan, omit modified-since. Follow nextCursor until it is null, sending each cursor without modified-since. Save syncStartedAt from the first page; send it as modified-since on the next run. Each cursor belongs to one partner account and one feed. Apply records by tourId and allow duplicate updates at time boundaries.
Initial scans contain only active, non-deleted, non-custom tours. Incremental product scans return a minimal INACTIVE record when a previously public tour is disabled, made custom or soft-deleted; remove or hide that tour and its saved schedule locally. No disabled tour content or prices are returned. The schedule feed stays active-only. The content embargo applies, except to your own supplier tours when your account is linked. Use syncStartedAt, not an item’s modifiedAt, as the next checkpoint.
Departure dates can pass without a database modification. Remove past dates locally; each incremental schedule update replaces the complete saved schedule for that tour.
/b2b/api/products/modified-sinceNew and modified products
An ACTIVE item contains tourId, slug, modifiedAt, status and product with full public tour content. An INACTIVE item contains only tourId, slug, modifiedAt and status; remove or hide the saved tour. If republished, it arrives again as ACTIVE. Net prices and private company fields are excluded.
| Parameter | Type | Description |
|---|---|---|
modified-since | ISO 8601 | Timestamp with a timezone offset, for example 2026-10-20T09:00:00Z. Omit for an initial full scan. |
count | integer | Page size from 1 to 500; defaults to 100. |
cursor | string | Opaque nextCursor from the previous page. Send it without modified-since. |
curl -G '{BASE_URL}/b2b/api/products/modified-since' -H 'X-API-Key: YOUR_API_KEY' --data-urlencode 'count=100'curl -G '{BASE_URL}/b2b/api/products/modified-since' -H 'X-API-Key: YOUR_API_KEY' --data-urlencode 'cursor=NEXT_CURSOR'{
"products": [
{
"tourId": 123,
"slug": "example-tour",
"modifiedAt": "2026-10-20T08:00:00+00:00",
"status": "ACTIVE",
"product": {
"id": 123,
"slug": "example-tour",
"name": "Example tour",
"itineraries": []
}
}
],
"nextCursor": null,
"syncStartedAt": "2026-10-20T09:00:00+00:00"
}{
"products": [
{
"tourId": 123,
"slug": "example-tour",
"modifiedAt": "2026-10-20T08:15:00+00:00",
"status": "INACTIVE"
}
],
"nextCursor": null,
"syncStartedAt": "2026-10-20T09:00:00+00:00"
}INACTIVE records contain no product details or prices. Only active tours appear in an initial scan. A complete incremental scan and saved checkpoint replace periodic full-catalogue comparison for later soft deletions. If active content is temporarily unavailable, retry the 503 page without advancing its cursor.
/b2b/api/availability/schedules/modified-sinceModified availability schedules
Each availabilitySchedules[] item contains tourId, slug, modifiedAt and rates: the complete current set of retail rates for that tour, not just the changed rate. Each rate includes its dates from today onward.
| Parameter | Type | Description |
|---|---|---|
modified-since | ISO 8601 | Timestamp with a timezone offset. Omit for an initial full scan. |
count | integer | Page size from 1 to 500; defaults to 100. |
cursor | string | Opaque nextCursor from the previous page. Send it without modified-since. |
curl -G '{BASE_URL}/b2b/api/availability/schedules/modified-since' -H 'X-API-Key: YOUR_API_KEY' --data-urlencode 'modified-since=2026-10-20T09:00:00Z'{
"availabilitySchedules": [
{
"tourId": 123,
"slug": "example-tour",
"modifiedAt": "2026-10-20T08:00:00+00:00",
"rates": [
{
"rateId": 456,
"startsAt": "2026-11-01",
"endsAt": "2026-11-30",
"minPax": 1,
"maxPax": 20,
"currency": "EUR",
"departureTypeId": "scheduled",
"description": {
"en": "Standard departure"
},
"prices": {
"regular": {
"dbl": 200
}
},
"dates": [
"2026-11-05"
]
}
]
}
],
"nextCursor": null,
"syncStartedAt": "2026-10-20T09:00:00+00:00"
}When the last rate is deleted, the tour is returned with rates: [] so you can clear its saved schedule. A rate can have a valid period but dates: []. Prices are retail only; supplier net prices and markup are never returned. Successful pages use Cache-Control: private, no-store.
Pricing & availability
Live departure availability and B2C retail prices. Partner API keys do not provide access to supplier net prices.
/b2b/api/apiv2/b2c/availability/{slug}Availability & price for a tour
Departure availability and retail pricing for one tour.
| Parameter | Type | Description |
|---|---|---|
roomType | string | sng, dbl or trp. Defaults to dbl. |
date | YYYY-MM-DD | A single departure date. |
startDate | YYYY-MM-DD | Start of a date range. |
endDate | YYYY-MM-DD | End of a date range. |
pax | integer | Number of travellers. |
service_type | string | Service level. Defaults to regular. |
rooms | array | Room breakdown, as [{ id, pax, count }]. |
include_prices | yes | no | Include the price breakdown. |
include_rooms | yes | no | Include room options. |
An unavailable, deleted, or embargoed tour returns 404.
/b2b/api/apiv2/b2c/prices/{type}Batch prices
One retail price per tour for a list of ids — what listing and category pages need.
| Parameter | Type | Description |
|---|---|---|
idsrequired | integer[] | Tour ids to price. |
roomType | string | sng, dbl or trp. Defaults to dbl. |
startDate | YYYY-MM-DD | Defaults to today. |
endDate | YYYY-MM-DD | Optional range end. |
Unavailable, deleted, and embargoed tours are left out of the response.
/b2b/api/apiv2/b2c/pricemonths/{type}/{id}Available months
Available month dates for a tour, for building a departure calendar. This endpoint does not return prices.
An unavailable, deleted, or embargoed tour returns 404.
Destinations & taxonomy
The reference data behind filters, menus and search boxes.
/b2b/api/apiv1/b2c/countries/listCountries
All countries with their id, name, slug and phone code.
/b2b/api/apiv1/b2c/quicksearch/locationsLocation search
Search destinations by name — cities, regions and countries.
| Parameter | Type | Description |
|---|---|---|
q | string | Search term. |
ids | string | Comma-separated ids, to resolve known locations instead of searching. |
limit | integer | Max results, 1–50. Defaults to 20. |
/b2b/api/apiv1/location-parents/{id}Location ancestors
The parent chain of a location (city → region → country), for breadcrumbs.
/b2b/api/apiv1/b2c/quicksearch/tour-categoriesTour categories
Category pages matching a term, with their titles and URLs.
| Parameter | Type | Description |
|---|---|---|
term | string | Search term. |
/b2b/api/apiv1/b2c/quicksearch/activitiesActivities
Activity list (hiking, diving, …) with id, name and slug — the values the activities filter accepts.
| Parameter | Type | Description |
|---|---|---|
term | string | Search term. |
/b2b/api/apiv1/b2c/tours/available-destinationsAvailable destinations
Destinations that currently have bookable tours.
/b2b/api/apiv1/b2c/tours/trending-destinationsTrending destinations
Destinations with the strongest recent demand.
Enquiries & bookings
Create demand on our side from your platform. Everything you create here is attributed to your partner account and appears in your dashboard.
/b2b/api/apiv1/bookings/new-enquiryCreate an enquiry
A traveller question about a tour. Creates an enquiry record and notifies the operator.
| Parameter | Type | Description |
|---|---|---|
tour_idrequired | integer | The tour being asked about. |
contact_namerequired | string | The traveller's name. |
contact_emailrequired | string | The traveller's email. |
messagerequired | string | The question, up to 5000 characters. |
phone_code | integer | Location id of the phone country code. |
contact_phone | string | Phone number without the country code. |
buyer_currency | USD | EUR | Currency to quote in. Defaults to EUR. |
No captcha is required on API-key requests — your key, quota and rate limit are the gate.
/b2b/api/apiv1/bookings/new-bookingCreate a booking
A booking checkout with travellers and rooms, priced from B2C retail availability. Returns the reference_id and token used for payment.
| Parameter | Type | Description |
|---|---|---|
tour_idrequired | integer | The tour being booked. |
paxrequired | integer | Number of travellers. |
daterequired | YYYY-MM-DD | Departure date, today or later. |
service_typerequired | string | Service level, as returned by availability. |
buyer_currencyrequired | string | Currency the traveller pays in. The seller currency is taken from the tour’s own price record. |
rooms | array | Required for tours with accommodation: [{ id: "dbl", pax, count }]. |
customersrequired | array | Travellers. The first is the lead and needs title, first_name, last_name, email, phone_code and phone. |
/b2b/api/apiv1/bookings/{referenceId}Booking detail
Read one booking. Pass the booking token as ?token= — the token scopes access to that single booking.
| Parameter | Type | Description |
|---|---|---|
tokenrequired | string | The token returned when the booking was created. |
/b2b/api/apiv1/bookings/{referenceId}/payment-requestOpen a payment request
Start a collection against a booking, for a payment page hosted on your own front end. The amount is calculated from the booking — the client cannot set it.
| Parameter | Type | Description |
|---|---|---|
tokenrequired | string | The booking token. |
payment_model | total | deposit | Collect the full remaining amount or just the deposit. |
/b2b/api/apiv1/bookings/{referenceId}/payment-request/{id}Payment request status
Poll the state of a payment request. Requires the same token.
| Parameter | Type | Description |
|---|---|---|
tokenrequired | string | The booking token. |
/b2b/api/apiv1/bookings/abandoned-cartReport an abandoned cart
Tell us a traveller started but did not finish a booking, so our team can follow up. Creates no booking record.
| Parameter | Type | Description |
|---|---|---|
sourcerequired | tsb | tst | Which storefront the cart came from. |
tourrequired | string | Tour slug. |
name | string | The traveller's name. |
email | string | The traveller's email. |
phone_code | integer | Location id of the phone country code. |
phone | string | Phone number. |
date | YYYY-MM-DD | Intended departure date. |
pax | integer | Number of travellers. |
Deprecated response fields
Since 3 November 2026, tour search, tour detail and the product feed return the fields below as null. The keys stay in the response until 1 May 2027 so existing integrations keep parsing; after that date they are removed. Do not build on them.
Tour fields: guides, transfers, additional_service_location, is_bokun_completed, free_cancelation, all_inclusive, type, vat, deleted_at, b2c, bokun_id, sales, meta_title, meta_description, content_score and defaults.content_score.
Reviews: reviews is null. The rating and review_count summary is still returned.
name_en keeps its value until 1 May 2027 and is removed after that. Use name instead.
Price fields: the trp_, chd_ and inf_ minimum prices (min_reg_price, min_prv_price and their _pax variants), sng_/dbl_/unit_/adl_min_reg_price_pax and min_prv_price_pax. Keep using price and the sng_/dbl_/unit_/adl_ min_reg_price, min_prv_price and min_prv_price_pax fields; for exact prices and availability, call the pricing endpoints.
In rates[].dates[], id, rate_id and tour_id are null; date is unchanged.
Errors
Errors use standard HTTP status codes, with a JSON body carrying a message and — for validation failures — an errors object keyed by field name.
| Code | Meaning | What to do |
|---|---|---|
| 200 | OK | The request succeeded. |
| 401 | Unauthorized | The X-API-Key header is missing, or the key is not recognised. Check that you are sending the key you generated, not the one it replaced. |
| 403 | Forbidden | Your API access is suspended or not yet approved. Contact us if you believe this is a mistake. |
| 403 | IP not allowed | The body carries code ip_not_allowed and the ip we saw. Your key is bound to the server IPs you registered and this request came from another address. Register that address in your developer dashboard, or send the request from a registered server. |
| 403 | Challenge page | An HTML page titled "Just a moment..." instead of JSON. The request came from an IP that is not on your allowlist and was stopped before it reached the API. Register your server’s outgoing IP in your developer dashboard; changes take effect within about a minute. |
| 404 | Not found | The tour, booking or location is unavailable, deleted, embargoed or unknown; for token-scoped endpoints, the token may not match. |
| 422 | Unprocessable | Validation failed. The body contains a message; field validation may also include an errors object. For change feeds, check the timestamp, count and cursor, and never send cursor with modified-since. |
| 429 | Too many requests | You exceeded either the per-minute rate limit or your daily quota. Check the X-RateLimit-Remaining header and back off. |
| 503 | Temporarily unavailable | The product change feed could not read complete tour content. Wait for Retry-After, then retry the same page without saving or advancing its cursor. |
| 500 | Server error | Something failed on our side. Retry with backoff; if it persists, contact us with the time of the request. |
{
"message": "The given data was invalid.",
"errors": {
"tour_id": ["The selected tour id is invalid."],
"customers.0.email": ["The customers.0.email must be a valid email address."]
}
}Versioning
Some endpoints retain the historical /b2b/api/apiv1/… or /b2b/api/apiv2/… path. Native partner endpoints, including the catalogue change feeds, live directly under /b2b/api/…. All are live and maintained.
Within a version we only make additive changes — new fields and new endpoints. We do not rename or remove fields you already rely on. Anything that would break an integration ships under a new version path, and we email every active partner before a version is retired.
Ready to build?
Apply for access and generate your key. Questions about which integration fits you? Our team is happy to help.