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 confirmed with an empty tickets[] and a warning saying why. Read status and tickets[], 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-Key
Turns a priced offer into a real reservation in the airline's system and returns the canonical 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"
        }
      ]
    }
  ]
}

Authorization

X-API-KeystringRequired
Opaque API key issued from the Norba dashboard. Sent on every request as the X-API-Key HTTP header. Missing or invalid keys are rejected with 401 Unauthorized.

Request body

offer_idstringRequired
The priced offer ID from POST /v1/offers/price. Required — omitting it returns 400 offer_id is required.
airlinestring
IATA code. Inferred from the offer ID prefix when omitted.
passengersarray<Passenger>Required
At 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[].documentobject
Travel document, required for international travel: type (P for passport), number, issuing_country (ISO 3166-1 alpha-2), expiry_date (YYYY-MM-DD), nationality.
contactobjectRequired
Booking-level contact: email and phone. Used for airline notifications.
paymentobject
method (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.cardobject
number, 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.authenticationobject
The 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.
pnrstring
Rarely used — attach the new order to an existing record locator.

Response fields

order_idstringRequired
Canonical order ID. With AA this is the airline's own order id, e.g. AA001Y1XD7ZA6.
airline_order_idstring
The airline's One Order / record id.
pnrstring
The carrier's 6-character record locator, e.g. JBQGEY — preferred over the gateway's own booking reference, which is also carried.
statusstringRequired
pending, 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_amountnumberRequired
Order total.
passengers[]arrayRequired
Travellers with airline-assigned IDs — use these from now on.
slices[]arrayRequired
The booked itinerary.
tickets[]array
E-tickets, once issued: ticket_number, passenger_id, status.
payments[]array
Payment history.
history[]array
Audit trail of {event_type, occurred_at, actor}. Returned inside the order — there is no separate history endpoint.

Error codes

400
offer_id is required, at least one passenger is required, invalid json, or unknown airline.
502
The airline refused the booking — expired or shopped-not-priced offer ID, or a multi-segment itinerary whose segments OrderCreate cannot reference.
503
coming_soon — order writes are disabled in this environment.
POST /v1/orders — Create order | Norba API