Sandbox orders are test bookings

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.

Orders

Create order

Book a priced offer — with seats and ancillaries sold in the same call.

POST/v1/ordersRequires X-API-Key

Books 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

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"
        }
      ]
    }
  ]
}

Authorization

X-API-KeystringRequired

Your API key from the dashboard. A missing or invalid key answers 401; see Authentication.

Request body

airlinestring

IATA code. Inferred from the offer ID prefix when omitted.

ancillariesarray<Ancillary>

Extras from GET /v1/offers/{offerID}/services, by ancillary_offer_id.

contactobjectRequired

Booking contact: email (one plain address, 400 invalid_contact_email otherwise) and phone.

corporate_idstring

Corporate client id (up to 64 characters), so the airline applies that account's fares and benefits.

localestring

Language of the emails sent to the booking contact about this order: en (default) or es.

offer_idstringRequired

The priced offer ID from POST /v1/offers/price.

passengersarray<Passenger>Required

At 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[].documentobject

Travel 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.

paymentobject

Omit 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.authenticationobject

A 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.cardobject

number (12–19 digits), holder, expiry_month, expiry_year (in the future), optional brand and cvv.

pnrstring

Attach 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_idstring

The airline's own order id.

ancillaries[]array

Extras on the order: ancillary_offer_id, type, name, price_amount, currency, passenger_id.

ancillaries_amountnumber

Seats and extras on the order, at the airline's prices.

base_farenumber

The airline's base fare, from the priced offer.

channelstring

api for an API-key booking, dashboard for one made in the dashboard.

currencystringRequired

ISO 4217 code of every amount on the order.

environmentstring

sandbox 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[]array

On reads only: {event_type, occurred_at, actor, payload}. The full record is GET /v1/orders/{orderID}/history.

offer_idstring

The priced offer the order was booked from.

order_idstringRequired

Order ID, e.g. AA001Y1XD7ZA6.

passengers[]arrayRequired

Travellers with the airline's ids. Use these from now on.

payments[]array

Payments 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.

pnrstring

The airline's 6-character record locator, e.g. JBQGEY.

search_idstring

The search that offer came from, when known.

seats[]array

Seats on the order: seat_id, row, column, segment_id, passenger_id, price.

service_feenumber

Your commission, included in total_amount.

slices[]arrayRequired

The booked itinerary.

statusstringRequired

pending, confirmed, ticketed, cancelled, partially_cancelled, disrupted, refunded or failed.

taxes_feesnumber

The airline's taxes and fees, from the priced offer.

tickets[]array

Issued 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_amountnumberRequired

Order total, including service_fee. Also as total_price {amount, currency}.

trip[]array

On a single-order read: every ticket of the trip in flying order (order_id, airline, pnr, status).

trip_idstring

The 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

400

idempotency_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…).

403

carrier_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.

409

offer_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.

413

request body too large.

422

The 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.

429

monthly_order_limit_exceeded: a ceiling agreed for your account (Retry-After: 3600).

502

booking_outcome_unknown with attempt_id and Retry-After: 60: the airline's answer was lost. Do not send it again.

503

airline_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.

POST /v1/orders — Create order | Norba API