Public REST API
One REST/JSON contract for shopping, pricing, seats and extras, and the full order lifecycle, the same for every airline.
The booking flow
| Step | Call | What it gives you |
|---|---|---|
| 1 | POST /v1/shopping/offers | Offers with a search-time offer ID. |
| 2 | GET /v1/offers/{offerID}/seats, /services | Seat map and extras for the offer (optional). |
| 3 | POST /v1/offers/price | The bookable offer ID, valid until price_guaranteed_until. The search ID cannot be booked. |
| 4 | POST /v1/orders | The order and its PNR. Send seats and extras here; add payment to ticket at once. |
| 5 | POST /v1/orders/{id}/commit | Pays a hold. No ticket exists until tickets[] is non-empty. |
Integrated Airlines
Which airlines you can book, in which environment, and what each one supports: /docs/airlines
Conventions
| Convention | Details |
|---|---|
| Auth | X-API-Key: <your-key> on every endpoint except GET /v1/health. Not a Bearer token. |
| Base URL | https://api.norba.io/v1/… |
| Environments | A 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. |
| Idempotency | Send 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. |
| IDs | Offer and order IDs are opaque airline strings: never parse them, and percent-encode them in a URL path. |
| Money | Plain numbers plus a currency field; Money objects are { "amount": 380.62, "currency": "EUR" }. |
| Times | RFC 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 outcome | 502 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. |
| Webhooks | Signed 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)
| Method | Path | Description |
|---|---|---|
| GET | /v1/health | Liveness only: 200 { "status": "ok" }. Says nothing about airline connectivity. |
Shopping
| Method | Path | Description |
|---|---|---|
| GET | /v1/network/airlines | The airlines a search with your key reaches. |
| POST | /v1/offers/price | Confirm the price and get the bookable offer ID. Required before booking; up to 10 offer_ids. |
| POST | /v1/offers/quote | Checkout total for an offer plus the seats and extras you intend to buy. No airline call. |
| GET | /v1/offers/{offerID}/seats | Seat map per segment (?segment_id=), with prices. |
| GET | /v1/offers/{offerID}/services | Extras for an offer. An empty list is a normal answer. |
| GET | /v1/shopping/calendar | Cheapest fare per day on a route, from your own recent searches. Never calls an airline. |
| POST | /v1/shopping/offers | Search every connected airline: one-way, round trip or multi-city (2–5 legs). |
| POST | /v1/shopping/offers/stream | The 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
| Method | Path | Description |
|---|---|---|
| GET | /v1/orders | Your orders, newest first. Filters: airline, pnr, status. Pagination: limit (1–100, default 20) and cursor (next_cursor). |
| POST | /v1/orders | Book 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}/commit | Pay a hold and issue tickets. There is no /pay. |
| POST | /v1/orders/{orderID}/quote | Reprice the order: original fare, new fare, penalty, difference. |
| POST | /v1/orders/{orderID}/reshop | Alternatives 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
| Method | Path | Description |
|---|---|---|
| POST | /v1/orders/{orderID}/cancel | Quote a cancellation: refund and penalty. Changes nothing. segment_ids quotes a partial one. |
| POST | /v1/orders/{orderID}/cancel/confirm | Cancel with cancellation_quote_id (plus segment_ids for a partial) and get the updated order back. |
| POST | /v1/orders/{orderID}/change | Quote a voluntary change. |
| POST | /v1/orders/{orderID}/change/confirm | Accept a reshop offer (offer_id). If it collects money, send payment for its total_amount in the same call. |
| GET | /v1/orders/{orderID}/history | The order's timeline, oldest first. |
| POST | /v1/orders/{orderID}/refund | Refund 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-eligibility | Whether the order can be refunded, and for how much. |
| GET | /v1/orders/{orderID}/seats | Seat map of a booked flight (?segment_id=). |
| POST | /v1/orders/{orderID}/seats | Assign seats from that map, per passenger. |
| GET | /v1/orders/{orderID}/services | Extras the airline still sells on this order. |
| POST | /v1/orders/{orderID}/services | Buy 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
| Method | Path | Description |
|---|---|---|
| GET | /v1/monitoring/airlines | Connection health per airline, for your key's environment. |
| GET | /v1/monitoring/airlines/{iataCode} | The same for one airline. |
Error codes
| HTTP | Class | Meaning |
|---|---|---|
| 400 | Validation | A field failed validation; the message names it. Nothing reached an airline. |
| 401 | Auth | Missing, unknown or expired X-API-Key. |
| 402 / 403 | Account | Billing or verification needed before this call; reason says which. |
| 404 | Not found | order_not_found, or an offer that no longer exists. |
| 409 | Conflict | idempotency_key_in_flight, idempotency_key_reused, illegal_transition (status forbids it), order_busy (another write on this order, retry after Retry-After). |
| 422 | Refused | Final, nothing booked or charged: airline_refused, flights_unavailable, hold_expired, offer_expired. Search or price again. |
| 429 | Limit | A volume ceiling agreed for your account. Retry-After says when. |
| 502 | Airline error | The airline failed a read; retry later. On a write, see Unknown outcome above. |
| 503 | Unavailable | Nothing 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.