Introduction

The Distribution API is the partner-facing channel for pool-based flight distribution. It lets approved partners search flights, re-price offers, hold seats, and create orders on behalf of their customers.

The API is a two-phase flow:

  1. Mint a session — the partner signs a request with their pre-shared secret (HMAC-SHA256) and exchanges it for a short-lived, scope-limited Bearer token (JWT).

  2. Call endpoints — every other endpoint is called with Authorization: Bearer <token>. The token carries the partner identity, currency, locale, and the granted scopes.

Important: Each airline has their own specific domain. The API endpoints are hosted on airline-specific domains.

Important: All API endpoints are prefixed with /api due to the servlet context path configuration.

To get your partner credentials and airline’s API domain, contact your technical integration manager. Onboarding and operational procedures (secret generation, rotation, kill switches) are covered separately in the partner onboarding guide.

Authentication

Phase 1 — Minting a session

POST /distribution/v1/sessions is the only unauthenticated endpoint. It is protected by an HMAC signature instead of a Bearer token. The partner sends the following headers:

Header Description

X-Partner-Id

The partner’s UUID, as issued during onboarding.

X-Timestamp

Unix epoch seconds at the time of the request. Requests outside the allowed clock-skew window are rejected.

X-Nonce

A unique value per request (UUIDv4 recommended). Replayed nonces are rejected.

X-Signature

Hex-encoded HMAC-SHA256 of the canonical message (see below), keyed with the partner’s pre-shared secret.

The canonical message that is signed is the newline-joined tuple:

<partner_id>\n<timestamp>\n<nonce>\n<sha256_hex(request_body_bytes)>

Example using openssl:

BODY='{"locale":"en_US","currency":"USD","scopes":["search:read","fare:read"]}'
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr 'A-Z' 'a-z')
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
SIGNATURE=$(printf '%s\n%s\n%s\n%s' "$PARTNER_ID" "$TIMESTAMP" "$NONCE" "$BODY_HASH" \
    | openssl dgst -sha256 -hmac "$PARTNER_SECRET" -hex | awk '{print $2}')

Phase 2 — Calling endpoints

All other endpoints require the minted token:

Authorization: Bearer <access_token>

Scopes

The token is granted a subset of scopes — the intersection of what the partner requests and what the partner is entitled to. Each endpoint requires a specific scope; a token missing the required scope receives 403 SCOPE_INSUFFICIENT.

Scope Grants access to

search:read

Flight search

fare:read

Offer re-pricing

order:write

Seat holds and order creation

order:read

Reading order details

ancillary:read

Listing available and selected ancillaries for an order

ancillary:write

Adding ancillaries to an order

assistance:read

Listing available and selected special assistance (SSR) for an order

assistance:write

Adding special assistance (SSR) to an order

wallet:read

Reading the partner wallet balance and transaction statement

Airline scoping

Requests may include an optional X-Airline-ID header. When present, it must match the partner’s configured airline; a mismatch is rejected with 403 AUTHZ_FAILED. When absent, the airline is resolved from the partner record.

Error Format

All errors share a consistent JSON envelope:

{
  "code": "SCOPE_INSUFFICIENT",
  "message": "session token does not carry the scope required by this endpoint"
}

Common codes:

HTTP Code Meaning

400

INVALID_REQUEST

Request body or parameters failed validation.

400

INVALID_PAX_COMPOSITION

Passenger composition violates inventory rules.

401

TOKEN_MISSING

Bearer token absent.

401

TOKEN_EXPIRED / TOKEN_REVOKED

Token no longer valid.

403

SCOPE_INSUFFICIENT

Token lacks the scope required by the endpoint.

403

AUTHZ_FAILED

X-Airline-ID did not match the partner’s airline.

404

ORDER_NOT_FOUND

Order does not exist or is not owned by the partner.

404

HOLD_NOT_FOUND

Hold does not exist, expired, or is not owned by the partner.

409

OFFER_SOLD_OUT / SUBQUOTA_EXCEEDED

Capacity is unavailable for the requested composition.

409

BOOKING_AMBIGUOUS

The order has multiple active bookings (e.g. after a partial-split exchange); resolve per pax-segment.

409

BOOKING_NOT_ACTIVATED

Special assistance was requested before the booking was activated (i.e. before the order was paid).

409

ORDER_NOT_REFUNDABLE

The order cannot be refunded. From the refund quote: the order has no PAID invoice to refund from. From POST …​/refund: the same plus a state conflict — the order is not in a refundable state, the voluntary-refund time limit before departure has elapsed, the requested pax-segment scope is non-refundable, or the segments were already refunded (a safe retry after a prior refund is rejected here rather than double-crediting).

410

OFFER_NOT_AVAILABLE / HOLD_EXPIRED

Offer or hold is no longer available.

429

RATE_LIMIT_EXCEEDED

Per-partner rate limit exceeded.

500

INTERNAL_ERROR

Unexpected internal error. Every /distribution/v1 endpoint returns this typed envelope instead of the platform’s numeric-code body.

503

SERVICE_UNAVAILABLE

A backing service was unavailable: partner-order ownership could not be persisted on POST /orders / POST …​/ancillaries (safe to retry, with the same Idempotency-Key if one was supplied), or the pricing/fare-detail service failed during POST /offer (safe to retry).

Note
When an Idempotency-Key is supplied on Create Order, Add Ancillaries, or Add Assistance, the idempotency layer surfaces failures in a slightly different envelope where code is a numeric domain code rather than a string: a blank/too-long key returns 400, and a replay while the original request is still in flight returns 409. This does NOT apply to Refund Order: its Idempotency-Key is required but informational — the refund path does not use the idempotency layer, so it never returns the numeric envelope or a replay-in-flight 409.
Note
A non-UUID {orderId} path parameter returns 400 INVALID_REQUEST on every /distribution/v1/orders/{orderId}/…​ endpoint.

Rate Limiting

Each partner has a configured requests-per-second limit. Exceeding it returns 429 RATE_LIMIT_EXCEEDED.

Endpoints Summary

Method Endpoint Scope Description

POST

/distribution/v1/sessions

— (HMAC)

Mint a session token

GET

/distribution/v1/ping

any valid token

Bearer-authenticated smoke test

POST

/distribution/v1/search

search:read

Pool-based flight search (one-way or round-trip)

GET

/distribution/v1/calendar

search:read

Dates with available flights for an O/D + range, each with the minimum price

GET

/distribution/v1/routes

search:read

Available O/D destinations on the channel

GET

/distribution/v1/flights/{flightId}/ancillaries

search:read

Pre-booking catalog of purchasable ancillaries for a flight

GET

/distribution/v1/flights/{flightId}/seatmap

search:read

Pre-booking seat-selection price tiers for a flight

POST

/distribution/v1/offer

fare:read

Batch re-price pool offers

POST

/distribution/v1/offers/pricing

fare:read

Per-pax ADT/CHD/INF price breakdown for a pool offer

POST

/distribution/v1/orders/holds

order:write

Hold seats on a pool offer

POST

/distribution/v1/orders

order:write

Create an order from a hold

GET

/distribution/v1/orders/{orderId}

order:read

Get order details

GET

/distribution/v1/orders/{orderId}/ticket

order:read

Get the order e-ticket (PNR + ticket numbers)

GET

/distribution/v1/orders/{orderId}/ticket/pdf

order:read

Download the e-ticket / itinerary receipt PDF

GET

/distribution/v1/orders/{orderId}/refund-quote

order:read

Get a read-only refund quote for the full order

POST

/distribution/v1/orders/{orderId}/refund

order:write

Refund a partner-owned order (full or partial)

POST

/distribution/v1/orders/{orderId}/payments/confirm

order:write

Confirm payment and settle from the partner wallet

GET

/distribution/v1/orders/{orderId}/ancillaries

ancillary:read

List available and selected ancillaries

POST

/distribution/v1/orders/{orderId}/ancillaries

ancillary:write

Add ancillaries to an order

GET

/distribution/v1/orders/{orderId}/assistance

assistance:read

List available and selected special assistance

POST

/distribution/v1/orders/{orderId}/assistance

assistance:write

Add special assistance to an order

GET

/distribution/v1/wallet/balance

wallet:read

Partner wallet balance (credit limit, balance, available)

GET

/distribution/v1/wallet/transactions

wallet:read

Partner wallet statement (paginated)

API Endpoints

Each request block below lists the headers that endpoint requires. Every endpoint except Mint Session requires the Authorization: Bearer <access_token> header; requests that carry a JSON body also require Content-Type: application/json.

Mint Session

Exchanges an HMAC-signed request for a short-lived Bearer token. See Authentication for the signing scheme.

HTTP Request

POST /distribution/v1/sessions
Content-Type: application/json
X-Partner-Id: <partner_uuid>
X-Timestamp: <unix_epoch_seconds>
X-Nonce: <unique_per_request>
X-Signature: <hmac_sha256_hex>

Request Body

{
  "locale": "en_US",
  "currency": "USD",
  "cart_id": null,
  "scopes": ["search:read", "fare:read", "order:write", "order:read"]
}

Response

{
  "session_id": "5f8d0a2c-1b3e-4c6a-9f1d-2e3b4c5d6e7f",
  "access_token": "<jwt>",
  "expires_at": "2026-07-15T12:30:00Z",
  "token_type": "Bearer",
  "scopes": ["search:read", "fare:read", "order:write", "order:read"]
}

The returned scopes reflect what was actually granted (the intersection of requested and entitled scopes), so the partner can detect any gap.

Ping

A smoke-test endpoint that echoes the caller’s identity from the Bearer token. Useful to confirm the mint → bearer pipeline works.

HTTP Request

GET /distribution/v1/ping
Authorization: Bearer <access_token>

Response

{
  "partner_id": "f8cc4f24-8f68-4ade-9033-ec1402fe1836",
  "session_id": "5f8d0a2c-1b3e-4c6a-9f1d-2e3b4c5d6e7f",
  "server_time": "2026-07-15T12:00:00Z"
}

Search Flights

Pool-based flight search. Requires scope search:read.

Note
Itineraries are direct (single-leg) only; connections are a later milestone. Supplying return_date makes this a round-trip search. A one-way search returns offers; a round-trip returns outbound_offers (each carrying the valid_return_offer_ids it can be combined with) plus a deduped return_offers list. Every offer_id is a real pool-offer id; pool pricing applies no round-trip discount, so the trip price is the sum of the chosen outbound and return offers.
Note
The request currency must equal the session currency (the currency minted into the token); a mismatch is rejected with 400 INVALID_REQUEST. This keeps /search and /offer prices comparable — both are denominated in the session currency.

HTTP Request

POST /distribution/v1/search
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "origin": "ALA",
  "destination": "OSS",
  "outbound_date": "2026-07-15",
  "pax": { "adt": 1, "chd": 0, "inf": 0 },
  "currency": "USD"
}

Response

The call always returns 200 OK; offers may be empty when no pool offers match.

{
  "offers": [
    {
      "offer_id": "22222222-0000-0000-0000-000000000001",
      "fare_frame_id": "ffffffff-0000-0000-0000-000000000001",
      "available_seats": 9,
      "flight_legs": [
        {
          "flight_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
          "flight_number": "FA101",
          "origin": "ALA",
          "origin_name": "Almaty International Airport",
          "origin_city": "Almaty",
          "destination": "OSS",
          "destination_name": "Osh Airport",
          "destination_city": "Osh",
          "aircraft": "A320",
          "departure_date": "2026-07-15",
          "arrival_date": "2026-07-15",
          "departure_time": "08:00",
          "arrival_time": "09:00",
          "duration_minutes": 60
        }
      ],
      "pricing": {
        "base_price": 50.00,
        "fare_price": 10.00,
        "total_price": 60.00,
        "currency": "USD",
        "adt_count": 1,
        "chd_count": 0,
        "inf_count": 0
      }
    }
  ],
  "currency": "USD"
}

available_seats is the route-level seat availability. base_price (pool base) and fare_price (selected fare frame’s fare) are per-seat, in the session currency. total_price is the indicative party total — per-pax-type discounts and taxes applied, so it is not (base_price + fare_price) × total pax; it may differ from the amount charged at order creation by a currency-rounding residual and order-level fees. A per-pax-type (ADT/CHD/INF) breakdown is available from the dedicated pricing endpoint.

For a round-trip request (with return_date), the response carries outbound_offers + return_offers instead of offers. Each outbound lists the valid_return_offer_ids it can pair with; the trip price is the sum of the chosen outbound and return offers (each priced per leg, no round-trip discount):

{
  "outbound_offers": [
    {
      "offer_id": "22222222-0000-0000-0000-000000000001",
      "fare_frame_id": "ffffffff-0000-0000-0000-000000000001",
      "available_seats": 5,
      "flight_legs": [ { "flight_id": "bbbbbbbb-...", "origin": "ALA", "destination": "OSS", "...": "..." } ],
      "valid_return_offer_ids": [ "22222222-0000-0000-0000-000000000002" ],
      "pricing": { "base_price": 50.00, "fare_price": 10.00, "total_price": 60.00, "currency": "USD", "adt_count": 1, "chd_count": 0, "inf_count": 0 }
    }
  ],
  "return_offers": [
    {
      "offer_id": "22222222-0000-0000-0000-000000000002",
      "fare_frame_id": "ffffffff-0000-0000-0000-000000000001",
      "available_seats": 3,
      "flight_legs": [ { "flight_id": "dddddddd-...", "origin": "OSS", "destination": "ALA", "...": "..." } ],
      "pricing": { "base_price": 30.00, "fare_price": 0.00, "total_price": 30.00, "currency": "USD", "adt_count": 1, "chd_count": 0, "inf_count": 0 }
    }
  ],
  "currency": "USD"
}

To book, pass the outbound offer_id as pool_offer_id and one of its valid_return_offer_ids as return_pool_offer_id to POST /orders/holds.

Re-price Offers

Re-checks a batch of pool offers by id. Requires scope fare:read. The whole call returns 200 OK; the partner inspects each entry’s status. Up to 100 ids per call.

current_price is the authoritative per-seat (1 adult) price — base + fare + per-seat taxes — computed by the same pricing engine as /search and order creation, in the session currency (the currency minted into the token), so re-price, search and the charged amount are directly comparable. An offer with multiple fare frames quotes its cheapest frame (a "from" price); when none of the offer’s frames can currently be priced (e.g. no seat available), current_price is null. Each result is also enriched with fares: one entry per fare_frame_id carrying the marketed fare name, baggage allowance, and change/refund rules. Change/refund `amount_value`s are expressed in the session currency.

HTTP Request

POST /distribution/v1/offer
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{ "offer_ids": ["22222222-0000-0000-0000-000000000001"] }

Response

{
  "results": [
    {
      "offer_id": "22222222-0000-0000-0000-000000000001",
      "status": "VALID",
      "current_price": 50.00,
      "currency": "USD",
      "fare_frame_ids": ["ffffffff-0000-0000-0000-000000000001"],
      "fares": [
        {
          "fare_frame_id": "ffffffff-0000-0000-0000-000000000001",
          "fare_name": "Flex Economy",
          "baggage": [
            {
              "type": "CHECKIN_BAGGAGE",
              "quantity": 1,
              "weight": 23,
              "weight_concept": "PIECE_SLOT"
            }
          ],
          "change_refund_rules": [
            {
              "type": "REFUND_BEFORE_DEPARTURE",
              "amount_value": 50.00,
              "amount_type": "PERCENT",
              "time_before_departure_hours": 120
            }
          ]
        }
      ]
    }
  ]
}

Per-offer status is one of VALID, NOT_FOUND, or EXPIRED. A NOT_FOUND entry carries a null current_price/currency and empty fares. fares is empty when the offer’s fare details cannot be resolved.

Failure case: 503 SERVICE_UNAVAILABLE when the pricing/fare-detail service is temporarily unavailable — safe to retry.

Price Offer

Returns an indicative per-passenger-type (ADT/CHD/INF) price breakdown and total for a single pool offer, using the same per-pax fares and taxes as the order flow (not a ratio estimate). Requires scope fare:read. The breakdown is denominated in the session currency.

Note
The breakdown is indicative, not a binding total. The authoritative total is computed at order creation and may differ by a currency-rounding residual (per-pax amounts are converted and summed here; the order converts the aggregate once) and by order-level fees, which are applied at booking.

offer_id and fare_frame_id come from a /search offer. The call returns 404 OFFER_NOT_AVAILABLE if the offer/fare frame is not priceable, 409 OFFER_SOLD_OUT if it is not available for the requested passengers, and 503 SERVICE_UNAVAILABLE on a downstream failure.

HTTP Request

POST /distribution/v1/offers/pricing
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "offer_id": "22222222-0000-0000-0000-000000000001",
  "fare_frame_id": "ffffffff-0000-0000-0000-000000000001",
  "pax": { "adt": 2, "chd": 1, "inf": 1 }
}

Response

{
  "offer_id": "22222222-0000-0000-0000-000000000001",
  "currency": "USD",
  "total_price": 285.00,
  "per_pax": [
    { "pax_type": "ADT", "count": 2, "unit_price": 100.00, "subtotal": 200.00 },
    { "pax_type": "CHD", "count": 1, "unit_price": 75.00, "subtotal": 75.00 },
    { "pax_type": "INF", "count": 1, "unit_price": 10.00, "subtotal": 10.00 }
  ]
}

unit_price is the per-passenger fare including per-pax taxes and charges; subtotal = unit_price × count; total_price is the sum of the subtotals. Order-level fees, if any, are applied at booking.

Flight Calendar

Returns the dates that have available flights for an origin/destination within a date range, each with the minimum sellable price, so a partner can render a price calendar. Requires scope search:read.

Note
The calendar reflects direct (single-leg) flights only, consistent with /search — every listed date is searchable as a direct flight for the same O/D.

dates[].min_price is the engine total_price of the date’s cheapest sellable offer candidate for the requested passenger composition — base + fare + per-pax taxes with pax-type discounts applied, priced by the same engine as /search, in the session currency (currency echoes it). Guaranteed never lower than any /search total; in rare configurations (flights on one date carrying different pax-type discount groups) it may name a slightly pricier offer than /search’s minimum. The composition is set by the optional `adt/chd/inf query parameters (defaults 1/0/0); adt is 1..9, chd/inf are 0..9, inf must not exceed adt, and the total must not exceed 9 — the same rules the airline’s own IBE enforces. min_price is null when the date is available by quota but none of its offers can currently be priced for the party.

The range is bounded: outbound_date_from must not be after outbound_date_to, and the span must not exceed 62 days — otherwise the call returns 400 INVALID_REQUEST. A downstream availability-service failure returns 503 SERVICE_UNAVAILABLE.

Tip
For a round-trip price calendar call the endpoint twice with the O/D reversed and sum the two min_price`s — pool pricing has no round-trip discount, so the sum is the flight-price total for the trip booked as two one-way orders (order-level fees, if configured, apply at booking — as everywhere in this API). For a single round-trip order treat the sum as a "from" price: round-trip `/search only pairs an outbound with returns sharing its fare_frame_id, so the cheapest bookable pair may cost more.

HTTP Request

GET /distribution/v1/calendar?origin=ALA&destination=OSS&outbound_date_from=2026-07-01&outbound_date_to=2026-07-31&adt=2&chd=1
Authorization: Bearer <access_token>

Response

The call returns 200 OK; dates may be empty when no flights are available in the range.

{
  "dates": [
    {"date": "2026-07-03", "min_price": 11000.00},
    {"date": "2026-07-10", "min_price": 12500.00},
    {"date": "2026-07-17", "min_price": null}
  ],
  "currency": "USD"
}

Available Routes

Returns the origin/destination pairs available on the channel as a nested origin→destinations tree, enriched with airport and city names. Requires scope search:read. A downstream failure returns 503 SERVICE_UNAVAILABLE.

Note
This list reflects direct (single-leg) O/D pairs only, consistent with /search — every pair is searchable as a direct flight.

HTTP Request

GET /distribution/v1/routes
Authorization: Bearer <access_token>

Response

The call returns 200 OK; routes may be empty.

{
  "routes": [
    {
      "airport_code": "ALA",
      "airport_name": "Almaty International Airport",
      "city_name": "Almaty",
      "destinations": [
        {
          "airport_code": "OSS",
          "airport_name": "Osh Airport",
          "city_name": "Osh"
        }
      ]
    }
  ]
}

Flight Ancillary Catalog

Returns the catalog of purchasable extra-service ancillaries (baggage, services) available for a flight, before any order exists — the pre-booking equivalent of the order-scoped List Ancillaries. Requires scope search:read. A downstream failure returns 503 SERVICE_UNAVAILABLE.

Prices are denominated in the partner session currency. Each entry carries the common fields (offer_id, offer_frame_id, category, name, price, max_per_pax) plus the fields specific to its category (omitted when not applicable).

Note
Two ancillary kinds are served elsewhere and are not in this catalog: seat selection has its own Flight Seat Map endpoint, and change/refund conditions are returned with the fare in Re-price Offers.

HTTP Request

GET /distribution/v1/flights/{flightId}/ancillaries
Authorization: Bearer <access_token>

Response

The call returns 200 OK; ancillaries may be empty.

{
  "flight_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
  "currency": "USD",
  "ancillaries": [
    {
      "offer_id": "11111111-0000-0000-0000-000000000001",
      "offer_frame_id": "ffffffff-0000-0000-0000-000000000001",
      "category": "BAGGAGE",
      "name": "Extra 23kg",
      "price": 10.00,
      "max_per_pax": 2,
      "baggage_type": "CHECKIN_BAGGAGE",
      "weight": 23,
      "weight_concept": "PIECE_SLOT"
    }
  ]
}

Failure cases: 400 INVALID_REQUEST (non-UUID {flightId}), 503 SERVICE_UNAVAILABLE (offer/ancillary service unavailable — safe to retry).

Flight Seat Map

Returns the seat-selection price tiers for a flight (tier type, cabin, the seat numbers in the tier, price, and a render colour), before any order exists. Requires scope search:read. Prices are denominated in the partner session currency.

Note
Already-taken seats — assigned in DCS or held by a pending (unpaid) order — are excluded, so only currently-sellable seats are returned. A seat sold by more than one tier appears once, in its cheapest tier.

HTTP Request

GET /distribution/v1/flights/{flightId}/seatmap
Authorization: Bearer <access_token>

Response

The call returns 200 OK; tiers may be empty.

{
  "flight_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
  "currency": "USD",
  "tiers": [
    {
      "offer_id": "11111111-0000-0000-0000-000000000002",
      "offer_frame_id": "ffffffff-0000-0000-0000-000000000002",
      "seat_selection_type": "SEAT_SELECTED",
      "cabin_class": "ECONOMY",
      "name": "Front seat",
      "price": 20.00,
      "seat_numbers": ["1A", "1B"],
      "color": "#00FF00"
    }
  ]
}

Failure cases: 400 INVALID_REQUEST (non-UUID {flightId}), 503 SERVICE_UNAVAILABLE (offer/seat service unavailable — safe to retry).

Hold Seats

Reserves seats on a pool offer for a configurable time-to-live, giving the partner time to collect passenger details before creating an order. Requires scope order:write.

For a round-trip (DEL-1710), also supply return_pool_offer_id (one of the chosen outbound offer’s valid_return_offer_ids from /search). Both legs are held under one hold_id and share the outbound fare_frame_id (the round-trip search prices them under one fare frame). The return offer is validated as a genuine reverse leg — it must fly the outbound’s destination back to its origin and leave at least the airline’s minimum connection time after the outbound arrives (the same min-time-between-routes rule the order step enforces, so a hold never succeeds for a pairing the order would reject); otherwise the hold is rejected with 400 INVALID_RETURN_PAIRING and nothing is reserved. If the return leg can’t be held the whole hold fails (nothing is reserved) and the return-leg outcome is returned. The resulting /orders call then books both legs as one round-trip order.

HTTP Request

POST /distribution/v1/orders/holds
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "pool_offer_id": "22222222-0000-0000-0000-000000000001",
  "fare_frame_id": "ffffffff-0000-0000-0000-000000000001",
  "pax": { "adt": 1, "chd": 0, "inf": 0 },
  "return_pool_offer_id": "22222222-0000-0000-0000-000000000002"
}

return_pool_offer_id is optional — omit it for a one-way hold. The return leg reuses the outbound fare_frame_id.

Response

201 Created:

{
  "hold_id": "66666666-6666-6666-6666-666666666666",
  "expires_at": "2026-07-15T12:15:00Z"
}

Failure cases: 409 OFFER_SOLD_OUT, 409 SUBQUOTA_EXCEEDED, 410 OFFER_NOT_AVAILABLE, 400 INVALID_PAX_COMPOSITION.

Create Order

Creates an order from a previously obtained hold. Requires scope order:write.

This endpoint is idempotent — callers MUST supply an Idempotency-Key header (UUIDv4 recommended). Repeating the call with the same key returns the original order instead of creating a duplicate.

HTTP Request

POST /distribution/v1/orders
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: <uuid>

Request Body

{
  "hold_id": "66666666-6666-6666-6666-666666666666",
  "passengers": [
    {
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1990-01-01",
      "gender": "MALE",
      "citizenship": "US",
      "document_type": "PASSPORT",
      "document_number": "123456789",
      "document_expiry_date": "2030-01-01"
    }
  ],
  "contact": {
    "name": "John Doe",
    "phone": "+1234567890",
    "email": "john@example.com"
  }
}

Seat selection

Each passengers[] entry may include seat_assignments and ancillary_offers (both optional, default empty). flight_id is the leg id from Search Flights (offers[].flight_legs[].flight_id); offer_frame_id and seat_number come from Flight Seat Map. The seat is booked on order creation, before payment.

A free seat needs only a seat_assignment and does not change total_amount:

{
  "first_name": "John",
  "last_name": "Doe",
  "date_of_birth": "1990-01-01",
  "seat_assignments": [
    { "flight_id": "3c4d5e6f-7a8b-9012-cdef-0123456789ab", "seat_number": "6D" }
  ]
}

A paid seat needs a seat_assignment plus a matching ancillary_offer: its offer_frame_id (a seat-map tier) must cover the assigned seat, and quantity must equal the number of matched seats. The tier price is added to total_amount:

{
  "first_name": "John",
  "last_name": "Doe",
  "date_of_birth": "1990-01-01",
  "seat_assignments": [
    { "flight_id": "3c4d5e6f-7a8b-9012-cdef-0123456789ab", "seat_number": "1A" }
  ],
  "ancillary_offers": [
    { "offer_frame_id": "4d5e6f7a-8b9c-0123-def0-123456789abc", "flight_id": "3c4d5e6f-7a8b-9012-cdef-0123456789ab", "quantity": 1 }
  ]
}

An infant (age < 2) does not need its own seat: give the infant the same seat_number as an accompanying adult and it is attached to the adult’s seat at no charge.

Response

201 Created:

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "CONFIRMED",
  "currency": "USD",
  "total_amount": 50.00
}

Failure cases: 404 HOLD_NOT_FOUND, 410 HOLD_EXPIRED. Seat and ancillary problems return 400 INVALID_REQUEST: a paid seat assigned without a matching ancillary_offer; an ancillary_offer whose offer_frame_id matches no assigned seat; quantity not equal to the number of matched seats; a seat_number that does not exist on the flight; a seat already taken; a flight_id not part of the passenger’s itinerary; or more than one seat for the same passenger on one flight.

Get Order

Returns order details for a partner-owned order. Requires scope order:read. Partners can only read their own orders; an unknown or foreign order returns 404 ORDER_NOT_FOUND.

HTTP Request

GET /distribution/v1/orders/{orderId}
Authorization: Bearer <access_token>

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "CONFIRMED",
  "currency": "USD",
  "total_amount": 50.00,
  "contact": { "name": "John Doe", "phone": "+1234567890", "email": "john@example.com" },
  "items": [
    {
      "item_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
      "type": "FLIGHT",
      "status": "CONFIRMED",
      "description": "ALA-OSS 2026-07-15",
      "amount": 50.00,
      "hold_expires_at": null
    }
  ],
  "invoice": {
    "invoice_id": "1a2b3c4d-5e6f-7081-9203-a4b5c6d7e8f9",
    "status": "ISSUED",
    "amount": 50.00,
    "currency": "USD",
    "payment_due_date": "2026-07-15T13:00:00Z",
    "payment_intent_id": "99999999-3333-0000-0000-000000000001",
    "issued_at": "2026-07-15T12:00:00Z",
    "paid_at": null
  }
}

Get Order Ticket

Returns the air ticket for a partner-owned order — the record locator (PNR) and per-passenger e-ticket numbers — so the passenger can check in and board. The ticket is issued by Farel as carrier. Requires scope order:read.

E-tickets exist only after the order has been paid and ticketed (see Confirm Wallet Payment). Before that the endpoint returns 409 TICKET_NOT_AVAILABLE. An unknown or foreign order returns 404 ORDER_NOT_FOUND. A passenger without a separate document (e.g. an infant) is still listed, with a null eticket_number.

Note
For the printable document use Get Order Ticket PDF.

HTTP Request

GET /distribution/v1/orders/{orderId}/ticket
Authorization: Bearer <access_token>

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "pnr": "ABC123",
  "passengers": [
    {
      "passenger_id": "33333333-3333-3333-3333-333333333333",
      "first_name": "John",
      "last_name": "Doe",
      "eticket_number": "0571234567890"
    }
  ]
}

Get Order Ticket PDF

Renders the passenger e-ticket (itinerary receipt) PDF for a paid and ticketed order — the same document served on the airline’s own channels, with per-airline branding. Requires scope order:read.

lang selects the document language per request (default en_US; supported: en_US, ru_RU, fr_FR, es_ES, pt_PT, ky_KG — anything else renders en_US, while the filename’s language tag echoes the requested locale; an unparseable value returns 400 INVALID_REQUEST). Gates are identical to Get Order Ticket: an unpaid/unticketed order returns 409 TICKET_NOT_AVAILABLE, an unknown or foreign order returns 404 ORDER_NOT_FOUND.

The response body is the PDF itself: Content-Type: application/pdf and Content-Disposition: attachment; filename*=UTF-8''…​ with a localized filename, e.g. Eticket AB12CD (en).pdf. Error responses remain JSON (DistributionErrorResponse).

HTTP Request

GET /distribution/v1/orders/{orderId}/ticket/pdf?lang=ru_RU
Authorization: Bearer <access_token>

Get Refund Quote

Returns a read-only refund quote for a partner-owned order — the net refundable amount, the penalty, and an informational gross fare / tax-and-charge / ancillary breakdown — without running the refund and without locking the order. Requires scope order:read. Use this to preview what a voluntary refund would return before committing to it.

refundable_amount is the authoritative total — the amount that would be credited to the partner — and penalty_amount is the withheld penalty. The fare_refund_amount, tax_and_charge_refund_amount, and offer_refund_amount fields are an informational gross breakdown of the refunded fare, tax-and-charge, and ancillary portions ONLY; they do not break out order-level fees or discounts and therefore do not necessarily sum to refundable_amount plus penalty_amount when the order carries fees or discounts.

v1 quotes the FULL order only; partial or offer-only scopes are planned as future work. An unknown or foreign order returns 404 ORDER_NOT_FOUND. An order with no PAID invoice to refund from is not refundable and returns 409 ORDER_NOT_REFUNDABLE; an order with multiple active bookings after an exchange returns 409 BOOKING_AMBIGUOUS. All amounts are expressed in the order’s currency.

HTTP Request

GET /distribution/v1/orders/{orderId}/refund-quote
Authorization: Bearer <access_token>

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "currency": "USD",
  "refundable_amount": 180.00,
  "penalty_amount": 20.00,
  "fare_refund_amount": 150.00,
  "tax_and_charge_refund_amount": 30.00,
  "offer_refund_amount": 20.00
}

Refund Order

Refunds a partner-owned order via the booking-refund engine. Requires scope order:write. By default the FULL order is refunded; supply pax_segment_ids to refund only those pax-segments (partial). The refund is ASYNCHRONOUS: the endpoint returns 200 immediately with a booking-derived refund status, and the partner’s wallet is credited automatically once farel-os-payments settles it — the partner is NOT credited synchronously and the response carries no credit-note or PSP refund reference (those surface on the wallet statement).

The Idempotency-Key header is required (for consistency with the other write endpoints) but is NOT currently used to replay a prior refund. A retry does not replay the original refund; instead, re-refunding already-refunded pax-segments is rejected by a segment-state guard with 409 ORDER_NOT_REFUNDABLE instead of being refunded again. A missing (or blank) Idempotency-Key header returns 400 INVALID_REQUEST. An unknown or foreign order returns 404 ORDER_NOT_FOUND; an order not in a refundable state returns 409 ORDER_NOT_REFUNDABLE; an order with multiple active bookings after an exchange returns 409 BOOKING_AMBIGUOUS. A broken internal invariant while refunding returns 500 INTERNAL_ERROR.

The returned status is a booking-derived refund status — exactly one of PROCESSING, REFUNDED, or PARTIALLY_REFUNDED (it is NOT the order-level OrderStatus and is never ACTIVATED/PAID). PROCESSING means the refund was accepted and settlement is still completing asynchronously; the booking moves to REFUNDED / PARTIALLY_REFUNDED once settled.

Note
The commercial order is intentionally left ACTIVATED with no credit-note issued (DEL-1881). GET /distribution/v1/orders/{orderId} does NOT reflect the refund — do not poll the order to confirm a refund. The wallet statement (GET /distribution/v1/wallet/transactions) is the source of truth for settlement.

HTTP Request

POST /distribution/v1/orders/{orderId}/refund
Authorization: Bearer <access_token>
Idempotency-Key: a3b2c1d0-e4f5-6789-abcd-ef0123456789

Request Body

{
  "pax_segment_ids": ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]
}

Omit pax_segment_ids (or send an empty array) to refund the whole order.

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "PROCESSING"
}

Confirm Wallet Payment

Confirms an externally-collected payment for a partner-owned order and settles it from the partner’s wallet. Requires scope order:write. The amount is taken server-side from the order’s invoice; the body carries only the PSP payment_reference (the unique payment id from the partner’s payment provider). The wallet is debited within its credit limit and the order is ticketed.

The payment_reference is the idempotency / dedup key: because a PSP payment id is unique per payment, replaying the same payment_reference does not double-debit or double-ticket (a replay after success returns 409 ORDER_ALREADY_PAID), and the value is stored against the wallet transaction so it can be matched back to the partner’s own PSP statement via GET /distribution/v1/wallet/transactions.

A 200 means the wallet was debited and the order is ticketed. A 202 means the wallet was debited but ticketing is still completing asynchronously — poll GET /distribution/v1/orders/{orderId} (which requires the order:read scope) for the final status. A missing, blank, or over-long payment_reference returns 400 INVALID_REQUEST in the standard distribution error envelope.

Errors: 400 INVALID_REQUEST (missing, blank, or over-long payment_reference), 402 INSUFFICIENT_WALLET_LIMIT (available limit below the invoice total; no debit, checked atomically by the wallet settlement service), 409 ORDER_ALREADY_PAID, 409 ORDER_NOT_PAYABLE (invoice not in a payable state, or the order is already activated — only the initial-purchase invoice is settled here), 404 ORDER_NOT_FOUND (unknown or foreign order), 502 WALLET_PAYMENT_FAILED (settlement service error), 500 WALLET_SETTLEMENT_INCONSISTENT (wallet debited but the order went terminal and cannot be ticketed — under reconciliation), 503 SERVICE_UNAVAILABLE (the partner-order ownership store was momentarily unreachable — a retriable infrastructure failure, distinct from a 404).

HTTP Request

POST /distribution/v1/orders/{orderId}/payments/confirm
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "payment_reference": "psp-pay-9f12ab34"
}

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "ACTIVATED",
  "transaction_ids": ["cccccccc-cccc-cccc-cccc-cccccccccccc"]
}

List Ancillaries

Lists the ancillaries (extra baggage, change-and-refund, other services) available for an order, together with those already selected, grouped by passenger and pax-segment. Requires scope ancillary:read. Seat selection is not included (separate endpoint, later milestone).

The orderId is resolved to its booking internally. An order with multiple active bookings (post-exchange) returns 409 BOOKING_AMBIGUOUS.

HTTP Request

GET /distribution/v1/orders/{orderId}/ancillaries
Authorization: Bearer <access_token>

Response

{
  "currency": "USD",
  "passengers": [
    {
      "passenger_id": "aaaaaaaa-0000-0000-0000-000000000001",
      "first_name": "John",
      "last_name": "Doe",
      "segments": [
        {
          "pax_segment_id": "bbbbbbbb-0000-0000-0000-000000000001",
          "available": [
            {
              "offer_frame_id": "cccccccc-0000-0000-0000-000000000001",
              "ancillary_id": "dddddddd-0000-0000-0000-000000000001",
              "category": "BAGGAGE",
              "name": "Extra 10kg",
              "description": "Additional checked baggage allowance",
              "image_url": null,
              "price": 15.00,
              "currency": "USD"
            }
          ],
          "selected": [
            {
              "offer_frame_id": "cccccccc-0000-0000-0000-000000000002",
              "ancillary_id": "dddddddd-0000-0000-0000-000000000002",
              "category": "OTHER",
              "name": "Priority boarding",
              "quantity": 1
            }
          ]
        }
      ]
    }
  ]
}

Add Ancillaries

Adds the partner-selected ancillaries to an order. Requires scope ancillary:write.

This endpoint is idempotent when an Idempotency-Key header is supplied (optional but strongly recommended; UUIDv4 recommended). Repeating the call with the same key does not add the ancillaries twice; it returns the result of the first call, re-read from the order’s current state. Without the header the add runs once with no replay protection.

Adding ancillaries produces a new additional order whose order_id is returned; that id is owned by the partner and can be used with Get Order. Each body pax_segment_id must belong to the URL orderId’s booking, otherwise `400 INVALID_REQUEST.

HTTP Request

POST /distribution/v1/orders/{orderId}/ancillaries
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: <uuid>

Request Body

{
  "passengers": [
    {
      "passenger_id": "aaaaaaaa-0000-0000-0000-000000000001",
      "segments": [
        {
          "pax_segment_id": "bbbbbbbb-0000-0000-0000-000000000001",
          "offers": [
            { "offer_frame_id": "cccccccc-0000-0000-0000-000000000001", "quantity": 1 }
          ]
        }
      ]
    }
  ]
}

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "currency": "USD",
  "total_amount": 15.00,
  "paid_amount": 0.00,
  "paid_currency": "USD"
}

Failure cases: 400 INVALID_REQUEST (unknown/foreign pax_segment_id, empty selection, or non-UUID {orderId}), 404 ORDER_NOT_FOUND, 409 BOOKING_AMBIGUOUS, 503 SERVICE_UNAVAILABLE (ownership write failed — safe to retry).

List Assistance

Lists the special-assistance options (SSR — wheelchair, blind/deaf passenger, infant bassinet, service animal, etc.) available for an order, together with those already selected, per pax-segment. Requires scope assistance:read. Special assistance is free of charge — there is no price, currency or quantity.

The response lists each pax-segment with its passenger_id (so options can be rendered/selected per traveler) and an ssr_not_available flag distinguishing a blocked segment from an empty catalog. applicable_pax_types lists which passenger types the SSR can be selected for; code is the IATA SSR code, type the internal enum value.

HTTP Request

GET /distribution/v1/orders/{orderId}/assistance
Authorization: Bearer <access_token>

Response

{
  "segments": [
    {
      "passenger_id": "aaaaaaaa-0000-0000-0000-000000000001",
      "pax_segment_id": "bbbbbbbb-0000-0000-0000-000000000001",
      "ssr_not_available": false,
      "available": [
        {
          "ssr_id": "eeeeeeee-0000-0000-0000-000000000001",
          "type": "WHEELCHAIR_SERVICE",
          "code": "WCHC",
          "name": "Wheelchair",
          "applicable_pax_types": ["ADT", "CHD"]
        }
      ],
      "selected": [
        {
          "ssr_id": "eeeeeeee-0000-0000-0000-000000000002",
          "type": "BLIND",
          "code": "BLND",
          "name": "Blind passenger assistance"
        }
      ]
    }
  ]
}

Add Assistance

Adds the partner-selected special assistance (SSR) to the pax-segments of an order, then returns the refreshed snapshot (same shape as List Assistance). Requires scope assistance:write.

This endpoint is idempotent when an Idempotency-Key header is supplied (optional but strongly recommended; UUIDv4 recommended). Repeating the call with the same key does not add the SSRs twice; it returns the booking’s current assistance snapshot. Without the header the add runs once with no replay protection.

Important
Special assistance can only be added once the booking is activated, i.e. after the order has been paid. Calling it on a not-yet-activated booking returns 409 BOOKING_NOT_ACTIVATED. Each body pax_segment_id must belong to the URL orderId’s booking, otherwise `400 INVALID_REQUEST.

HTTP Request

POST /distribution/v1/orders/{orderId}/assistance
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: <uuid>

Request Body

{
  "segments": [
    {
      "pax_segment_id": "bbbbbbbb-0000-0000-0000-000000000001",
      "ssr_ids": ["eeeeeeee-0000-0000-0000-000000000001"]
    }
  ]
}

Response

200 OK — the refreshed assistance snapshot, identical in shape to List Assistance.

Failure cases: 400 INVALID_REQUEST (unknown/foreign pax_segment_id, SSR not applicable to the passenger type, empty selection, or non-UUID {orderId}), 404 ORDER_NOT_FOUND, 409 BOOKING_NOT_ACTIVATED (booking not yet activated), 409 BOOKING_AMBIGUOUS.

Operator Endpoints

Note
The endpoints in this section are called by airline operators (via the Farel admin console), not by partners. Under the wallet/agency settlement model the airline — not the partner — provisions the partner’s wallet and sets its credit limit; they are documented here so partners understand how their wallet is set up. These endpoints live on the /v1/admin/…​ path, use airline-operator authentication carrying the DISTRIBUTION_MANAGEMENT role (not the partner session token or scopes), and are tenant-scoped via the required X-Airline-ID header. Application-level errors return the standard admin ErrorResponse envelope ({message, code, params}), not the partner {code, message} envelope described in Error Format: 404 when the partner is unknown or belongs to another airline, and 400 when credit_limit is negative or the X-Airline-ID header is missing. A 403 for a missing DISTRIBUTION_MANAGEMENT role is enforced at the security layer and returns the framework’s standard 403 response (no envelope body).

Both operations are idempotent: the wallet is keyed by the partner id, so re-running neither duplicates the wallet nor stacks the credit limit.

Method Endpoint Description

POST

/v1/admin/distribution/partners/{partnerId}/wallet

Create or link the partner’s wallet (in the airline currency)

POST

/v1/admin/distribution/partners/{partnerId}/credit-limit

Set the partner’s wallet credit limit

Provision Partner Wallet

Creates the partner’s wallet in the airline’s default currency, or returns the existing one. Idempotent — re-running returns the same wallet without creating a duplicate.

HTTP Request

POST /v1/admin/distribution/partners/{partnerId}/wallet
X-Airline-ID: <airline_uuid>

Response

{
  "wallet_id": "8a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "holder_id": "f8cc4f24-8f68-4ade-9033-ec1402fe1836",
  "credit_limit": 0,
  "current_balance": 0,
  "currency": "KGS"
}

holder_id equals the partner id. currency is the wallet’s own currency — an existing wallet keeps the currency it was created with.

Set Partner Credit Limit

Sets the credit limit on the partner’s wallet, resolving (and, if necessary, creating) the wallet first. Idempotent — credit_limit is an absolute value, not a delta.

HTTP Request

POST /v1/admin/distribution/partners/{partnerId}/credit-limit
Content-Type: application/json
X-Airline-ID: <airline_uuid>

Request Body

{
  "credit_limit": 500000,
  "remark": "Initial provisioning"
}

credit_limit must be zero or positive; remark is optional (max 2000 characters).

Response

{
  "wallet_id": "8a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "holder_id": "f8cc4f24-8f68-4ade-9033-ec1402fe1836",
  "credit_limit": 500000,
  "current_balance": 0,
  "currency": "KGS"
}

Wallet Balance

Returns the calling partner’s wallet balance. Requires scope wallet:read. The wallet is resolved from the authenticated session, so a partner only ever sees its own balance. available is the spendable remainder — current_balance + credit_limit (a debit lowers current_balance, which may be negative).

HTTP Request

GET /distribution/v1/wallet/balance
Authorization: Bearer <access_token>

Response

{
  "credit_limit": 500000,
  "current_balance": -150000,
  "available": 350000,
  "currency": "KGS"
}

A 503 SERVICE_UNAVAILABLE is returned if the wallet settlement service is temporarily unreachable.

Wallet Transactions

Returns a paginated statement of the calling partner’s wallet operations, most recent first. Requires scope wallet:read. Pagination is via page (0-based, default 0) and size (default 50, max 100). Invalid parameters — a negative page, or a size outside 1..100 — return 400 INVALID_REQUEST. A page past the end of the history is valid and returns 200 with an empty items list. total is the full (filtered) operation count.

For self-service reconciliation the statement can be narrowed with optional query parameters, combinable with each other and with pagination:

Parameter Meaning

from, to

ISO-8601 instants (e.g. 2026-07-01T00:00:00Z) bounding an inclusive [from, to] timestamp window. Either end may be omitted for an open-ended range. A malformed instant, or from after to, returns 400 INVALID_REQUEST. (DEL-1634 — pull all movements in a period and match them against your PSP statement.)

order_id

Returns the wallet operations carrying that order’s current-invoice payment (the initial-purchase capture). The order must belong to the calling partner, otherwise 404 ORDER_NOT_FOUND (an unknown or foreign order is not distinguished, to avoid leaking existence). An owned order with no payment yet returns an empty page. Movements on a different intent of the same order (a refund, or an addition/exchange invoice) are not matched here — full per-order reconciliation arrives with the per-row fields below. (DEL-1742.)

Each item carries order_id and payment_reference. order_id is populated only when you filter by order_id (every returned row then belongs to that order); it is null on the unfiltered statement. payment_reference carries the PSP reference supplied at payment (DEL-2054); it is null for operations without one (e.g. top-ups). The settlement service does not yet propagate an order id onto each operation; reconcile the unfiltered statement via the order_id filter and the intent_id.

HTTP Request

GET /distribution/v1/wallet/transactions?page=0&size=50&from=2026-07-01T00:00:00Z&to=2026-07-31T23:59:59Z
Authorization: Bearer <access_token>

Response

{
  "page": 0,
  "size": 50,
  "total": 2,
  "items": [
    {
      "transaction_id": "wtx-000123",
      "type": "PAYMENT",
      "amount": 50000,
      "timestamp": "2026-07-15T12:00:00Z",
      "method": "WALLET",
      "intent_id": "99999999-3333-0000-0000-000000000001",
      "remark": null,
      "order_id": null,
      "payment_reference": null
    }
  ]
}

A 503 SERVICE_UNAVAILABLE is returned if the wallet settlement service is temporarily unreachable.

Machine-readable Contract

The authoritative, always-current request/response schema is published as an OpenAPI document and served by each deployment at /api/swagger-ui/index.html. This page is a narrative companion to that contract.