Business Platform
TravelShop

API Reference

TravelShop Booking API

Search 10,000+ tours, pull live pricing and availability, and create bookings from your own platform. This reference is public — read it before you apply.

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

    Apply

    Tell us about your platform. We review every request by hand, usually within 1–2 business days.

  2. 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. 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
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'
node
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()
Call the API from your server, never from browser code — a key shipped to the browser is a public key. We store only a hashed copy of your key, so it is shown exactly once, when you create it. Lost it? Generate a new one; the old one dies immediately.

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
curl -4 https://api.ipify.org
curl -6 https://api64.ipify.org

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

403 response
{
  "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.

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.

GET/b2b/api/products/modified-since

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

ParameterTypeDescription
modified-sinceISO 8601Timestamp with a timezone offset, for example 2026-10-20T09:00:00Z. Omit for an initial full scan.
countintegerPage size from 1 to 500; defaults to 100.
cursorstringOpaque nextCursor from the previous page. Send it without modified-since.
Initial request
curl -G '{BASE_URL}/b2b/api/products/modified-since' -H 'X-API-Key: YOUR_API_KEY' --data-urlencode 'count=100'
Next page
curl -G '{BASE_URL}/b2b/api/products/modified-since' -H 'X-API-Key: YOUR_API_KEY' --data-urlencode 'cursor=NEXT_CURSOR'
Response (product abbreviated)
{
  "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"
}
Incremental removal marker
{
  "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.

GET/b2b/api/availability/schedules/modified-since

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

ParameterTypeDescription
modified-sinceISO 8601Timestamp with a timezone offset. Omit for an initial full scan.
countintegerPage size from 1 to 500; defaults to 100.
cursorstringOpaque nextCursor from the previous page. Send it without modified-since.
Incremental request
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'
Response
{
  "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.

GET/b2b/api/apiv2/b2c/availability/{slug}

Availability & price for a tour

Departure availability and retail pricing for one tour.

ParameterTypeDescription
roomTypestringsng, dbl or trp. Defaults to dbl.
dateYYYY-MM-DDA single departure date.
startDateYYYY-MM-DDStart of a date range.
endDateYYYY-MM-DDEnd of a date range.
paxintegerNumber of travellers.
service_typestringService level. Defaults to regular.
roomsarrayRoom breakdown, as [{ id, pax, count }].
include_pricesyes | noInclude the price breakdown.
include_roomsyes | noInclude room options.

An unavailable, deleted, or embargoed tour returns 404.

POST/b2b/api/apiv2/b2c/prices/{type}

Batch prices

One retail price per tour for a list of ids — what listing and category pages need.

ParameterTypeDescription
idsrequiredinteger[]Tour ids to price.
roomTypestringsng, dbl or trp. Defaults to dbl.
startDateYYYY-MM-DDDefaults to today.
endDateYYYY-MM-DDOptional range end.

Unavailable, deleted, and embargoed tours are left out of the response.

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

GET/b2b/api/apiv1/b2c/countries/list

Countries

All countries with their id, name, slug and phone code.

GET/b2b/api/apiv1/b2c/quicksearch/locations

Location search

Search destinations by name — cities, regions and countries.

ParameterTypeDescription
qstringSearch term.
idsstringComma-separated ids, to resolve known locations instead of searching.
limitintegerMax results, 1–50. Defaults to 20.
GET/b2b/api/apiv1/location-parents/{id}

Location ancestors

The parent chain of a location (city → region → country), for breadcrumbs.

GET/b2b/api/apiv1/b2c/quicksearch/tour-categories

Tour categories

Category pages matching a term, with their titles and URLs.

ParameterTypeDescription
termstringSearch term.
GET/b2b/api/apiv1/b2c/quicksearch/activities

Activities

Activity list (hiking, diving, …) with id, name and slug — the values the activities filter accepts.

ParameterTypeDescription
termstringSearch term.
GET/b2b/api/apiv1/b2c/tours/available-destinations

Available destinations

Destinations that currently have bookable tours.

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.

POST/b2b/api/apiv1/bookings/new-enquiry

Create an enquiry

A traveller question about a tour. Creates an enquiry record and notifies the operator.

ParameterTypeDescription
tour_idrequiredintegerThe tour being asked about.
contact_namerequiredstringThe traveller's name.
contact_emailrequiredstringThe traveller's email.
messagerequiredstringThe question, up to 5000 characters.
phone_codeintegerLocation id of the phone country code.
contact_phonestringPhone number without the country code.
buyer_currencyUSD | EURCurrency to quote in. Defaults to EUR.

No captcha is required on API-key requests — your key, quota and rate limit are the gate.

POST/b2b/api/apiv1/bookings/new-booking

Create a booking

A booking checkout with travellers and rooms, priced from B2C retail availability. Returns the reference_id and token used for payment.

ParameterTypeDescription
tour_idrequiredintegerThe tour being booked.
paxrequiredintegerNumber of travellers.
daterequiredYYYY-MM-DDDeparture date, today or later.
service_typerequiredstringService level, as returned by availability.
buyer_currencyrequiredstringCurrency the traveller pays in. The seller currency is taken from the tour’s own price record.
roomsarrayRequired for tours with accommodation: [{ id: "dbl", pax, count }].
customersrequiredarrayTravellers. The first is the lead and needs title, first_name, last_name, email, phone_code and phone.
GET/b2b/api/apiv1/bookings/{referenceId}

Booking detail

Read one booking. Pass the booking token as ?token= — the token scopes access to that single booking.

ParameterTypeDescription
tokenrequiredstringThe token returned when the booking was created.
POST/b2b/api/apiv1/bookings/{referenceId}/payment-request

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

ParameterTypeDescription
tokenrequiredstringThe booking token.
payment_modeltotal | depositCollect the full remaining amount or just the deposit.
GET/b2b/api/apiv1/bookings/{referenceId}/payment-request/{id}

Payment request status

Poll the state of a payment request. Requires the same token.

ParameterTypeDescription
tokenrequiredstringThe booking token.
POST/b2b/api/apiv1/bookings/abandoned-cart

Report an abandoned cart

Tell us a traveller started but did not finish a booking, so our team can follow up. Creates no booking record.

ParameterTypeDescription
sourcerequiredtsb | tstWhich storefront the cart came from.
tourrequiredstringTour slug.
namestringThe traveller's name.
emailstringThe traveller's email.
phone_codeintegerLocation id of the phone country code.
phonestringPhone number.
dateYYYY-MM-DDIntended departure date.
paxintegerNumber 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.

CodeMeaningWhat to do
200OKThe request succeeded.
401UnauthorizedThe 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.
403ForbiddenYour API access is suspended or not yet approved. Contact us if you believe this is a mistake.
403IP not allowedThe 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.
403Challenge pageAn 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.
404Not foundThe tour, booking or location is unavailable, deleted, embargoed or unknown; for token-scoped endpoints, the token may not match.
422UnprocessableValidation 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.
429Too many requestsYou exceeded either the per-minute rate limit or your daily quota. Check the X-RateLimit-Remaining header and back off.
503Temporarily unavailableThe product change feed could not read complete tour content. Wait for Retry-After, then retry the same page without saving or advancing its cursor.
500Server errorSomething failed on our side. Retry with backoff; if it persists, contact us with the time of the request.
422 response
{
  "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.