Guides · API Reference

Public REST API

One REST/JSON contract over airline NDC. Shopping, offer pricing, seats and services, the full order lifecycle and connector health — every airline normalized into one canonical shape. American Airlines is available in the sandbox today; production availability is announced per airline in the changelog.

Base URL
api.norba.io
Version
v1
Protocol
HTTPS / JSON
The sandbox: real offers, real PNRs, no tickets
Every endpoint on this page works against real carrier inventory in the sandbox. A search returns real offers and you can create a real booking with a carrier PNR. Sandbox orders are never ticketed and raise no booking or servicing fee, so exercise the whole flow freely — and cancel what you book, because the test inventory is shared.

Start here — a search that returns real offers

Two rules decide whether your first calls book or bounce.

  1. Search the way you sell. One-way, round trip, connections and multi-city all book. The one thing withheld from results by default is a connection where a partner airline operates a leg, because the carrier does not confirm it at booking — see the airline’s page. Mind max_stops: absent means “no constraint”, 0 means “non-stop only”.
  2. Price the offer before you book it. The offer ID from search is transient and the airline will not book it. POST /v1/offers/price mints a new ID, and only that one is accepted by POST /v1/orders.
export NORBA_KEY="nbr_live_…"

curl --request POST \
  --url https://api.norba.io/v1/shopping/offers \
  --header "X-API-Key: $NORBA_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "origin": "BCN",
    "destination": "JFK",
    "departure_date": "2026-10-20",
    "adults": 1,
    "cabin_class": "economy",
    "currency": "EUR",
    "max_stops": 0
  }'

A search like this returns real offers, every one with a resolved segment graph, fare family and parsed fare rules.

The booking flow, end to end

StepCallWhat it gives you
1POST /v1/shopping/offersOffers with transient AA-X… IDs.
2GET /v1/offers/{offerID}/servicesAncillary catalogue. An empty list is a normal answer.
3GET /v1/offers/{offerID}/seatsSeat map per segment, with prices and features.
4POST /v1/offers/priceThe bookable AA-P… ID, guaranteed ~20 min.
5POST /v1/ordersThe reservation, with the carrier's PNR. Seats and ancillaries go in this call — and payment, if you want to book and ticket in one step.
6POST /v1/orders/{id}/commitPayment and ticketing for an order created as a hold. Read tickets[] on the response.
Sell seats and ancillaries inside step 5, not after it
Their offer references belong to the shopping context of the offer you are booking, and that is the only place the airline still recognises them. Adding them once the order exists means re-pricing against passenger and segment IDs the carrier reassigns at booking time — the T1 you sent comes back as something like PAX96101. Paid-seat assignment on an existing order is not available on every carrier; the airline’s own page says whether it is.

Integrated Airlines

American Airlines supports all eleven NDC operations through the API: AirShopping, OfferPrice, ServiceList, SeatAvailability, OrderCreate, OrderRetrieve, OrderChange, OrderCancel, OrderReShop, OrderQuote and OrderCommit. Where a carrier does not offer an operation, the API answers with a clear error rather than fabricated data.

AirlineIATANDCBookable coverage
American AirlinesAA24.1Global — one-way, round trip and multi-city bookable

New carriers are announced in the changelog as they become available.

Conventions

ConventionDetails
Base URLhttps://api.norba.io — every path below is prefixed with /v1/.
AuthX-API-Key: <your-key> on every single endpoint except GET /v1/health and GET /v1/health/ready. Not a Bearer token.
TimesRFC 3339 (2026-10-20T08:30:00Z). Calendar dates in requests are plain YYYY-MM-DD.
MoneyTotals are plain numbers plus a currency field; Money objects are { "amount": 380.62, "currency": "EUR" }.
IDssearch_id is a ULID (srch_…). Offer and order IDs are airline-owned opaque strings — never parse them.
ErrorsNDC endpoints return { "error": "<human sentence>" } — one field, no code, no request_id. Aviation Data returns { data, meta, errors[] }.
CaseRequest bodies are snake_case throughout. The payment / change / reshop / commit blocks accepted CamelCase in early versions; single-word CamelCase keys still decode.
If you integrated before August 2026, four bodies changed
payment (on order create), and the whole bodies of PUT /v1/orders/{id}, POST /v1/orders/{id}/commit and POST /v1/orders/{id}/reshop, used to be CamelCase and silently dropped any snake_case key you sent. They are now snake_case like the rest of the API.

Single-word keys are unaffected in both directions, so Action and Amount still decode. What changed is the multi-word ones: SegmentIDs, NewDepartureDate and ExpiryMonth no longer match — send segment_ids, new_departure_date and expiry_month. That break is deliberate: it turns a field that used to disappear without a word into one that visibly does not exist.
# AA offer IDs contain | and . — percent-encode them in a URL path.
#   AA-P2E80EC16-51FD-48AD-924B-1|P2E80EC16-51FD-48AD-924B-1-1
# becomes
#   AA-P2E80EC16-51FD-48AD-924B-1%7CP2E80EC16-51FD-48AD-924B-1-1

curl --request GET \
  --url "https://api.norba.io/v1/offers/AA-P2E80EC16-51FD-48AD-924B-1%7CP2E80EC16-51FD-48AD-924B-1-1/seats" \
  --header "X-API-Key: $NORBA_KEY"

Public endpoints (no auth)

MethodPathDescription
GET/v1/healthLiveness probe. Returns 200 { "status": "ok" }. One of only two endpoints that do not need an API key (the other is the readiness probe below) — and it deliberately checks nothing, so a 200 does not imply airline connectivity.
GET/v1/health/readyReadiness probe. Answers 503 when the service cannot actually serve traffic. This is what a deploy gate should look at.

Shopping

MethodPathDescription
POST/v1/shopping/offersSearch flights across every connected airline. One-way, round trip and 2–5 bound multi-city; passenger mix, cabin, currency and max_stops. Rate limited to 60/min per agency by default — see Rate limits and quota below.
POST/v1/shopping/offers/streamThe same search as Server-Sent Events, so you can render offers as each airline answers. Events: search → offers (0..n) → done | error.
POST/v1/offers/priceConfirm the price and mint the bookable offer ID. Mandatory before booking — the shopped ID is rejected by the airline.
POST/v1/offers/quoteThe whole checkout total — fare plus any seats and ancillaries you intend to buy, priced by the same engine that will charge the order. Cheap to call on every basket change: no airline call, no reservation.
GET/v1/offers/{offerID}/servicesAncillary catalogue for an offer — or for an existing order, by passing the order ID. An empty list is a normal, successful answer: some fares carry no à-la-carte services.
GET/v1/offers/{offerID}/seatsSeat map with rows, availability, price bands and decoded features, per segment. Accepts an order ID too.

SearchRequest body

{
  "origin":         "BCN",          // IATA code; required unless "legs" is used
  "destination":    "JFK",
  "departure_date": "2026-10-20",   // YYYY-MM-DD; a past date is refused with 400
  "return_date":    "2026-10-27",   // optional — makes it a round trip (2 segments)
  "adults":         1,              // must be >= 1
  "children":       0,              // ages 2-11
  "infants":        0,              // under 2, no seat; more than "adults" is a 400
  "cabin_class":    "economy",      // economy | premium_economy | business | first
  "currency":       "EUR",          // ISO 4217
  "max_stops":      0               // absent = no constraint; 0 = non-stop only
}
Absent max_stops and max_stops: 0 are different
Absent means “no constraint”; 0 means “non-stop only”. Sending 0 by accident makes a route the airline serves with one connection look like a route it does not fly at all.

Multi-city itineraries

{
  "adults": 1,
  "cabin_class": "economy",
  "legs": [
    { "origin": "MAD", "destination": "JFK", "departure_date": "2026-10-20" },
    { "origin": "JFK", "destination": "LAX", "departure_date": "2026-10-24" },
    { "origin": "LAX", "destination": "MAD", "departure_date": "2026-10-28" }
  ]
}

2–5 bounds, flown in order, chronological dates, each leg’s origin and destination must differ. legs replaces origin/destination/departure_date/return_date, and one offer covers the whole itinerary.

Orders

MethodPathDescription
POST/v1/ordersCreate an order from a PRICED offer plus passengers and contact — with seats and ancillaries in the same call. Returns the order with the carrier's PNR, created as an unpaid hold whose expires_at is the airline's payment time limit.
GET/v1/ordersList orders stored for your agency, newest first. Filters: status, airline, pnr. Keyset pagination — limit (1-100, default 20) and cursor, not page numbers. Needs a key with an agency behind it.
GET/v1/orders/{orderID}Full order state — passengers with airline-assigned IDs, itinerary, seats, ancillaries, tickets and payments. Answered from Norba's copy; add ?refresh=true to ask the airline and store what it returns. An id your agency does not hold is 404.
PUT/v1/orders/{orderID}Raw OrderChange. action: ticketing | ancillaries | reissue | cancel_segments | complete_payment. The airline is inferred from your stored order; ?airline= is optional and only checked for a mismatch.
DELETE/v1/orders/{orderID}Cancel at the airline and verify it actually closed. Returns 204 with no body. Airline inferred from the stored order, same as the other servicing calls below.
POST/v1/orders/{orderID}/quoteReprice the order: original fare, new fare, penalty, difference. An unchanged order returns an even quote.
POST/v1/orders/{orderID}/commitSettle payment on a hold and finalise an upsell. This is the payment endpoint — there is no /pay. Read tickets[] on the response: until it is non-empty, no ticket exists.
POST/v1/orders/{orderID}/reshopAlternatives for a voluntary change (new dates) or a cancellation quote (empty body or partial_cancel). Each reshop offer's total_amount is the difference against the order — positive to pay, negative owed back.

Order create request

{
  "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" },

  "payment": { "method": "CARD", "currency": "EUR", "amount": 380.62 }
}

Note first_name/last_name (not given_name/family_name), issuing_country for the document, and thepayment block, which is optional — omit it entirely to create an unpaid hold, which is the normal agency flow.

Servicing

The structured post-booking surface is a quote → confirm pair for changes and cancellations, plus refund endpoints. There are no /orders/{id}/seats, /ancillaries or /ssrs endpoints — seats and ancillaries are sold inside POST /v1/orders, and travel documents go in the passenger objects there.

MethodPathDescription
POST/v1/orders/{orderID}/changeQuote a voluntary change. Returns a ChangeQuote. The body is accepted but not acted on — the whole order is repriced.
POST/v1/orders/{orderID}/change/confirmAccept a reshop offer by offer_id and reissue. When the change carries an add-collect, use PUT /v1/orders/{orderID} with action reissue and a payment block instead, so the difference is paid in the same call.
POST/v1/orders/{orderID}/cancelQuote a cancellation — refund and penalty amounts, valid 30 minutes. Changes nothing despite being a POST. Derived from an order reprice, not an airline refund quote.
POST/v1/orders/{orderID}/cancel/confirmCancel and return the updated order with status: cancelled. Use this over DELETE when you want the order object back.
GET/v1/orders/{orderID}/refund-eligibilityWhether a refund is possible and for how much. A GET, so it is not blocked by the booking feature gate. Derived, and the deadline is a flat 24 hours.
POST/v1/orders/{orderID}/refundCancel the order and record it as refunded. Returns 202. No money is moved and no amount is quoted — reconcile settlement separately.
The airline is inferred from your stored order, not from ?airline=
Every servicing endpoint above resolves the carrier from the order row itself, scoped to your agency. ?airline= is still accepted for backward compatibility, but it is now optional and only checked for agreement — a value that names a different carrier than the one holding the booking is refused with 400 airline does not match the order, since that is either a client bug or an attempt to point a servicing call at an airline that never issued it.

Connector health

MethodPathDescription
GET/v1/monitoring/airlinesPer-connector health board, refreshed on a schedule rather than probed per request. The check never reaches an airline, so polling it does not touch your look-to-book ratio — but each call is a metered data request like any other.
GET/v1/monitoring/airlines/{iataCode}The same health record for one carrier.

Worth wiring up early: flight search answers 200 with an empty offers array both when there is genuinely no availability and when the route filter dropped the carrier, so search alone cannot tell you a connector is unreachable.

Error codes

NDC endpoints return, at minimum, a single-field body — { "error": "<human sentence>" } — carrying the airline’s own message on a 502, with no request_id in the body; correlate with the X-Request-Id header instead. A few responses add a stable field on top of it: an unrecognised order id carries code: "order_not_found" (and did_you_mean when one of your own orders is a close typo), and the quota/feature-gate responses covered below carry reason. Aviation Data endpoints are different again: they use the envelope { data, meta, errors[] } with errors[{ code, message }] inside it.

HTTPClassMeaning
400ValidationYour request body failed handler validation, before any airline is called. The message says which field: adults must be >= 1, origin must be a 3-letter IATA code, origin and destination must differ, departure_date must not be in the past, infants must not outnumber adults.
401Authmissing X-API-Key or invalid api key. Also returned by GET /v1/orders when the key has no agency behind it.
402QuotaA standing check failed — reason is payment_required, payment_method_required, delinquent or past_due. Carries reason, message, plan_code and the billing period. See Billing & Payments.
403Email not verifiedreason: email_verification_required — the account has not verified its email address, so it gets no free allowance. Open the verification link, or add a card. This is the 403 a new integrator hits first. Same structured body as 402.
403Unknown planreason: unknown_plan — the account's plan code matches nothing in the pricing catalogue. Same structured body as 402.
404Not foundcode: order_not_found — no order with that id in your agency's store, whatever ?airline= you send. Carries did_you_mean when one of your own orders is a one-character typo away. Or offer no longer available at airline.
429Rate limitMore than 60 searches in the last minute (Retry-After: 60), or reason allowance_burst — more calls in two minutes than the free requests you have left (Retry-After: 120). Nothing is owed; retry.
502Airline errorThe carrier refused the request; the message is the airline's own text, e.g. [230000002] Invalid or Expired Offer. Also covers all airline connections failed.
503Feature gatecoming_soon — order writes are disabled in this environment. Carries Retry-After: 3600. Reads keep working.
Search validates before the airline is called
A same-city search, an airport code that is not three letters such as BARCELONA, a past departure date and more infants than adults are all refused by the handler with 400 and a message naming the field — origin and destination must differ, origin must be a 3-letter IATA code, departure_date must not be in the past, infants must not outnumber adults. Nothing reaches a carrier first, so 502 all airline connections failed — please retry later is a genuine carrier or connectivity failure and should be treated as one. A route the carrier simply does not fly is neither: the route filter drops the connector and the search answers 200 with offers: null — note null, not an empty array, so guard before reading .length. Validate airport codes yourself against the airports endpoint.

Rate limits and quota

EndpointLimitScope
POST /v1/shopping/offers, /stream60 / min (default)Per agency, sliding window — adjustable per account
POST /v1/shopping/offers, /streamDaily + monthly ceiling, look-to-book policyContractual limits some airlines require, not a capacity limit — most agencies have none set
POST /v1/ordersMonthly order ceilingContractual, when agreed for the account — most agencies have none
/v1/aviation/*, /v1/reference/*No rate limitThey never reach an airline — but every call is still a metered data request: it counts against the allowance, and €0.0025 past it
Everything elseno explicit limitMetered against your plan quota

There is no platform capacity limit. What is enforced on shopping and booking exists because airlines police the look-to-book ratio — searches per booking — and throttle or fine agencies that drift: the per-minute search rate, the daily/monthly search and order ceilings, and the look-to-book policy (an X-Norba-L2B warning header before any throttle) are the same idea over different windows. An identical search repeated within a short window is served from cache and never reaches the airline, so caching works in your favour here. On 429 the response carries a Retry-After header.

What consumes the quota, what a 402 means, how the free allowance and any card on file interact with booking, and how usage settles into an invoice each month are covered in full on Billing & Payments. The session-authenticated routes that manage a card and read invoices — /v1/dev/billing/* and the public GET /v1/dev/pricing rate card — are covered there too; they take a developer-portal session cookie, not an X-API-Key, so they are not part of this page’s contract.

Full machine-readable contract

The complete OpenAPI 3.0 document is published at /openapi.yaml and covers every schema referenced here, including the Aviation Data envelope. Generate a client from it, or load it into your API tooling.

Flight Booking REST API Reference — All Endpoints | Norba