Skip to content

Booking Details API

Authenticated endpoint for retrieving comprehensive booking details including offer, product, and passenger information.

The Booking Details API provides an admin-only endpoint for accessing complete booking information scoped to specific markets and languages.

Authentication: Requires active admin session (web middleware + auth) and admin role.

Base URL Pattern: /api/{market}/{lang}/bookings/{bookingReference}

GET /api/{market}/{lang}/bookings/{bookingReference}

Section titled “GET /api/{market}/{lang}/bookings/{bookingReference}”

Get complete booking details by reference code.

Parameters:

Name In Type Required Description
market path string Yes Market code (case-insensitive)
lang path string Yes Language code (e.g., “en”, “es”, “ca”)
bookingReference path string Yes Booking reference code (e.g., “BK-ABC12345”)

Response: 200 OK

{
"data": {
"booking_reference": "BK-ABC12345",
"status": "in_progress",
"status_label": "In Progress",
"total_amount": "2500.00",
"deposit_amount": "980.00",
"currency": "EUR",
"currency_symbol": "€",
"booked_at": "2024-12-15T10:30:00+00:00",
"number_of_travelers": 2,
"market_code": "es",
"customer_service_phone": "+34 919 49 45 52",
"footer_copyright": "© BY VOLARE SL. Todos los derechos reservados",
"offer": {
"id": 123,
"sku": "ES-5CMB10-CA1-2024-01-15",
"departure_date": "2024-01-15",
"return_date": "2024-01-26",
"trip_days": 12,
"final_price": "1300.00",
"marketing_price_per_pax": "650.00",
"room_type": "double",
"room_type_label": "Double Room",
"departure_airport": {
"iata_code": "BCN",
"name": "Barcelona-El Prat Airport",
"city": "Barcelona"
}
},
"product": {
"id": 10,
"sku": "ES-5CMB10-CA1",
"title": "Tour de Sri Lanka",
"subtitle": "Descobreix l'illa maragda",
"short_description": "Una aventura increible...",
"highlights": ["Sigiriya", "Kandy", "Yala"],
"itinerary": [
{
"day": 1,
"date": "2024-01-15",
"title": "Arrival in Colombo",
"details": "Arrive, meet your host, private transfer to the hotel.",
"image_url": "https://cdn.example.com/itineraries/day1.jpg",
"is_flight_day": false
}
],
"hero_image": "https://cdn.example.com/images/sri-lanka.jpg",
"trip_duration_days": 10
},
"passengers": [
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"age_category": "adult",
"date_of_birth": "1985-03-15",
"is_lead_passenger": true,
"email": "john@example.com",
"phone": "+34612345678"
}
],
"hotels": [
{
"name": "Cinnamon Lodge Habarana",
"city": "Habarana",
"nights": 2,
"check_in": "2024-01-15",
"check_out": "2024-01-17",
"image_urls": ["https://cdn.example.com/hotels/cinnamon.jpg"],
"is_upgrade": false,
"category": "luxury"
}
],
"flights": {
"cabin_class": "ECONOMY",
"outbound": {
"date": "2024-01-15",
"departure_time": "10:30",
"arrival_time": "23:45",
"departure_airport": { "iata_code": "BCN", "city": "Barcelona" },
"arrival_airport": { "iata_code": "CMB", "city": "Colombo" },
"segments": [
{
"flight_number": "EK186",
"airline_code": "EK",
"airline_name": "Emirates",
"departure_airport": { "iata_code": "BCN", "city": "Barcelona" },
"arrival_airport": { "iata_code": "DXB", "city": "Dubai" },
"departure_time": "10:30",
"arrival_time": "20:15"
}
],
"stopovers": [
{ "airport": { "iata_code": "DXB", "city": "Dubai" }, "layover_minutes": 120 }
],
"total_duration_minutes": 795
},
"inbound": {
"date": "2024-01-25",
"departure_time": "01:30",
"arrival_time": "12:15",
"departure_airport": { "iata_code": "CMB", "city": "Colombo" },
"arrival_airport": { "iata_code": "BCN", "city": "Barcelona" },
"segments": [],
"stopovers": [],
"total_duration_minutes": 780
}
},
"experiences": [
{
"name": "Visita guiada a la roca de Sigiriya",
"city": "Sigiriya",
"date": "2024-01-16",
"image_url": "https://cdn.example.com/activities/sigiriya.jpg",
"tier": "included"
},
{
"name": "Safari en jeep por el Parque Nacional de Yala",
"city": "Yala",
"date": "2024-01-17",
"image_url": "https://cdn.example.com/activities/yala-safari.jpg",
"tier": "extra"
}
],
"optional_services": [
{
"name": "Traslado privado aeropuerto - hotel",
"description": "Vehículo privado con conductor desde el aeropuerto de Colombo hasta el hotel."
}
],
"insurance": {
"status": "issued",
"product_name": "Horizonte Selected",
"provider": "Intermundial",
"policy_number": "IM-2026-004871",
"coverage_start": "2026-03-15",
"coverage_end": "2026-03-29",
"insured_count": 2,
"is_included": false,
"price": 89.0,
"certificate_url": "https://api.example.com/api/trip/4821/insurance-certificate?expires=1790000000&signature=…",
"assistance_phones": ["+34 910 848 794", "+34 917 586 733"],
"assistance_whatsapp": "+34 633 227 839",
"documents": [
{ "type": "conditions", "url": "https://api.example.com/insurance-policies/12/conditions" }
]
},
"section_prices": {
"hotels": 0.0,
"experiences": 120.0,
"services": 60.0,
"insurance": 89.0
}
}
}
{
"message": "Unauthenticated."
}
{
"message": "Access denied. Admin role required."
}

Returned when booking does not exist, or does not belong to requested market/locale.

{
"success": false,
"error": "booking_not_found",
"message": "Booking with reference 'XYZ' not found."
}
{
"success": false,
"error": "language_not_supported",
"message": "Language 'de' is not supported by market 'ES'. Supported languages: es, ca"
}

The flights field is populated from multiple sources in priority order:

Priority Source Cabin Class When Used
1 booking.flight_selection (with outbound) From the selection Every booking that stores its international flights: economy, business upgrade, or a leg entered by hand
2 booking_upsells.flight_search_params BUSINESS Business upgrades stored without international flights in flight_selection
3 booking.flight_selection (no outbound) From the selection Selections holding only domestic legs
4 dynamic_flight_cache (itinerary_index=1) ECONOMY Legacy fallback for older bookings

The business upgrade’s flight_search_params is a copy of flight_selection taken at payment, so the selection wins: it carries cities, per-segment times and layovers, and it is what an admin rewrites when entering a flight bought outside Arkana.

The resource handles both enriched format (with nested airport objects containing iata_code and city) and legacy format (plain IATA code strings) for backward compatibility.

Internal flights the customer paid for are carried in flights.domestic_legs, extracted from the stored selection’s legs[] entries marked type: 'domestic'. Alongside the raw route string ("SCL-IPC"), each leg also ships departure_airport and arrival_airport as { iata_code, city } pairs so the client can render them like the international legs (“Santiago (SCL) – Isla de Pascua (IPC)”).

Domestic legs are persisted with only the route string — they never go through the airport enrichment BookingFinalizationService applies to outbound/inbound — so the cities are resolved on read, in a single airports lookup covering every leg. This works for bookings already stored without any data migration. Either endpoint is null when the route cannot be parsed, and city is null when the code is not in the catalog; the raw route remains the client’s fallback in both cases.

Each segment carries both airline_code (IATA code, e.g. EK) and airline_name (full carrier name, e.g. Emirates). For stored economy selections and business-upgrade legs, the airline is resolved per segment from the segments[*] entries (one per flight number), falling back to the leg’s distinct airlines/airline_names arrays by index and finally to the flight-number prefix. The per-segment source matters on multi-segment same-airline legs, where indexing the distinct arrays would mislabel later segments (e.g. flight 907 shown as 90). Cached economy legs resolve the name from the dynamic_flight_cache_segments.airline_name column, falling back to the operating carrier code.

offer.return_date (YYYY-MM-DD) and offer.trip_days (inclusive day count) are derived from the bound international flight via Offer::getTravelDates() — door-to-door, outbound departure day to return arrival-home day (same source as the booking emails). Both are null when no flight is bound (land-only / pre-flight); the frontend then falls back to the product.trip_duration_days land-tour estimate. product.trip_duration_days (itinerary nights + 1) remains unchanged and can under-report long-haul trips whose return leg spans an extra travel day.

In addition to the booking core fields, the response includes market-aware metadata used by the frontend:

Field Type Description
market_code string Lowercased market code for frontend routing (defaults to es)
customer_service_phone string | null Market customer-service phone number
footer_copyright string | null Footer copyright text, resolved from CmsFooter via the market’s default locale

The trip page doubles as the quotation page sent to clients. For pre-payment bookings it renders a price + deposit block, so the resource exposes a customer-facing status and a resolved deposit alongside the total.

Field Type Description
status string Customer-facing status (ClientBookingStatus value) — see below
status_label string Human label for the status, localized to the booking’s own market — see below
total_amount string Stored decimal (decimal:2) — the booking’s total price
deposit_amount string | null Resolved deposit (decimal:2 string) — persisted, derived, or null (see below)
section_prices object Per-section extra totals: { hotels, experiences, services, insurance } (floats)

status is a projection of the operational Booking::$status, computed via ClientBookingStatus::fromBookingStatus(). The raw operational status (the flight/land fulfillment pipeline) is never exposed. The client-facing values:

Value status_label (en / es) Operational statuses projected onto it
in_progress In Progress / En curso Draft, Checkout
quote Quotation / Presupuesto QuotationRequested, QuotationConfirmed
pending_payment Pending Payment / Pendiente de pago PendingPayment, PaymentProcessing
confirmed Confirmed / Confirmada PendingFlightBooking, FlightBookingInProgress, FlightBookingFailed, FlightsConfirmed, PendingLandConfirmation, Confirmed, FullyPaid
balance_due Balance Due / Pago pendiente AwaitingBalance
completed Completed / Completada Completed
cancelled Cancelled / Cancelada Cancelled, Expired

status_label is not localized against the request. The trip-access endpoints (/api/trip/*) carry no locale segment, unlike the checkout routes (/{market}/{lang}/…), so the app locale there is the APP_LOCALE=en default and every market would read English.

Instead the customer-facing resources resolve it from the booking’s own market: ClientBookingStatus::getLabel() takes an optional language, and BookingDetailsResource / ClientBookingListResource pass Market::getDefaultLanguage(). Two consequences worth knowing:

  • Markets store region-qualified locales (es_ES, en_GB); the translation files are keyed by language (lang/es, lang/en). getDefaultLanguage() derives the language, since passing the raw locale silently falls back to the fallback locale.
  • Only en and es translations exist today, so a de_DE market falls back to English.

Admin callers (Filament) omit the argument and keep the app locale, so the admin stays English.

Source: backend/app/Enums/ClientBookingStatus.php, backend/lang/{en,es}/bookings.php.

deposit_amount is resolved by BookingDetailsResource::resolveDepositAmount():

  1. Persisted deposit — if the booking has a stored deposit_amount column (set when payment is initiated), it is emitted verbatim.

  2. Derived deposit — otherwise, for pre-payment bookings without a persisted deposit (quote and in_progress), it is derived from the checkout snapshot via PaymentCalculatorService::depositFromCheckout():

    deposit = round(raw_flight_cost + margin% × total_price, 2)
    deposit = min(deposit, total_price)

    raw_flight_cost comes from flight_selection.fare_total_price (business, all legs pax-scaled) or the snapshot flight_base_price (economy).

  3. Null — any other status with a null persisted deposit stays null.

pending_payment is deliberately excluded from the derivation set: it always carries a persisted deposit (set when the booking is promoted to payment), so it emits the stored value from step 1, never a derived one.

Gotcha: the derived deposit only becomes usable once the checkout snapshot carries flight pricing. The initial draft snapshot (checkout step flights, before advancing) only holds step_timestamps, so the derivation yields 0.00 until the booking advances past the flights step.

Source: BookingDetailsResource::resolveDepositAmount() / deriveDepositFromCheckout(); PaymentCalculatorService::depositFromCheckout().

section_prices aggregates the per-section extra totals shown in the price/quote view, built by BookingDetailsResource::buildSectionPrices() from Booking::checkoutExtrasBreakdown(). It works for both quote and finalized bookings (the breakdown reconciles with the snapshot extras_price). Upsell types map to the trip-page sections:

Key Upsell types (BookingUpsellType)
hotels Hotel
experiences Activity
services Transfer, FlightUpgrade
insurance Insurance

Insurance has its own key rather than sitting in services, because the policy is no longer one of the optional-service rows: leaving it in services would show that section a header total larger than the rows beneath it. The sum across the keys is unchanged, so the extras total still reconciles — a frontend adding these up must include insurance.

Source: BookingDetailsResource::buildSectionPrices().

Each hotel stay groups consecutive nights at the same hotel.

Field Type Description
name string Hotel name
city string | null POI city name
nights int Number of nights
check_in string Check-in date (YYYY-MM-DD)
check_out string Check-out date (YYYY-MM-DD)
image_urls string[] Full URLs for all hotel images (empty array when none)
is_upgrade bool Whether the stay is a selected hotel upgrade (luxury or grand-luxury tier). The highest purchased tier wins: grand-luxury takes precedence over luxury, which takes precedence over the base selection hotel
category string | null The hotel’s own ServiceTier category (selection / luxury / grand_luxury), independent of the tier slot it fills in the tour; null when the hotel is uncategorized. The trip page renders it as a 5★ badge on the shared hotel card (frontend/src/shared/utils/hotelCategory.ts) — see Service Tiers

When a supplier can’t confirm a hotel for one client and books an alternative, bookings.hotel_overrides (original supplier_hotel_id => replacement supplier_hotel_id) shows the real hotel on that booking’s trip page without editing the shared tour, which would affect every booking. After each day’s hotel is resolved (selection hotel or purchased upgrade), a matching override swaps in the replacement: name, city, images and description come from the replacement hotel, and an upgraded stay keeps its is_upgrade flag and price. A replacement id that doesn’t exist is skipped, so the original hotel is kept; NULL/empty overrides change nothing.

It is a whole-hotel swap (every night at that hotel, never part of a multi-night stay). There is no admin UI yet: a developer sets it, e.g. $booking->update(['hotel_overrides' => [238 => 887]]), which also writes an activity-log entry. It is not applied in Trips to Deliver, which still shows the tour’s hotel.

Source: BookingDetailsResource::buildHotelsData(), Booking::hotelOverrideReplacements().

product.itinerary is a flat, per-day list — one entry per calendar day, sequential from the destination-arrival date. Each item: { day, date, city, title, details, image_url, is_flight_day }. city is the stop the day belongs to, carried through from the authored itinerary so the traveler can tell where they are on any given day; it is null on the synthesized flight days, which belong to no stop. The tour’s stops (which can span multiple nights) are flattened into individual days: per-day text (title/details) comes from the localized translation, and image_url is resolved from the tour template’s per-day image (day_image), or null when the tour has no image for that day. is_flight_day is false for these content days.

The list may also contain virtual flight-day entries (is_flight_day: true) prepended at the start and/or appended at the end — one per overnight flight night, carrying “en vuelo” copy and an endpoint image. Each night’s text is the market product’s published flight-day text (arrival_day1_description etc. on the translation) when set, otherwise the FlightDayCopy default for the product’s language; titles always come from FlightDayCopy. They are dated to the real booked flight (outbound entries to the home-departure date, return entries landing on the home-arrival date). The rendered count per direction is the tour’s authored flight_days (see Product Templates) clamped to the booked flight’s actual span, so an overnight day is never shown for a leg that departs and lands the same calendar day. On a 2-night flight the standard copy is always the first night (Mientras duermes… / El final de un gran viaje, primary image) and the second night has its own fixed title (A una película de distancia / Próximamente en los mejores destinos, day-2 image, falling back to the primary when unset) — see Flight-day copy and ordering. When no flight day is rendered, the content days keep their own text and image; nothing from the endpoint is merged onto them.

Source: BookingDetailsResource::buildItineraryData().

These arrays surface the activities and services for the booking:

  • experiences — the tour’s included activities plus the extra activity upsells the customer added at checkout. Each item: { name, city, date, image_url, tier } where tier is included or extra. Included activities are derived from the tour itinerary; extras come from activity upsells. The date is derived from the booking’s departure date plus the activity’s tour day (null when no day is recorded). Entries are ordered chronologically. (A future substitution tier is not yet emitted.)
  • optional_services — transfer upsells and the Business flight upgrade. Each item: { name, description, conditions_url, price, is_extra, day, location }. Travel insurance is not listed here: it has its own insurance block (below), and emitting both showed the traveller the same policy twice.

Activity and transfer names/descriptions are localized to the market’s default locale.

insurance is the traveller-facing policy block on the trip page — what a passenger needs when something happens in destination, not a priced line item: { status, product_name, provider, policy_number, coverage_start, coverage_end, insured_count, is_included, price, certificate_url, assistance_phones, assistance_whatsapp, documents }.

status is one of:

Status When What is emitted
issued The insurer has emitted the policy The full block, policy number, insured count and certificate_url included
pending Paid, emission still in flight (a few minutes) Same block without the policy number — there is cover, but no number yet
quote No contract yet; read from the checkout snapshot Product, price, coverage dates and documents; never a policy number

The field is null when there is nothing the traveller should see: no insurance on the booking, or an emission that failed or was cancelled — an internal problem that surfaces in the admin’s insurance contract list, never as an error on the customer’s page.

assistance_phones / assistance_whatsapp are the insurer’s fixed 24h numbers from config/intermundial.php, shared with the pre-departure email so the two surfaces cannot disagree. documents are stable per-policy app routes (conditions, ipid, price_summary), never raw storage URLs: the uploads are stored private, so a disk URL would fail for the very traveller the link is for.

Source: BookingDetailsResource::buildInsuranceData().

Component File
Controller backend/app/Http/Controllers/Api/BookingController.php
Booking Resource backend/app/Http/Resources/BookingDetailsResource.php
Offer Resource backend/app/Http/Resources/OfferSummaryResource.php
Passenger Resource backend/app/Http/Resources/PassengerSummaryResource.php
Route backend/routes/api.php (market-scoped routes)