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
confirmedwith an emptytickets[]and a warning saying why. Readstatusandtickets[], not the HTTP code — that is the check production relies on too.
Shopping
Search offers
The core flight search — one request, every connected airline, normalized into one shape.
POST
/v1/shopping/offersRequires X-API-KeyThe primary entry point for flight shopping. Send origin, destination, dates and passenger counts, and Norba fans the request out to every registered NDC connector in parallel, then normalizes every answer into one canonical
- A
-
- A reconciling price breakdown:
-
-
-
-
- A one-way, non-stop search on a route AA actually flies returns real, bookable inventory (a BCN→JFK economy search returns ~115 AA offers).
- An identical repeat search made within a short window is served from cache and does not consume airline look-to-book quota. Empty result sets are never cached.
- **A route AA does not fly returns
- **Bad input is refused with
Offer.What you get back- A
search_id and expires_at for the result set-
offers[] — every bookable itinerary, each with a full slice/segment graph- A reconciling price breakdown:
base_fare + taxes_fees == total_amount on every offer-
fare_family branding (American Airlines returns e.g. *Basic Economy*, *Main Cabin*, *Main Plus*, *Premium Economy*)-
fare_rules parsed from the airline's marketing prose into structured flags-
baggage_included, cabin_class, quality_score-
expires_at per offer — the airline's own offer TTLLive behaviour with American Airlines- A one-way, non-stop search on a route AA actually flies returns real, bookable inventory (a BCN→JFK economy search returns ~115 AA offers).
- An identical repeat search made within a short window is served from cache and does not consume airline look-to-book quota. Empty result sets are never cached.
- **A route AA does not fly returns
200 with offers: null** — not [], and not a 400. No connected airline serves the route, so none is called. Guard for null before reading .length.- **Bad input is refused with
400 before any airline is called.** Same origin and destination answers origin and destination must differ; an unparseable airport code such as BARCELONA answers origin must be a 3-letter IATA code; a departure date in the past answers departure_date must not be in the past; more infants than adults answers infants must not outnumber adults. So a 502 all airline connections failed really does mean the carrier could not be reached — not that your request was wrong.Partner-operated connections. A connection where a partner airline operates a leg is withheld from results by default, because the carrier does not confirm it at booking; POST /v1/orders refuses such an offer up front rather than letting it fail at the airline. Non-stop codeshares, round trips, connections flown by the carrier itself and multi-city itineraries all book.Rate limit: 60 requests/minute per agency by default (adjustable per account) — a search-specific gate; there is no platform capacity limit on this or any other endpoint. Past it, 429 rate limit exceeded. Two more gates sit behind it, both rooted in what airlines contractually require rather than server capacity: a daily and a monthly search ceiling (429 daily_search_limit_exceeded / monthly_search_limit_exceeded), and a look-to-book policy that, once an account has enough volume to judge, warns, throttles or blocks searching against too few bookings (429 look_to_book_exceeded). There is deliberately no cap on how many searches one agency has in flight at once — a real fan-out takes 10-20 seconds, and one key is routinely used by more than one person at a time.Request example
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 }'Response example
Response example
{
"search_id": "srch_01K2M8ZC3AC5N8XWQ7R4YB",
"expires_at": "2026-08-14T10:14:52Z",
"offers": [
{
"offer_id": "AA-X2E80EC16-51FD-48AD-924B.1-1|X2E80EC16-51FD-48AD-924B.1-1-1",
"airline_offer_id": "X2E80EC16-51FD-48AD-924B.1-1",
"airline": "AA",
"source": "ndc_direct",
"source_version": "NDC-21.3",
"currency": "EUR",
"total_amount": 380.62,
"base_fare": 193,
"taxes_fees": 187.62,
"total_price": {
"amount": 380.62,
"currency": "EUR"
},
"expires_at": "2026-08-14T09:44:52Z",
"cabin_class": "economy",
"baggage_included": false,
"fare_family": {
"id": "AAAT-BASIC",
"name": "Basic Economy",
"brand_id": "AAAT-BASIC",
"benefits": [
{
"type": "checked_bag",
"included": false,
"description": "Pay more to check bags"
},
{
"type": "seat_selection",
"included": false,
"description": "Pay more for seat selection"
},
{
"type": "changes",
"included": false,
"description": "No changes allowed"
},
{
"type": "refund",
"included": false,
"description": "No refunds allowed"
}
]
},
"fare_rules": {
"refundable": false,
"changeable": false,
"raw_text": "No changes allowed / No refunds allowed"
},
"passengers": [
{
"type": "ADT",
"count": 1,
"base_fare": 193,
"taxes": 187.62,
"total": 380.62
}
],
"slices": [
{
"id": "OD1",
"origin": "BCN",
"destination": "JFK",
"departure_utc": "2026-10-20T08:30:00Z",
"arrival_utc": "2026-10-20T16:59:00Z",
"duration_minutes": 509,
"stops": 0,
"segments": [
{
"segment_id": "SGM010012275a20c",
"origin": "BCN",
"destination": "JFK",
"departure_utc": "2026-10-20T08:30:00Z",
"arrival_utc": "2026-10-20T16:59:00Z",
"duration_minutes": 509,
"flight_number": "AA67",
"marketing_carrier": "AA",
"operating_carrier": "AA",
"aircraft": "772",
"departure_terminal": "1",
"arrival_terminal": "8",
"cabin_class": "economy",
"booking_class": "B",
"fare_basis": "OKX5C5BW"
}
]
}
],
"quality_score": 0.95
}
],
"meta": {
"total": 1,
"airlines": [
"AA"
],
"sources": [
"ndc_direct"
],
"min_price": 380.62
}
}Request body
originstringRequiredIATA airport code for departure (3 letters). Required unless
legs is supplied.Example:
BCNdestinationstringRequiredIATA airport code for arrival. Required unless
legs is supplied.Example:
JFKdeparture_datestring (YYYY-MM-DD)RequiredOutbound date, ISO calendar date. A date in the past is refused by the handler with
400 departure_date must not be in the past.Example:
2026-10-20return_datestring (YYYY-MM-DD)Return date, which makes the search a round trip.
Example:
2026-10-27adultsintegerRequiredAdults (12+). Must be ≥ 1 or the request is rejected with
400.Example:
1childrenintegerChildren aged 2–11. Must be ≥ 0.
Example:
0infantsintegerLap infants under 2, no seat. Must be ≥ 0, and **must not exceed
adults** — the lap-infant rule is enforced here, with 400 infants must not outnumber adults.Example:
0cabin_classstringeconomy, premium_economy, business or first. The filter reaches the airline: a business search returns only business fares.Example:
economycurrencystringISO 4217 code. The airline may quote in its own filed currency and convert.
Example:
EURmax_stopsintegerAbsent means no constraint;
0 means non-stop only; 1+ allows that many connections. Absent and 0 are genuinely different — do not send 0 unless you mean non-stop only.Example:
0legsarray<object>Multi-city itinerary: 2–5 bounds of
{origin, destination, departure_date} flown in order. When present it replaces origin/destination/departure_date/return_date. Dates must be chronological and each leg's origin and destination must differ. Multi-city is inherently multi-segment — see the caveat above.Response fields
search_idstringRequiredULID-based id for this search (
srch_…). Recorded against your agency for look-to-book accounting.expires_atstring (RFC 3339)RequiredWhen the cached result set expires.
offers[].offer_idstringRequiredCanonical offer ID. For American Airlines this is
AA- plus the airline's composite token, e.g. AA-X2E80…|X2E80…-1-1. It contains | and . — percent-encode it when putting it in a URL path.offers[].airline_offer_idstringThe airline's own untouched offer id.
offers[].airlinestringRequiredIATA airline code, e.g.
AA.offers[].sourcestringRequiredndc_direct, gds_fallback or lcc_direct.offers[].source_versionstringNDC schema version behind the offer, e.g.
NDC-21.3.offers[].total_amountnumberRequiredTotal for all passengers. Always equals
base_fare + taxes_fees.offers[].base_farenumberRequiredTotal base fare before taxes and carrier fees.
offers[].taxes_feesnumberRequiredTotal taxes and carrier-imposed fees.
offers[].total_priceobjectThe same total as a
{amount, currency} Money object.offers[].expires_atstring (RFC 3339)RequiredAirline offer TTL. Past this point the offer cannot be priced or booked.
offers[].baggage_includedbooleanRequiredWhether a checked bag is in the fare. AA's Basic Economy returns
false; Main Cabin and above return true.offers[].cabin_classstringRequiredDominant cabin for the itinerary.
offers[].fare_familyobjectBranded fare:
id, name, brand_id, description and benefits[]. Each benefit carries type, included and a description — a paid item such as "Pay more to check bags" is reported as included: false.offers[].fare_rulesobjectStructured flexibility:
refundable, changeable, penalty amounts and types, plus the airline's raw_text. Nullable booleans are null when the airline did not say.offers[].passengers[]arrayPer-passenger-type breakdown of
type, count, base_fare, taxes, total. Reconciles to the offer total.offers[].slices[]arrayRequiredDirectional bounds. One-way = 1 slice. Each carries
origin, destination, departure_utc, arrival_utc, duration_minutes, stops and segments[].offers[].slices[].segments[]arrayRequiredIndividual flights, each with
segment_id, flight_number, marketing_carrier, operating_carrier, aircraft, terminals, booking_class and fare_basis.offers[].quality_scorenumber0.0–1.0 completeness score for the offer's data. Gaps are flagged, never silently dropped.
metaobjecttotal, airlines[], sources[] and min_price, summarised from the offers[] actually returned. When the route filter drops every connector, total is 0 and airlines is [].Error codes
400origin must be a 3-letter IATA code, origin and destination must differ, adults must be >= 1, infants must not outnumber adults, invalid departure_date, departure_date must not be in the past, legs supports at most 5 bounds, leg dates must be in chronological order.429More than the per-minute search rate (
Retry-After: 60), a daily/monthly search ceiling, or the account's look-to-book policy engaging — see the rate limit note above. All carry a Retry-After.502all airline connections failed — please retry later. The request body is validated before any airline is called, so this is a carrier or connectivity failure, not a bad request.