# Pay a held booking

> Pay a held order, issue the tickets and debit the agency balance. Requires Read+Write permission.

Endpoint: `POST /api/v1/flights/pay_order`

Source: https://fx-port.com/docs/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.


> [!WARNING]
> **Payment cannot revive an expired hold.** `pay_order` fails when the PNR expired or was cancelled, the deadline has passed or the airline released the hold early; airlines can release a PNR before the time that was displayed. Read `success`, `paymentSucceeded` and `paymentDetails.status` of the answer: do not confirm the traveler, and do not charge them in your own system, until the payment succeeded.

> [!CAUTION]
> `pay_order` has **no `send_email` switch**: the confirmation e-mail is always requested (also in the sandbox, where `emailSent` can turn `true`).


## Request example


**Request**

```bash
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.

~~~json
{
  "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": "TRAVELER@EXAMPLE.COM",
      "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](https://fx-port.com/docs/flights/pricing.md). |
| `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`](https://fx-port.com/docs/flights/get-booking.md) and `postProcessing.bookingDbId` of [`book_flight`](https://fx-port.com/docs/flights/book.md). 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](https://fx-port.com/docs/flights/pricing.md).

## 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 / pnr)`). |
| `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`](https://fx-port.com/docs/flights/get-booking.md) 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.

## Related

- [Quote a held booking](https://fx-port.com/docs/flights/get-order-quote.md), [Cancel a held booking](https://fx-port.com/docs/flights/cancel-order.md) and [Booking flows](https://fx-port.com/docs/flights/booking-flows.md).
