# Sandbox

> How the sandbox works, the limits of the suppliers' own test systems and what to expect while testing.

Source: https://fx-port.com/docs/sandbox

Sandbox keys (`fxp_test_...`) connect to the **test systems of our suppliers**: Amadeus test, Duffel test, the hotel supplier's test contract and the eSIM supplier's sandbox. Nothing you do with a sandbox key moves your real balance, and sandbox data is kept apart from live data: a sandbox key never sees a live booking and the other way round.


> [!WARNING]
> The suppliers' test systems are **not under FX-Port's control**. They can have short outages, and some routes, airlines or hotels fail during pricing or booking in the sandbox although they work on live. When one supplier fails, the answer of FX-Port still arrives and the entry of that supplier says `success: false`. Wait and retry, or try another route.


## What the sandbox does

| Behaviour | Detail |
|---|---|
| **No balance debit** | Financial operations are skipped: bookings do not charge your balance and `GET /balances` has nothing to show. |
| **Real data structures, test fares** | Flight offers have the shape of live offers but come from test systems. Not every route exists. |
| **Same limits as live** | The [rate limits](https://fx-port.com/docs/rate-limits.md) are the same, so load tests belong on a schedule, not in a loop. |
| **No webhooks** | FX-Port does not call a URL of yours, in the sandbox or on live. Poll the booking: [get booking](https://fx-port.com/docs/flights/get-booking.md) for flights, [booking detail](https://fx-port.com/docs/account/booking-detail.md) for everything else. |
| **`environment` everywhere** | Every JSON body says `"environment": "sandbox"` and every response has the `X-Environment: sandbox` header. |

`GET /balances` answers an envelope with an empty list, not a bare array:

~~~json
{
  "environment": "sandbox",
  "success": true,
  "data": [],
  "meta": {
    "agencyId": "00000000-0000-0000-0000-000000000000",
    "environment": "sandbox",
    "financialOperationsEnabled": false,
    "message": "Sandbox bookings do not debit live balances; no sandbox balance ledger is exposed."
  }
}
~~~

## Flights

The sandbox has two flight suppliers (see [Suppliers](https://fx-port.com/docs/suppliers.md)):

| Supplier | Routes | Notes |
|---|---|---|
| `duffel_1` | Worldwide | Instant ticketing only. Duffel allows **one booking per search**: to book a second offer, search again (a second booking on the same search fails with `offer_request_already_booked`). Offers are randomised and expire: an offer that searched fine can be gone when you price it, so search again. |
| `amadeus_aqc_dz_1` | Routes that touch Algeria | Supports hold and same-day void. Some airlines or routes reject at the booking step even though they were in the results: try another route (for example `ALG → ORN`) or another offer. The cheapest holds fail more often than the others. |

To test an ACD booking, see [ACD documents](https://fx-port.com/docs/flights/acd-documents.md): `CDG → ALG` on `amadeus_aqc_dz_1` returns offers with ACD pricing.

Set `send_email` to `false` on bookings while you test, so no message goes to the address you typed.

## Hotels


> [!CAUTION]
> In the sandbox you can **search and price any hotel, but you can only book the supplier's test hotels.** Booking a random hotel does not work: `pre_book` answers `409 hotel_not_bookable_in_sandbox` and lists the test hotels (before 2026-11-01 it answered `200` with an empty booking form, and `book_hotel` then failed with `order_not_found`). Use `test_hotel_do_not_book` (works with every residency) or `test_hotel` (works only with residency `dz`, `ru` or `us`), and **cancel every booking you make**: sandbox hotel bookings are real orders on a supplier test contract.


See [Hotel sandbox testing](https://fx-port.com/docs/hotels/sandbox-testing.md) for the ids, a copy-paste script that books and cancels a test hotel, the date rules and the known quirks.

## eSIMs

- A sandbox order really provisions an eSIM on the supplier's sandbox, with a dummy LPA string and a QR code image hosted by the sandbox.
- Order and top-up answers carry three flags that make the "no money moved" rule visible: `balanceDebited`, `transactionCreated` and `payoutCreated` (twins `balance_debited`, `transaction_created`, `payout_created`), all `false`.
- Usage is static: a sandbox eSIM reports `ACTIVE` with its full allowance and the figures do not move.
- A sandbox top-up works end to end and shows a balance of `0.0`.
- Sandbox bookings appear in your [history](https://fx-port.com/docs/account/history.md) and booking detail like any other.

## Testing checklist

Before you switch to live keys, verify in the sandbox:

1. **Search**: your request is accepted and returns offers. Try one bad input on purpose (a wrong airport code) and check that you handle the [error](https://fx-port.com/docs/errors.md).
2. **Price**: re-pricing an offer works and the pricing fields are what you expect.
3. **Book (issue)**: the instant booking completes and returns a PNR and an e-ticket.
4. **Book (hold)**: a hold on `amadeus_aqc_dz_1` creates a reservation with `status: awaiting_payment`.
5. **Pay**: `pay_order` with the `order_id` of the hold succeeds.
6. **Get booking**: `get_booking` returns the booking, and `bucket_link` (the PDF) once it exists.
7. **Cancel a hold**: `cancel_order` releases an unpaid held PNR. Voiding or refunding an issued ticket, exchanges and re-issues stay manual support operations (Duffel orders can use the self-service [cancel quote](https://fx-port.com/docs/flights/cancel-quote.md)).
8. **Hotels**: run the whole flow on `test_hotel_do_not_book` and cancel it.
9. **eSIMs**: order a package, read its instructions and usage, top it up.
10. **Failure paths**: a read-only key on a write endpoint (`403`), an unknown id (`404`), a missing field (`400`), and a retry after a `429`.
