Sandbox: real offers, real PNRs, no tickets
In the sandbox these endpoints work against real carrier test inventory: a search returns real offers and you can create a real booking with a carrier PNR. Two things are worth knowing before you build against them.
- Sandbox orders are test reservations. Nobody flies on them, and they raise no fee. Cancel what you create; anything left over expires on its own payment time limit.
- Sandbox orders are never ticketed. A paid order comes back
confirmedwith an emptytickets[]and a warning saying why. Readstatusandtickets[], not the HTTP code — that is the check production relies on too.
Orders
Create order
Book a priced offer — with seats and ancillaries sold in the same call.
POST
/v1/ordersRequires X-API-KeyTurns a priced offer into a real reservation in the airline's system and returns the canonical
2. Sell seats and ancillaries here, not afterwards. Their offer references belong to this offer's shopping context, which is the only place the airline still recognises them. Adding them post-booking means re-pricing against passenger and segment IDs the airline reassigns at booking time — and paid-seat assignment on an existing order is not available on every carrier.Passengers are reassigned IDs. The
Order with the carrier's PNR.The two things that make this call succeed1. Use the priced offer ID. The ID from POST /v1/offers/price (AA-P…), never the one from search (AA-X…). A shopped ID is caught before the airline is called and answers 502 with offer … came from the search and cannot be booked directly — call OfferPrice with it first and book the offer_id that returns.2. Sell seats and ancillaries here, not afterwards. Their offer references belong to this offer's shopping context, which is the only place the airline still recognises them. Adding them post-booking means re-pricing against passenger and segment IDs the airline reassigns at booking time — and paid-seat assignment on an existing order is not available on every carrier.Passengers are reassigned IDs. The
T1 you send becomes something like PAX96101 on the returned order. Use the returned IDs for anything afterwards; the old ones reference a passenger that no longer exists.Orders are created unpaid. With AA the reservation comes back confirmed with expires_at set to the airline's payment time limit — typically a three-day hold, which is the normal agency flow. total_amount is what will be owed. Travel documents and contact details are carried through from your request because AA does not echo them back.Partner-operated connections are refused here, before the airline call. A connection where a partner airline operates a leg is not confirmed by the carrier at booking, so such an offer — one you still hold from an older search — gets an immediate error rather than a 502 after your traveller has filled in everything. Round trips, connections flown by the carrier itself and multi-city itineraries book normally.Feature gate. Every write under /v1/orders returns 503 {"error":"coming_soon"} with Retry-After: 3600 unless booking is enabled for the environment. Reads keep working.Monthly order ceiling. When one has been agreed for the agency's account, past it this call answers 429 monthly_order_limit_exceeded with Retry-After: 3600 — a contractual ceiling some airlines impose, not a platform capacity limit, and most agencies have none set.Persistence. An order is stored against your agency only when the API key resolves to one — every key issued from the dashboard does. A key with no agency behind it leaves the booking only at the airline, and it will never appear in the dashboard.Request example
curl --request POST \
--url https://api.norba.io/v1/orders \
--header "X-API-Key: $NORBA_KEY" \
--header "Content-Type: application/json" \
--data '{ "offer_id": "AA-P2E80EC16-51FD-48AD-924B-1|P2E80EC16-51FD-48AD-924B-1-1", "airline": "AA", "passengers": [ { "id": "T1", "type": "ADT", "first_name": "MARIA", "last_name": "GARCIA", "date_of_birth": "1985-04-12", "gender": "F", "document": { "type": "P", "number": "XDA123456", "issuing_country": "ES", "expiry_date": "2032-05-30", "nationality": "ES" }, "contact": { "email": "traveller@example.com", "phone": "34600111222" } } ], "contact": { "email": "traveller@example.com", "phone": "34600111222" } }'Response example
Response example
{
"order_id": "AA001Y1XD7ZA6",
"airline_order_id": "AA001Y1XD7ZA6",
"pnr": "JBQGEY",
"airline": "AA",
"source_type": "ndc_direct",
"status": "confirmed",
"created_at": "2026-08-14T09:15:00Z",
"updated_at": "2026-08-14T09:15:00Z",
"expires_at": "2026-08-17T21:59:00Z",
"currency": "EUR",
"total_amount": 380.62,
"total_price": {
"amount": 380.62,
"currency": "EUR"
},
"passengers": [
{
"id": "PAX96101",
"type": "ADT",
"first_name": "MARIA",
"last_name": "GARCIA",
"date_of_birth": "1985-04-12",
"gender": "F",
"document": {
"type": "P",
"number": "XDA123456",
"issuing_country": "ES",
"expiry_date": "2032-05-30",
"nationality": "ES"
},
"contact": {
"email": "traveller@example.com",
"phone": "34600111222"
}
}
],
"slices": [
{
"origin": "BCN",
"destination": "JFK",
"stops": 0,
"segments": [
{
"segment_id": "SGM2D231",
"flight_number": "AA67",
"aircraft": "772",
"origin": "BCN",
"destination": "JFK"
}
]
}
]
}Request body
offer_idstringRequiredThe priced offer ID from
POST /v1/offers/price. Required — omitting it returns 400 offer_id is required.airlinestringIATA code. Inferred from the offer ID prefix when omitted.
passengersarray<Passenger>RequiredAt least one — an empty array returns
400 at least one passenger is required. Each: id, type (ADT/CHD/INF), first_name, last_name, date_of_birth (YYYY-MM-DD), gender (M/F/X), document, contact. Note **first_name/last_name**, not given_name/family_name.passengers[].documentobjectTravel document, required for international travel:
type (P for passport), number, issuing_country (ISO 3166-1 alpha-2), expiry_date (YYYY-MM-DD), nationality.contactobjectRequiredBooking-level contact:
email and phone. Used for airline notifications.paymentobjectmethod (CARD, TRANSFER, WALLET), currency (ISO 4217), amount, optional card, and optional authentication. Omit entirely to create an unpaid hold, which is the normal agency flow. Card data is forwarded to the airline and never persisted.payment.cardobjectnumber, holder, expiry_month, expiry_year, brand, and optional cvv. Send the security code only where the carrier requires it — a code sent where it is not needed is exposure for nothing. The number is checked (12–19 digits) and the expiry must be in the future before anything reaches the airline.payment.authenticationobjectThe 3-D Secure result your provider returned, for carriers that accept one:
version (1/2), authentication_value, eci, transaction_status (Y N U A C R), plus ds_transaction_id (version 2) or xid (version 1), and optional authentication_status, cavv_algorithm, channel. Norba never authenticates a cardholder — there is none present on this call, and the tokens are bound to one transaction, so they cannot be reused. Omit it where the transaction is exempt (a commercial card under a corporate payment process is the usual case). Sending it to a carrier that cannot forward it is refused, never dropped silently. See [Payment Flow](/docs/payment-flow).seatsarray<Seat>Seats to sell with the flight, from
GET /v1/offers/{offerID}/seats. Each needs segment_id, row, column and the passenger_id it is for.ancillariesarray<Ancillary>Ancillaries to sell with the flight, from
GET /v1/offers/{offerID}/services. Reference the airline's id via ancillary_offer_id.pnrstringRarely used — attach the new order to an existing record locator.
Response fields
order_idstringRequiredCanonical order ID. With AA this is the airline's own order id, e.g.
AA001Y1XD7ZA6.airline_order_idstringThe airline's One Order / record id.
pnrstringThe carrier's 6-character record locator, e.g.
JBQGEY — preferred over the gateway's own booking reference, which is also carried.statusstringRequiredpending, confirmed, ticketed, cancelled, partially_cancelled, disrupted, refunded or failed.expires_atstring (RFC 3339)The airline's payment time limit on an unpaid hold. Pay or lose the reservation.
total_amountnumberRequiredOrder total.
passengers[]arrayRequiredTravellers with airline-assigned IDs — use these from now on.
slices[]arrayRequiredThe booked itinerary.
tickets[]arrayE-tickets, once issued:
ticket_number, passenger_id, status.payments[]arrayPayment history.
history[]arrayAudit trail of
{event_type, occurred_at, actor}. Returned inside the order — there is no separate history endpoint.Error codes
400offer_id is required, at least one passenger is required, invalid json, or unknown airline.502The airline refused the booking — expired or shopped-not-priced offer ID, or a multi-segment itinerary whose segments
OrderCreate cannot reference.503coming_soon — order writes are disabled in this environment.