Multi-Market API
Path-based routing API for market and language-specific product discovery with localized content.
Overview
Section titled “Overview”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
Quick Start
Section titled “Quick Start”Get Market Products (with language)
Section titled “Get Market Products (with language)”curl -X GET "https://api.example.com/api/es/ca/products" \ -H "Accept: application/json"Get Single Product by ID
Section titled “Get Single Product by ID”curl -X GET "https://api.example.com/api/es/es/products/10" \ -H "Accept: application/json"Get Single Product by Slug
Section titled “Get Single Product by Slug”curl -X GET "https://api.example.com/api/es/ca/products/slug/tour-de-barcelona" \ -H "Accept: application/json"Get Single Product by SKU
Section titled “Get Single Product by SKU”curl -X GET "https://api.example.com/api/es/ca/products/sku/ES-2CMB10-CA1" \ -H "Accept: application/json"Get Market Configuration
Section titled “Get Market Configuration”curl -X GET "https://api.example.com/api/de/config" \ -H "Accept: application/json"Endpoints
Section titled “Endpoints”GET /api/{market}/{lang}/products
Section titled “GET /api/{market}/{lang}/products”List all active products for a market and language with localized content.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| market | path | string | Yes | Market code (case-insensitive, e.g., “es”, “US”, “De”) |
| lang | path | string | Yes | Language 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", "departure_airports": [ { "iata_code": "BCN", "name": "Barcelona-El Prat Airport", "city": "Barcelona", "country": "Spain" } ] } ], "meta": { "market": "ES", "locale": "ca_ES" }}GET /api/{market}/{lang}/products/{id}
Section titled “GET /api/{market}/{lang}/products/{id}”Get a single product by its ProductByMarket ID.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| market | path | string | Yes | Market code (case-insensitive) |
| lang | path | string | Yes | Language code |
| id | path | integer | Yes | ProductByMarket ID |
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 price from active offers for a product. Use this to display “From $XXX” pricing.
Response: 200 OK
{ "data": { "product_by_market_id": 23, "price_from": "1249", "currency": "EUR", "offers_count": 5 }}When no active offers exist, price_from is null and offers_count is 0.
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 /api/{market}/config
Section titled “GET /api/{market}/config”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).
Error Responses
Section titled “Error Responses”Market Not Found (404)
Section titled “Market Not Found (404)”{ "success": false, "error": "market_not_found", "message": "Market 'xyz' not found."}Market Inactive (403)
Section titled “Market Inactive (403)”{ "success": false, "error": "market_inactive", "message": "Market 'FR' is currently not available."}Language Not Supported (400)
Section titled “Language Not Supported (400)”{ "success": false, "error": "language_not_supported", "message": "Language 'de' is not supported by market 'ES'. Supported languages: es, ca"}Architecture
Section titled “Architecture”Route Structure
Section titled “Route Structure”routes/api.php | vRoute::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/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}/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 /travelers -> CheckoutController@updateTravelerDataComponents
Section titled “Components”| Component | File | Purpose |
|---|---|---|
| Controller | app/Http/Controllers/Api/ProductByMarketController.php | Handle product requests |
| Checkout Controller | app/Http/Controllers/Api/CheckoutController.php | Handle checkout flow |
| Checkout Session Service | app/Services/Checkout/CheckoutSessionService.php | Manage checkout session state |
| Checkout Hotel Service | app/Services/Checkout/CheckoutHotelService.php | Extract upsell hotel options |
| Hotel Price Calculator | app/Services/Checkout/HotelPriceCalculatorService.php | Calculate upsell price differences |
| Checkout Transfer Service | app/Services/Checkout/CheckoutTransferService.php | Extract transfer options from itineraries |
| Transfer Price Calculator | app/Services/Checkout/TransferPriceCalculatorService.php | Calculate per-trip transfer pricing |
| Middleware | app/Http/Middleware/ResolveMarket.php | Validate market, resolve locale |
| Product Resource | app/Http/Resources/ProductByMarketResource.php | Transform product with translations |
| Checkout Session Resource | app/Http/Resources/CheckoutSessionResource.php | Transform checkout session |
| Market Resource | app/Http/Resources/MarketResource.php | Transform market config |
Frontend Integration (JavaScript)
Section titled “Frontend Integration (JavaScript)”// Fetch products for a market and languageasync 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 configurationasync function getMarketConfig(marketCode) { const response = await fetch(`/api/${marketCode}/config`); return response.json();}
// Usageconst config = await getMarketConfig('es');console.log('Supported languages:', config.data.supported_languages);Performance
Section titled “Performance”Eager Loading
Section titled “Eager Loading”All product queries use eager loading to prevent N+1 queries:
->with(['productTemplate', 'translation', 'departureAirports'])Database Indexes
Section titled “Database Indexes”-- ProductByMarket queriesINDEX (market_id, status, sort_order)
-- Locale-based lookupsUNIQUE (product_template_id, market_id, locale)
-- Translation lookupsUNIQUE (locale, url_slug)Checkout Endpoints
Section titled “Checkout Endpoints”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.
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": 1699.00, "extras_price": 0.00, "total_price": 1699.00, "currency": { "code": "EUR", "symbol": "€" }, "pax_count": 2 }}Errors:
404- Offer not found, inactive, or belongs to different market
GET /api/{market}/{lang}/checkout
Section titled “GET /api/{market}/{lang}/checkout”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.29, "hotel_name": "Luxury Safari Lodge", "location": "Nairobi" } ], "activity_selections": [ { "activity_id": 5, "day_number": 2, "price": 45.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" } ], "traveler_data": [...], "base_price": 1699.00, "extras_price": 335.29, "total_price": 2034.29, "currency": { "code": "EUR", "symbol": "€" }, "pax_count": 2 }}Selection Item Fields:
| Selection Type | Field | Description |
|---|---|---|
| hotel_selections | upsell_hotel_id | ID of the upsell hotel |
nights_start / nights_end | Tour day numbers for this stay | |
price_difference | Total price for the room upgrade (per room, not per person) | |
hotel_name | Name of the selected upsell hotel | |
location | City/location of the hotel | |
| activity_selections | activity_id | ID of the activity |
day_number | Tour day number | |
price | Per-person price | |
activity_name | Name of the activity | |
location | Destination/location of the activity | |
| transfer_selections | transfer_id | ID of the transfer |
day_number | Tour day number | |
price | Per-trip price (flat rate, not per person) | |
transfer_name | Name of the transfer service | |
location | Destination/location of the transfer |
PUT /api/{market}/{lang}/checkout/flights
Section titled “PUT /api/{market}/{lang}/checkout/flights”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": 1699.00, "extras_price": 1000.00, "total_price": 2699.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_classmust 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": 1699.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": 1699.00, "cabinClass": "BUSINESS", "source_type": "live_search", "has_flights": true, "outbound_options": [ { "id": "fare-abc-outbound-1", "finalPrice": 2899.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": 1699.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.29, "guaranteedHotelName": "Standard Safari Hotel" } ] }}Fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique key: {hotelId}-{startDay}-{endDay} |
hotelId | int | The actual hotel ID |
priceDifference | float|null | Extra cost in EUR, null if pricing unavailable |
guaranteedHotelName | string|null | Name of the included hotel being replaced |
nights.start / nights.end | int | Tour day numbers for this stay |
Price Calculation:
- Looks up per-night rates for both upsell and guaranteed hotels
- Uses room type ‘2A’ (2 Adults)
- Converts to EUR via exchange rates
- Returns
nullif pricing data is unavailable
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": 1699.00, "price_per_person": 849.50, "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": 45.00 } ] } ] }}Fields:
| Field | Type | Description |
|---|---|---|
included_activities | array | Activities included in the tour (names only) |
available_activities | array | Upsell activities available for purchase |
price | float|null | Per-person price in EUR, null if unavailable |
Price Calculation:
- Looks up per-person rates from the activity’s SupplierService
- Uses
per_personroom type for pricing - Converts to EUR via exchange rates
- Returns
nullif 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": 1699.00, "price_per_person": 849.50, "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:
| Field | Type | Description |
|---|---|---|
included_transfers | array | Transfers included in the tour (names only) |
available_transfers | array | Upsell transfers available for purchase |
vehicle_type | string|null | Type of vehicle (e.g., luxury_sedan, minivan) |
duration_minutes | int|null | Estimated transfer duration |
price | float|null | Per-trip price in EUR, null if unavailable |
Price Calculation:
- Looks up per-trip rates from the transfer’s SupplierService
- Uses
per_triproom type for pricing (not per-person) - Converts to EUR via exchange rates
- Returns
nullif 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": 95.00, "transfer_name": "VIP Airport Pickup", "location": "Mombasa" } ], "base_price": 1699.00, "extras_price": 215.00, "total_price": 1914.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_idmust be a positive integerday_numbermust 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": 1699.00, "price_per_person": 849.50, "departure_date": "2026-04-20", "return_date": "2026-04-27", "pax_count": 2, "currency": { "code": "EUR", "symbol": "€" } }}Fields:
| Field | Type | Description |
|---|---|---|
offer_id | int | The offer ID |
offer_sku | string | The offer SKU code |
tour_name | string|null | Localized tour name from product translation |
product_url | string|null | Frontend product page path |
final_price | float | Total price for all travelers |
price_per_person | float | Price divided by pax count |
departure_date | string | Tour start date (Y-m-d format) |
return_date | string|null | Tour end date (Y-m-d format) |
pax_count | int | Number of travelers |
currency | object | Currency code and symbol |
Errors:
404- Offer not found, inactive, 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", "dni": "12345678A", "passport_number": null, "passport_expiry": null, "birth_date": "1990-05-15", "phone": "+34612345678", "email": "john@example.com" }, { "first_name": "Jane", "last_name": "Doe", "nationality": "ES", "dni": null, "passport_number": "AB1234567", "passport_expiry": "2030-01-01", "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": [...], "traveler_data": [ { "first_name": "John", "last_name": "Doe", "nationality": "ES", "dni": "12345678A", "passport_number": null, "passport_expiry": null, "birth_date": "1990-05-15", "phone": "+34612345678", "email": "john@example.com" } ], "base_price": 1699.00, "extras_price": 0.00, "total_price": 1699.00, "currency": { "code": "EUR", "symbol": "€" }, "pax_count": 2 }}Traveler Fields:
| Field | Type | Required | Description |
|---|---|---|---|
first_name | string | Yes | First name (max 100 chars) |
last_name | string | Yes | Last name (max 100 chars) |
nationality | string | Yes | ISO 2-letter country code |
dni | string | Conditional | National ID document |
passport_number | string | Conditional | Passport number |
passport_expiry | date | Conditional | Passport expiry (required if passport_number provided) |
birth_date | date | Yes | Date of birth (must be in past) |
phone | string | Yes | Phone number |
email | string | Yes | Valid email address |
Validation Rules:
- Number of travelers must match the offer’s
pax_count - At least one of
dniorpassport_numbermust be provided for each traveler passport_expiryis required whenpassport_numberis providedpassport_expirymust be a future datebirth_datemust be a past date
Pricing:
- Traveler data does NOT affect pricing
extras_priceandtotal_priceremain 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
Context Logging
Section titled “Context Logging”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);