Skip to content

Multi-Market API

Path-based routing API for market and language-specific product discovery with localized content.

The Multi-Market API provides endpoints for accessing products and configuration data scoped to specific geographical markets and languages. All product endpoints use path-based routing with both market code and language code in the URL path.

Base URL Pattern: /api/{market}/{lang}/...

Key Features:

  • Case-insensitive market and language codes (ES, es, Es all work)
  • Language-specific product content via locale
  • Content from ProductByMarketTranslation (no fallback to template)
  • Active product filtering with locale matching
  • Market configuration with supported languages
  • Structured error responses for invalid/inactive markets
Terminal window
curl -X GET "https://api.example.com/api/es/ca/products" \
-H "Accept: application/json"
Terminal window
curl -X GET "https://api.example.com/api/es/es/products/10" \
-H "Accept: application/json"
Terminal window
curl -X GET "https://api.example.com/api/es/ca/products/slug/tour-de-barcelona" \
-H "Accept: application/json"
Terminal window
curl -X GET "https://api.example.com/api/es/ca/products/sku/ES-2CMB10-CA1" \
-H "Accept: application/json"
Terminal window
curl -X GET "https://api.example.com/api/de/config" \
-H "Accept: application/json"

List all active products for a market and language with localized content.

Parameters:

NameInTypeRequiredDescription
marketpathstringYesMarket code (case-insensitive, e.g., “es”, “US”, “De”)
langpathstringYesLanguage code (e.g., “en”, “es”, “ca”)

Response: 200 OK

{
"data": [
{
"id": 10,
"product_template_id": 5,
"sku": "ES-5CMB10-CA1",
"locale": "ca_ES",
"status": "active",
"sort_order": 0,
"trip_duration_days": 10,
"title": "Tour de Sri Lanka",
"subtitle": "Descobreix l'illa maragda",
"short_description": "Una aventura increible...",
"long_description": "Descripció completa del tour...",
"highlights": ["Sigiriya", "Kandy", "Yala"],
"destination_info": "Sri Lanka",
"url_slug": "tour-sri-lanka",
"hero_image": "https://cdn.example.com/images/sri-lanka.jpg",
"country_name": "Sri Lanka",
"country_slug": "sri-lanka",
"departure_airports": [
{
"iata_code": "BCN",
"name": "Barcelona-El Prat Airport",
"city": "Barcelona",
"country": "Spain"
}
]
}
],
"meta": {
"market": "ES",
"locale": "ca_ES"
}
}

Get a single product by its ProductByMarket ID.

Parameters:

NameInTypeRequiredDescription
marketpathstringYesMarket code (case-insensitive)
langpathstringYesLanguage code
idpathintegerYesProductByMarket ID

Detail-only data: Detail endpoints (by ID, slug, SKU, and preview) return additional data not present on the listing endpoint:

  • accommodations — Hotel assignments grouped by tier (selection, luxury, grand luxury) with day ranges
  • itinerary[].days[].activities — Per-day activities from the SupplierTour, grouped by tier (see Activity Shape below)

GET /api/{market}/{lang}/products/slug/{slug}

Section titled “GET /api/{market}/{lang}/products/slug/{slug}”

Get a product by its URL slug. First checks translated slug, then falls back to base product template slug.

GET /api/{market}/{lang}/products/sku/{sku}

Section titled “GET /api/{market}/{lang}/products/sku/{sku}”

Get a product by its market-specific SKU code.

SKU Format: <MARKET>-<TEMPLATEID><IATA><DAYS>-<LANG><VERSION>

  • Example: ES-5CMB10-CA1 = Spain market, template 5, CMB airport, 10 days, Catalan, version 1

GET /api/{market}/{lang}/products/{id}/leading-price

Section titled “GET /api/{market}/{lang}/products/{id}/leading-price”

Get the minimum per-person price (marketing_price_per_pax) from bookable offers for a product. Use this to display “From $XXX /person” pricing. Only considers offers that are Active AND have departure dates at least 5 days in the future (see Bookability).

Response: 200 OK

{
"data": {
"product_by_market_id": 23,
"price_from": "1249",
"currency": "EUR",
"offers_count": 5
}
}

When no bookable offers exist, price_from is null and offers_count is 0.

GET /api/{market}/{lang}/products/{id}/configurator

Section titled “GET /api/{market}/{lang}/products/{id}/configurator”

Get all data needed to render the trip configurator wizard on the PDP (Product Detail Page). Returns available departure dates grouped by month, departure airports, and room types — with non-bookable and sold-out offers already removed.

Bookability filtering: Only offers passing the bookable() scope are included — Active status AND departure date at least 5 days in the future (see Bookability). This prevents near-departure offers from appearing in the calendar.

Availability filtering: The controller batch-checks allotment for all bookable offers via AllotmentService::getOffersAvailability() before building the response. Offers where all allotment is consumed are silently excluded, so the configurator only shows bookable dates. The payment-time check remains as a safety net for race conditions.

Response: 200 OK

{
"data": {
"product_id": 1,
"departure_airports": [
{ "iata_code": "MAD", "city": "Madrid" }
],
"months": [
{
"year": 2026,
"month": 3,
"label": "MARZO",
"min_price": "1808",
"min_price_per_person": "904",
"dates": [
{
"offer_id": 42,
"date": "2026-03-07",
"day_label": "Sábado, 7",
"price": "1808",
"price_per_person": "904",
"available": true,
"departure_airport": "MAD"
}
]
}
],
"room_types": [
{ "code": "2A", "label": "2 Adultos" }
],
"labels": {
"information": "...",
"origin": "...",
"travelers": "...",
"rooms": "...",
"choose_date": "...",
"configure_trip": "...",
"help_text": "...",
"per_person": "..."
},
"currency": { "code": "EUR", "symbol": "" }
}
}

Source: backend/app/Http/Controllers/Api/ProductByMarketController.php (configurator()), backend/app/Http/Resources/TripConfiguratorResource.php, backend/app/Services/Allotment/AllotmentService.php

GET /api/{market}/{lang}/products/{id}/preview

Section titled “GET /api/{market}/{lang}/products/{id}/preview”

Preview a product regardless of status (draft/inactive). Requires a signed URL.

Middleware: signed (Laravel signed URL validation)

Usage: Generate signed URLs via ProductByMarket::generatePreviewUrl(). The frontend accepts this as a base64-encoded preview query parameter.

Token Expiration: 60 minutes (configurable)

Source: backend/app/Http/Controllers/Api/ProductByMarketController.php

Get market configuration including locale, currency, timezone, supported languages, and departure airports.

Response: 200 OK

{
"data": {
"code": "ES",
"name": "Spain",
"locale": "es_ES",
"supported_languages": ["es", "ca"],
"tour_path_slugs": { "es": "circuito", "ca": "circuit" },
"currency": {
"code": "EUR",
"name": "Euro"
},
"timezone": "Europe/Madrid",
"departure_airports": [
{
"iata_code": "MAD",
"name": "Adolfo Suarez Madrid-Barajas Airport",
"city": "Madrid",
"is_primary": true
}
]
}
}

Note: tour_path_slugs provides localized URL path segments for product pages (e.g., /es/circuito/product-slug for Spanish, /es/ca/circuit/product-slug for Catalan).

{
"success": false,
"error": "market_not_found",
"message": "Market 'xyz' not found."
}
{
"success": false,
"error": "market_inactive",
"message": "Market 'FR' is currently not available."
}
{
"success": false,
"error": "language_not_supported",
"message": "Language 'de' is not supported by market 'ES'. Supported languages: es, ca"
}
routes/api.php
|
v
Route::prefix('{market}')
->middleware(['market'])
->whereAlpha('market')
|
+-- GET /config -> ProductByMarketController@config
|
+-- Route::prefix('{lang}')->whereAlpha('lang')
|
+-- GET /products -> ProductByMarketController@index
+-- GET /products/{id} -> ProductByMarketController@show
+-- GET /products/{id}/leading-price -> ProductByMarketController@leadingPrice
+-- GET /products/{id}/configurator -> ProductByMarketController@configurator
+-- GET /products/slug/{slug} -> ProductByMarketController@showBySlug
+-- GET /products/sku/{sku} -> ProductByMarketController@showBySku
|
+-- Route::prefix('checkout')
+-- GET /{offerId}/flights -> CheckoutController@flights
+-- GET /{offerId}/hotels -> CheckoutController@hotels
+-- GET /{offerId}/activities -> CheckoutController@activities
+-- GET /{offerId}/transfers -> CheckoutController@transfers
+-- GET /{offerId}/contact -> CheckoutController@contact
+-- GET /{offerId}/travelers -> CheckoutController@travelers
+-- POST /{offerId}/business-flights -> CheckoutController@businessFlights
|
+-- Route::middleware('stateful.api') // Session-based
+-- POST /{offerId} -> CheckoutController@start
+-- GET / -> CheckoutController@show
+-- PUT /flights -> CheckoutController@updateFlightSelection
+-- PUT /hotels -> CheckoutController@updateHotelSelection
+-- PUT /activities -> CheckoutController@updateActivitySelection
+-- PUT /transfers -> CheckoutController@updateTransferSelection
+-- PUT /contact -> CheckoutController@updateClientData
+-- PUT /travelers -> CheckoutController@updateTravelerData
ComponentFilePurpose
Controllerapp/Http/Controllers/Api/ProductByMarketController.phpHandle product requests
Checkout Controllerapp/Http/Controllers/Api/CheckoutController.phpHandle checkout flow
Checkout Session Serviceapp/Services/Checkout/CheckoutSessionService.phpManage checkout session state
Checkout Hotel Serviceapp/Services/Checkout/CheckoutHotelService.phpExtract upsell hotel options
Hotel Price Calculatorapp/Services/Checkout/HotelPriceCalculatorService.phpCalculate upsell price differences
Checkout Transfer Serviceapp/Services/Checkout/CheckoutTransferService.phpExtract transfer options from itineraries
Transfer Price Calculatorapp/Services/Checkout/TransferPriceCalculatorService.phpCalculate per-trip transfer pricing
Allotment Serviceapp/Services/Allotment/AllotmentService.phpBatch-check offer availability for configurator
Middlewareapp/Http/Middleware/ResolveMarket.phpValidate market, resolve locale
Product Resourceapp/Http/Resources/ProductByMarketResource.phpTransform product with translations
Configurator Resourceapp/Http/Resources/TripConfiguratorResource.phpTransform configurator wizard data
Checkout Session Resourceapp/Http/Resources/CheckoutSessionResource.phpTransform checkout session
Market Resourceapp/Http/Resources/MarketResource.phpTransform market config
// Fetch products for a market and language
async function getMarketProducts(marketCode, langCode) {
const response = await fetch(`/api/${marketCode}/${langCode}/products`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
// Get market configuration
async function getMarketConfig(marketCode) {
const response = await fetch(`/api/${marketCode}/config`);
return response.json();
}
// Usage
const config = await getMarketConfig('es');
console.log('Supported languages:', config.data.supported_languages);

All product queries use eager loading to prevent N+1 queries:

  • Listing: productTemplate, translation, flightConfigs.airport
  • Detail: Adds supplierTours.itineraries.{selectionHotel,luxuryHotel,grandLuxuryHotel,includedActivities,extraActivities,substitutionActivities}, cmsCountries.translations
-- ProductByMarket queries
INDEX (market_id, status, sort_order)
-- Locale-based lookups
UNIQUE (product_template_id, market_id, locale)
-- Translation lookups
UNIQUE (locale, url_slug)

Detail endpoints merge per-day activities from the linked SupplierTour into each itinerary day entry. Activities are not present on the listing endpoint (index) to avoid N+1 queries. This data is display-only (no pricing) — for purchasable activities with prices, see the checkout activities endpoint.

Each itinerary day’s activities array contains objects with this shape:

FieldTypeDescription
idintSupplierActivity ID
namestringActivity name
descriptionstring|nullActivity description
tierstringOne of: included, extra, substitution
tier_labelstringHuman-readable tier label (e.g., “Included”, “Extra”)
image_urlstring|nullFull URL to first activity image
citystring|nullCity name formatted as “City, Country”
reasonsstring[]|nullWhy-visit reasons
amenitiesobject[]|nullEach with icon, label, description

Activity Tiers:

  • Included — Activities included in the base tour price
  • Extra — Optional paid activities (prices shown only in checkout context)
  • Substitution — Alternative activities that replace an included one

Source: backend/app/Http/Resources/ProductByMarketResource.php (mergeActivitiesIntoItinerary(), buildActivityMap())

Endpoints for the checkout flow, including flight selection and session management.

POST /api/{market}/{lang}/checkout/{offerId}

Section titled “POST /api/{market}/{lang}/checkout/{offerId}”

Start a checkout session for an offer. Overwrites any existing checkout session. The offer must be bookable (Active + departure >= today + 5 days).

Middleware: stateful.api (requires session cookies)

Response: 201 Created

{
"success": true,
"data": {
"offer_id": 123,
"started_at": "2026-01-21T10:30:00+00:00",
"flight_selection": null,
"base_price": 1700.00,
"extras_price": 0.00,
"total_price": 1700.00,
"currency": {
"code": "EUR",
"symbol": ""
},
"pax_count": 2
}
}

Errors:

  • 404 - Offer not found, not bookable, or belongs to different market
  • 410 - Offer is Active but within the 5-day booking lead time (error: "offer_expired")

Get the current checkout session state.

Middleware: stateful.api (requires session cookies)

Response: 200 OK - Same structure as POST response

Errors:

  • 404 - No active checkout session (error: "no_checkout_session")

Full Session Response Structure:

{
"success": true,
"data": {
"offer_id": 123,
"started_at": "2026-01-21T10:30:00+00:00",
"flight_selection": {
"cabin_class": "BUSINESS",
"business_extra_price_per_person": 500.00,
"outbound": { ... },
"inbound": { ... }
},
"hotel_selections": [
{
"upsell_hotel_id": 4,
"nights_start": 1,
"nights_end": 3,
"price_difference": 170.00,
"hotel_name": "Luxury Safari Lodge",
"location": "Nairobi"
}
],
"activity_selections": [
{
"activity_id": 5,
"day_number": 2,
"price": 50.00,
"activity_name": "Safari Quad Excursion",
"location": "Nairobi"
}
],
"transfer_selections": [
{
"transfer_id": 8,
"day_number": 1,
"price": 120.00,
"transfer_name": "Private Luxury Transfer",
"location": "Nairobi"
}
],
"client_data": {
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+34612345678"
},
"traveler_data": [...],
"base_price": 1700.00,
"extras_price": 340.00,
"total_price": 2040.00,
"currency": { "code": "EUR", "symbol": "" },
"pax_count": 2
}
}

Selection Item Fields:

Selection TypeFieldDescription
hotel_selectionsupsell_hotel_idID of the upsell hotel
nights_start / nights_endTour day numbers for this stay
price_differenceTotal price for the room upgrade (per room, not per person)
hotel_nameName of the selected upsell hotel
locationCity/location of the hotel
activity_selectionsactivity_idID of the activity
day_numberTour day number
pricePer-person price
activity_nameName of the activity
locationDestination/location of the activity
transfer_selectionstransfer_idID of the transfer
day_numberTour day number
pricePer-trip price (flat rate, not per person)
transfer_nameName of the transfer service
locationDestination/location of the transfer

Update the flight selection in the current checkout session. Recalculates pricing for business class.

Middleware: stateful.api (requires session cookies)

Request Body:

{
"cabin_class": "BUSINESS",
"outbound": {
"departure_date": "2026-02-15",
"departure_time": "09:10",
"arrival_time": "15:15",
"departure_airport": "MAD",
"arrival_airport": "NBO",
"flight_numbers": ["EK123", "EK456"],
"airlines": ["EK"],
"stops": 1,
"stopover_airports": ["DXB"]
},
"inbound": {
"departure_date": "2026-02-22",
"departure_time": "08:00",
"arrival_time": "14:30",
"departure_airport": "NBO",
"arrival_airport": "MAD",
"flight_numbers": ["EK789"],
"airlines": ["EK"],
"stops": 0,
"stopover_airports": []
},
"business_extra_price_per_person": 500.00
}

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"started_at": "2026-01-21T10:30:00+00:00",
"flight_selection": { ... },
"base_price": 1700.00,
"extras_price": 1000.00,
"total_price": 2700.00,
"currency": { "code": "EUR", "symbol": "" },
"pax_count": 2
}
}

Price Calculation: extras_price = business_extra_price_per_person × pax_count

Errors:

  • 400 - No checkout session or validation error
  • Validation: cabin_class must be ECONOMY or BUSINESS, airport codes must be 3 characters

GET /api/{market}/{lang}/checkout/{offerId}/flights

Section titled “GET /api/{market}/{lang}/checkout/{offerId}/flights”

Get available flight options for an offer (from cache).

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"tour_name": "Safari Adventure",
"final_price": 1700.00,
"cabinClass": "ECONOMY",
"source_type": "cache",
"has_flights": true,
"outbound_options": [...],
"inbound_options": [...]
}
}

POST /api/{market}/{lang}/checkout/{offerId}/business-flights

Section titled “POST /api/{market}/{lang}/checkout/{offerId}/business-flights”

Search for business class flights (live API call). Rate limited to 10 requests/minute.

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"tour_name": "Safari Adventure",
"original_final_price": 1700.00,
"cabinClass": "BUSINESS",
"source_type": "live_search",
"has_flights": true,
"outbound_options": [
{
"id": "fare-abc-outbound-1",
"finalPrice": 2900.00,
"extraPrice": 1200.00,
"departureTime": "10:00",
"arrivalTime": "22:00",
"duration": "12h 00min",
"stops": 0
}
],
"inbound_options": [...]
}
}

Price Calculation: finalPrice = (flightPrice + landBasePrice) × (1 + margin/100)

The extraPrice is the difference between the business class final price and the original economy offer price.

GET /api/{market}/{lang}/checkout/{offerId}/hotels

Section titled “GET /api/{market}/{lang}/checkout/{offerId}/hotels”

Get upsell hotel options for an offer. Returns hotels that can upgrade the guaranteed (included) hotels.

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"offer_sku": "SAFARI-2026",
"tour_name": "Safari Adventure",
"final_price": 1700.00,
"departure_date": "2026-04-20",
"return_date": "2026-04-27",
"pax_count": 2,
"currency": {
"code": "EUR",
"symbol": ""
},
"hotels": [
{
"id": "4-1-2",
"hotelId": 4,
"name": "Luxury Safari Lodge",
"location": "Nairobi",
"nights": { "start": 1, "end": 2 },
"imageUrl": "http://example.com/hotel.jpg",
"isIncluded": false,
"priceDifference": 170.00,
"guaranteedHotelName": "Standard Safari Hotel"
}
]
}
}

Fields:

FieldTypeDescription
idstringUnique key: {hotelId}-{startDay}-{endDay}
hotelIdintThe actual hotel ID
descriptionstring|nullHotel description text from SupplierHotel
priceDifferencefloat|nullExtra cost in EUR, null if pricing unavailable
guaranteedHotelNamestring|nullName of the included hotel being replaced
nights.start / nights.endintTour day numbers for this stay

Price Calculation:

  • Looks up per-night rates for both upsell and guaranteed hotels
  • Uses room_type query parameter if provided (1A, 2A, 3A, 4A), otherwise falls back to session’s actual_room_type, then to 2A
  • Converts to EUR via exchange rates
  • Returns null if pricing data is unavailable for the selected room type (hotel shown as non-selectable)

Grouping Logic:

  • Consecutive nights at the same guaranteed hotel are grouped together
  • Same upsell hotel can appear multiple times for different stays

GET /api/{market}/{lang}/checkout/{offerId}/activities

Section titled “GET /api/{market}/{lang}/checkout/{offerId}/activities”

Get activity options for an offer. Returns included and upsell activities organized by day.

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"offer_sku": "SAFARI-2026",
"tour_name": "Safari Adventure",
"final_price": 1700.00,
"price_per_person": 850.00,
"departure_date": "2026-04-20",
"return_date": "2026-04-27",
"pax_count": 2,
"currency": { "code": "EUR", "symbol": "" },
"days": [
{
"day_number": 1,
"destination": "Nairobi",
"included_activities": [
{ "name": "City Tour" }
],
"available_activities": [
{
"id": 5,
"name": "Safari Quad Excursion",
"description": "Explore the savanna...",
"image_url": "http://example.com/activity.jpg",
"price": 50.00
}
]
}
]
}
}

Fields:

FieldTypeDescription
included_activitiesarrayActivities included in the tour (names only)
available_activitiesarrayUpsell activities available for purchase
pricefloat|nullPer-person price in EUR, null if unavailable

Price Calculation:

  • Looks up per-person rates from the activity’s SupplierService
  • Uses per_person room type for pricing
  • Converts to EUR via exchange rates
  • Returns null if pricing data is unavailable

GET /api/{market}/{lang}/checkout/{offerId}/transfers

Section titled “GET /api/{market}/{lang}/checkout/{offerId}/transfers”

Get transfer options for an offer. Returns included and upsell transfers organized by day.

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"offer_sku": "SAFARI-2026",
"tour_name": "Safari Adventure",
"product_url": "/es/circuito/safari-adventure",
"final_price": 1700.00,
"price_per_person": 850.00,
"departure_date": "2026-04-20",
"return_date": "2026-04-27",
"pax_count": 2,
"currency": { "code": "EUR", "symbol": "" },
"days": [
{
"day_number": 1,
"destination": "Nairobi",
"included_transfers": [
{ "name": "Airport Transfer" }
],
"available_transfers": [
{
"id": 5,
"name": "Private Luxury Transfer",
"description": "Travel in comfort with a private vehicle...",
"image_url": "http://example.com/transfer.jpg",
"vehicle_type": "luxury_sedan",
"duration_minutes": 45,
"price": 120.00
}
]
}
]
}
}

Fields:

FieldTypeDescription
included_transfersarrayTransfers included in the tour (names only)
available_transfersarrayUpsell transfers available for purchase
vehicle_typestring|nullType of vehicle (e.g., luxury_sedan, minivan)
duration_minutesint|nullEstimated transfer duration
pricefloat|nullPer-trip price in EUR, null if unavailable

Price Calculation:

  • Looks up per-trip rates from the transfer’s SupplierService
  • Uses per_trip room type for pricing (not per-person)
  • Converts to EUR via exchange rates
  • Returns null if pricing data is unavailable

Filtering Logic:

  • Only days with upsell transfers are returned
  • Days with only included transfers are hidden (nothing to upgrade)

Source: backend/app/Http/Controllers/Api/CheckoutController.php:631

PUT /api/{market}/{lang}/checkout/transfers

Section titled “PUT /api/{market}/{lang}/checkout/transfers”

Update the transfer selections in the current checkout session.

Middleware: stateful.api (requires session cookies)

Request Body:

{
"transfer_selections": [
{
"transfer_id": 5,
"day_number": 1
},
{
"transfer_id": 8,
"day_number": 3
}
]
}

Security Note: Prices are NOT sent from the client - they are calculated server-side based on the transfer’s SupplierService rates.

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"started_at": "2026-01-21T10:30:00+00:00",
"flight_selection": { ... },
"hotel_selections": [...],
"activity_selections": [...],
"transfer_selections": [
{ "transfer_id": 5, "day_number": 1, "price": 120.00, "transfer_name": "Private Luxury Transfer", "location": "Nairobi" },
{ "transfer_id": 8, "day_number": 3, "price": 100.00, "transfer_name": "VIP Airport Pickup", "location": "Mombasa" }
],
"base_price": 1700.00,
"extras_price": 220.00,
"total_price": 1920.00,
"currency": { "code": "EUR", "symbol": "" },
"pax_count": 2
}
}

Price Calculation:

  • Transfer prices are per-trip (flat rate, NOT multiplied by pax_count)
  • Unlike activities which are per-person
  • extras_price = flight_extras + hotel_extras + activity_extras + transfer_extras

Validation:

  • transfer_id must be a positive integer
  • day_number must be at least 1
  • Transfer must exist as an upsell option for the specified day

Errors:

  • 400 - No checkout session (error: "no_checkout_session")
  • 400 - Validation error (missing fields, invalid values)
  • 422 - Transfer not available for specified day

Source: backend/app/Http/Controllers/Api/CheckoutController.php

GET /api/{market}/{lang}/checkout/{offerId}/travelers

Section titled “GET /api/{market}/{lang}/checkout/{offerId}/travelers”

Get offer summary for the traveler data entry page. This is simpler than other checkout endpoints since there are no upsell options - only offer metadata.

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"offer_sku": "SAFARI-2026",
"tour_name": "Safari Adventure",
"product_url": "/es/circuito/safari-adventure",
"final_price": 1700.00,
"price_per_person": 850.00,
"departure_date": "2026-04-20",
"return_date": "2026-04-27",
"pax_count": 2,
"currency": {
"code": "EUR",
"symbol": ""
}
}
}

Fields:

FieldTypeDescription
offer_idintThe offer ID
offer_skustringThe offer SKU code
tour_namestring|nullLocalized tour name from product translation
product_urlstring|nullFrontend product page path
final_pricefloatTotal price for all travelers
price_per_personfloatPrice divided by pax count
departure_datestringTour start date (Y-m-d format)
return_datestring|nullTour end date (Y-m-d format)
pax_countintNumber of travelers
currencyobjectCurrency code and symbol

Errors:

  • 404 - Offer not found, not bookable, or belongs to different market

Source: backend/app/Http/Controllers/Api/CheckoutController.php:754

PUT /api/{market}/{lang}/checkout/travelers

Section titled “PUT /api/{market}/{lang}/checkout/travelers”

Update traveler personal data in the current checkout session. Does NOT affect pricing.

Middleware: stateful.api (requires session cookies)

Request Body:

{
"travelers": [
{
"first_name": "John",
"last_name": "Doe",
"nationality": "ES",
"passport_number": "AB1234567",
"passport_expiry": "2030-01-01",
"birth_date": "1990-05-15",
"phone": "+34612345678",
"email": "john@example.com"
},
{
"first_name": "Jane",
"last_name": "Doe",
"nationality": "ES",
"passport_number": "CD7654321",
"passport_expiry": "2031-06-15",
"birth_date": "1992-08-20",
"phone": "+34612345679",
"email": "jane@example.com"
}
]
}

Response: 200 OK

{
"success": true,
"data": {
"offer_id": 123,
"started_at": "2026-01-21T10:30:00+00:00",
"flight_selection": { ... },
"hotel_selections": [...],
"activity_selections": [...],
"transfer_selections": [...],
"client_data": {
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+34612345678"
},
"traveler_data": [
{
"first_name": "John",
"last_name": "Doe",
"nationality": "ES",
"passport_number": "AB1234567",
"passport_expiry": "2030-01-01",
"birth_date": "1990-05-15",
"phone": "+34612345678",
"email": "john@example.com"
}
],
"base_price": 1700.00,
"extras_price": 0.00,
"total_price": 1700.00,
"currency": { "code": "EUR", "symbol": "" },
"pax_count": 2
}
}

Traveler Fields:

FieldTypeRequiredDescription
first_namestringYesFirst name (max 100 chars)
last_namestringYesLast name (max 100 chars)
nationalitystringYesISO 2-letter country code
passport_numberstringYesPassport number (max 50 chars)
passport_expirydateYesPassport expiry (must be future date)
birth_datedateYesDate of birth (must be in past)
phonestringYesPhone number
emailstringYesValid email address

Validation Rules:

  • Number of travelers must match the offer’s pax_count
  • passport_number and passport_expiry are both required for each traveler
  • passport_expiry must be a future date
  • birth_date must be a past date

Pricing:

  • Traveler data does NOT affect pricing
  • extras_price and total_price remain unchanged

Errors:

  • 400 - No checkout session (error: "no_checkout_session")
  • 400 - Validation error (pax count mismatch, missing fields, invalid dates)

Source: backend/app/Http/Controllers/Api/CheckoutController.php

The ResolveMarket middleware automatically adds context to all logs:

Context::add('market_code', $market->code);
Context::add('market_id', $market->id);
Context::add('locale', $locale);