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.
Shopping

Search offers

The core flight search — one request, every connected airline, normalized into one shape.

POST/v1/shopping/offersRequires X-API-Key
The 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 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 TTL
Live 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
  }
}

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

originstringRequired
IATA airport code for departure (3 letters). Required unless legs is supplied.
Example: BCN
destinationstringRequired
IATA airport code for arrival. Required unless legs is supplied.
Example: JFK
departure_datestring (YYYY-MM-DD)Required
Outbound 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-20
return_datestring (YYYY-MM-DD)
Return date, which makes the search a round trip.
Example: 2026-10-27
adultsintegerRequired
Adults (12+). Must be ≥ 1 or the request is rejected with 400.
Example: 1
childreninteger
Children aged 2–11. Must be ≥ 0.
Example: 0
infantsinteger
Lap 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: 0
cabin_classstring
economy, premium_economy, business or first. The filter reaches the airline: a business search returns only business fares.
Example: economy
currencystring
ISO 4217 code. The airline may quote in its own filed currency and convert.
Example: EUR
max_stopsinteger
Absent 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: 0
legsarray<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_idstringRequired
ULID-based id for this search (srch_…). Recorded against your agency for look-to-book accounting.
expires_atstring (RFC 3339)Required
When the cached result set expires.
offers[].offer_idstringRequired
Canonical 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_idstring
The airline's own untouched offer id.
offers[].airlinestringRequired
IATA airline code, e.g. AA.
offers[].sourcestringRequired
ndc_direct, gds_fallback or lcc_direct.
offers[].source_versionstring
NDC schema version behind the offer, e.g. NDC-21.3.
offers[].total_amountnumberRequired
Total for all passengers. Always equals base_fare + taxes_fees.
offers[].base_farenumberRequired
Total base fare before taxes and carrier fees.
offers[].taxes_feesnumberRequired
Total taxes and carrier-imposed fees.
offers[].total_priceobject
The same total as a {amount, currency} Money object.
offers[].expires_atstring (RFC 3339)Required
Airline offer TTL. Past this point the offer cannot be priced or booked.
offers[].baggage_includedbooleanRequired
Whether a checked bag is in the fare. AA's Basic Economy returns false; Main Cabin and above return true.
offers[].cabin_classstringRequired
Dominant cabin for the itinerary.
offers[].fare_familyobject
Branded 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_rulesobject
Structured 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[]array
Per-passenger-type breakdown of type, count, base_fare, taxes, total. Reconciles to the offer total.
offers[].slices[]arrayRequired
Directional bounds. One-way = 1 slice. Each carries origin, destination, departure_utc, arrival_utc, duration_minutes, stops and segments[].
offers[].slices[].segments[]arrayRequired
Individual flights, each with segment_id, flight_number, marketing_carrier, operating_carrier, aircraft, terminals, booking_class and fare_basis.
offers[].quality_scorenumber
0.0–1.0 completeness score for the offer's data. Gaps are flagged, never silently dropped.
metaobject
total, 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

400
origin 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.
429
More 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.
502
all 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.
POST /v1/shopping/offers — Search offers | Norba API