B2B Seat Fulfilment API
Trigger seat fulfilment for one or more passengers on a PNR without using the AllAncillaries UI. Send us a JSON payload describing the seats you want assigned and we respond with whether none, some or all of those seats were successfully fulfilled.
This endpoint uses the same authentication as the encrypted seatmap
checkout link API. If you already have a partner integration that
generates seatmap links, you can re-use the same api_key / api_secret.
Endpoint
POST https://<your-allancillaries-host>/api/v1/fulfil-seats
Content-Type: application/json
Authorization: Bearer <token>
Tokens are obtained from POST /generate-bearer-token using your brand
api_key and api_secret. Tokens are valid for 24 hours by default.
Request payload
| Field | Type | Required | Notes |
|---|---|---|---|
pnr |
string | yes | The booking reference, e.g. "ABC123". |
seats |
array of object | yes | One entry per seat you want assigned. Must contain at least one seat. |
lastName |
string | no | Last name of the booking. Stored against the transaction record for traceability. |
targetCity |
string | no | Overrides the servicing office the booking is actioned against. Defaults to the booking's own. |
currency |
string | yes, if you send any price limit | Three-letter code (e.g. "AUD") of the maxPrice / expectedPrice values. See Price protection. |
substitution |
object or boolean | no | Opt in to equivalent seats when a requested seat is taken. See Seat substitution. |
Each seats[] entry:
| Field | Type | Required | Notes |
|---|---|---|---|
passengerName |
string | yes* | Full passenger name as it appears in the PNR, e.g. "John Smith". |
seatNumber |
string | yes | Seat designation, e.g. "12A". |
segmentNumber |
string | yes | Segment number on the PNR (e.g. "1") or a carrier+flight-number string (e.g. "AV312"). |
nameNumber |
string | no | Sabre name number (e.g. "1.1"). Skips lookup if supplied. |
maxPrice |
number | no** | The most you will accept us booking this seat for. See Price protection. |
expectedPrice |
number | no** | The price you expect the seat to cost. See Price protection. |
* Either passengerName or nameNumber must be present.
** Optional, but if any seat carries maxPrice or expectedPrice then every seat must, and
currency is required.
Price protection
By default we assign the seats you ask for at whatever the airline charges. To make sure a
seat is never booked for more than you are willing to pay, send a price limit on every seat,
plus a top-level currency.
- Prices are the raw airline cost of the seat. They do not include any markup or your own
selling price, so a seat the airline gives away for free costs
0. maxPriceis a hard ceiling. A seat is booked when its cost is at or belowmaxPrice, and is not booked when it is above.expectedPriceis the cost you were shown. If you send onlyexpectedPrice, it is also the ceiling. If you send both,maxPriceis the ceiling andexpectedPricecannot be greater than it. If the actual cost differs fromexpectedPrice(and is still within the ceiling), the seat is booked and itspricing.priceChangedistrue. When the seat was converted from another currency,priceChangedisnull: your rate and ours differ, so it would fire on exchange-rate noise.currencyis the currency your limits are in. The airline prices each booking in its own currency, which may differ from yours. When it does, we convert the airline's price into your currency using the European Central Bank's daily reference rate, rounded up to the next cent, and compare that with your limit. So a seat the airline prices at NZD 20.00 is judged as AUD 18.00 at a rate of 0.9. The seat'spricingblock shows what we used:quotedPrice(in your currency),quotedCurrencyandquotedPriceOriginal(the airline's own figure), andfx(rate,asOfdate,source).- Reference rates are mid-market and published once per business day, so they can differ a little
from the rate you used to set your price. If yours is lower than ours you will see occasional
rejections at the margin; add a small headroom to
maxPriceif that matters to you. - If we cannot get a current rate, the seat is not booked (
FX_RATE_UNAVAILABLE). We keep using the last good rate while its date is still within 5 days, so a brief outage at the rate source does not stop bookings; only a source that is down for longer, or a rate we cannot trust, does. A free seat never needs a rate. - A seat whose cost we cannot confirm is never booked (
PRICE_UNAVAILABLE). A seat the airline shows as occupied, blocked or otherwise not selectable is reported asSEAT_UNAVAILABLEinstead, with the reason, because no price is going to fix it. A free seat is a free seat: it books under amaxPriceof 0 or above. - If an airline prices a seat differently for different passengers on the booking, we judge the seat against the highest of those prices.
- Limits can be at most 1,000,000. A seat can appear only once per request when limits are sent;
a repeated seat is not booked (
DUPLICATE_SEAT_REQUEST), because two limits for one seat are ambiguous, and the rest of that flight is held back with it.
Seats on one flight are booked together or not at all. If any seat on a flight is over its
limit, none of the seats on that flight are booked, so a group is not split across the cabin.
Other flights on the same request are judged on their own, so the first flight can be booked while
the second is not. The same holds when the airline refuses a seat: see
Flights are booked independently. Always read
fulfilled_seats and failed_seats.
You are still billed the fulfilment fee for every seat we book, including free seats.
errorType |
Meaning |
|---|---|
PRICE_EXCEEDS_MAX |
This seat costs more than its ceiling. The reason states the cost and the ceiling. |
FLIGHT_PRICE_GUARD |
This seat was fine, but another seat on the same flight failed a price check. |
FX_RATE_UNAVAILABLE |
The seat is priced in another currency and no usable exchange rate was available. Usually retryable (see retryable). |
PRICE_CURRENCY_MISMATCH |
The airline did not say what currency the seat is priced in, so it could not be compared. |
PRICE_UNAVAILABLE |
The seat's cost could not be confirmed. Retryable only if the flight's prices did not load. |
SEAT_UNAVAILABLE |
The seat is occupied, blocked or not selectable on this flight, or is not on its seatmap. |
DUPLICATE_SEAT_REQUEST |
The same seat was sent more than once with a price limit. |
SUBSTITUTE_NOT_HELD |
An earlier request substituted this seat, and the booking no longer holds the substitute. See Seat substitution. |
SUBSTITUTION_HISTORY_UNAVAILABLE |
We could not read our record of earlier substitutions, so nothing was booked with substitution on. Retryable. |
BOOKING_ALREADY_SUBSTITUTED |
Seats on this booking were substituted earlier and this is not an exact repeat of that request. Change or add seats through the seat map. |
SUBSTITUTION_NOT_POSSIBLE |
Substitution was requested on a route that appears twice on the booking and the passenger already holds a seat on it. Resend without substitution. |
A failed seat carries "retryable": true only when the failure was transient: we could not fetch a
price or an exchange rate, or read the booking, as opposed to the price being too high. Repeat the same request after a
minute. A seat held back only because another seat on its flight hit such a failure is marked the
same way. The key is absent otherwise.
Every seat we price-checked carries a pricing block (a seat that was already fulfilled, or that
failed before we could price it, does not). The top-level pricingGuard is "applied" (or
"not_requested" when you sent no limits), and priceAudit reports the check we make after
booking. If priceAudit.breaches is ever non-empty, the airline recorded a price above your
ceiling after we booked the seat; we cannot release a booked seat automatically, so treat it as an
incident and contact us. The check compares in your currency, converting the airline's recorded price the same way
(a breach then also carries recordedPrice and recordedCurrency). A seat we cannot compare, such as
a recorded price in a currency we have no rate for, or a booking record we cannot read, is listed
under priceAudit.unverified instead, so treat a non-empty unverified as "not confirmed", not as "fine".
Example: two flights, one over the limit
{
"pnr": "ABC123",
"lastName": "SMITH",
"currency": "AUD",
"seats": [
{"passengerName": "John Smith", "seatNumber": "12A", "segmentNumber": "1", "maxPrice": 17},
{"passengerName": "Jane Smith", "seatNumber": "12B", "segmentNumber": "1", "maxPrice": 17},
{"passengerName": "John Smith", "seatNumber": "20A", "segmentNumber": "2", "maxPrice": 17},
{"passengerName": "Jane Smith", "seatNumber": "20B", "segmentNumber": "2", "maxPrice": 17}
]
}
If 20B costs AUD 30.00, the response is 207: 12A and 12B are fulfilled, and neither seat on
the second flight is booked.
{
"success": true,
"outcome": "partially_fulfilled",
"pricingGuard": "applied",
"fulfilled_seats": [
{"passengerName": "John Smith", "seatNumber": "12A", "segmentNumber": "1", "status": "fulfilled",
"pricing": {"quotedPrice": 0.0, "currency": "AUD", "expectedPrice": null, "maxPrice": 17.0, "priceChanged": false}},
{"passengerName": "Jane Smith", "seatNumber": "12B", "segmentNumber": "1", "status": "fulfilled",
"pricing": {"quotedPrice": 0.0, "currency": "AUD", "expectedPrice": null, "maxPrice": 17.0, "priceChanged": false}}
],
"failed_seats": [
{"passengerName": "John Smith", "seatNumber": "20A", "segmentNumber": "2", "status": "failed",
"errorType": "FLIGHT_PRICE_GUARD",
"reason": "Not booked: another seat on this flight failed the price check, and seats on a flight are only booked together.",
"pricing": {"quotedPrice": 0.0, "currency": "AUD", "expectedPrice": null, "maxPrice": 17.0, "priceChanged": false}},
{"passengerName": "Jane Smith", "seatNumber": "20B", "segmentNumber": "2", "status": "failed",
"errorType": "PRICE_EXCEEDS_MAX",
"reason": "Seat 20B costs AUD 30.00, above your maximum of AUD 17.00, so it was not booked.",
"pricing": {"quotedPrice": 30.0, "currency": "AUD", "expectedPrice": null, "maxPrice": 17.0, "priceChanged": false}}
]
}
Example request
{
"pnr": "ABC123",
"lastName": "SMITH",
"seats": [
{
"passengerName": "John Smith",
"seatNumber": "12A",
"segmentNumber": "1"
},
{
"passengerName": "Jane Smith",
"seatNumber": "12B",
"segmentNumber": "1"
}
]
}
You can also identify segments by carrier + flight number instead of a positional index:
{
"pnr": "ABC123",
"lastName": "ANDERSON",
"seats": [
{
"passengerName": "Rebecca Anderson",
"seatNumber": "12C",
"segmentNumber": "AV312"
},
{
"passengerName": "Rebecca Anderson",
"seatNumber": "22K",
"segmentNumber": "AV522"
}
]
}
Seat substitution
By default, a seat that is no longer available fails with SEAT_UNAVAILABLE. If you opt in, we
look for equivalent seats for the whole party on that flight and book those instead, and tell
you exactly what we did.
"substitution": {"maxRowDistance": 5, "allowOppositeSide": true}
| Setting | Default | Notes |
|---|---|---|
maxRowDistance |
5 |
How many rows forward or back the party may move, from 0 to 10. |
allowOppositeSide |
true |
Whether the party may move to the mirror-image seats on the other side of the aircraft. |
"substitution": true uses both defaults. Leave it out, or send false, to keep today's behaviour.
Substitution needs price protection with an expectedPrice on every seat
(0 for a free seat), because a taken seat no longer shows its price and your figure is how we keep
a free seat free and a paid seat paid.
What "equivalent" means. Every rule applies to every passenger, and if any passenger cannot be moved, nobody is:
- The party stays together, in the same shape. Everyone moves the same number of rows, and to the other side of the aircraft only together. Two travellers side by side stay side by side; a family across two rows stays across two rows; a couple across the aisle stays across an aisle. Passengers whose own seats are still free move with the rest, so nobody is left behind.
- The same kind of seat. A window seat is replaced by a window seat, a middle by a middle, an aisle by an aisle. On the other side of the aircraft, the passenger in the window seat still gets the window.
- The same features. An exit row stays an exit row, extra legroom stays extra legroom, and a bassinet seat keeps a bassinet. We never move a passenger onto a crew seat. We never move a passenger onto a restricted seat, a window seat with no window, a seat with limited recline, a bulkhead, or a seat the airline bars to infants, children or unaccompanied minors, unless the seat they asked for was one too, and never next to a lavatory or galley if they chose a seat away from one.
- The same price. A free seat is replaced only by a free seat and a paid seat only by a paid seat,
and never above your
maxPrice(orexpectedPrice, if that is your ceiling), not even by a cent. The same exchange-rate rules apply as for any other seat. - Rows mean rows. Distance counts the rows that exist, so on an aircraft with no row 13, rows 12 and 14 are one row apart.
Airlines that do not describe their seats. Some airlines send no seat details (window, exit row, legroom and so on) for part or all of a cabin. There, a seat is only replaced by another undescribed seat, judged on what the cabin layout shows: the same window, middle or aisle position, neither seat in or beside an exit row or in the first or last row of the cabin, and not moved beside a row with missing seats (usually a galley, lavatory or door) unless the requested seat was beside one. Legroom and bulkheads cannot be checked on those seats.
expectedPrice must be the airline's own cost, not your selling price: 0 for a seat the airline
gives away. It is how we keep a free seat free and a paid seat paid, so an expectedPrice above 0 for a
free seat means no free seat will ever be offered as its substitute. Your selling limit belongs in
maxPrice.
When several moves would work we choose the nearest: the other side of the same row first, then one row away on the same side, then one row away on the other side, and so on, forward before back. If neither the seat map nor the cabin layout says what kind of seat you asked for, or an undescribed seat is beside an exit or at the end of the cabin, no substitute is booked. We substitute only from the seat map, before booking: if the airline then refuses the substitute, it is reported like any refused seat and we do not try a third seat.
Each flight is judged on its own, so one flight can be substituted while another is booked as asked.
We do not substitute on a flight where two passengers on the booking share the same name, or on a
flight whose route appears twice on the booking (the same city pair on two dates), because we could
not record reliably which passenger or flight the substitute belongs to. Seats on such a flight are
still booked as asked when they are available. On a route that appears twice, if the passenger already
holds a seat on it we cannot tell which flight that seat is on, so nothing is booked
(SUBSTITUTION_NOT_POSSIBLE); send that request without substitution.
If a passenger on the flight already holds the seat they asked for, or cannot be matched to the
booking, that flight's party is not moved. On a mirrored move, a passenger in the centre seat of a
block keeps that seat; only the passengers whose seat changed are marked substituted.
That passenger's entry carries "substitution": {"kept": true, "rowShift": 0, "oppositeSide": true, "reason": "..."}
so it is clear the seat stayed on purpose and the party moved together. For example, on a 3-seat block
D-E-G, a party in D and E moves to E and G: the passenger in E is kept, the other is substituted.
Seat letters are the airline's, and not every aircraft has every letter (there is no F in that block),
so check the booked seats against the aircraft's own letters. Do not read substituted on some seats
of a flight and not others as a split party; the party is split only if its booked seats are not together.
Repeating a request is safe. We keep a record of every seat we book for you, by passenger name,
flight and date. If you send a request again for a seat we substituted, and the passenger still holds
the seat we gave them, it is returned in already_fulfilled_seats with requestedSeatNumber and
"substituted": true, and nothing is moved or billed again, even if the seat you originally asked for
has come free since. Seats the airline assigned on its own are never mistaken for a substitution.
Once we have substituted anyone on a booking, that booking is locked for substitution. With
substitution on, we then accept only an exact repeat of the earlier request (and report seats
already held exactly as asked). Every other seat is refused with BOOKING_ALREADY_SUBSTITUTED and
nothing is booked, even if the seat is free: we cannot always tell a repeat from a new request once a
name, flight or seat has changed, and we will not risk moving and billing a party twice. To change or
add seats on a substituted booking, send the customer to the seat map.
Also:
- if the airline has since taken a passenger's substitute away, that seat fails with
SUBSTITUTE_NOT_HELD, naming the seat we had booked and the one they hold now, and the rest of the party is left where it is. Contact us: the party may have been split; - if we cannot read our own record at that moment, nothing is booked with substitution on
(
SUBSTITUTION_HISTORY_UNAVAILABLE, retryable): try again shortly.
In the response, a substituted seat is in fulfilled_seats with the seat we booked as
seatNumber, the seat you asked for as requestedSeatNumber, "substituted": true, and how it moved
(rowShift: rows forward (negative) or back (positive); oppositeSide). summary.substituted counts
them and the top-level substitution block echoes your settings. Substituted seats are billed like any
other fulfilled seat. A seat we looked for a substitute for and could not find stays in failed_seats
as SEAT_UNAVAILABLE, with a substitution block saying why. The reason names the free seats we found
near the party and how many seats it needs, because a free seat on its own is not enough: the party needs
a free seat for each passenger that matches the one asked for (window, middle or aisle, same features),
kept in the same arrangement. A full response looks like this:
{
"outcome": "all_fulfilled",
"summary": {"requested": 2, "fulfilled": 2, "already_fulfilled": 0, "failed": 0, "substituted": 2, "outcome": "all_fulfilled"},
"fulfilled_seats": [
{"passengerName": "John Smith", "seatNumber": "7F", "requestedSeatNumber": "7A", "substituted": true,
"substitution": {"rowShift": 0, "oppositeSide": true}, "segmentNumber": "1", "status": "fulfilled",
"pricing": {"quotedPrice": 0.0, "currency": "AUD", "expectedPrice": 0.0, "maxPrice": 0.0, "priceChanged": false}},
{"passengerName": "Jane Smith", "seatNumber": "7E", "requestedSeatNumber": "7B", "substituted": true,
"substitution": {"rowShift": 0, "oppositeSide": true}, "segmentNumber": "1", "status": "fulfilled",
"pricing": {"quotedPrice": 0.0, "currency": "AUD", "expectedPrice": 0.0, "maxPrice": 0.0, "priceChanged": false}}
],
"substitution": {"requested": true, "maxRowDistance": 5, "allowOppositeSide": true, "substitutedSeats": 2}
}
Always tell the traveller when substituted is true: they asked for a different seat.
Response
The HTTP status code reflects the fulfilment outcome:
| Status | Meaning |
|---|---|
200 |
All requested seats were successfully fulfilled. |
207 |
Some seats fulfilled, some failed. |
422 |
None of the requested seats were fulfilled. |
400 |
Payload was missing or invalid, including a price limit without a currency, or substitution without an expectedPrice on every seat. |
401 |
Missing or invalid bearer token. |
500 |
The fulfilment call raised an exception on our side. The body carries a transaction_id; the request may be repeated. |
Example success response (200)
{
"success": true,
"outcome": "all_fulfilled",
"transaction_id": "B2B-7K2H9XQM4N3D",
"pnr": "ABC123",
"brand": "examplebrand",
"target_city": "XXXX",
"summary": {
"requested": 2,
"fulfilled": 2,
"failed": 0,
"outcome": "all_fulfilled"
},
"fulfilled_seats": [
{"passengerName": "John Smith", "seatNumber": "12A", "segmentNumber": "1", "status": "fulfilled"},
{"passengerName": "Jane Smith", "seatNumber": "12B", "segmentNumber": "1", "status": "fulfilled"}
],
"failed_seats": [],
"billing": {
"currency": "AUD",
"ratePerFulfilledSeat": 1.5,
"billableSeats": 2,
"billableAmount": 3.0
}
}
Flights are booked independently
We first try every seat on the request in one go. If that fails, we send each flight on its own, so a flight the airline will not seat never stops the flights that can be booked. Seats on one flight are booked together or not at all: if the airline refuses any seat on a flight, none of the seats on that flight are booked, for any passenger. Other flights are unaffected.
errorType |
Meaning |
|---|---|
PROVIDER_REJECTED |
The airline refused this seat for this passenger. |
FLIGHT_SEAT_REJECTED |
This seat was fine, but the airline refused another seat on the same flight. |
AIRPORT_CONTROL |
The flight is under airport control and cannot be seated. |
PARTY_SEAT_MISMATCH |
The airline needs every passenger on this flight seated in the same request (Sabre NUMBER IN PARTY or NR IN PTY). Resend with a seat for each passenger on the flight. |
SEAT_PREVIOUSLY_ASSIGNED |
The airline says a seat is already assigned for a passenger on this flight (Sabre PRS PREVIOUSLY ASSIGNED). Nothing on the flight was changed. |
SEAT_LIMIT_REACHED |
The airline's limit on advance seat assignments for this flight has been reached (Sabre BEYOND PRS MAX). Seats may open closer to departure. |
These reasons quote the airline's own message where there is one, for example
PRS NOT ALLOWED THIS FLIGHT on a PROVIDER_REJECTED seat.
Some failures happen before we contact the airline at all:
errorType |
Meaning |
|---|---|
PASSENGER_UNRESOLVED |
No passenger on the booking matches passengerName. Send every given name (including middle names) and then the surname, as the booking shows them, without a title. |
SEGMENT_UNRESOLVED |
No single flight on the booking matches segmentNumber. Send the carrier code and flight number as the booking shows them (for example QR899), or the segment number. |
RESERVATION_UNAVAILABLE |
We could not read the booking from the airline system (a timeout or an empty read). Nothing was changed. Retryable. |
If the airline answers the seat request in a way we cannot determine (it did not reply in time
and the seat is not on the booking when we re-read it), the seat fails with
SEAT_WRITE_OUTCOME_UNKNOWN. Nothing is billed. It is retryable: a repeat request either finds the
seat held (and reports it in already_fulfilled_seats) or books it.
An airline refusal we do not recognise yet fails with PROVIDER_ERROR, and its reason carries the
airline's message. Every failed seat has an errorType. Wherever a request can safely be sent
again, the failed seat also carries "retryable": true; without it, sending the same request again
gives the same answer. Only fulfilled_seats are billed.
Bookings made through an airline's NDC channel
Some airlines sell through NDC, and their bookings are held as an airline order rather than a
standard reservation. We assign and pay for seats on those orders directly with the airline. The
same request shape, price limits and billing apply. A paid NDC seat needs a maxPrice (or
expectedPrice). The airline's price is converted into your request currency for the check,
exactly as for other bookings. Failures specific to NDC orders:
errorType |
Meaning |
|---|---|
NDC_NOT_SUPPORTED |
Seat assignment on NDC orders is not enabled for your account. Not billed, not retryable. |
NDC_PRICE_LIMIT_REQUIRED |
A paid NDC seat was requested without a price limit. Resend with maxPrice. |
NDC_ORDER_UNAVAILABLE |
We could not read the airline order. Usually retryable (see retryable). |
NDC_SEATMAP_UNAVAILABLE |
The airline did not return a seat map for this flight. |
NDC_SEATS_NOT_SELLABLE |
The airline is not offering seats on this flight for sale through us right now. |
NDC_PASSENGER_UNRESOLVED |
No passenger on the airline order matches passengerName. |
NDC_SEGMENT_UNRESOLVED |
No flight on the airline order matches segmentNumber. |
NDC_SEAT_SWAP_NOT_POSSIBLE |
The passenger already holds a seat on this flight that cannot be swapped this way. |
NDC_CHANGE_REJECTED |
The airline refused the seat; its message is in reason. |
NDC_PRICE_CHANGED |
The airline's price rose above your limit while the seat was being added. |
NDC_PAYMENT_FAILED |
The seat is held but payment to the airline did not complete. Retryable when retryable is set: the repeat request tries the payment again. |
NDC_CHANGE_NOT_CONFIRMED |
We sent the change but could not confirm its result. We check these by hand; further requests on the booking answer NDC_ORDER_BUSY until then. |
NDC_PAID_NOT_CONFIRMED |
As above, after a payment was sent. |
NDC_ORDER_BUSY |
Another change on this booking is in progress or awaiting our check. Retryable later. |
Repeating a request
Sending the same request again is safe. A seat the PNR already holds is returned in
already_fulfilled_seats and is never billed twice.
A held seat is not always confirmed for ticketing. If its status is still awaiting confirmation
(for example awaiting payment) we finish the hand-off to ticketing on the repeat request, so a
request that was interrupted the first time is completed by sending it again. Such a seat carries
awaitingConfirmation (the status we saw, for example HD) and a note, and queuePlacement
reports whether the hand-off worked:
{
"already_fulfilled_seats": [
{
"passengerName": "Jane Smith",
"seatNumber": "37A",
"segmentNumber": "UL604",
"status": "already_fulfilled",
"awaitingConfirmation": "HD",
"note": "Seat is held in the PNR but not yet confirmed for ticketing (status HD). It has been passed to ticketing (see queuePlacement). Not billed."
}
],
"queuePlacement": {"attempted": true, "success": true, "queue": "Q65"}
}
A confirmed seat (status HK or KK) has no awaitingConfirmation and nothing further is done.
Always check queuePlacement.success: a booked seat can be reported as fulfilled while the
hand-off to ticketing failed, and that is the only field that says so.
Example partial response (207)
{
"success": true,
"outcome": "partially_fulfilled",
"transaction_id": "B2B-7K2H9XQM4N3D",
"summary": {"requested": 2, "fulfilled": 1, "failed": 1, "outcome": "partially_fulfilled"},
"fulfilled_seats": [
{"passengerName": "John Smith", "seatNumber": "12A", "segmentNumber": "1", "status": "fulfilled"}
],
"failed_seats": [
{
"passengerName": "Jane Smith",
"seatNumber": "12B",
"segmentNumber": "1",
"status": "failed",
"reason": "Flight under airport control - seat could not be assigned",
"errorType": "AIRPORT_CONTROL"
}
],
"billing": {"currency": "AUD", "ratePerFulfilledSeat": 1.5, "billableSeats": 1, "billableAmount": 1.5}
}
Billing
- Settlement is post-paid, invoiced monthly.
- You are billed AUD $1.50 for each successfully fulfilled seat.
- Failed seats are recorded in our database for transparency but are not billable.
- Each call returns a
billingblock summarising the billable amount for that transaction so you can reconcile internally.
Every transaction is stored in our database with:
- a unique
transaction_id(returned in the response), - per-seat
TransactionItemrows describing exactly what was requested, - per-seat
Fulfillmentrows recording success or failure with the underlying reason. This keeps the audit trail clear regardless of whether the request fully, partially or completely failed.
Python snippet
A copy-paste ready Python example is available at:
public_docs/python/fulfil_seats_example.py
It performs the bearer token exchange and a single fulfilment call, and prints the per-seat results.