Sandbox keys reach the airlines’ test systems: nobody flies on these orders and no booking fee is charged. Check status and tickets[], not the HTTP code.
Create order
Book a priced offer — with seats and ancillaries sold in the same call.
/v1/ordersRequires X-API-KeyBooks a priced offer and returns the Order with the airline's PNR (passenger name record).
Essentials
- Use the offer_id from POST /v1/offers/price.
- Seats and extras can be sold here (seats, ancillaries) or added later on the order.
- Use the passenger ids on the returned order from now on; the airline assigns its own.
- **Without payment** the order is an unpaid hold (confirmed) until expires_at; pay it with POST /v1/orders/{orderID}/commit. **With payment** it is paid and ticketed in the same call.
- Card only (400 payment_method_not_allowed otherwise). A payment needs the offer priced in the last 30 minutes (409 offer_not_priced); amount must cover the airline's price for the offer and its extras, in the offer's currency. See Payment Flow.
- At most nine passengers, or fewer if your agency or the airline sets a lower cap (400 passenger_cap). Infants hold no seat.
Self-transfer trips. A priced trip (offer_id starting CONN-P) books one order per airline and returns a Trip: trip_id, self_transfer, status (held or paid), currency, total_amount, transfers[], orders[].
- Every ticket is booked or none. On a failure the tickets already made are cancelled and the error's trip object names failed_ticket, failed_airline, released_orders and unreleased_orders (tickets the airline kept).
- Seats and extras are added on each order afterwards (422 extras_not_supported here).
Idempotency. Idempotency-Key (up to 128 characters) is required with a payment (400 idempotency_key_required). Send one key per booking and reuse it on every retry.
- A retry of a booking already made returns the order: 200 with Idempotent-Replayed: true.
- 409 idempotency_key_in_flight: the first request is still running; retry after Retry-After.
- 409 idempotency_key_reused: the key was sent before with a different body.
- 409 offer_already_ordered: these travellers are already booked on this offer under another key; the body names order_id when there is one.
Failures
- 422: the airline refused the booking (a named code, or airline_refused). Nothing was booked or charged.
- 503 airline_unreachable or ledger_unavailable: nothing was sent; retry with the same key.
- 502 booking_outcome_unknown: the airline's answer was lost. Do not resend; the order, if made, appears in GET /v1/orders and the order.created webhook.
- 201 with X-Norba-Payment-Outcome: unknown: booked, payment outcome unknown. Do not pay again; payment writes on the order answer 409 booking_outcome_unknown until it is settled.
In production, an airline that needs its own agreement answers 403 carrier_agreement_required until it is approved (Dashboard → Go Live).
Request example
curl --request POST \
--url https://api.norba.io/v1/orders \
--header "X-API-Key: $NORBA_KEY" \
--header "Idempotency-Key: 3f2b8c1e-6d4a-4e9b-9a57-0c1d2e3f4a5b" \
--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
{
"order_id": "AA001Y1XD7ZA6",
"airline_order_id": "AA001Y1XD7ZA6",
"pnr": "JBQGEY",
"airline": "AA",
"environment": "sandbox",
"channel": "api",
"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
airlinestringIATA code. Inferred from the offer ID prefix when omitted.
ancillariesarray<Ancillary>Extras from GET /v1/offers/{offerID}/services, by ancillary_offer_id.
contactobjectRequiredBooking contact: email (one plain address, 400 invalid_contact_email otherwise) and phone.
corporate_idstringCorporate client id (up to 64 characters), so the airline applies that account's fares and benefits.
localestringLanguage of the emails sent to the booking contact about this order: en (default) or es.
offer_idstringRequiredThe priced offer ID from POST /v1/offers/price.
passengersarray<Passenger>RequiredAt least one adult. Each: id, type (ADT/CHD/INF, default ADT), first_name, last_name (up to 64 characters), date_of_birth (YYYY-MM-DD), gender (M/F/X/U), document, contact, loyalty_programs. Infants must not outnumber adults. On the returned order, type is the code the airline files (e.g. CNN for a child).
passengers[].documentobjectTravel document: type (P passport, default; ID; V visa), number, issuing_country and nationality (ISO 3166-1 alpha-2), expiry_date (YYYY-MM-DD).
passengers[].loyalty_programsarray<object>Up to five memberships: airline (programme owner's IATA code) and account_number. The returned order may add tier.
paymentobjectOmit for an unpaid hold. Otherwise: card (required), amount (at most two decimals, at least the airline's price for the offer and its extras), currency (the offer's currency; default) and optional authentication. method is ignored: every payment is a card payment. Card data is passed to the airline and never stored.
payment.authenticationobjectA 3-D Secure result from your own provider, for airlines that accept one: version (1/2), authentication_value, eci, transaction_status (Y N U A C R), ds_transaction_id (v2) or xid (v1), optional authentication_status, cavv_algorithm, channel. Sent to an airline that cannot carry it, the booking is refused with 422. See the airline pages.
payment.cardobjectnumber (12–19 digits), holder, expiry_month, expiry_year (in the future), optional brand and cvv.
pnrstringAttach the order to an existing record locator. Rarely needed.
seatsarray<Seat>Seats from GET /v1/offers/{offerID}/seats: segment_id, row, column, passenger_id.
Response fields
airline_order_idstringThe airline's own order id.
ancillaries[]arrayExtras on the order: ancillary_offer_id, type, name, price_amount, currency, passenger_id.
ancillaries_amountnumberSeats and extras on the order, at the airline's prices.
base_farenumberThe airline's base fare, from the priced offer.
channelstringapi for an API-key booking, dashboard for one made in the dashboard.
currencystringRequiredISO 4217 code of every amount on the order.
environmentstringsandbox or production. An order is serviced in the environment it was booked in.
expires_atstring (RFC 3339)Payment deadline of an unpaid hold. Unpaid by then, the airline releases it.
history[]arrayOn reads only: {event_type, occurred_at, actor, payload}. The full record is GET /v1/orders/{orderID}/history.
offer_idstringThe priced offer the order was booked from.
order_idstringRequiredOrder ID, e.g. AA001Y1XD7ZA6.
passengers[]arrayRequiredTravellers with the airline's ids. Use these from now on.
payments[]arrayPayments as the airline reports them, plus a refunded entry once a refund is recorded: id, method, status (pending, authorized, captured, failed, refunded), amount, currency, reference, processed_at.
pnrstringThe airline's 6-character record locator, e.g. JBQGEY.
search_idstringThe search that offer came from, when known.
seats[]arraySeats on the order: seat_id, row, column, segment_id, passenger_id, price.
service_feenumberYour commission, included in total_amount.
slices[]arrayRequiredThe booked itinerary.
statusstringRequiredpending, confirmed, ticketed, cancelled, partially_cancelled, disrupted, refunded or failed.
taxes_feesnumberThe airline's taxes and fees, from the priced offer.
tickets[]arrayIssued documents: id, ticket_number, passenger_id, status (issued, open, flown, void, exchanged, refunded, cancelled), type (ticket or emd for a paid seat or bag), type_code, issued_at, coupon_status, each when the airline reports it.
total_amountnumberRequiredOrder total, including service_fee. Also as total_price {amount, currency}.
trip[]arrayOn a single-order read: every ticket of the trip in flying order (order_id, airline, pnr, status).
trip_idstringThe self-transfer trip this order belongs to (TRP-…).
warnings[]array<string>Advisories about this response, e.g. a document that could not be issued. Read them: an order can be confirmed with a warning. Not stored, so later reads do not repeat them.
Error codes
400idempotency_key_required (a payment without Idempotency-Key), payment_method_not_allowed (a payment without a card), passenger_cap, invalid_contact_email, or a validation message (offer_id is required, passengers, seats for infants, payment.card.…, payment.amount…, payment.currency…).
403carrier_agreement_required (production: the airline's agreement is not approved yet), booking_limit_exceeded (above the member's booking limit), or a key with no agency behind it.
409offer_not_priced (a payment on an offer not priced in the last 30 minutes), offer_already_ordered (with order_id when known), idempotency_key_in_flight (Retry-After: 5), idempotency_key_reused, booking_outcome_unknown (with attempt_id: do not book again), or order_conflict.
413request body too large.
422The airline refused the booking: offer_expired, flights_unavailable, extra_not_priced (with kind, ref), a 3-D Secure result the airline cannot carry, or airline_refused. extras_not_supported: seats or extras sent with a self-transfer trip. Nothing was booked or charged.
429monthly_order_limit_exceeded: a ceiling agreed for your account (Retry-After: 3600).
502booking_outcome_unknown with attempt_id and Retry-After: 60: the airline's answer was lost. Do not send it again.
503airline_unreachable or ledger_unavailable (Retry-After: 5): nothing was sent, retry with the same key. coming_soon: order writes are switched off in this environment. With no code: the airline is not available in this environment.