Pay a held booking
Pay a held order, issue the tickets and debit the agency balance. Requires Read+Write permission.
POST/api/v1/flights/pay_order
Pay for a held booking and issue the tickets. The agency balance is debited by pricing.b2bPrice.
Permission: Read+Write. Rate group: booking (1,200 requests per minute per key). In the sandbox no money moves.
The amount and the currency are resolved by the server from the stored booking. Do not send currency: the request is refused with 400 (Do not pass 'currency'. Currency is resolved automatically...). An amount is not read; it is ignored.
Identifying the order
| Field | Description |
|---|---|
order_id | The supplier's order id (booking.reference of the booking response). Preferred. |
booking_id | The PNR of the booking. |
pnr | The airline record locator. Not unique: prefer order_id. |
The first one present wins, in this order; another one that holds a different value is dropped, and an ignored_field warning names it. The FX-Port UUID is not accepted. Other fields:
| Field | Type | Description |
|---|---|---|
payment_type | string | balance (default), card or arc_bsp_cash. Only Duffel reads it; Amadeus Algeria and Travelport pay from the balance whatever you send. |
three_d_secure_session_id | string | For a Duffel card payment that needs 3-D Secure. |
price_guaranteed | boolean | Default true: pay the price of the hold. Leave it out. false asks FX-Port to re-price at payment time. |
supplier_id | string | Only used when the booking has no supplier of its own; default duffel_1. |
Legacy names: orderId, bookingId, paymentType, threeDSecureSessionId, priceGuaranteed, supplierId still work and are answered with a deprecated_field warning.
Request example
curl https://api.fx-port.com/api/v1/flights/pay_order \
-H "Authorization: Bearer fxp_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "eJzTd9ePNA9wdA0FAAs8AmE",
"payment_type": "balance"
}'Response example
The held Amadeus Algeria order of the previous pages, paid. Real response of 2026-11-01, trimmed (the itinerary, the traveler pricing and the owner are shortened); the agency and the contact data are fake. There is no exchangeRate because both currencies are DZD.
{
"environment": "sandbox",
"success": true,
"paymentSucceeded": true,
"requestId": "1ad06b56",
"orderId": "eJzTd9ePNA9wdA0FAAs8AmE",
"processingTime": 2.981,
"paymentDetails": {
"paymentId": null,
"status": "succeeded",
"type": "issuance",
"paymentAmount": 4925.44,
"currency": "DZD",
"createdAt": "2026-11-01T15:26:21.896668",
"message": "Payment successful! Tickets have been issued."
},
"booking": {
"reference": "eJzTd9ePNA9wdA0FAAs8AmE",
"status": "confirmed",
"pnr": "Y7PAEU",
"type": "instant",
"createdAt": "2026-11-01T15:26:21.896668",
"cancelledAt": null,
"content": "managed",
"syncedAt": "2026-11-01T15:26:21.896668",
"availableActions": [],
"voidableUntil": "2026-11-01T23:00:00Z",
"cancellation": null
},
"supplier": { "id": "amadeus_aqc_dz_1", "technology": "amadeus_aqc", "name": "Amadeus AQC", "offerId": "1" },
"agency": { "currency": "DZD" },
"pricing": {
"supplierCurrency": "DZD",
"agencyCurrency": "DZD",
"basePrice": 3160.0,
"taxesAndFees": 1765.44,
"agencyCommission": 4.93,
"finalPrice": 4930.37,
"b2bPrice": 4925.44,
"additionalServices": [],
"acd": { "applied": false }
},
"payment": { "status": "paid", "paidAt": "2026-11-01T15:26:21.896710", "paymentRequiredBy": null, "priceGuaranteeExpiresAt": null, "awaitingPayment": false },
"documents": [
{
"type": "electronic_ticket",
"documentType": "ETICKET",
"uniqueIdentifier": "124-2446977397",
"status": "ISSUED",
"passengerIds": ["1"],
"segmentIds": ["1"]
}
],
"bookingReferences": [{ "pnr": "Y7PAEU", "carrier": { "iataCode": "AH", "name": "Air Algerie" } }],
"passengers": [
{
"id": "1",
"type": "ADULT",
"title": "mr",
"givenName": "JAMES",
"familyName": "SMITH",
"gender": "male",
"bornOn": "1988-06-15",
"email": "[email protected]",
"phoneNumber": "15555550123",
"infantPassengerId": null,
"loyaltyProgrammeAccounts": [],
"ticketNumber": "124-2446977397"
}
],
"travelerPricing": ["..."],
"itinerary": ["..."],
"internalBooking": { "bookingId": "Y7PAEU", "internalId": "711371c3-00aa-4be2-b790-9f0ca2ccfdb7" },
"meta": { "paymentStatus": "paid", "orderConfirmed": true, "supportHold": true, "supportVoid": true, "voidDeadline": "2026-11-01T23:00:00Z" },
"postProcessing": { "bucketLink": null, "emailSent": true }
}Field meanings
| Field | Meaning |
|---|---|
paymentSucceeded | true or false: check it and paymentDetails.status. |
paymentDetails.status | succeeded once the tickets are issued. |
paymentDetails.type | issuance for a hold that is paid and ticketed. |
paymentDetails.paymentAmount | What was debited: equals pricing.b2bPrice. |
booking.status | confirmed now (it was pending while held); booking.type is instant. |
booking.voidableUntil | The end of the same-day void window: midnight in the supplier's time zone of the day of the payment. A hold created days ago is voidable until midnight of the day it is paid. |
documents[n].uniqueIdentifier | The e-ticket number. |
pricing | The price block; pricing.acd on Amadeus Algeria. See Flight pricing. |
requiresACD | Present and true for an ACD booking: send the passports. |
internalBooking.internalId | The FX-Port id of the booking (a UUID): the same as id of get_booking and postProcessing.bookingDbId of book_flight. Use it as the key of the booking in your own database. internalBooking.bookingId is the PNR. |
Rounding can make finalPrice and b2bPrice one hundredth different from get_booking: see Flight pricing.
Errors
A refusal by the supplier is a 200 with success: false and paymentSucceeded: false: the HTTP status is not enough. error.code is a number as text for Amadeus and a name for Duffel.
| Where | What you see | Meaning |
|---|---|---|
200, success: false | error.code 4926, "The flightOrder is already issued" | The order was already paid. |
200, success: false | error.code 9112, "NEED ITINERARY for record ..." | The reservation is cancelled or expired (a cancelled hold). |
200, success: false | order_type_not_eligible_for_payment (Duffel), with a userMessage | A Duffel order that was bought instantly cannot be paid later. |
400 | bad_request | None of order_id, booking_id, pnr was sent (Missing required parameter: order_id (or booking_id /). |
400 | bad_request | currency was in the body (Do not pass 'currency'...). |
400 | invalid_booking_id, invalid_payment_type, invalid_identifier, invalid_field_type | An id is not a valid token (letters, digits and - _ . : = ~, up to 128 characters), payment_type is not balance, card or arc_bsp_cash, or price_guaranteed is not a boolean. |
402 | insufficient_balance (details: available, required, currency) | Live only. |
403 | read_write_permission_required | A read-only key. |
403 | agency_not_active | Live only. |
404 | not_found | The order is not a booking of your agency in this environment: Booking not found for order_id: <id>. Cannot process payment. An unknown order and the order of another agency are answered the same, and nothing is paid. |
503 | balance_lock_timeout | Live only: another operation of your agency holds the balance in the same currency for a moment. Nothing was paid: the lock is taken before the supplier is called, so you can send the same request again after a few seconds. The message is always the fixed sentence The agency balance is busy with another operation. Retry in a few seconds.; errors[0].details.currency is the currency of your balance. No Retry-After header is sent. |
Do not retry a payment blindly: read the booking with get_booking first. A second pay_order for an order that is paid answers 4926, which is harmless. 503 balance_lock_timeout is safe to repeat: the payment never started.