---
title: "Sandbox: book an offer — simulated, free, production responses and timings"
method: POST
path: "/sandbox/flights/book"
tags: ["sandbox"]
---

# Sandbox: book an offer — simulated, free, production responses and timings

`POST /sandbox/flights/book`

## Request

- Base URL: `https://letsfg.co/developers/api/v1`
- URL: `https://letsfg.co/developers/api/v1/sandbox/flights/book`
- Auth: API key in header `X-API-Key`

Identical request and responses to POST /flights/book, for an offer from a sandbox search. Nothing is held and nothing is bought: the booking is simulated, walking production's states on production's clock (4–11 minutes), pausing on the same questions and ending the same ways. Every response carries `sandbox: true`.

The optional `sandbox` block picks what happens. `scenario`: success (default), price_change, extra, seller_failed, needs_attention, payment_declined, payment_method_required, temporarily_unavailable. `speed`: realistic (default) or fast (about a minute, for CI). want_seat: true adds a seat map question. Production ignores the `sandbox` block, so it is safe to leave in.

## Request body

- object
  - `search_id` string, required: search_id from the search response. An offer is bookable only inside its own search.
  - `offer_id` string, required: id of one offer from that search
  - `passengers` DeveloperPassenger[], required
    - `given_name` string, required: First/given name exactly as on the travel document
    - `family_name` string: Surname exactly as on the travel document
    - `no_family_name` boolean: True when the traveller genuinely has no surname
    - `born_on` string: YYYY-MM-DD
    - `gender` string: 'm' or 'f' — the airline form's title is derived from it
    - `email` string, email: The lead passenger's email receives the airline confirmation
    - `phone_number` string
    - `phone_country` string: ISO-3166 alpha-2, e.g. PL
    - `nationality` string: ISO-3166 alpha-2
    - `address_line1` string
    - `address_line2` string
    - `address_city` string
    - `address_state` string
    - `address_postal` string
    - `address_country` string: ISO-3166 alpha-2 country of residence
    - `passport_number` string: Required for every traveller - some sellers will not issue a ticket without it. A missing one is answered as missing_fields.
    - `passport_country` string: ISO-3166 alpha-2 issuing country. Required.
    - `passport_expiry` string: YYYY-MM-DD. Required; must be valid through the last flight.
    - `passport_issue` string: YYYY-MM-DD, the date printed as 'Date issued'. Required.
    - `passenger_type` 'adult' | 'child' | 'infant'
  - `contact_email` string, email: Where the confirmation goes. Defaults to the lead passenger's email.
  - `idempotency_key` string: Retrying with the same key returns the same booking instead of opening a second hold.
  - `want_seat` boolean: Pause at the airline's seat map and ask.
  - `sandbox` object
    - `scenario` 'success' | 'price_change' | 'extra' | 'seller_failed' | 'needs_attention' | 'payment_declined' | 'payment_method_required' | 'temporarily_unavailable'
    - `speed` 'realistic' | 'fast'

## Response `202`

Simulated hold placed.

- BookAccepted
  - `ok` boolean
  - `booking_id` string
  - `state` string
  - `dispatched` boolean
  - `held` object: What is HELD on the connected method — not taken.
  - `charged` number: Always 0 here. Capture happens only against a real PNR.
  - `offer` object
  - `poll_url` string, uri
  - `message` string

## Other responses

- `400`: missing_fields, invalid_fields or currency_unsupported, as production.
- `402`: payment_method_required or payment_declined (by scenario).
- `404`: search_not_found or offer_not_found.
- `409`: too_many_attempts.
- `503`: temporarily_unavailable (by scenario).

---

[API](https://skmtc.dev/letsfg/apis/letsfg-public-developer-api.md) · [All operations](https://skmtc.dev/letsfg/apis/letsfg-public-developer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/letsfg/apis/letsfg-public-developer-api/revisions/516ab6ba1d2e?raw)
