Guides · API Reference

Public REST API

One REST/JSON contract for shopping, pricing, seats and extras, and the full order lifecycle, the same for every airline.

Base URL
api.norba.io
Version
v1
Protocol
HTTPS / JSON
Sandbox: real offers, test tickets
Sandbox keys reach each airline's test system and create test bookings. Paid orders get test tickets nobody can fly on and raise no fee. Cancel what you book.

The booking flow

StepCallWhat it gives you
1POST /v1/shopping/offersOffers with a search-time offer ID.
2GET /v1/offers/{offerID}/seats, /servicesSeat map and extras for the offer (optional).
3POST /v1/offers/priceThe bookable offer ID, valid until price_guaranteed_until. The search ID cannot be booked.
4POST /v1/ordersThe order and its PNR. Send seats and extras here; add payment to ticket at once.
5POST /v1/orders/{id}/commitPays a hold. No ticket exists until tickets[] is non-empty.
Sell seats and extras at booking
Not every airline sells them on an existing order; the airline’s page says which.

Integrated Airlines

Which airlines you can book, in which environment, and what each one supports: /docs/airlines

Conventions

ConventionDetails
AuthX-API-Key: <your-key> on every endpoint except GET /v1/health. Not a Bearer token.
Base URLhttps://api.norba.io/v1/…
EnvironmentsA key is either sandbox or production, and every request runs in its environment. Orders and webhooks never cross: an order from the other environment answers 404.
Errors{ "error": "<message>", "code": "<stable code>" }, plus Retry-After when waiting helps. Quote the X-Request-Id header to support.
IdempotencySend an Idempotency-Key (up to 128 characters) on every write, and the same key on every retry of it. Required on every write that can move money: payments, commit, change/confirm, cancellations and refunds (400 idempotency_key_required otherwise). A replay returns the first answer with Idempotent-Replayed: true.
IDsOffer and order IDs are opaque airline strings: never parse them, and percent-encode them in a URL path.
MoneyPlain numbers plus a currency field; Money objects are { "amount": 380.62, "currency": "EUR" }.
TimesRFC 3339 with the airport's own offset (departure_utc / arrival_utc included): print the clock part as it reads. Request dates are YYYY-MM-DD.
Unknown outcome502 booking_outcome_unknown (or a *_outcome_unknown servicing variant): the write reached the airline and its answer was lost. Do not resend it; read GET /v1/orders or wait for the webhook.
WebhooksSigned order events, pushed to the endpoints you register in Dashboard → Webhooks. See /docs/webhooks.
# Offer IDs may contain | — percent-encode it (%7C) in a URL path.
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 only: 200 { "status": "ok" }. Says nothing about airline connectivity.

Shopping

MethodPathDescription
GET/v1/network/airlinesThe airlines a search with your key reaches.
POST/v1/offers/priceConfirm the price and get the bookable offer ID. Required before booking; up to 10 offer_ids.
POST/v1/offers/quoteCheckout total for an offer plus the seats and extras you intend to buy. No airline call.
GET/v1/offers/{offerID}/seatsSeat map per segment (?segment_id=), with prices.
GET/v1/offers/{offerID}/servicesExtras for an offer. An empty list is a normal answer.
GET/v1/shopping/calendarCheapest fare per day on a route, from your own recent searches. Never calls an airline.
POST/v1/shopping/offersSearch every connected airline: one-way, round trip or multi-city (2–5 legs).
POST/v1/shopping/offers/streamThe same search as Server-Sent Events: search → offers (0..n) → done | error.

SearchRequest body

{
  "origin":         "BCN",          // IATA code
  "destination":    "JFK",
  "departure_date": "2027-06-15",   // up to 361 days ahead
  "return_date":    "2026-12-22",   // optional: round trip
  "adults":         1,              // >= 1; at most 9 travellers in all
  "children":       0,              // 2-11
  "infants":        0,              // under 2; never more than adults
  "cabin_class":    "economy",      // economy | premium_economy | business | first
  "currency":       "EUR",          // ISO 4217
  "max_stops":      0               // absent = any; 0 = non-stop only; up to 5
}

No result is 200 with offers empty or null: guard before reading .length.

Multi-city itineraries

{
  "adults": 1,
  "cabin_class": "economy",
  "legs": [
    { "origin": "MAD", "destination": "JFK", "departure_date": "2027-06-15" },
    { "origin": "JFK", "destination": "LAX", "departure_date": "2026-12-19" },
    { "origin": "LAX", "destination": "MAD", "departure_date": "2026-12-23" }
  ]
}

2–5 legs in date order. legs replaces origin, destination and the dates; one offer covers the whole trip.

Orders

MethodPathDescription
GET/v1/ordersYour orders, newest first. Filters: airline, pnr, status. Pagination: limit (1–100, default 20) and cursor (next_cursor).
POST/v1/ordersBook a priced offer. Without payment it is a hold until expires_at; with payment it is ticketed.
DELETE/v1/orders/{orderID}Cancel the order at the airline. 204, no body.
GET/v1/orders/{orderID}The order. ?refresh=true reads it from the airline instead of Norba's copy.
PUT/v1/orders/{orderID}Low-level change. action: ancillaries | reissue | seats | ticketing. Prefer the servicing endpoints below.
POST/v1/orders/{orderID}/commitPay a hold and issue tickets. There is no /pay.
POST/v1/orders/{orderID}/quoteReprice the order: original fare, new fare, penalty, difference.
POST/v1/orders/{orderID}/reshopAlternatives for a change (new_departure_date, new_return_date, new_origin, new_destination) or a cancellation (partial_cancel, segment_ids). total_amount is what the change collects.

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

  "payment": {
    "currency": "EUR",
    "amount":   380.62,
    "card": {
      "number": "4111111111111111", "holder": "MARIA GARCIA",
      "expiry_month": 12, "expiry_year": 2030, "brand": "VISA"
    }
  }
}

Omit payment to create an unpaid hold. Payments are by card only; a payment without card is refused with 400 payment_method_not_allowed.

Servicing

MethodPathDescription
POST/v1/orders/{orderID}/cancelQuote a cancellation: refund and penalty. Changes nothing. segment_ids quotes a partial one.
POST/v1/orders/{orderID}/cancel/confirmCancel with cancellation_quote_id (plus segment_ids for a partial) and get the updated order back.
POST/v1/orders/{orderID}/changeQuote a voluntary change.
POST/v1/orders/{orderID}/change/confirmAccept a reshop offer (offer_id). If it collects money, send payment for its total_amount in the same call.
GET/v1/orders/{orderID}/historyThe order's timeline, oldest first.
POST/v1/orders/{orderID}/refundRefund a paid order, cancelling it if still open. 202 with the refund and penalty amounts; the airline returns the money.
GET/v1/orders/{orderID}/refund-eligibilityWhether the order can be refunded, and for how much.
GET/v1/orders/{orderID}/seatsSeat map of a booked flight (?segment_id=).
POST/v1/orders/{orderID}/seatsAssign seats from that map, per passenger.
GET/v1/orders/{orderID}/servicesExtras the airline still sells on this order.
POST/v1/orders/{orderID}/servicesBuy extras from that list; a ticketed order pays in the same call.

The airline comes from the stored order; ?airline= is optional. Step by step in the servicing guide.

Connector health

MethodPathDescription
GET/v1/monitoring/airlinesConnection health per airline, for your key's environment.
GET/v1/monitoring/airlines/{iataCode}The same for one airline.

Error codes

HTTPClassMeaning
400ValidationA field failed validation; the message names it. Nothing reached an airline.
401AuthMissing, unknown or expired X-API-Key.
402 / 403AccountBilling or verification needed before this call; reason says which.
404Not foundorder_not_found, or an offer that no longer exists.
409Conflictidempotency_key_in_flight, idempotency_key_reused, illegal_transition (status forbids it), order_busy (another write on this order, retry after Retry-After).
422RefusedFinal, nothing booked or charged: airline_refused, flights_unavailable, hold_expired, offer_expired. Search or price again.
429LimitA volume ceiling agreed for your account. Retry-After says when.
502Airline errorThe airline failed a read; retry later. On a write, see Unknown outcome above.
503UnavailableNothing was sent (airline_unreachable): retry after Retry-After with the same Idempotency-Key.

Every code, and the rate limits: Errors & Rate Limits. There is no per-key request rate; every call is metered (pricing).

Full machine-readable contract

The OpenAPI 3.0 document is at /openapi.yaml. Generate a client from it or load it into your API tooling.

Flight Booking REST API Reference — All Endpoints | Norba