Skip to content
FX-Port/Docs
GuidesAPI referenceChangelog
Get API keys
  • Introduction
  • Quickstart
  • Security
  • Authentication
  • Environments and Postman
  • Sandbox
  • Conventions and naming
  • Legacy names and twins
  • Errors
  • Rate limits and fair use
  • Streaming search results

Concepts

  • Overview
  • Booking flows
  • Passengers and ages
  • Pricing
  • Post-booking operations

Search and price

  • Airport autocomplete
  • Search flights
  • Basic routes
  • Passengers and cabins
  • Filters
  • Supplier selection
  • Open-jaw and multi-city
  • Flexible dates
  • Price an offer

Book, pay and cancel

  • Book
  • Quote a held booking
  • Pay a held booking
  • Cancel a held booking
  • Quote a cancellation
  • Confirm a cancellation

After booking

  • Get a booking
  • Get the ticket PDF
  • Get the ticket number
  • ACD passport uploads
  • Use these docs with AI

Flights ยท Book, pay and cancel

Book a flight

Create a booking, either an instant issue or a hold reservation. Requires Read+Write permission.

POST/api/v1/flights/book_flight

Create a booking. booking_type is "issue" (the default) for instant ticketing, or "hold" for an unpaid reservation when the offer supports it.

Permission: Read+Write. Rate group: booking (1,200 requests per minute per key). In the sandbox nothing is charged: the response says balanceDebited: false.

Price first. The offer must have been priced with price_flight within the last 12 hours, with the same search_id and offer_id. An offer that was never priced is refused with 404 offer_not_priced.

Hold depends on the priced offer. Send "booking_type": "hold" only when supportHold is true after price_flight. Otherwise the answer is 400 hold_not_supported. A hold is not ticketed and the airline can release it before the displayed deadline.

Send "send_email": false while you test: otherwise a confirmation e-mail goes to the address you typed.

Request body

FieldTypeRequiredDescription
search_idstringYesThe searchId of the supplier entry the offer came from (the same search that was priced).
offer_idstringYesThe id of the priced offer.
booking_typestringNoissue (default) or hold, in any letter case. Any other value is rejected with 400 invalid_booking_type.
passengersarrayYesOne object per traveler, lap infants included: see below. At most 18.
contact_detailsobjectYes{ "email", "phone_number" }: both are required. Use an international phone number (+213...): Duffel validates the format. FX-Port accepts digits with spaces, dots, dashes, slashes and brackets, one leading + (also (+213)), 5 to 24 digits and 32 characters at most. A dial code alone (+213) is 400 invalid_phone_number.
send_emailbooleanNofalse suppresses the confirmation e-mail. Default true.
commission_overrideobjectNoReplaces your agency commission for this booking: { "type": "percentage" or "fixed", "value": number >= 0 } (a numeric text such as "10" is read as a number with a coerced_value warning). percentage is a share of b2bPrice; fixed is value times the number of travelers. pricing.agencyCommission and pricing.finalPrice change, b2bPrice does not, in the response and in the stored booking. Example: 10 % on a b2bPrice of 28679.0 gave a commission of 2867.9 and a finalPrice of 31546.9.
agent_idstringNoYour own id of the agent who books; stored on the booking.

Passenger object

FieldTypeRequiredDescription
typestringYesadult, senior, young_adult, child, seated_infant or infant (a lap infant), in any letter case. More spellings are accepted: see Flight passengers & ages. An unknown value is 400 invalid_passenger_type.
genderstringYesMALE or FEMALE (M and F too); X, U or UNSPECIFIED for a passport marked X.
first_namestringYesGiven name as in the passport. Spaces and line breaks around it are removed before the checks.
last_namestringYesFamily name as in the passport.
date_of_birthstringYesYYYY-MM-DD; tomorrow is still accepted (time-zone slack), later is 400 invalid_date.
identity_documentsarrayNoThe passport. Send it for every traveler: airlines can require it.
idstringNoAn id of yours for the passenger.

passengers must match the passengers of the search and the offer's travelerPricing (count and type). FX-Port does not compare them for you: a mismatch is refused by the supplier. Names: any letter of any alphabet is accepted by FX-Port, but airlines need Latin letters; see the warning in Flight passengers & ages.

Identity document object

FieldTypeRequiredDescription
typestringNopassport (default).
numberstringNoThe document number: 1 to 40 letters, digits, spaces and - . / _.
issuing_countrystringNoISO 3166-1 alpha-2 (DZ). A website value such as GB-SCT or USPR (2 to 4 letters, optionally - and 1 to 3 letters or digits) is passed on in upper case: see Flight passengers & ages.
expiry_datestringNoYYYY-MM-DD.
nationalitystringNoAccepted and ignored: suppliers use the issuing country.

Legacy names

Every field below has a previous spelling that still works and is answered with a deprecated_field warning. Send the name in the second column. If a body holds a previous spelling and its replacement with different values, the replacement wins and an ignored_field warning names the dropped one.

Legacy nameUse instead
searchId, offerIdsearch_id, offer_id
bookingType, sendEmail, commissionOverride, agentIdbooking_type, send_email, commission_override, agent_id
contact, contacts, contactDetailscontact_details
contact_details.phone, .phoneNumber, .mobilecontact_details.phone_number
contact_details.emailAddresscontact_details.email
passengers[].firstName, .givenName, .given_namepassengers[].first_name
passengers[].lastName, .familyName, .family_name, .surnamepassengers[].last_name
passengers[].dateOfBirth, .bornOn, .born_on, .dobpassengers[].date_of_birth
passengers[].identityDocuments, .documentspassengers[].identity_documents
passengers[].passengerTypepassengers[].type
identity_documents[].issuingCountry, .issuing_country_code, .issuingCountryCodeidentity_documents[].issuing_country
identity_documents[].expiryDate, .expires_onidentity_documents[].expiry_date
identity_documents[].documentNumber, .unique_identifieridentity_documents[].number

phone and phoneNumber at the top level of the body are not a spelling of contact_details: the contact is an object.

Request examples

curl https://api.fx-port.com/api/v1/flights/book_flight \
  -H "Authorization: Bearer fxp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "search_id": "fs_dfx_88eb64c9-6c36-47e6-bdaa-9ad3c1377ac4",
  "offer_id": "fx_offer_b7a95439",
  "booking_type": "issue",
  "send_email": false,
  "passengers": [
    {
      "type": "adult",
      "gender": "MALE",
      "first_name": "James",
      "last_name": "Smith",
      "date_of_birth": "1988-06-15",
      "identity_documents": [
        {
          "type": "passport",
          "number": "A12345678",
          "issuing_country": "DZ",
          "expiry_date": "2031-04-18",
          "nationality": "DZ"
        }
      ]
    }
  ],
  "contact_details": {
    "email": "[email protected]",
    "phone_number": "+15555550123"
  }
}'
curl https://api.fx-port.com/api/v1/flights/book_flight \
  -H "Authorization: Bearer fxp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "search_id": "fs_85ac5f12-7456-4edf-a2fb-1d51239d278b",
  "offer_id": "fx_offer_9de60e2a",
  "booking_type": "hold",
  "send_email": false,
  "passengers": [
    {
      "type": "adult",
      "gender": "FEMALE",
      "first_name": "Maria",
      "last_name": "Garcia",
      "date_of_birth": "1991-03-22",
      "identity_documents": [
        {
          "type": "passport",
          "number": "B87654321",
          "issuing_country": "DZ",
          "expiry_date": "2030-11-15"
        }
      ]
    }
  ],
  "contact_details": {
    "email": "[email protected]",
    "phone_number": "+15555550123"
  }
}'
curl https://api.fx-port.com/api/v1/flights/book_flight \
  -H "Authorization: Bearer fxp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "search_id": "fs_dfx_88eb64c9-6c36-47e6-bdaa-9ad3c1377ac4",
  "offer_id": "fx_offer_b7a95439",
  "booking_type": "issue",
  "send_email": false,
  "passengers": [
    {
      "type": "adult",
      "gender": "MALE",
      "first_name": "Omar",
      "last_name": "Nguyen",
      "date_of_birth": "1985-04-10",
      "identity_documents": [
        {
          "type": "passport",
          "number": "P11223344",
          "issuing_country": "DZ",
          "expiry_date": "2031-04-18"
        }
      ]
    },
    {
      "type": "adult",
      "gender": "FEMALE",
      "first_name": "Priya",
      "last_name": "Nguyen",
      "date_of_birth": "1987-08-20",
      "identity_documents": [
        {
          "type": "passport",
          "number": "P44332211",
          "issuing_country": "DZ",
          "expiry_date": "2030-12-01"
        }
      ]
    },
    {
      "type": "child",
      "gender": "MALE",
      "first_name": "Leo",
      "last_name": "Nguyen",
      "date_of_birth": "2018-05-14",
      "identity_documents": [
        {
          "type": "passport",
          "number": "P99887766",
          "issuing_country": "DZ",
          "expiry_date": "2029-05-14"
        }
      ]
    }
  ],
  "contact_details": {
    "email": "[email protected]",
    "phone_number": "+15555550123"
  }
}'

Response example: instant issue

A Duffel instant issue: the supplier prices in USD and the agency settles in DZD, so exchangeRate is at the top level. The itinerary, the traveler pricing and the owner are shortened here; they have the shape described in Get a flight booking.

{
  "environment": "sandbox",
  "success": true,
  "requestId": "7b166b31",
  "searchId": "fs_dfx_88eb64c9-6c36-47e6-bdaa-9ad3c1377ac4",
  "offerId": "fx_offer_b7a95439",
  "processingTime": 0.408,
  "exchangeRate": 250.0,
  "booking": {
    "reference": "ord_0000BC0UuqWJKkrDO331Xc",
    "status": "confirmed",
    "pnr": "BOKT52",
    "type": "instant",
    "createdAt": "2026-11-01T14:24:41.453265Z",
    "cancelledAt": null,
    "content": "managed",
    "syncedAt": "2026-11-01T14:24:41Z",
    "availableActions": ["cancel", "change"],
    "voidableUntil": null,
    "cancellation": null
  },
  "supplier": { "id": "duffel_1", "technology": "duffel", "name": "Duffel", "offerId": "off_034q4rP6NMvbRSHMkUPbTE" },
  "agency": { "currency": "DZD" },
  "pricing": {
    "supplierCurrency": "USD",
    "agencyCurrency": "DZD",
    "basePrice": 16342.51,
    "taxesAndFees": 5216.69,
    "agencyCommission": 21.56,
    "finalPrice": 21580.76,
    "b2bPrice": 21559.2,
    "additionalServices": []
  },
  "payment": { "status": "paid", "paidAt": "2026-11-01T14:24:41Z", "paymentRequiredBy": null, "priceGuaranteeExpiresAt": null, "awaitingPayment": false },
  "documents": [{ "type": "electronic_ticket", "uniqueIdentifier": "1", "passengerIds": ["pas_0000BC0UogJGYlTqCNHhye"] }],
  "bookingReferences": [
    { "pnr": "BOKT52", "carrier": { "iataCode": "ZZ", "name": "Duffel Airways", "logoUrl": "https://assets.duffel.com/img/airlines/for-light-background/full-color-logo/ZZ.svg", "logoLockupUrl": null } }
  ],
  "passengers": [
    {
      "id": "pas_0000BC0UogJGYlTqCNHhye",
      "type": "ADULT",
      "title": "mr",
      "givenName": "Audit",
      "familyName": "Tester",
      "gender": "m",
      "bornOn": "1990-01-15",
      "email": "[email protected]",
      "phoneNumber": "+15555550123",
      "infantPassengerId": null,
      "loyaltyProgrammeAccounts": [],
      "ticketNumber": "1"
    }
  ],
  "travelerPricing": ["..."],
  "itinerary": ["..."],
  "conditions": { "refundBeforeDeparture": { "allowed": false }, "changeBeforeDeparture": { "allowed": true } },
  "owner": { "id": "arl_00009VME7D6ivUu8dn35WK", "iataCode": "ZZ", "name": "Duffel Airways", "conditionsOfCarriageUrl": "https://duffelairways.com/dummy-url/conditions-of-carriage", "...": "..." },
  "services": [],
  "meta": { "supportHold": false, "supportVoid": false, "voidDeadline": null, "voidWindowEndsAt": null, "airlineInitiatedChanges": [], "changes": [], "users": [], "orderMetadata": null },
  "postProcessing": { "bookingDbId": "8f1e6b52-3c1d-4d62-9d8a-0c6a1f6e5a11", "bucketLink": null, "emailSent": false },
  "balanceDebited": false,
  "transactionCreated": false,
  "payoutCreated": false
}

Response example: hold

The same request with "booking_type": "hold" on an Amadeus Algeria offer. The agency and the supplier price in DZD, so there is no exchangeRate. The order is unpaid: booking.status is pending and payment.awaitingPayment is true. Real response of 2026-11-01, trimmed, with the agency id and the contact data replaced by fake values (the shortened parts are marked):

{
  "environment": "sandbox",
  "success": true,
  "requestId": "66e4b1a8",
  "searchId": "fs_85ac5f12-7456-4edf-a2fb-1d51239d278b",
  "offerId": "fx_offer_9de60e2a",
  "processingTime": 2.624,
  "booking": {
    "reference": "eJzTd9ePNA9wdA0FAAs8AmE",
    "status": "pending",
    "pnr": "Y7PAEU",
    "type": "hold",
    "createdAt": "2026-11-01T15:26:00.000",
    "cancelledAt": null,
    "content": "managed",
    "syncedAt": "2026-11-01T15:26:00.000",
    "availableActions": ["confirm", "cancel"],
    "voidableUntil": null,
    "cancellation": null
  },
  "supplier": { "id": "amadeus_aqc_dz_1", "technology": "amadeus_aqc", "name": "Amadeus AQC", "offerId": "3" },
  "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": "awaiting_payment", "paidAt": null, "paymentRequiredBy": null, "priceGuaranteeExpiresAt": null, "awaitingPayment": true },
  "documents": [],
  "bookingReferences": [{ "pnr": "Y7PAEU", "carrier": { "iataCode": "AH", "name": "Air Algerie", "logoUrl": "https://assets.duffel.com/img/airlines/for-light-background/full-color-logo/AH.svg", "logoLockupUrl": "https://assets.duffel.com/img/airlines/for-light-background/full-color-lockup/AH.svg" } }],
  "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": null
    }
  ],
  "travelerPricing": ["..."],
  "itinerary": ["..."],
  "conditions": { "refundBeforeDeparture": { "allowed": true }, "changeBeforeDeparture": { "allowed": true } },
  "owner": { "id": null, "iataCode": "AH", "name": "Air Algerie", "...": "..." },
  "services": [],
  "meta": { "supportHold": true, "supportVoid": true, "voidDeadline": "2026-11-01T23:00:00Z", "voidWindowEndsAt": null, "airlineInitiatedChanges": [], "changes": [], "users": [], "orderMetadata": null },
  "fareRules": { "refund": { "allowed": true }, "exchange": { "allowed": true }, "agencyCurrency": "DZD" },
  "postProcessing": { "bookingDbId": "d6a05380-ccab-4a2f-bc13-f65ae9fdad7e", "bucketLink": null, "emailSent": false },
  "balanceDebited": false,
  "transactionCreated": false,
  "payoutCreated": false
}

Keep booking.reference as the order_id of the next calls and continue with Quote a held booking and Pay a held booking, or release the hold with Cancel a held booking.

Key fields

FieldDescription
booking.referenceThe supplier's order id. Use it as order_id in the later calls (twin: booking.order_id). In cancel_order the same key holds the PNR: read orderId there.
booking.pnrThe airline record locator. Not unique across suppliers: use reference as the key.
booking.statusconfirmed for an instant issue, pending for an unpaid hold (later calls say awaiting_payment).
booking.typeinstant or hold; pay_order turns a hold into instant.
booking.availableActionsWhat the supplier lets you do next: confirm, cancel, change.
booking.voidableUntilThe end of the same-day void window, in UTC: midnight in the supplier's time zone of the day the PNR was paid (not created). null for an unpaid hold or when voiding is not supported.
booking.createdAtA timestamp whose format depends on the supplier: with Z (Duffel) or without a zone (Amadeus).
supplierid, technology, name and offerId (the supplier's own offer id). name is Amadeus AQC here and Amadeus Algeria in a search: use supplier.id.
paymentstatus (paid or awaiting_payment), paidAt, paymentRequiredBy, priceGuaranteeExpiresAt, awaitingPayment.
documents[n]The e-ticket: uniqueIdentifier is the ticket number (Amadeus 124-2446977396), or a short opaque id in the Duffel sandbox ("1"). Treat it as opaque. Empty for a hold.
passengers[n]The travelers as the supplier stored them. Values differ by supplier: gender is m (Duffel) or male (Amadeus), names are upper case on Amadeus.
exchangeRateAt the top level, when supplierCurrency and agencyCurrency differ; see Flight pricing.
postProcessing.bookingDbIdThe FX-Port id of the booking: the id that get_booking returns.
postProcessing.bucketLinkThe PDF link. null right after a booking: read it with get_booking or get_ticket.
requiresACDPresent and true only for an ACD booking: upload passports with ACD passport uploads.
balanceDebited, transactionCreated, payoutCreatedSandbox only, always false: nothing moved.

Next to these keys the response has the snake_case twins request_id, search_id, offer_id, processing_time, exchange_rate, booking.order_id, pricing.base_price ... pricing.b2b_price, meta.support_hold, support_void, void_deadline, supplier.supplier_id, supplier_name, supplier_offer_id, and in each passenger first_name, last_name, date_of_birth, phone_number, passenger_id, passenger_type.

Getting the confirmation PDF

The PDF is generated after the booking, usually within 10 to 30 seconds. Poll get_booking until booking.bucket_link has a value, or call get_ticket until ticket_ready is true.

Behaviour to know

  • The checks run in this order: the required fields, the booking lock of the offer, the search, the offer in that search, the priced offer, the ACD exemption, the hold capability, and (on live) the agency status and balance. Only then the supplier is called.
  • One booking per offer. The offer is locked while it is booked and after it succeeded: booking it again is 409 duplicate_booking (with pnr). A booking that fails releases the offer.
  • Duffel books one offer per search. A second Duffel booking from the same search_id fails with 409 offer_request_already_booked: search again.
  • A failed booking leaves a trace. A booking that the supplier refuses is stored with a reference that starts with FAILED- and the status failed; it shows in your history. Ignore it.

Errors

StatusCodeWhenWhat to do
400bad_requestsearch_id or offer_id is missing: Missing required parameters: search_id and offer_id are required.Send both.
400missing_fieldpassengers or one of contact_details.email and contact_details.phone_number is missing; or a passenger lacks gender, first_name, last_name or date_of_birth.Send what errors[0].field names (for example passengers[0].first_name).
400invalid_booking_type, invalid_passenger_type, invalid_gender, invalid_name, invalid_email, invalid_phone_number, invalid_date, invalid_country_code, invalid_commission_override, invalid_identifier, invalid_field_typeA value is not accepted. errors[0].field names it. An unknown booking_type is refused here and never becomes an order.Fix it.
400too_many_passengersMore than 18 passengers.Split the booking.
400hold_not_supportedhold on an offer that cannot be held.Book it with issue, or choose another offer.
402insufficient_balanceLive only: the balance is lower than b2bPrice. details has available, required, currency.Top up, then retry.
403read_write_permission_requiredA read-only key.Use a read+write key.
403agency_not_activeLive only: the agency is not approved.Contact support.
404search_not_foundThe search_id is unknown, older than 24 hours, or belongs to another agency (the answer is the same).Search again.
404offer_not_foundThe offer is not in that search.Check the pair search_id and offer_id.
404offer_not_pricedThe offer was never priced (or the priced offer is older than 12 hours).Call price_flight.
409duplicate_bookingThe offer was already booked; pnr is the existing booking.Read that booking.
409offer_request_already_booked, offer_already_booked, order_already_existsThe supplier says this offer or search was already booked (Duffel allows one booking per search).Search again, or read the booking before you retry.
409booking_in_progress, booking_lock_failedAnother request is booking this offer.Wait, then read the booking before you retry.
409acd_exemption_invalidThe ACD exemption of the search is not valid any more.Search again without it.
422validation_type, born_on_does_not_match and other supplier codesThe supplier refused a detail of the booking: the body has error.code, error.type (validation_error) and error.details[] (with source.field).Fix the detail.
503The supplier's own code, or supplier_errorThe supplier itself failed: it answered with a status of 500 or more (meta.supplierStatusCode).Read the booking before you retry: a booking can exist at the airline.
503balance_lock_timeoutLive only, for a booking that is paid when it is made (issue): another operation of your agency holds the balance in the same currency for a moment. Nothing was booked and nothing was charged, and the offer is released. 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.Send the same request again after a few seconds.
500post_booking_failedThe booking was created at the supplier but FX-Port could not finish storing it. The message is generic and the body carries the requestId.Do not book again: look for the booking in your history or contact support with the request_id.

A failed booking is always a JSON body: 409 when the offer or search was already booked, 503 when the supplier failed with a 5xx status (or your balance was busy, see above), 422 for every other refusal. You never receive an HTML 502 page from this endpoint. The refusal of the supplier is in error.code, error.type and error.message.

See Errors for the body of an error.

Related

  • Price a flight offer, Flight passengers & ages and Booking flows.
  • Get a flight booking and Get the ticket PDF.
PreviousPrice a flight offerNext Quote a held booking

On this page

  • Request body
  • Passenger object
  • Identity document object
  • Legacy names
  • Request examples
  • Response example: instant issue
  • Response example: hold
  • Key fields
  • Getting the confirmation PDF
  • Behaviour to know
  • Errors
  • Related

ยฉ 2026 FX-Port

TermsPrivacy[email protected]