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, settlement 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, order creation and online check-in

order:read

Reading order details, tickets, the online check-in state and boarding passes

ancillary:read

Listing available and selected ancillaries for an order

ancillary:write

Buying ancillaries for a paid order, and settling or cancelling that addition

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.

400

CURRENCY_NOT_SUPPORTED

The currency requested at session mint is not a settlement currency the airline accepts (returned by POST /distribution/v1/sessions).

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.

404

FLIGHT_NOT_FOUND

A flight_id supplied to GET /distribution/v1/rules does not exist, or the flight_id of a boarding pass request is not a flight of the order.

404

PASSENGER_NOT_ON_FLIGHT

A passenger_id in Check In Passengers is not a passenger of the order on that flight.

409

OFFER_SOLD_OUT / SUBQUOTA_EXCEEDED

Capacity is unavailable for the requested composition.

409

BOOKING_NOT_ACTIVATED

Special assistance was requested before the booking was activated (i.e. before the order was paid). Assistance wanted before payment goes into POST /orders (ssrs).

409

ORDER_NOT_ACTIVATED

An infant (see Add Infant) or an ancillary (see Add Ancillaries) was added to an order that is not paid yet. Infants and services wanted before payment go into POST /orders — as a passenger of type INF and as ancillary_offers.

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

409

INFANT_ADDITION_NOT_ELIGIBLE

The order cannot take another infant: every adult already accompanies one, a segment has departed or checked in, the itinerary is interline, or the order is not in the airline’s domestic currency. The message names the case.

409

INFANT_ADDITION_NOT_CANCELLABLE

The infant addition is already settled, already cancelled, or its invoice expired.

409

ANCILLARY_ADDITION_NOT_ELIGIBLE

The order cannot take this ancillary: the passenger segment is checked in, boarded or its flight departed, the offer carries taxes or charges the addition cannot bill, the itinerary is interline, or the order is not in the airline’s domestic currency. The message names the case.

409

ANCILLARY_ADDITION_NOT_CANCELLABLE

The ancillary addition is already settled, already cancelled, or its invoice expired.

409

ANCILLARY_LIMIT_EXCEEDED

The requested quantity is above what this passenger may still buy of that offer on that segment.

409

ANCILLARY_PRICE_CHANGED

The offer was repriced between the catalog read and the commit; read List Ancillaries again and repeat with a fresh Idempotency-Key.

410 / 409

OFFER_NOT_AVAILABLE / HOLD_EXPIRED

Offer or hold is no longer available. 410 from Hold Seats and Create Order; 409 from the exchange endpoints, Add Infant and Add Ancillaries.

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

The online check-in endpoints (see Online Check-In and Boarding Pass) answer a business refusal with 200 and the same envelope, not with a 409. Branch on the presence of code in the body, not on the HTTP status:

HTTP Code Meaning

200

TICKET_NOT_AVAILABLE

The order is not paid and ticketed yet, or is mid-exchange or mid-refund (the same gate as Get Order Ticket, which answers it with 409).

200

CHECK_IN_NOT_OPEN

Online check-in for the flight has not opened yet.

200

CHECK_IN_CLOSED

Online check-in for the flight has closed.

200

FLIGHT_CANCELED

The flight is cancelled.

200

SEAT_UNAVAILABLE

The seat is already taken.

200

SEAT_NOT_ELIGIBLE

The seat does not exist on the flight, is outside the passenger’s cabin class, is blocked for the passenger type, has no adult next to a child or infant who needs one, or the passenger is already boarded.

200

PAID_SEAT_NOT_PURCHASED

The seat is a paid seat the passenger has not bought. Only a free seat, a paid seat already bought, or a paid seat the airline’s automatic seating set aside for that passenger can be taken.

200

PASSENGER_NOT_ELIGIBLE

The passenger cannot check in online: invalid passenger data, an unaccompanied minor, a passenger whose segment is not active, or an airline that requires passenger weights at check-in.

200

PASSENGER_NOT_CHECKED_IN

No passenger of the order is checked in on the flight, so there is no boarding pass.

200

BOARDING_PASS_NOT_AVAILABLE

The boarding pass could not be issued for a checked-in passenger. The message names the case.

Rate Limiting

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

Statuses

Every status you can observe in the API, and how it changes. Where a value exists in the platform but never reaches the partner channel, this section says so. Do not build against those.

Order status

Seven other responses use a field of the same name for their own vocabulary: Commit Exchange, Add Infant, Add Ancillaries, the 202 of Settle Exchange Payment, of Settle Infant Addition Payment and of Settle Ancillary Addition Payment, and Refund Order. Each is described where it is returned. Do not resolve those values against the table below, even when they spell REFUNDED or PARTIALLY_REFUNDED.

A partner order moves through:

Status Meaning

CONFIRMED

Order created and held, awaiting payment. Its invoice is ISSUED.

ACTIVATED

Payment captured and the booking is active. This is the working state of a paid order, and it survives exchanges. It says nothing about tickets. ticketed can be false on an ACTIVATED order: briefly after payment, for as long as a committed exchange stays unpaid, and permanently for an airline that does not issue e-tickets. To find out whether tickets are available, read ticketed (see Ticket). Never infer it from the order status.

EXPIRED

A CONFIRMED order that was not paid before its invoice payment-due date. The system expired it. Terminal.

CANCELLED

The order was cancelled before payment. Terminal.

PARTIALLY_REFUNDED

Part of the order has been refunded and settled. Only an airline-side refund reaches this state. Your own Refund Order call leaves the order ACTIVATED.

REFUNDED

The whole order has been refunded and settled. Same source as PARTIALLY_REFUNDED. Terminal.

Before payment: CONFIRMED → ACTIVATED on payment, CONFIRMED → EXPIRED when the payment window elapses, CONFIRMED → CANCELLED when the order is cancelled first. There is no partner cancel endpoint, so you cannot drive CANCELLED yourself. It is not always airline-initiated either. If the payment system cancels the order’s payment intent, the order is cancelled with nobody acting on it, and the outstanding invoice is voided on the way, so invoice.status then reads CANCELLED rather than ISSUED. Do not read CANCELLED as "the airline cancelled us".

After payment, an airline-side refund moves ACTIVATED → PARTIALLY_REFUNDED → REFUNDED, or straight to REFUNDED when the whole order goes at once. Most of the time you will see ACTIVATED, plus the terminal EXPIRED and CANCELLED. The two refunded states appear only on orders the airline refunded for you.

Note
A refund you initiate does not change the order status. The money is returned and the order stays ACTIVATED, so confirm it from the Refund Order response and the wallet statement rather than from the order. PARTIALLY_REFUNDED and REFUNDED are real, reachable states, but only an airline-side refund produces them. Your own call never will. Read them when you see them, and never treat their absence as proof that no refund happened. Nothing at all assigns PAID or PARTIALLY_PAID, so do not depend on them. status_reason is not part of the order payload, so expect no such field rather than a null one. DRAFT is a transient pre-confirm state you will never receive.

Order-item status

status on each item in items[] (see Get Order). An order is a flat list of items: a flight for a passenger, an ancillary, a fee. This is the per-item state.

Status Meaning

ACTIVE

Live item.

EXCHANGED

An item that was superseded when an exchange was paid. It covers the whole replaced flight block, not just the flight leg. The flight item and everything attached to it (taxes, discounts, ancillaries) go EXCHANGED together. Only the flight item has a direct counterpart among the new items. The rest are re-issued as part of the new block.

CANCELLED

The item is no longer live. Two different things put it here. A pending (unpaid) exchange, infant addition or ancillary addition that is cancelled or expires leaves its new items CANCELLED, and for those that is the end state. An airline-side refund also parks the items it refunds in CANCELLED for as long as the payout is in flight. Those turn REFUNDED once the money settles, or go back to ACTIVE if the payment provider refuses the refund. So CANCELLED is not terminal, and it does not mean "an exchange was cancelled". Re-read the order to tell the two apart. Watch total_amount here: it counts ACTIVE items only, so it drops as soon as items leave ACTIVE, before the refund has completed. A refund you initiate never produces this state. Your items stay ACTIVE.

REFUNDED

The item was refunded out. Only an airline-side refund produces it. See the note below.

Note
A refund you initiate does not change the item. It stays ACTIVE, because Refund Order never touches the order aggregate (see the note there). The automatic refund that follows a flight cancellation (see Flight status) leaves the items ACTIVE too. REFUNDED is written only when airline staff refund the order themselves and the money settles, and not even on every such refund. So REFUNDED may well appear on your order, but never treat its absence as proof that no refund happened. Confirm every refund from the Refund Order response and the wallet statement, not from the item status.
Note
While an exchange is committed but unpaid, the new items are ACTIVE alongside the old ones, which are ACTIVE too. No status marks the exchange as pending, and the order stays ACTIVATED. total_amount sums ACTIVE items only, so for that whole window it counts both the old and the new legs plus the exchange penalty, and it drops to the real figure once the exchange is paid and the old items become EXCHANGED. Track a pending exchange by the invoice_id returned by Commit Exchange with status: PENDING_PAYMENT. No endpoint reads a single invoice by id, so poll Get Order and match invoice.invoice_id against it. invoice always reports the most recently issued invoice, which during that window is the exchange invoice (see Invoice status). Match on the id rather than the status. An ISSUED invoice on its own does not tell you which invoice it belongs to.
Note
type on an item is one of FLIGHT (a flight leg or one of its price components), ANCILLARY, TAX, DISCOUNT, TRANSACTION_FEE (booking and transaction fees) or PENALTY (an exchange penalty). Parse it as an open set. More types may appear, and one value that exists in the platform is never assigned today, so it will not reach you.

Pax segment status

pax_segment_status on flight-leg anchors (see Get Order) is where the passenger stands on that leg. It is the only field that answers "did this person travel". status on the same row is the commercial life of the line item and flight_status is the airline’s operation on the aircraft; neither moves when a passenger boards.

A passenger travelled if the value is BOARDED or FLOWN. Take both, not just FLOWN. ARRIVED is not a value here at all: it belongs to the flight, not to the passenger.

Status Meaning

CREATED

The order exists but is not paid. This is what every anchor reads before payment.

ISSUED

Paid and live, the normal state of an upcoming leg. It says nothing about tickets: a segment is ISSUED even for an airline that issues no e-tickets. Read ticketed for that (see Ticket).

CHECKED_IN

The passenger checked in for this leg.

BOARDED

The passenger boarded. Already a positive answer to "did they travel".

FLOWN

The flight was closed with this passenger on board.

NO_SHOW

The passenger did not travel on this leg. The only negative answer.

Parse the field as an open set. Other values exist for legs that were refunded, exchanged, cancelled or expired, and new ones may appear; branch on the values above and treat the rest as "no travel answer for this leg": some of them, such as an owner transfer or an exchange still in flight, sit on a leg that is very much going to be flown.

Note
FLOWN lags the real flight. It is written when the flight is closed, either by an operator or by the scheduler that consumes end-of-flight PRL messages. Neither is driven by the schedule, so the gap between landing and FLOWN is however long that takes. Never read "not FLOWN yet" as "did not travel": a passenger who boarded sits at BOARDED until the flight is closed, and a leg whose booking we could not read has no value at all. Only NO_SHOW is a negative answer.
Note
The value is not terminal. Reverting a closed flight moves FLOWN back to BOARDED, and undoing a check-in moves the passenger back to ISSUED. Re-read rather than caching a "has flown" flag.
Note
NO_SHOW means "not travelling on this leg", not "this flight departed without them". Missing one leg can mark the passenger’s later legs, including the return direction, days before those flights depart.
Note
A leg that was cancelled, or that was mid-exchange when its flight closed, receives neither FLOWN nor NO_SHOW, so the travel question stays unanswered for it. The value still moves afterwards for other reasons: an automatic involuntary refund takes a cancelled leg to REFUNDING and then REFUNDED, and an exchange resolves to CHANGED when it is paid or back to ISSUED when it is abandoned. Keep re-reading; do not infer travel from silence.
Note
The words repeat across fields with different meanings. CANCELLED appears in all three of status, flight_status and this field, and REFUNDED in both status and this field, meaning something different in each. Read each field against its own table.
Note
passenger_id on the anchor identifies the traveller on that leg, but it is not stable across an exchange. A paid exchange issues a new passenger record, so the superseded leg and its replacement carry different values for the same human, and Get Order Ticket only ever returns the live one.

Refund status

status in the Refund Order response. This is a different field from the order status above, and it reuses two of the same words. It is exactly one of PROCESSING, REFUNDED or PARTIALLY_REFUNDED, and it describes the refund of the booking, never the state of the order.

PROCESSING means the refund was accepted and is still settling. REFUNDED and PARTIALLY_REFUNDED mean the booking side of the refund has completed. The credit reaches your wallet separately, so reconcile the money from GET /distribution/v1/wallet/transactions either way.

Note
Reading REFUNDED here does not mean the order status moved. After a refund you initiate, the order stays ACTIVATED and its items stay ACTIVE (see Order status and Order-item status). Do not feed this field into the same mapping you use for the order and item statuses.

Invoice status

invoice.status (see Get Order). The invoice field is the most recently issued invoice for the order.

Status Meaning

ISSUED

Awaiting payment.

PAID

Settled.

CANCELLED

Voided because the order, a pending exchange, a pending infant addition or a pending ancillary addition was cancelled.

EXPIRED

Not paid before the payment-due date.

Note
Every committed exchange issues a delta invoice, including an involuntary or free one. From then on, invoice reflects that invoice’s lifecycle rather than the original purchase invoice, and it never reverts to the earlier one. If the delta invoice is cancelled or expires, invoice.status keeps reporting CANCELLED or EXPIRED on an order that is fully paid and ACTIVATED, until a newer invoice is issued on it. invoice.status is the status of one invoice, never a status of the order, so read order liveness from status and items[].status. A refund does not change the invoice at all. invoice.status keeps whatever it already had: PAID when the newest invoice is the paid purchase invoice, and still CANCELLED or EXPIRED when the newest one is a cancelled or expired delta invoice.
Note
The invoice payload carries no reason field. invoice gives you invoice_id, status, amount, currency, payment_due_date, payment_intent_id, issued_at and paid_at, and nothing else. There is no reason or status_reason, so the payload never tells you why an invoice was cancelled or expired, nor whether it is the original purchase invoice, an exchange delta, an infant addition or an ancillary addition. Identify it by invoice_id, matching the id Commit Exchange, Add Infant or Add Ancillaries returned.
Note
Add Infant and Add Ancillaries issue an invoice the same way an exchange does, so the same rule applies to them: from the commit on, invoice reflects the addition invoice, including after it is cancelled or expires. With more than one addition outstanding it reflects the most recently issued one, so match on invoice_id rather than expecting the one you are waiting for.

Ticket

There is no ticket status value. ticketed (boolean, see Get Order) is true when every live flight leg on the order sits on a booking in a paid state and at least one of those legs carries an e-ticket number. It is an order-level flag, not a per-passenger one, so ticketed: true does not promise a number for every passenger. An infant travelling on an adult’s ticket has none, and Get Order Ticket returns a null eticket_number for such a passenger. Get Order Ticket applies its own gate: the order’s head booking must be paid and at least one passenger on it must carry an e-ticket number. A passenger added through Add Infant lives on a child booking, so an unsettled addition never blocks the ticket, and a settled one never appears in it. Read ticketed: true as the moment to call it, not as a guarantee of what it will answer.

Note
ticketed reflects issuance, not current validity, and it is not a receipt for your refund in either direction. Refunding only some passengers (pax_segment_ids) never moves it. It stays true throughout, and the refunded passenger keeps a ticket number. Refunding the whole order is different. From your Refund Order call until the refund settles, which is the window Refund Order reports as PROCESSING, ticketed reads false and both Get Order Ticket and the ticket PDF answer 409 TICKET_NOT_AVAILABLE. Once it settles, ticketed returns to true and the same ticket numbers are served again. An airline-side refund that settles behaves the other way round: the refunded legs leave ACTIVE, so ticketed can read false while Get Order Ticket still answers 200 with the old numbers. Read neither value as a refund result. Confirm refunds from the Refund Order response and the wallet statement. There is no separate ticket status beyond this boolean.
Note
ticketed also turns false on a live, paid order for reasons that have nothing to do with issuance. It is false for as long as an exchange or an infant addition is committed and unsettled, because the order then carries a second, still-unpaid booking. An unsettled ancillary addition is the exception: it buys no seat and creates no booking, so ticketed and the ticket endpoints are unaffected by it. The ticket endpoints react differently: an unpaid exchange makes them answer 409 TICKET_NOT_AVAILABLE, an unsettled infant addition does not. It is false again when an airline cancellation takes every flight segment of the order. A cancellation that hits only one leg of a multi-leg itinerary leaves the rest live, and ticketed stays true. So never read ticketed as a cancellation signal. Poll flight_status for that. Get Order Ticket returns 409 TICKET_NOT_AVAILABLE during that same exchange window, because the order has more than one live booking. An exchange issues a new booking, so once it is paid the PNR and every eticket_number are new. Re-read Get Order Ticket rather than caching them.
Note
ticketed fails closed. If the platform cannot resolve issuance state for the order, Get Order reports ticketed: false rather than failing, so a single false reading is not proof that no e-ticket exists. Re-read before acting on it.

Flight status

flight_status on flight-leg items (see Get Order) reports the airline’s operation on the booked flight. When the flight behind an item cannot be resolved, the key is omitted rather than sent as null. Treat an absent flight_status as unknown and re-read it. Never default it to SCHEDULED.

Status Meaning

SCHEDULED

The airline has neither cancelled nor rescheduled the flight. This is the default value, so it is also what a delayed, already-departed or already-arrived flight reports. It is not a positive confirmation that the flight is running to plan.

CANCELLED

The airline cancelled the flight.

RESCHEDULED

The airline changed the flight’s time.

Note
flight_status describes the flight, not the journey of a passenger on it. It answers one question: has the airline cancelled or moved this flight. It never turns to a departed or arrived state. Departed has nothing behind it: no actual departure time is recorded anywhere, so there is nothing to derive it from. Arrival exists in the separate departure-control lifecycle, but it is a property of the flight rather than of your passenger, and this field does not expose it. Delays and re-routing are not represented at all. To tell a completed journey from an upcoming one, read pax_segment_status instead (see Pax segment status).
Note
flight_status is independent of the order status. A CANCELLED flight can coexist with a paid order, and after the disruption is processed the order, its items and the invoice are left as they were. Only the money moves. ticketed is the one exception. If the cancellation takes every flight on the order, ticketed turns false the moment the cancellation is processed, before any money moves, and it may turn back to true once the automatic refund settles. If it takes only some of the flights, ticketed stays true throughout. So do not read ticketed as "this order is still live" on a disrupted order. Detect the cancellation from flight_status, and reconcile the money from the wallet statement.
Important
When the airline cancels a flight, the affected segments of an already-paid partner order are refunded automatically, whenever the airline applies its involuntary-refund rule to them. You do not call Refund Order for them. Two cases produce no automatic refund: the airline may re-accommodate the passengers onto an alternative flight instead, and an order that was not yet paid has nothing to return. A Refund Order call that names those segments in pax_segment_ids is rejected with 409 ORDER_NOT_REFUNDABLE once they are refunding. A whole-order call (pax_segment_ids omitted) is not rejected. It silently skips the segments already refunding and refunds everything else that is still refundable. On a round trip whose other direction was not cancelled, that refunds the flight your passenger is still going to take. Never use the whole-order form to probe whether the automatic refund has happened, and do not read a 409 from it as that signal either. That form returns 409 ORDER_NOT_REFUNDABLE only when nothing on the order is refundable any more, including when the remaining segments are merely past the refund time limit before departure. The API does not call you back, and there is no partner-facing webhook, so detect the cancellation by polling flight_status and reconcile the money from the wallet statement (GET /distribution/v1/wallet/transactions). Treat flight_status: CANCELLED as something to reconcile, not something to act on.

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 map: price tiers + full per-seat cabin enumeration

GET

/distribution/v1/ssr/find

search:read

Pre-booking catalog of special assistance (SSR) offered on every flight of an itinerary

GET

/distribution/v1/rules

search:read

Machine-readable booking rules for 1–2 flights (passenger composition, age tiers, per-flight quotas)

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}/check-in

order:read

Online check-in state per flight and passenger

POST

/distribution/v1/orders/{orderId}/check-in

order:write

Check passengers in on one flight and assign their seats

GET

/distribution/v1/orders/{orderId}/boarding-pass?flight_id=

order:read

Boarding passes (IATA BCBP barcode data) of the checked-in passengers on one flight

GET

/distribution/v1/orders/{orderId}/boarding-pass/pdf?flight_id=

order:read

Download the boarding pass PDF of the checked-in passengers on one flight

GET

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

order:read

Get a read-only refund quote for the full order

POST

/distribution/v1/orders/{orderId}/exchanges/quote

order:read

Quote an exchange (preview delta + penalty) without committing

POST

/distribution/v1/orders/{orderId}/exchanges

order:write

Commit an exchange (leaves a delta invoice to settle)

POST

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

order:write

Settle an exchange delta from the partner wallet

POST

/distribution/v1/orders/{orderId}/exchanges/{invoiceId}/cancel

order:write

Cancel an unpaid exchange

POST

/distribution/v1/orders/{orderId}/infants

order:write

Add an infant to a paid order (leaves an invoice to settle)

POST

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

order:write

Settle an infant addition from the partner wallet

POST

/distribution/v1/orders/{orderId}/infants/{invoiceId}/cancel

order:write

Cancel an unsettled infant addition

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

Buy ancillaries for a paid order

POST

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

ancillary:write

Settle an ancillary addition from the partner wallet

POST

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

ancillary:write

Cancel an unsettled ancillary addition

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"]
}
Note
currency is the session’s settlement currency — the order, its invoice, and the wallet payment are all denominated in it (as are /search and /offer prices). It must be a currency the airline accepts for settlement (its domestic currency, or one of its configured payment currencies) and for which an exchange rate is available; otherwise minting is rejected with 400 CURRENCY_NOT_SUPPORTED. Payment settles against the partner’s wallet in that same currency, so for a non-domestic settlement currency the partner must already hold a wallet provisioned in it before an order can be paid — funding a foreign-currency wallet through the admin UI is not yet supported.

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. Each leg is priced in round-trip mode — if the airline configures a global round-trip discount it is already included in the per-leg total_price — 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 leg priced in round-trip mode, with the airline’s global round-trip discount — if configured — already included):

{
  "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. A bare offer_id carries no trip context, so current_price is always the one-way price: for a leg of a round trip it excludes the airline’s global round-trip discount (if configured) that round-trip /search and the order apply. 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. For an offer taken from a round-trip search pass "round_trip": true — the breakdown then includes the airline’s global round-trip discount (if configured) and matches the per-leg /search total_price; without it the offer is priced one-way. 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 },
  "round_trip": false
}

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 — 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 indicative only: the airline’s global round-trip discount (if configured) makes the round-trip order cheaper than two one-ways, while the shared-`fare_frame_id pairing rule of round-trip /search can push the cheapest bookable pair above the two independent minima.

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 full cabin map for a flight, before any order exists: the seat-selection price tiers (tiers — tier type, cabin, the currently-sellable seat numbers in the tier, price, and a render colour) plus a per-seat enumeration of the whole cabin (seats, DEL-2297). Requires scope search:read. Prices are denominated in the partner session currency.

Note
In tiers, already-taken seats — assigned in DCS or held by a pending (unpaid) order — are excluded, so only currently-sellable seats are listed, and a seat sold by more than one tier appears once, in its cheapest tier. seats is the physical complement: every seat of the cabin with its live status; selling stays expressed by tiers — join the two by seat number.

seats[] semantics:

  • status is the seat’s physical state: AVAILABLE, OCCUPIED (assigned in DCS or held by a pending unpaid order — occupancy is time-varying, a held seat frees up when the order expires), or BLOCKED (blocked by the airline in the aircraft configuration).

  • To buy a seat listed in a paid tier, send that tier’s offer_frame_id in ancillary_offers plus the matching seat_assignment on POST /orders; a seat in a zero-price tier or in no tier at all is requested with a bare seat_assignment. A seat whose paid tiers are all temporarily sold out appears AVAILABLE but outside every tier and is rejected at order creation — re-fetch the seat map and pick again.

  • restrictions (omitted when empty) lists passenger types the airline does not seat there: a seat with ["CHD","INF"] cannot be assigned to a child or an infant (enforced at order/check-in time).

  • seats may be absent when aircraft-configuration data is temporarily unavailable — tiers remains authoritative for selling.

Tip
Airlines: rows not intended for sale (e.g. crew rest) should be blocked in the aircraft configuration (BLOCKED_TO_ALL) — an unmapped AVAILABLE seat is requestable free of charge.

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"],
      "color": "#00FF00"
    }
  ],
  "seats": [
    { "number": "1A", "status": "AVAILABLE" },
    { "number": "1B", "status": "OCCUPIED" },
    { "number": "2B", "status": "BLOCKED" },
    { "number": "2C", "status": "AVAILABLE", "restrictions": ["CHD"] }
  ]
}

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

Special Assistance Catalog

Lists the special assistance (SSR — wheelchair, blind or deaf passenger, infant bassinet, service animal, etc.) offered on every flight in flight_ids, before any order exists. Requires scope search:read. Special assistance is free of charge — there is no price, currency or quantity.

Pass all flights of the itinerary in one call: only options available on each of them are listed. Availability depends on the time left before departure, so an option can disappear between this call and Create Order. Select options per passenger in Create Order (ssrs).

HTTP Request

GET /distribution/v1/ssr/find?flight_ids=3c4d5e6f-7a8b-9012-cdef-0123456789ab,4e5f6a7b-8c9d-0123-ef01-23456789abcd
Authorization: Bearer <access_token>

Response

200 OK:

{
  "ssr_list": [
    {
      "ssr_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeee01",
      "marketing_name": "Wheelchair",
      "paxes": ["ADT", "CHD"],
      "assistance": "WHEELCHAIR_SERVICE"
    },
    {
      "ssr_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeee03",
      "marketing_name": "Bassinet",
      "paxes": ["INF"],
      "assistance": "BASSINET"
    }
  ]
}

paxes lists the passenger types the option can be selected for; assistance is the assistance type. ssr_list may be empty.

Failure cases: 400 INVALID_REQUEST (flight_ids missing or non-UUID, or an unknown flight).

Rules

Returns the machine-readable booking rules the partner should enforce client-side, so a booking form validates against the same source of truth the API does instead of hardcoding constants. The rules object is an extensible envelope: it will grow new named sections (and existing sections new fields) over time — clients MUST ignore unknown keys, and an absent section means "not published", never "no constraint". Breaking changes ship only under a new API version. Requires scope search:read.

The endpoint is flight-scoped: supply one flight_id (one-way) or two (round trip) — flight_id is required. It returns the passenger-composition rules (max_pax, adult_required, infant_per_adult); pax_types — the ADT/CHD/INF age boundaries resolved for the journey’s combined direction (any international leg makes the whole journey international, which can widen or narrow the child/infant bands; use them to validate passenger dates of birth, with age_reference naming the date the age is taken at — the outbound departure); and each flight’s special-quota ceilings in context.flights[].quotas.

Note
Per-adult ratios. infant_per_adult is 1 (inf ⇐ adt). There is no child-per-adult ratio — children are bounded only by max_pax and seat availability (so one adult may travel with up to eight children). Infants count toward max_pax but occupy no seat (occupies_seat: false).
Note
Unaccompanied minors. An unaccompanied minor is not a bookable passenger type — there is no UM pax type or flag in the request, and pax_types never lists UNN. Because every booking must contain at least one adult (adult_required), the minimum age to travel without an accompanying adult equals the airline’s ADT age_min in pax_types. Caveat: a lone passenger whose age falls in the airline’s UM band (which may overlap the adult band — e.g. 12–15) can be booked as the required adult; the airline then applies its own unaccompanied-minor handling (the per-flight UNN quota in context.flights[].quotas and a departure-time window) after the order is created, and that handling is not surfaced in this API.
Note
Per-flight special quotas. Each flight carries configured special-quota ceilings by passenger type, reported as context.flights[].quotas (e.g. { "ADT": …, "CHD": …, "INF": …, "UNN": … }; a type appears only when the flight configures it). These are the configured limits, independent of seat availability, not live remaining availability. On the partner path the infant (INF) quota is the enforced one: a flight that cannot fit your infants is omitted from /search, and POST /orders returns 409 OFFER_SOLD_OUT when infant capacity is exhausted, even after a successful hold (holds do not reserve infant capacity). The other quotas are the airline’s operational ceilings and are not separately enforced on the partner booking path.

HTTP Request

GET /distribution/v1/rules?flight_id={outbound}
GET /distribution/v1/rules?flight_id={outbound}&flight_id={inbound}
Authorization: Bearer <access_token>

Response

The call returns 200 OK (age boundaries and quota values below are illustrative — they come from the airline’s configuration for the resolved flights):

{
  "rules": {
    "pax_composition": {
      "max_pax": 9,
      "adult_required": true,
      "infant_per_adult": 1,
      "age_reference": "OUTBOUND_DEPARTURE_DATE",
      "pax_types": [
        { "code": "ADT", "age_min": 12, "age_max": 99, "occupies_seat": true },
        { "code": "CHD", "age_min": 2,  "age_max": 11, "occupies_seat": true },
        { "code": "INF", "age_min": 0,  "age_max": 1,  "occupies_seat": false }
      ]
    }
  },
  "context": {
    "direction": "INTERNATIONAL",
    "flights": [
      { "flight_id": "aaaaaaaa-1111-2222-3333-444444444444", "flight_number": "FZ100", "quotas": { "ADT": 180, "CHD": 20, "INF": 20, "UNN": 5 } },
      { "flight_id": "bbbbbbbb-1111-2222-3333-444444444444", "flight_number": "FZ101", "quotas": { "ADT": 180, "CHD": 20, "INF": 12, "UNN": 5 } }
    ]
  }
}

Failure cases: 400 INVALID_REQUEST (missing flight_id, more than two values, duplicate ids, or a non-UUID flight_id), 404 FLIGHT_NOT_FOUND (a supplied flight_id does not exist).

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, 400 INVALID_RETURN_PAIRING.

Create Order

Creates an order from a previously obtained hold. For a round-trip hold the order covers both legs — total_amount is the whole trip: the sum of the per-leg /search prices, plus order-level fees if the airline configures them, and possibly a currency-rounding residual on non-domestic settlement currencies — as everywhere in this API. 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.

Special assistance

Each passengers[] entry may include ssrs (optional, default empty): the special assistance chosen for that passenger, one entry per flight of the itinerary, with ssr_id from Special Assistance Catalog and flight_id from Search Flights (offers[].flight_legs[].flight_id). Special assistance is free of charge and does not change total_amount. It is recorded on order creation, before payment, and appears in List Assistance under selected.

{
  "first_name": "John",
  "last_name": "Doe",
  "date_of_birth": "1990-01-01",
  "ssrs": [
    { "flight_id": "3c4d5e6f-7a8b-9012-cdef-0123456789ab", "ssr_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeee01" },
    { "flight_id": "4e5f6a7b-8c9d-0123-ef01-23456789abcd", "ssr_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeee01" }
  ]
}
  • Assistance applies to the passenger’s whole itinerary: an option sent for any of the passenger’s flights is recorded on all of them. Send one entry per flight.

  • An ssr_id that is unknown, inactive or no longer offered on every flight of the itinerary is skipped without an error, and so is an entry whose flight_id is not part of the itinerary. Check selected in List Assistance after the order is created.

  • An option whose paxes excludes the passenger’s type (for example BASSINET for an adult) rejects the whole request with 400 INVALID_REQUEST. No order is created and the hold can be used again.

  • An infant can receive only BASSINET. Send at most one option per infant: this limit is not checked at order creation.

  • An order carrying special assistance cannot be exchanged through the API: Quote Exchange and Commit Exchange return 409 EXCHANGE_NOT_ELIGIBLE.

  • Creating the order consumes the hold, so repeating the call after a successful create returns 404 HOLD_NOT_FOUND, whatever ssrs it carries; the selection cannot be changed by resending the request.

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, 409 OFFER_SOLD_OUT (the held offer sold out before the order could be created — the hold is a soft reservation, not an inventory lock; search again and re-hold), 410 OFFER_NOT_AVAILABLE (the flight closed for sale between hold and create), 503 SERVICE_UNAVAILABLE (safe to retry with the same Idempotency-Key). 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. Special assistance whose paxes excludes the passenger’s type also returns 400 INVALID_REQUEST (see Special assistance).

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": 6000.00,
  "ticketed": false,
  "contact": { "name": "John Doe", "phone": "+1234567890", "email": "john@example.com" },
  "items": [
    {
      "item_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
      "type": "FLIGHT",
      "status": "ACTIVE",
      "description": "Flight f1600cfb… for ALMANBET ZHUMABAI UULU",
      "amount": 0.00,
      "hold_expires_at": null,
      "leg_net_amount": 6000.00,
      "pax_segment_id": "0d0f6f31-1b8e-4a3d-9c7a-2f9a1c4e5b60",
      "flight_id": "f1600cfb-6daf-4a04-ba7d-c8f0f0621f11",
      "flight_status": "SCHEDULED",
      "passenger_id": "5f2c1a70-9d3b-4e21-8a55-6b7c8d9e0f12",
      "pax_segment_status": "ISSUED"
    },
    {
      "item_id": "1b2c3d4e-5f60-7182-93a4-b5c6d7e8f901",
      "type": "FLIGHT",
      "status": "ACTIVE",
      "description": "FARE component",
      "amount": 500.00,
      "hold_expires_at": null,
      "linked_item_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"
    },
    {
      "item_id": "2c3d4e5f-6071-8293-a4b5-c6d7e8f90123",
      "type": "FLIGHT",
      "status": "ACTIVE",
      "description": "POOL_OFFER component",
      "amount": 5500.00,
      "hold_expires_at": null,
      "linked_item_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"
    }
  ],
  "invoice": {
    "invoice_id": "1a2b3c4d-5e6f-7081-9203-a4b5c6d7e8f9",
    "status": "ISSUED",
    "amount": 6000.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
  }
}
Note
A flight is represented as a flight-leg anchor row plus its price-component rows — all type: FLIGHT. The anchor carries leg_net_amount (fare + pool offer − discounts, the leg price to show the passenger) and no linked_item_id; its own amount is 0. Each price-component row instead carries linked_item_id pointing back to its anchor and no leg_net_amount. So among type: FLIGHT rows the anchor is the one with leg_net_amount (equivalently, the one without linked_item_id) — that is the exchangeable leg. Pass only anchors into an exchange’s items_to_replace; passing a price-component row is rejected with 409 (order.exchange.item-not-anchor) — distinct from a malformed request (INVALID_REQUEST) or an unavailable offer.
Note
The flight-leg anchor carries four fields the price-component rows omit. pax_segment_id is the pax-segment this leg books. It is the id the order-management endpoints address segments by: Refund Order takes a pax_segment_ids array, while Add Ancillaries and Add Assistance take one pax_segment_id per row. flight_id is the inventory flight the leg is booked on. flight_status is the operational status of that flight, SCHEDULED normally, or CANCELLED / RESCHEDULED on an airline-driven disruption (IROP); poll it to detect one. It never reports departure or arrival, because it describes the flight rather than the journey of a passenger on it — see Flight status. passenger_id and pax_segment_status answer the passenger question instead, and are described in Pax segment status.
Note
pax_segment_id comes from the order itself, while passenger_id and pax_segment_status are read from the booking. If that read fails, the last two are omitted and the first is still there, so an anchor with a pax_segment_id and no pax_segment_status means "we could not read it", not "this passenger did not travel".
Note
ticketed tells you whether e-tickets have been issued for the order’s current flight segments. Payment does not guarantee it: Confirm Wallet Payment can return 202 with ticketing deferred to the PAYMENT_CAPTURED webhook, so status: ACTIVATED and invoice.status: PAID can appear while ticketed is still false. Poll this field instead of probing Get Order Ticket (which returns 409 TICKET_NOT_AVAILABLE until issuance); once ticketed is true, fetch the e-ticket numbers there. ticketed reflects the order’s current flight legs — it is true only while their booking is paid and e-tickets are assigned, so it stays false before issuance, for an airline that does not issue e-tickets, and after the booking is cancelled or refunded.

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>

Online Check-In and Boarding Pass

Partners can check their passengers in online and hand out boarding passes, with the same check-in window and rules as the airline’s own website. The order must be paid and ticketed (the gate of Get Order Ticket). Every flight of the order is checked in separately.

The usual flow:

  1. Get Check-In State shows, per flight, whether online check-in is open and which passengers are checked in.

  2. Pick a seat per passenger from Flight Seat Map (scope search:read): its seats[].status shows which seats are free right now.

  3. Check In Passengers checks passengers in on one flight and assigns their seats.

  4. Get Boarding Passes returns the barcode data, or Get Boarding Pass PDF the printable document.

Important
A business refusal (check-in not open, seat taken, order not ticketed, …​) is answered with 200 and an error envelope (code, message), not with a 409. Check the body for code before reading it as a success. 404 is reserved for references that do not match the order: an unknown or foreign order (ORDER_NOT_FOUND), a boarding pass request whose flight_id is not a flight of the order (FLIGHT_NOT_FOUND), or a check-in request naming a passenger_id that is not on that flight of the order — including a passenger of the order who flies only on another flight, and every passenger when the flight_id itself is not a flight of the order (PASSENGER_NOT_ON_FLIGHT). The codes are listed under Error Format.

Get Check-In State

Returns, per flight of the order, the online check-in window and, per passenger, the check-in status and seat. Requires scope order:read.

check_in_window is one of OPEN, NOT_OPENED_YET, CLOSED, FLIGHT_CANCELED. Passenger status is one of NOT_CHECKED_IN, CHECKED_IN, BOARDED. seat_number is null until the passenger has a seat.

Failure cases: 404 ORDER_NOT_FOUND; refusal 200 TICKET_NOT_AVAILABLE.

HTTP Request

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

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "flights": [
    {
      "flight_id": "11111111-1111-1111-1111-111111111111",
      "check_in_window": "OPEN",
      "passengers": [
        {
          "passenger_id": "33333333-3333-3333-3333-333333333333",
          "first_name": "John",
          "last_name": "Doe",
          "status": "NOT_CHECKED_IN",
          "seat_number": null
        }
      ]
    }
  ]
}

Check In Passengers

Checks passengers in on one flight of the order and assigns each one a seat. Requires scope order:write.

  • A passenger can take a free seat, a paid seat they already bought, or a paid seat the airline’s automatic seating set aside for them. Any other paid seat is refused with PAID_SEAT_NOT_PURCHASED. Paid seats are sold at order creation (see Create Order seat_assignments).

    The API does not expose the seat automatic seating set aside, so the same paid seat may be accepted for one passenger and refused for another.

  • An infant is checked in with the seat number of the accompanying adult, in the same request or after the adult.

  • Each passenger_id may appear only once per request.

  • No email is sent to the passenger. Hand the boarding pass over yourself.

Failure cases: 400 INVALID_REQUEST (empty passengers, a blank seat_number, or a repeated passenger_id), 404 ORDER_NOT_FOUND, 404 PASSENGER_NOT_ON_FLIGHT (also when flight_id is not a flight of the order); refusals 200 with TICKET_NOT_AVAILABLE, CHECK_IN_NOT_OPEN, CHECK_IN_CLOSED, FLIGHT_CANCELED, SEAT_UNAVAILABLE, SEAT_NOT_ELIGIBLE, PAID_SEAT_NOT_PURCHASED, PASSENGER_NOT_ELIGIBLE. A refusal checks no one in: the request applies to all its passengers or to none.

HTTP Request

POST /distribution/v1/orders/{orderId}/check-in
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "flight_id": "11111111-1111-1111-1111-111111111111",
  "passengers": [
    { "passenger_id": "33333333-3333-3333-3333-333333333333", "seat_number": "12A" }
  ]
}

Response

{
  "flight_id": "11111111-1111-1111-1111-111111111111",
  "passengers": [
    { "passenger_id": "33333333-3333-3333-3333-333333333333", "seat_number": "12A" }
  ]
}

Refusal

{
  "code": "SEAT_UNAVAILABLE",
  "message": "The seat 12A is occupied"
}

Get Boarding Passes

Returns one boarding pass per checked-in passenger on one flight of the order. barcode is the IATA BCBP (Bar Coded Boarding Pass) data string to render as a PDF417 barcode. Requires scope order:read.

Failure cases: 404 ORDER_NOT_FOUND, 404 FLIGHT_NOT_FOUND (the flight is not in the order); refusals 200 with TICKET_NOT_AVAILABLE, PASSENGER_NOT_CHECKED_IN, BOARDING_PASS_NOT_AVAILABLE.

HTTP Request

GET /distribution/v1/orders/{orderId}/boarding-pass?flight_id=11111111-1111-1111-1111-111111111111
Authorization: Bearer <access_token>

Response

{
  "flight_id": "11111111-1111-1111-1111-111111111111",
  "boarding_passes": [
    {
      "passenger_id": "33333333-3333-3333-3333-333333333333",
      "barcode": "M1DOE/JOHN            EABC123 ALATSEFA 0101 275Y012A0001 100"
    }
  ]
}

Get Boarding Pass PDF

Renders the boarding pass PDF of the checked-in passengers on one flight of the order — the same document the airline’s website issues, one page per passenger. Requires scope order:read.

The response body is the PDF itself: Content-Type: application/pdf and Content-Disposition: attachment with the filename boarding-pass-<order number>.pdf. Error responses and refusals remain JSON (DistributionErrorResponse).

Failure cases: 404 ORDER_NOT_FOUND, 404 FLIGHT_NOT_FOUND; refusals 200 (JSON) with TICKET_NOT_AVAILABLE, PASSENGER_NOT_CHECKED_IN.

HTTP Request

GET /distribution/v1/orders/{orderId}/boarding-pass/pdf?flight_id=11111111-1111-1111-1111-111111111111
Authorization: Bearer <access_token>

Quote Exchange

Prices a single-leg, single-direction exchange of the FLIGHT items in items_to_replace onto a pool offer (pool_offer_id / fare_frame_id) and returns the extra amount (delta) and change penalty the customer would owe — WITHOUT committing the exchange and WITHOUT persisting anything (pricing runs in a rolled-back transaction). Requires scope order:read. Use it to preview an exchange before committing.

Pass the returned delta back as expected_delta when committing the exchange, so the commit is rejected if the price moved in between. A downgrade prices to a delta of 0 — the fare drop is forfeited, not refunded, and that lost amount is reported as forfeited_amount so you can show it to the passenger before they confirm. An involuntary exchange — the airline cancelled or rescheduled the flight — is free of penalty and delta: involuntary is true and free_reason names why (ALTERNATIVE_FLIGHT or INVOLUNTARY_FULL). valid_until is advisory; the commit re-checks the price regardless.

A malformed request — items_to_replace empty, listing the same item twice, or naming an item that is not on the order — returns 400 INVALID_REQUEST. An unknown or foreign order returns 404 ORDER_NOT_FOUND. An exchange the engine cannot make — multi-leg or multi-direction, exchanging only some passengers of a booking, ancillaries on the changing leg, special assistance on a changing or carried leg, past the change cut-off, and similar — returns 409 EXCHANGE_NOT_ELIGIBLE; a target offer that is sold out, no longer available, or over its subquota returns 409 OFFER_SOLD_OUT / OFFER_NOT_AVAILABLE / SUBQUOTA_EXCEEDED. All amounts are in the order’s currency, echoed as currency.

HTTP Request

POST /distribution/v1/orders/{orderId}/exchanges/quote
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "items_to_replace": ["a1b2c3d4-e5f6-7890-1234-56789abcdef0"],
  "pool_offer_id": "b2c3d4e5-f6a7-8901-2345-6789abcdef01",
  "fare_frame_id": "c3d4e5f6-a7b8-9012-3456-789abcdef012"
}

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "delta": 33.00,
  "penalty": 5.00,
  "forfeited_amount": 0.00,
  "involuntary": false,
  "free_reason": null,
  "currency": "USD",
  "valid_until": "2026-08-05T12:00:00Z"
}

Commit Exchange

Commits the exchange previewed by Quote Exchange. Pass the quoted delta and penalty back as expected_delta and expected_penalty: if the live price no longer matches — because inventory or fares moved since the quote — the commit is rejected with 409 EXCHANGE_PRICE_CHANGED, and the partner should re-quote. Requires scope order:write. The Idempotency-Key header is required; repeating a commit with the same key replays the original result rather than exchanging twice — but while the original commit is still in flight, a repeat with the same key returns 409 IDEMPOTENCY_CONFLICT.

On success the exchange is committed and the new booking lands awaiting the delta payment. The response carries status (PENDING_PAYMENT while a delta invoice is outstanding, or COMMITTED when nothing is payable) and, when pending, the invoice_id, amount_to_pay (which may be 0), and payment_intent_id. Settle it with Settle Exchange Payment against that invoice_id — the exchange delta invoice is settled by that dedicated endpoint, not by the initial-purchase Confirm Wallet Payment. The same eligibility and availability rejections as Quote Exchange apply (EXCHANGE_NOT_ELIGIBLE, OFFER_SOLD_OUT, OFFER_NOT_AVAILABLE, SUBQUOTA_EXCEEDED).

HTTP Request

POST /distribution/v1/orders/{orderId}/exchanges
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff

Request Body

{
  "items_to_replace": ["a1b2c3d4-e5f6-7890-1234-56789abcdef0"],
  "pool_offer_id": "b2c3d4e5-f6a7-8901-2345-6789abcdef01",
  "fare_frame_id": "c3d4e5f6-a7b8-9012-3456-789abcdef012",
  "expected_delta": 33.00,
  "expected_penalty": 5.00
}

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "PENDING_PAYMENT",
  "invoice_id": "e5f6a7b8-c9d0-1234-5678-9abcdef01234",
  "amount_to_pay": 33.00,
  "payment_intent_id": "f6a7b8c9-d0e1-2345-6789-abcdef012345",
  "involuntary": false,
  "free_reason": null
}

Settle Exchange Payment

Debits the partner wallet for the exchange delta invoice returned by Commit Exchange and finalizes the exchanged booking. invoice_id is the one from the commit response; payment_reference doubles as the idempotency key and is stored against the wallet transaction for reconciliation. A 0-amount delta is settled the same way. Requires scope order:write.

If the wallet is debited but ticketing cannot complete synchronously the response is 202 with status: TICKETING_DEFERRED and the PAYMENT_CAPTURED webhook finalizes the booking. In the rarer case where the wallet is debited but the exchange cannot be finalized and cannot be safely deferred, the response is 500 WALLET_SETTLEMENT_INCONSISTENT and the debit is flagged for reconciliation (the exchange is not activated). An insufficient wallet limit returns 402 INSUFFICIENT_WALLET_LIMIT; an already-settled or non-payable delta returns 409.

HTTP Request

POST /distribution/v1/orders/{orderId}/exchanges/{invoiceId}/payments/confirm
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "payment_reference": "psp-pay-9f12ab34"
}

Response

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

Cancel Exchange

Cancels an exchange whose delta invoice has not been paid, restoring the original booking. invoice_id is the exchange delta invoice from the commit response. Requires scope order:write and a required Idempotency-Key header. A paid or already-cancelled exchange returns 409 EXCHANGE_NOT_CANCELLABLE; an unknown order or invoice returns 404.

HTTP Request

POST /distribution/v1/orders/{orderId}/exchanges/{invoiceId}/cancel
Authorization: Bearer <access_token>
Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff

Response

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

Add Infant

Adds a lap infant to a paid order. The infant is priced on the flights the order’s first adult flies, and the call issues an invoice for it; the infant travels once that invoice is settled with Settle Infant Addition Payment. Requires scope order:write and a required Idempotency-Key header.

The body is the passenger object of Create Order with the same field names and validation. The passenger type is not sent: an added passenger is always an infant, and a date of birth that is not an infant’s on the departure date of the adult’s first flight returns 400 INVALID_REQUEST. seat_assignments, ancillary_offers and ssrs are rejected with 400 INVALID_REQUEST rather than silently dropped.

HTTP Request

POST /distribution/v1/orders/{orderId}/infants
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff

Request Body

{
  "first_name": "ALI",
  "last_name": "UULU",
  "date_of_birth": "2026-03-01",
  "gender": "MALE",
  "citizenship": "KG",
  "document_type": "BIRTH_CERTIFICATE",
  "document_number": "KG123456",
  "document_expiry_date": null
}

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "PENDING_PAYMENT",
  "invoice_id": "88888888-8888-8888-8888-888888888888",
  "invoice_amount": 45.00,
  "currency": "KGS",
  "payment_intent_id": "99999999-9999-9999-9999-999999999999",
  "payment_due_date": "2026-09-16T12:30:00Z"
}

status is the state of the addition invoice, not of the order: PENDING_PAYMENT while it awaits settlement, COMMITTED once it is paid — by this call itself when the infant is free, or by an earlier Settle Infant Addition Payment when the call is a replay. A replay after the addition was cancelled or expired reads CANCELLED or EXPIRED. The price appears only here — there is no preview endpoint — so a partner that does not accept it cancels the addition with Cancel Infant Addition; an untouched invoice expires on its own shortly after payment_due_date (the offer’s payment time limit, the same window a new order gets) and releases the seat hold. Expiry runs on a sweep, so the invoice still reads ISSUED for up to a minute past the deadline.

Replaying the same Idempotency-Key returns the original invoice. While the original call is still in flight, the repeat answers 409 IDEMPOTENCY_CONFLICT. The eligibility checks run again before the replay is recognised, so a flight that closed for sale in the meantime answers 409 OFFER_NOT_AVAILABLE instead of replaying.

Important
An Idempotency-Key is scoped to the airline, not to the order, the partner or the endpoint. Generate a fresh key for every mutating call and never reuse one. A reused key protects nothing: depending on which call it was spent on, the repeat either answers 409 IDEMPOTENCY_CONFLICT and changes nothing — a key spent on another infant addition — or is executed as a brand-new call, adding a second infant, when it was spent on a call whose result cannot be replayed here, such as Cancel Infant Addition or Commit Exchange.

Limits of this version

  • One infant per call. Repeat the call for a second infant, on an order that still has an adult free.

  • An infant needs an adult of its own: the airline carries at most one lap infant per adult, counting infants already on the order and infants whose addition is still awaiting payment. Otherwise 409 INFANT_ADDITION_NOT_ELIGIBLE.

  • Orders not in the airline’s domestic currency are refused with 409 INFANT_ADDITION_NOT_ELIGIBLE.

  • Special assistance cannot be requested for an added infant — neither in this call nor through Add Assistance, which addresses the order’s original booking only.

  • An added infant does not appear in Get Order Ticket or Get Order Ticket PDF. It does appear in Get Order as its own items[].

  • While the addition is unsettled, invoice in Get Order is the addition invoice, not the order’s original one. Identify it by the invoice_id returned here.

  • While the addition is unsettled, Get Order counts it: total_amount already includes the infant’s price, and ticketed reads false because the order carries a second, still-unpaid booking. Neither changes back on its own — settle the invoice or let it expire. Get Order Ticket keeps answering 200 for the passengers of the original booking throughout.

  • Committing the addition e-mails the order’s contact — a request to pay for the infant, or a confirmation when the infant is free — the same messages the airline’s own site sends. There is no partner-facing webhook for it.

  • On an order whose flights were exchanged, the infant is added to the booking the exchange produced, not to the replaced one.

  • A partial Refund Order must not mix the adult’s and the added infant’s pax_segment_ids: the infant’s segments sit on their own booking, and a request spanning two bookings answers 409 ORDER_NOT_REFUNDABLE. Refund them in separate calls.

  • Commit Exchange moves only the segments it names. An infant added afterwards stays on the original flight, and when the exchanged adult was the only one, the infant is left on that flight alone — the airline sees it as an infant without an adult. Do not exchange an order that carries an added infant.

  • Order ownership expires 30 days after creation; an older order answers 404 ORDER_NOT_FOUND.

Settle Infant Addition Payment

Settles an infant addition invoice from the partner wallet and issues the infant. invoice_id is the one returned by Add Infant, and only this endpoint settles it: Confirm Wallet Payment and Settle Exchange Payment answer 409 ORDER_NOT_PAYABLE for it, as this one does for their invoices. payment_reference doubles as the idempotency key of the wallet debit; sending it again after a successful settlement answers 409 ORDER_ALREADY_PAID. A free infant needs no settlement — its addition already answered COMMITTED. Requires scope order:write.

202 means the wallet was debited but issuing is still in flight; it completes on the payment webhook, and Get Order shows the infant’s pax segments as ISSUED once it does. A 500 WALLET_SETTLEMENT_INCONSISTENT means the wallet was debited and the addition could not be finalized — do not retry, contact support with order_id and invoice_id.

Failure cases: 400 INVALID_REQUEST (missing, blank or malformed payment_reference), 402 INSUFFICIENT_WALLET_LIMIT, 404 ORDER_NOT_FOUND, 409 ORDER_NOT_PAYABLE (the invoice is cancelled, expired, or not an infant addition awaiting payment), 502 WALLET_PAYMENT_FAILED.

HTTP Request

POST /distribution/v1/orders/{orderId}/infants/{invoiceId}/payments/confirm
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "payment_reference": "psp-pay-9f12ab34"
}

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "ACTIVATED",
  "transaction_ids": ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]
}

Cancel Infant Addition

Cancels an infant addition whose invoice has not been settled: the invoice is voided and the seat hold released. The infant’s own booking is closed, and the order returns to what it was before the addition. Requires scope order:write and a required Idempotency-Key header.

A settled or expired addition, or one on an order that is no longer ACTIVATED, returns 409 INFANT_ADDITION_NOT_CANCELLABLE; an invoice of the order that is not an infant addition — the purchase invoice, an exchange delta or an ancillary addition — returns 400 INVALID_REQUEST; an unknown order or invoice returns 404 ORDER_NOT_FOUND. Repeating the call with the same key on the same invoice returns 200. An Idempotency-Key already spent on another invoice of this order returns 409 IDEMPOTENCY_CONFLICT and cancels nothing. A key already spent on a different kind of operation, such as Add Infant, cannot be replayed here at all and answers 500 INTERNAL_ERROR without cancelling anything. Use a fresh key per operation; a reused one is never a safety net.

HTTP Request

POST /distribution/v1/orders/{orderId}/infants/{invoiceId}/cancel
Authorization: Bearer <access_token>
Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff

Response

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

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. All amounts are expressed in the order’s currency.

The non_refundable_reason field carries a machine-readable cause when a refund is blocked by the departure time cutoff — currently the only value, TIME_LIMIT_ELAPSED — and is null otherwise. Beyond the invoice and booking checks above, the quote applies the SAME departure time cutoff as POST …​/refund: once the voluntary-refund window before departure has elapsed, the quote returns 200 with non_refundable_reason: "TIME_LIMIT_ELAPSED" and the amount fields still describing what WOULD have been refundable, instead of promising an amount that the refund would then reject with 409 ORDER_NOT_REFUNDABLE. It captures the time cutoff specifically, not every non-refundable state: an order with no PAID invoice is still rejected by the checks above with 409 ORDER_NOT_REFUNDABLE, while an already-refunded order returns a quote with refundable_amount of 0 and non_refundable_reason of null. Treat a positive refundable_amount together with non_refundable_reason == null as the go-ahead. Involuntary refunds (airline-driven cancel or reschedule) are not blocked by this cutoff.

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,
  "non_refundable_reason": null,
  "involuntary": false
}

When the voluntary-refund window before departure has closed, the same call returns the computed amounts but marks the order non-refundable:

{
  "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,
  "non_refundable_reason": "TIME_LIMIT_ELAPSED",
  "involuntary": false
}

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. 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), and items[].status stays ACTIVE, so the order aggregate never shows the refund. What does move is pax_segment_status on the refunded legs, from REFUNDING to REFUNDED (see Pax segment status). Read that to see which passengers and legs a refund touched. It does not tell you the money landed: the wallet statement (GET /distribution/v1/wallet/transactions) remains the source of truth for settlement, and the response status above is what you poll for completion.

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.

Note
available is empty until the booking is activated (i.e. until the order is paid) — ancillaries are offered for addition only post-payment. Services wanted before payment go into POST /orders (ancillary_offers) and are paid together with the flight.

Three things this catalog does not tell you, each of which Add Ancillaries can still refuse on:

  • selected lists only ancillaries that are provisioned. An addition whose invoice is still unsettled is not there — it appears once Settle Ancillary Addition Payment succeeds.

  • available does not apply the check-in and departure rule, so it keeps listing frames for a segment that can no longer be sold to. Such a purchase answers 409 ANCILLARY_ADDITION_NOT_ELIGIBLE.

  • There is no per-passenger allowance in the payload. How many of a frame a passenger may still buy is discovered by buying: over the allowance answers 409 ANCILLARY_LIMIT_EXCEEDED.

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

Buys ancillaries for one passenger segment of a paid order and issues an invoice for them. The ancillaries are provisioned once that invoice is settled from the partner wallet with Settle Ancillary Addition Payment; an unsettled invoice expires at payment_due_date and releases the inventory hold. Requires scope ancillary:write and an Idempotency-Key header.

Ancillaries are added only after the order is paid — an order still awaiting payment returns 409 ORDER_NOT_ACTIVATED. Services wanted before payment go into POST /orders (ancillary_offers) and are paid together with the flight.

Offer the frames from List Ancillaries for that pax segment: a frame the catalog does not list for it returns 409 OFFER_NOT_AVAILABLE — including an offer whose per-flight quota ran out, since the catalog stops listing it — and a quantity above the remaining per-passenger allowance returns 409 ANCILLARY_LIMIT_EXCEEDED. The price is the catalog price of the frame; the server computes the total itself, so no expected amount is sent.

HTTP Request

POST /distribution/v1/orders/{orderId}/ancillaries
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff

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",
  "status": "PENDING_PAYMENT",
  "invoice_id": "88888888-8888-8888-8888-888888888888",
  "invoice_amount": 15.00,
  "currency": "USD",
  "payment_intent_id": "99999999-9999-9999-9999-999999999999",
  "payment_due_date": "2026-09-17T12:30:00Z"
}

status is the state of the addition invoice, not of the order: PENDING_PAYMENT while it awaits settlement, COMMITTED once it is paid — by this call itself when the ancillary is free, or by an earlier Settle Ancillary Addition Payment when the call is a replay. A replay after the addition was cancelled or expired reads CANCELLED or EXPIRED. The price appears only here — there is no preview endpoint — so a partner that does not accept it cancels the addition with Cancel Ancillary Addition; an untouched invoice expires on its own shortly after payment_due_date and releases the inventory hold. Expiry runs on a sweep that ticks once a minute, so the invoice still reads ISSUED for up to a minute past the deadline. Do not read that as extra time to pay: treat payment_due_date as the deadline.

Replaying the same Idempotency-Key returns the original invoice: as PENDING_PAYMENT while it awaits settlement, and as CANCELLED or EXPIRED once it has been cancelled or has expired. While the original call is still in flight, the repeat answers 409 IDEMPOTENCY_CONFLICT.

Caution
The eligibility checks run before the replay is recognised, so a replay is only reliable while the addition is unsettled. Once the invoice is settled the ancillaries are provisioned, those units count against the per-passenger allowance, and the very same call that succeeded now answers 409 ANCILLARY_LIMIT_EXCEEDED rather than echoing the settled invoice. The same happens with 409 OFFER_NOT_AVAILABLE when the offer stopped being sold in the meantime. Treat the 200 you got the first time as the record of the purchase; do not use a replay to check whether it went through — read Get Order instead.
Important
An Idempotency-Key is scoped to the airline, not to the order, the partner or the endpoint. Generate a fresh key for every mutating call and never reuse one. A reused key protects nothing: depending on which call it was spent on, the repeat either answers 409 IDEMPOTENCY_CONFLICT and changes nothing, or is executed as a brand-new call, buying the ancillary twice.

Limits of this version

  • One pax segment per call, under its own passenger. A bag on both legs of a round trip is two calls and two invoices; a body naming two pax_segment_id values answers 400 INVALID_REQUEST, and so does one naming two passenger_id values or a passenger_id that does not own the segment. passenger_id is checked, not decorative.

  • Offers that carry taxes or charges are refused with 409 ANCILLARY_ADDITION_NOT_ELIGIBLE. The addition bills the offer price alone, so an offer whose catalog price exceeds its base price cannot be sold here without under-charging the airline.

  • Not after check-in or departure. Once the passenger segment is checked in, boarded, flown, or its flight has left, the call answers 409 ANCILLARY_ADDITION_NOT_ELIGIBLE. List Ancillaries does not apply that rule and keeps listing the frames, so a listed frame is not a promise that it can still be bought.

  • Only orders in the airline’s home currency — others answer 409 ANCILLARY_ADDITION_NOT_ELIGIBLE.

  • The addition invoice replaces the order invoice in Get Order while it is unsettled, and total_amount counts the unsettled addition. Additions do not queue: a second call issues a second invoice with its own deadline, invoice then shows the most recent one and total_amount counts both. Settle or cancel each by the invoice_id this call returned; Get Order is not the way to find an addition invoice.

  • Seat selection is not sold here (see Create Order seat_assignments), and an added ancillary cannot be removed or refunded through the API.

  • Order ownership expires 30 days after creation; later calls answer 404 ORDER_NOT_FOUND.

Failure cases: 400 INVALID_REQUEST (missing Idempotency-Key, empty selection, more than one pax segment, more than one passenger_id, a passenger_id that does not own the named pax_segment_id, a pax_segment_id outside the order, or non-UUID {orderId}), 404 ORDER_NOT_FOUND, 409 ORDER_NOT_ACTIVATED, 409 ANCILLARY_ADDITION_NOT_ELIGIBLE, 409 OFFER_NOT_AVAILABLE, 409 OFFER_SOLD_OUT, 409 ANCILLARY_LIMIT_EXCEEDED, 409 ANCILLARY_PRICE_CHANGED, 409 IDEMPOTENCY_CONFLICT.

Settle Ancillary Addition Payment

Debits the partner wallet for an ancillary addition invoice and provisions the ancillaries. Requires scope ancillary:write.

Only this endpoint settles that invoice: Confirm Wallet Payment, Settle Exchange Payment and Settle Infant Addition Payment answer 409 ORDER_NOT_PAYABLE for it, as this one does for their invoices.

payment_reference is the partner’s own reference for the debit and doubles as the wallet idempotency key, so repeating the call for the same invoice with the same reference debits the wallet once. The key is derived from the reference together with the invoice’s payment intent, so the same reference sent for a different invoice is a different debit, not a replay.

Whether a rejected debit comes back as 402 INSUFFICIENT_WALLET_LIMIT or 502 WALLET_PAYMENT_FAILED depends on how the wallet words its rejection, so do not treat 402 as the only signal of an exhausted limit. In both cases no money moved and the addition is untouched: top up and call again, or cancel the addition.

A 202 means the wallet was debited but provisioning is still in flight; the ancillaries appear in List Ancillaries shortly. Do not retry a 202 — the money already moved. A 500 WALLET_SETTLEMENT_INCONSISTENT means the wallet was debited and the addition could not be activated; do not retry, contact support with the request_id.

Failure cases: 400 INVALID_REQUEST (malformed payment_reference), 402 INSUFFICIENT_WALLET_LIMIT, 404 ORDER_NOT_FOUND, 409 ORDER_ALREADY_PAID, 409 ORDER_NOT_PAYABLE (the invoice is not an ancillary addition awaiting payment), 500 WALLET_SETTLEMENT_INCONSISTENT, 502 WALLET_PAYMENT_FAILED.

HTTP Request

POST /distribution/v1/orders/{orderId}/ancillaries/{invoiceId}/payments/confirm
Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

{
  "payment_reference": "partner-ref-2026-09-17-0001"
}

Response

{
  "order_id": "77777777-7777-7777-7777-777777777777",
  "status": "ACTIVATED",
  "transaction_ids": ["dddddddd-0000-0000-0000-000000000001"]
}

Cancel Ancillary Addition

Cancels an ancillary addition whose invoice has not been settled: the invoice is voided and the inventory hold released. The order returns to what it was before the addition. Requires scope ancillary:write and an Idempotency-Key header.

A settled or expired addition, or one on an order that is no longer ACTIVATED, returns 409 ANCILLARY_ADDITION_NOT_CANCELLABLE; an invoice of the order that is not an ancillary addition — the purchase invoice, an exchange delta or an infant addition — returns 400 INVALID_REQUEST; an unknown order or invoice returns 404 ORDER_NOT_FOUND. Repeating the call with the same key on the same invoice returns 200. An Idempotency-Key already spent on another invoice of this order returns 409 IDEMPOTENCY_CONFLICT and cancels nothing. A key already spent on a different kind of operation — an Add Ancillaries call, say — cannot be replayed here at all and answers 500 INTERNAL_ERROR without cancelling anything. Use a fresh key per operation; a reused one is never a safety net.

HTTP Request

POST /distribution/v1/orders/{orderId}/ancillaries/{invoiceId}/cancel
Authorization: Bearer <access_token>
Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff

Response

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

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.

Special assistance sent in Create Order (ssrs) appears under selected as soon as the order is created, before payment. Until the order is paid available is informational only: Add Assistance returns 409 BOOKING_NOT_ACTIVATED.

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; assistance wanted before payment goes into POST /orders (ssrs). Each body pax_segment_id must belong to the URL orderId’s booking, otherwise `400 INVALID_REQUEST. That booking is the order’s head booking — the original one, or the one an exchange produced. A passenger added through Add Infant lives on a child booking, so its segments never appear in List Assistance and a pax_segment_id of theirs answers 400 INVALID_REQUEST here.

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

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 of that order: the current-invoice payment (the initial-purchase capture) and every refund of it. 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. (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 — reconcile the unfiltered statement via the order_id filter and the intent_id. payment_reference carries the PSP reference supplied at payment (DEL-2054); it is null for operations without one (e.g. top-ups and refunds — take the reference from the order’s PAYMENT row in the same filtered view).

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.