# FX-Port API Documentation --- > Complete endpoint guides, request and response examples, passenger rules, pricing fields, and security guidance. --- # Authentication Source: /authentication All requests must include a Bearer token in the `Authorization` header. ```http Authorization: Bearer fxp_live_YOUR_KEY ``` Get your API keys at [fx-port.com/dashboard/api](https://fx-port.com/dashboard/api). ## Permission levels [#permission-levels] | Permission | What it allows | | -------------- | --------------------------------------------------------------------------------------- | | **Read** | Search, price, get quotes, retrieve bookings, airport lookups, balances, history | | **Read+Write** | All read access **plus** creating bookings, paying held orders, cancelling reservations | A read key returning `403 read_write_permission_required` means the operation is state-changing (booking, payment, or cancellation). Use a read+write key for those. ## Key prefixes and environments [#key-prefixes-and-environments] There is **one base URL** for all environments. The environment is determined entirely by the key prefix. | Prefix | Environment | Behaviour | | ----------- | ----------- | ----------------------------------------------------------- | | `fxp_live_` | **Live** | Real agency data and live financial ledger | | `fxp_test_` | **Sandbox** | Isolated test bookings; balance endpoints return empty data | External keys (issued to agencies) automatically resolve the agency identity from the key itself. ## IP restrictions [#ip-restrictions] You can restrict a live key to specific IP addresses in the dashboard. Recommended for production Read+Write keys. ## Security [#security] Key handling, minimal-permission keys, booking-ownership authorization, and general API security hygiene now live on their own page: see [Security](/security). --- # Environments Source: /environments There is **one base URL** for all environments: ``` https://api.fx-port.com ``` The environment is determined entirely by the **API key prefix** — there are no separate URLs. | Key prefix | Environment | Behaviour | | ----------- | ----------- | ----------------------------------------------------------------------- | | `fxp_live_` | **Live** | Real agency data; debits real balance; real bookings | | `fxp_test_` | **Sandbox** | Isolated test bookings; no real charges; balance endpoints return empty | ## Getting API keys [#getting-api-keys] Create keys in the FX-Port dashboard at [fx-port.com/dashboard/api](https://fx-port.com/dashboard/api). Both sandbox and live keys can have **Read** or **Read+Write** permissions: | Permission | Sandbox key use | Live key use | | ---------- | ---------------------------------------------- | ---------------------------------------------- | | Read | Search, price, get quotes, retrieve bookings | Same | | Read+Write | Search + **create test bookings, pay, cancel** | Search + **real bookings, real balance debit** | ## Postman collections [#postman-collections] Pre-built collections with every endpoint, auto-wired test scripts, and randomised (Latin/ASCII) passenger names. Download, import into Postman, set your API key as the `api_key` variable, and every request is ready to run. **The collections contain no API keys** — add your own. Search, price, book, hold/pay, cancel, get booking, get ticket, airports. Package catalog, compatible devices, orders, topups, usage, bookings, and installation instructions. Balances, financial ledger, and booking history. Discover enabled suppliers and capabilities. Direct links: * [FX-Port-Flights.postman\_collection.json](/postman/FX-Port-Flights.postman_collection.json) * [FX-Port-eSIMs.postman\_collection.json](/postman/FX-Port-eSIMs.postman_collection.json) * [FX-Port-Account.postman\_collection.json](/postman/FX-Port-Account.postman_collection.json) * [FX-Port-Suppliers.postman\_collection.json](/postman/FX-Port-Suppliers.postman_collection.json) Get your API keys at [fx-port.com/dashboard/api](https://fx-port.com/dashboard/api) — sandbox keys are free. --- # Introduction Source: / **FX-Port** is a unified travel API. One integration gives you **flights, hotels, eSIMs, and more**, sourced across many suppliers and returned with the best available prices — no per-supplier code. * **Flights** — search, price, book, hold/pay across GDS, NDC, LCC, and direct airlines. **Available now.** * **Hotels** — global hotel inventory. **Live; docs landing shortly.** * **eSIMs** — data packages in 190+ countries. **Live; docs landing shortly.** * **More products** are on the way. **Work in progress.** FX-Port is actively expanding: more suppliers, more GDS/NDC connections, and new products. We aim to **avoid breaking changes** — any breaking or major change ships under a **new API version** so your current integration keeps working. Have feedback, a feature request, or found a bug? Email us at [dev@fx-port.com](mailto:dev@fx-port.com). **API keys can trigger real financial debits.** A Read+Write key creates bookings and charges your agency balance. **Never** expose keys in front-end code, mobile apps, or public repos — always call the API from your backend. See [Security](/security) for the full guide. ## Get API access in minutes [#get-api-access-in-minutes] There is **no approval queue, no agreement to sign, and no deposit** required to start building. Self-serve from day one: **Create an account** — sign up at [fx-port.com/register](https://fx-port.com/register). Takes about a minute. **Generate your API keys** — open the [API page](https://fx-port.com/dashboard/api) in your dashboard and create a key. Start with a sandbox key (`fxp_test_`) to build and test for free. **Start building** — your first search call works immediately. See the [quick start](#quick-start-flights-in-3-calls) below. **Sandbox is free and unlimited to explore.** You only need a funded balance when you want to issue real bookings with a `fxp_live_` key — see [Environments](/environments). ## Base URL [#base-url] ``` https://api.fx-port.com ``` All paths are prefixed with `/api/v1/`. **One URL for all environments** — live vs sandbox is determined by your API key prefix (`fxp_live_` or `fxp_test_`). ## Where to start [#where-to-start] Key handling, booking-ownership authorization, and API security best practices. Read this first. API keys, permission levels, and environments. Live vs sandbox, and downloadable Postman collections. Discover every active supplier and its capabilities. Balances, financial ledger, and booking history across all products. Search, price, book, hold, pay, cancel, get PDF. Package catalog, orders, topups, usage, and installation instructions. Coming to the docs shortly — already live on the API. ## Quick start (flights, in 3 calls) [#quick-start-flights-in-3-calls] **Search** — `POST /api/v1/get_flights`. Save `suppliers[0].searchId` and an offer `id`. **Price** — `POST /api/v1/flights/price_flight` with that `search_id` and `offer_id`. Required before booking. **Book** — `POST /api/v1/flights/book_flight` with `booking_type: "issue"`, the passengers, and contact details. Tickets are issued immediately. ```bash # 1. Search curl --request POST \ --url https://api.fx-port.com/api/v1/get_flights \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 } }' ``` Continue with the full [Flight booking flow →](/flights/booking-flows) ## Quick start (eSIMs, in 3 calls) [#quick-start-esims-in-3-calls] **List packages** — `GET /api/v1/esims/packages?country=france`. Save a package `id`. **Create the order** — `POST /api/v1/esims/orders` with that `package_id` plus the traveler's `email` and `phone` (both required — the traveler receives the QR code and setup instructions by email). Returns the eSIM `iccid` immediately. **Top up later** — `GET /api/v1/esims/topup-packages/{iccid}` to see what's available for that exact eSIM, then `POST /api/v1/esims/topups` with one of the returned package IDs. ```bash # 1. List packages curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/packages?country=france' \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' ``` Continue with the full [eSIMs overview →](/esims) ## Built with AI tools? [#built-with-ai-tools] Point your AI coding assistant at these machine-readable files for accurate, up-to-date context: | URL | Content | | ---------------------------------- | ----------------------------------------------------- | | [`/llms.txt`](/llms.txt) | Page index — titles, URLs, descriptions (lightweight) | | [`/llms-full.txt`](/llms-full.txt) | Full documentation text | ## Contact [#contact] Questions, feedback, or issues? Email the developer team at [dev@fx-port.com](mailto:dev@fx-port.com). Get your API keys at [fx-port.com/dashboard/api](https://fx-port.com/dashboard/api). --- # Sandbox notice Source: /sandbox Sandbox API keys (`fxp_test_...`) connect to the **sandbox environments of our underlying suppliers** (Amadeus test, Duffel test, Airalo sandbox, etc.). Supplier sandbox environments are **not under FX-Port's control**. They may experience brief outages for maintenance, and some flights or airlines may return errors during pricing or booking that would not occur on live. This is a known limitation of third-party sandbox environments — it often works correctly on live even when sandbox fails. ## What sandbox does [#what-sandbox-does] | Behaviour | Detail | | ------------------------------------- | ---------------------------------------------------------------------------------------- | | **No real balance debit** | All financial operations are skipped. Sandbox bookings do not charge your balance. | | **Realistic but synthetic inventory** | Flight offers are real data structures but use test fares. Not all routes are available. | | **Balance endpoints return empty** | `GET /api/v1/balances` returns `[]` in sandbox. This is expected. | | **Webhooks are sent** | Sandbox webhooks fire to the sandbox webhook URL you configure. | ## Common sandbox quirks [#common-sandbox-quirks] * **Duffel sandbox**: offer inventory is randomised and changes frequently. An offer available in search may expire by the time you price it. This is normal — retry with a fresh search. * **Amadeus test**: some airline codes or routes may reject at the booking step even though they appeared in search results. Try a different route (e.g. `ALG → ORN`) if you hit repeated errors. * **Outages**: if a supplier's sandbox is down, FX-Port will still respond but the supplier's entry in the response will have `success: false`. Wait and retry. ## Testing checklist [#testing-checklist] Before switching to live keys, verify in sandbox: 1. **Search** — your search request structure is accepted and returns offers 2. **Price** — re-pricing a cached offer works and pricing fields match your expectations 3. **Book (issue)** — instant booking completes and returns a PNR and e-ticket 4. **Book (hold)** — hold booking creates a reservation with `status: awaiting_payment` 5. **Pay** — `pay_order` with the held reservation's `order_id` succeeds 6. **Get booking** — `get_booking` returns the full booking including `bucket_link` (PDF) once generated 7. **Cancel a hold** — `cancel_order` releases an unpaid held PNR. Issued-ticket void, exchange, re-issue, and refund remain manual support operations. --- # Security Source: /security An FX-Port API key with **Read+Write** permission can create bookings and trigger real financial debits from your agency balance. Treat it with the same care as a payment credential. ## Always keep keys server-side [#always-keep-keys-server-side] **Never expose your FX-Port API key in:** * Browser JavaScript (React client components, Vue, Angular, plain JS) * Mobile apps (React Native, Expo, Flutter, native iOS/Android) * Public repositories, commit history, or CI/CD environment variables visible in build logs * Client-side `.env` files shipped with a compiled app Your app's **backend** (Node.js, Python, Go, etc.) should be the only code that ever reads and uses the FX-Port API key. If your front-end needs to trigger a booking, it should call **your own authenticated backend**, which then calls FX-Port on the user's behalf. ``` Browser/App → Your backend → FX-Port API ↑ API key lives here only ``` ## Minimal-permission keys [#minimal-permission-keys] Use a **Read-only** key for integrations that only display search results, balances, or status. Reserve the **Read+Write** key for the backend service that actually creates bookings. See [Authentication](/authentication) for what each permission level allows. ## Key rotation [#key-rotation] Rotate keys immediately if you suspect a leak. The dashboard lets you invalidate and replace a key without downtime. Rotate on a routine schedule too (e.g. every 90 days) rather than only after an incident — routine rotation limits how long a silently-leaked key stays useful to an attacker. ## Enforce booking ownership in your own backend [#enforce-booking-ownership-in-your-own-backend] FX-Port scopes every response to the agency that owns the API key — one agency's key can never return another agency's bookings, balances, or account data. But that only protects the FX-Port API boundary. **If your own application lets multiple end-clients share one FX-Port key** (a common pattern), you are responsible for authorization **inside your own backend** so one client cannot view, modify, or cancel another client's booking. A booking reference, order ID, ICCID, or PNR is **not a secret** — it can be guessed, enumerated, logged, or leaked through a shared link. Never treat "the caller supplied a valid booking ID" as proof that the caller is entitled to see it. Concretely: * Store which of **your own** end-users/clients created or is entitled to each booking (a `client_id` / `user_id` column alongside the FX-Port `booking_id` you save). * On every read or mutation your backend exposes (get booking, get ticket, cancel, pay, request a refund, view usage, etc.), check that the authenticated caller in **your** system owns that booking **before** forwarding the request to FX-Port — do not rely on the booking ID alone as an access-control token. * Apply the same rule to balances, transaction/financial history, and account data: scope every query by the authenticated caller's own identity, never by an ID taken directly from the request path or query string. * If you use a database with row-level security (Supabase, Postgres RLS, etc.), enforce this at the database layer as well as in application code — defense in depth. A backend bug that forgets an `owner_id` filter should not be enough, on its own, to expose another client's data. * Test this explicitly: as client A, try to fetch/cancel a booking that belongs to client B by ID. It should fail with `403`/`404`, not succeed. Treat this as a required test case for every booking, balance, and account endpoint you expose — for flights, hotels, eSIMs, and any other product — not an edge case to defer. This is Broken Object Level Authorization (OWASP API Security Top 10, API1:2023) — the single most common vulnerability class in booking/travel APIs. It is entirely preventable with a per-request ownership check and is far cheaper to build in from day one than to retrofit after a data leak. ## Validate and sanitize webhook/callback payloads [#validate-and-sanitize-webhookcallback-payloads] If you build a webhook receiver or callback endpoint for booking status updates, treat every incoming payload as untrusted input, not as fact: * Verify a signature/secret on every callback before acting on it, rather than trusting the sender because the request "looks right". * Re-fetch authoritative state from the FX-Port API before making a financial or booking decision based on a webhook payload, instead of trusting fields inside the webhook body directly. * Make callback handlers idempotent — a retried or duplicated webhook delivery should not double-book, double-refund, or double-charge. ## Rate limiting and abuse protection [#rate-limiting-and-abuse-protection] Search and pricing endpoints are inexpensive to call but not free to abuse: * Apply your own rate limiting in front of any public-facing feature that triggers an FX-Port call on user input (a search box, a price checker), so a single abusive user or bot can't exhaust your key's rate limit or run up unnecessary supplier calls. * Cache read-only, non-personalized responses (e.g. [suppliers](/suppliers) or [airports](/flights/airports)) client-side or at your edge where appropriate, instead of re-querying on every page load. ## Logging and data retention [#logging-and-data-retention] * Never log full API keys, even in debug/error logs — log a short prefix (`fxp_live_ab12…`) if you need to identify which key made a call. * Avoid logging full traveler PII (passport numbers, full card/payment details) in plaintext application logs. If you must retain it for support, encrypt it at rest and restrict access. * Apply a retention policy to logs and cached search/pricing responses containing personal data, rather than keeping it indefinitely by default. ## Broader security hygiene [#broader-security-hygiene] These rules apply to every sensitive credential in your application stack, not just FX-Port keys. * **Supabase / database API keys** — never expose the `service_role` key in a browser or mobile app. Only the `anon` key can be used client-side, and even that should be behind Row-Level Security policies. * **Third-party vendor keys** (Stripe, Twilio, etc.) — always consumed server-side. A leaked Stripe secret key or Twilio auth token can cause direct financial loss. * **WordPress and open-source CMS plugins** — themes and plugins are a frequent attack vector. Credentials stored in a WordPress database are regularly exfiltrated through plugin vulnerabilities. If you run a CMS alongside your booking integration, keep credentials in separate, isolated infrastructure and audit installed plugins regularly. * **Dependency CVEs** — keep your runtime and framework versions current. Subscribe to security advisories for your stack ([nodejs.org/en/security](https://nodejs.org/en/security), [github.com/advisories](https://github.com/advisories)) and apply patches promptly. * **Least privilege everywhere** — the same "minimal-permission key" principle above applies to cloud IAM roles, CI/CD service accounts, and internal admin tooling. A credential should only be able to do what its specific job requires. ## Questions? [#questions] Found a vulnerability or want a security review of your integration? Email [dev@fx-port.com](mailto:dev@fx-port.com). --- # GET /api/v1/balances/{balance_id}/history Source: /account/balance-history The complete financial ledger for a specific currency balance. Returns every money movement recorded against the balance. **Permission:** Read Call `GET /api/v1/balances` first to obtain the `balance_id` for the currency you want to query. **Sandbox:** Returns empty `data` array. ## The `source` field [#the-source-field] Each entry has a `source` field: | `source` | What it represents | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"transactions"` | **Authoritative balance movements.** Recorded every time the balance changes: top-up, booking charge, or refund. **Use these for reconciliation totals.** | | `"flight_balance_operations"` | **Per-booking flight audit records.** Granular events within a booking lifecycle (initial debit, FX surcharge, adjustment, refund). One booking can produce multiple rows. | | `"hotel_balance_operations"` | **Per-booking hotel audit records.** Same concept as flight operations. | A single action (e.g. confirming a flight) produces **both** a `transactions` row and a `flight_balance_operations` row. Both appear in this response. Sum only `transactions` rows for financial totals. ## Path parameter [#path-parameter] | Parameter | Description | | -------------- | --------------------------------------------------- | | `{balance_id}` | UUID of the balance row from `GET /api/v1/balances` | ## Query parameters [#query-parameters] | Parameter | Type | Description | | --------- | ------- | ---------------------------------------------------------------------------------- | | `from` | string | Inclusive start. ISO-8601 date (`2026-07-01`) or datetime (`2026-07-01T00:00:00Z`) | | `to` | string | Inclusive end. Bare dates expand to `23:59:59 UTC` | | `type` | string | Filter by type: `CREDIT` \| `DEBIT` \| `REFUND` (applies to `transactions` only) | | `page` | integer | 1-based page number. Default: `1` | | `limit` | integer | Items per page. Range 1–100. Default: `20` | ## Transaction types [#transaction-types] | Type | Direction | When it appears | | ------------------- | ------------- | ----------------------------------------------------------------------- | | `CREDIT` / `credit` | Funds **in** | Manual top-ups, deposit returns when a hold is released | | `DEBIT` / `debit` | Funds **out** | Booking confirmations, FX exchange surcharges, manual debit adjustments | | `REFUND` / `refund` | Funds **in** | Full or partial refund after a booking is cancelled or voided | Uppercase values appear in `transactions` rows; lowercase in `*_balance_operations` rows. ## Request example [#request-example] ```bash curl --get \ --url https://api.fx-port.com/api/v1/balances/db002f7a-c136-4f32-b19d-38a2542f2af9/history \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --data-urlencode 'from=2026-07-01' \ --data-urlencode 'to=2026-07-31' \ --data-urlencode 'limit=50' ``` ## Response example [#response-example] ```json { "environment": "live", "success": true, "data": [ { "source": "transactions", "id": "3e8b1c02-d7a4-4f5e-9b2a-1c0d8e7f3a91", "type": "CREDIT", "amount": 2500.00, "currency": "DZD", "description": "Balance top-up — bank transfer ref TXN-20260711", "balanceId": "db002f7a-c136-4f32-b19d-38a2542f2af9", "createdAt": "2026-07-11T09:15:00.000000+00:00" }, { "source": "flight_balance_operations", "id": "658185df-234c-43ba-8175-ba9a5abe6007", "type": "debit", "amount": 87.50, "currency": "DZD", "description": "Exchange fee debit for flight booking 4YEK64", "bookingRecordId": "136dc334-b581-48c9-8e4c-7ec87cc5f41a", "createdAt": "2026-07-09T14:30:00.000000+00:00" }, { "source": "transactions", "id": "31936df0-299d-429e-b0f5-056a6c0587cc", "type": "DEBIT", "amount": 5081.44, "currency": "DZD", "description": "Booking confirmation — flight XE4YCB (ALG → CDG)", "balanceId": "db002f7a-c136-4f32-b19d-38a2542f2af9", "createdAt": "2026-07-01T15:42:34.272792+00:00" }, { "source": "transactions", "id": "a4c72d11-08b1-4e3f-b9d8-2e5a1f6c8b02", "type": "REFUND", "amount": 125.00, "currency": "DZD", "description": "Partial refund for hotel booking HMPSHEPI1", "balanceId": "db002f7a-c136-4f32-b19d-38a2542f2af9", "createdAt": "2026-07-01T05:00:09.901817+00:00" } ], "pagination": { "page": 1, "limit": 50, "total": 4, "hasMore": false }, "meta": { "agencyId": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "balanceId": "db002f7a-c136-4f32-b19d-38a2542f2af9", "balanceCurrency": "DZD", "environment": "live" } } ``` ## Field reference [#field-reference] | Field | Present when | Description | | ----------------- | --------------------------- | ------------------------------------------------------------------------------------- | | `source` | Always | Entry type — see table above | | `id` | Always | Entry UUID | | `type` | Always | `CREDIT`, `DEBIT`, or `REFUND` (uppercase for transactions; lowercase for operations) | | `amount` | Always | Absolute value — always positive. Direction conveyed by `type`. | | `currency` | Always | ISO 4217 code — always matches the requested balance currency | | `description` | Always | Human-readable label including booking reference and action | | `balanceId` | `transactions` only | UUID of the balance this transaction is recorded against | | `bookingRecordId` | `*_balance_operations` only | UUID of the internal booking record — use to correlate with a specific booking | | `createdAt` | Always | ISO-8601 datetime | **Reconciliation tip:** A single booking event produces both a `transactions` row and a `flight_balance_operations` row. When computing net balance changes, sum only `transactions` rows. When tracing a specific booking, filter by `source = "flight_balance_operations"` and `bookingRecordId`. --- # GET /api/v1/balances Source: /account/balances Returns the current balance for each currency the agency holds. **Permission:** Read Capture the `id` field from each balance row — you need it as the `{balance_id}` path parameter to query that currency's financial history. **Sandbox:** Returns empty `data` array and `meta.financialOperationsEnabled: false`. ## Request example [#request-example] ```bash curl --request GET \ --url https://api.fx-port.com/api/v1/balances \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' ``` ## Response example [#response-example] An agency can hold multiple currency balances. Each row has its own `id`. ```json { "environment": "live", "success": true, "data": [ { "id": "db002f7a-c136-4f32-b19d-38a2542f2af9", "agency_id": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "currency_code": "EGP", "available_balance": 412500.00, "created_at": "2024-08-22T10:24:06.533800+00:00", "updated_at": "2026-08-17T04:45:15.697817+00:00" }, { "id": "a71c9e34-2b18-40df-9c77-6b0a1f2e5d40", "agency_id": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "currency_code": "EUR", "available_balance": 18750.50, "created_at": "2024-08-22T10:24:06.533800+00:00", "updated_at": "2026-08-16T22:10:03.120400+00:00" }, { "id": "c5f0a812-7d63-4e9a-8b21-4e9f0c6a2b11", "agency_id": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "currency_code": "NGN", "available_balance": 9250000.00, "created_at": "2025-01-14T09:02:41.000000+00:00", "updated_at": "2026-08-15T11:30:55.000000+00:00" } ], "meta": { "agencyId": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "environment": "live" } } ``` ## Field reference [#field-reference] | Field | Description | | ------------------- | ---------------------------------------------------------------------- | | `id` | Balance UUID — use as `{balance_id}` for the history endpoint | | `agency_id` | Agency UUID | | `currency_code` | ISO 4217 currency code (e.g. `EGP`, `EUR`, `NGN`, `DZD`, `USD`, `INR`) | | `available_balance` | Usable balance, rounded to 2 decimal places | | `created_at` | When the balance row was created | | `updated_at` | Last movement against this balance | --- # GET /api/v1/bookings/{id} Source: /account/booking-detail Returns the detail record for a single booking. You can look it up by the **internal booking id** (a UUID) or by a public reference. **Permission:** Read | Path parameter | Accepts | Description | | -------------- | ------------------------------------------------------------------------ | -------------------- | | `{id}` | internal `id` (UUID) · PNR / booking reference · order id · ICCID (eSIM) | The booking to fetch | **Prefer the internal `id` (UUID).** PNRs and booking references are **not globally unique** — the same PNR can be issued by a different airline or supplier, so a lookup by PNR can be ambiguous. The internal `id` returned in every booking/response payload is unique and stable. Store it and use it as your primary key; treat the PNR as a display reference only. ## Query parameters [#query-parameters] | Parameter | Default | Description | | --------- | ------- | -------------------------------------------------------------------------------------------- | | `product` | `all` | Scope to a specific product for faster resolution: `flights`, `hotels`, `esims`, `transfers` | ## Request examples [#request-examples] ```bash # Recommended: internal id (UUID), unambiguous curl --get \ --url https://api.fx-port.com/api/v1/bookings/e133d887-fd3c-4a44-a19e-4f20ddb690d4 \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --data-urlencode 'product=flights' # Also supported: PNR (may be ambiguous across suppliers) curl --get \ --url https://api.fx-port.com/api/v1/bookings/XE4YCB \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --data-urlencode 'product=flights' ``` ## Response example [#response-example] ```json { "success": true, "data": { "product": "flights", "bookingId": "XE4YCB", "status": "confirmed", "record": { "booking_id": "XE4YCB", "status": "confirmed", "total_price": 5086.53, "agency_commission": 5.09, "currency": "DZD", "departure_airport": "ALG", "arrival_airport": "CDG", "departure_date": "2026-09-15T08:30:00", "ticket_number": "124-2442997931", "bucket_link": "https://s3.example.com/flight-bookings/XE4YCB-JAMES-SMITH.pdf", "supplier_id": "amadeus_aqc_dz_1", "environment": "live", "created_at": "2026-08-17T11:22:00+00:00", "paid_at": "2026-08-17T11:22:05+00:00" }, "pricing": [ { "supplier_currency": "DZD", "agency_currency": "DZD", "base_price": 3160.00, "taxes_and_fees": 1921.44, "agency_commission": 5.09, "final_price": 5086.53, "b2b_price": 5081.44, "environment": "live" } ], "travelerPricing": [ { "traveler_id": "1", "traveler_type": "ADULT", "fare_option": "STANDARD", "supplier_currency": "DZD", "agency_currency": "DZD", "base": 3160.00, "total": 5086.53, "b2b_price": 5081.44, "environment": "live" } ], "balanceOperations": [] }, "meta": { "agencyId": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "environment": "live" } } ``` ## Field reference [#field-reference] | Field | Description | | ------------------------------ | ---------------------------------------------------------------------- | | `record.bucket_link` | Confirmation PDF download URL (flights only) | | `record.ticket_number` | E-ticket number(s) | | `pricing[0].b2b_price` | Amount debited from agency balance = `final_price − agency_commission` | | `pricing[0].taxes_and_fees` | Taxes + platform fees. Excludes `agency_commission`. | | `travelerPricing[n].b2b_price` | Per-traveler debit amount | --- # GET /api/v1/history Source: /account/history Booking summaries across one or all products, sorted by `createdAt` descending. **Permission:** Read Returns lightweight records — enough for status tracking, reconciliation, and dashboards. For full traveler details, fare rules, and pricing breakdown, use the product-specific booking endpoints (e.g. `GET /api/v1/bookings/{id}` or `POST /api/v1/flights/get_booking`). ## Query parameters [#query-parameters] | Parameter | Type | Default | Description | | --------- | ------- | ------- | ----------------------------------------------------------------------------- | | `product` | string | `all` | `all` \| `flights` \| `hotels` \| `esims` \| `transfers` | | `status` | string | — | Filter to a single status (e.g. `confirmed`, `awaiting_payment`, `cancelled`) | | `from` | string | — | Inclusive start. ISO-8601 date or datetime | | `to` | string | — | Inclusive end. ISO-8601 date or datetime | | `page` | integer | `1` | 1-based page number | | `limit` | integer | `20` | Items per page. Range 1–100 | ## Status reference [#status-reference] | Status | Products | | ------------------ | ------------------------------------- | | `confirmed` | Flights, hotels, esims, transfers | | `awaiting_payment` | Flights (held orders not yet paid) | | `pending` | Flights, hotels (booking in progress) | | `cancelled` | All products | | `failed` | All products | | `refunded` | Flights, hotels | ## Request examples [#request-examples] ```bash # All recent bookings curl --request GET \ --url https://api.fx-port.com/api/v1/history \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' # Confirmed flights only, this month curl --get \ --url https://api.fx-port.com/api/v1/history \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --data-urlencode 'product=flights' \ --data-urlencode 'status=confirmed' \ --data-urlencode 'from=2026-08-01' ``` ## Response example [#response-example] ```json { "success": true, "data": [ { "product": "flights", "id": "e133d887-fd3c-4a44-a19e-4f20ddb690d4", "bookingId": "XE4YCB", "status": "confirmed", "totalPrice": 6350.00, "currency": "EGP", "createdAt": "2026-08-17T11:22:00+00:00", "paidAt": "2026-08-17T11:22:05+00:00", "departureAirport": "CAI", "arrivalAirport": "DXB" } ], "pagination": { "page": 1, "limit": 20, "total": 1, "hasMore": false }, "meta": { "agencyId": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "environment": "live", "product": "all" } } ``` Each entry includes both the internal `id` (a stable UUID) and the public `bookingId` (PNR / reference). **Use `id` as your primary key** when storing or re-fetching a booking — PNRs are not globally unique. Fetch full details with [`GET /api/v1/bookings/{id}`](/account/booking-detail) using that `id`. --- # Account API overview Source: /account Read-only endpoints for querying account state: balances, financial transaction history, and booking summaries. **Permission:** Read (all endpoints in this section) These endpoints expose account-level data for reconciliation, dashboards, and integration monitoring. They are **not** the booking operations API. For full traveler details, fare rules, and booking actions, use the product-specific endpoints (e.g. `/api/v1/flights/get_booking`). ## Endpoints [#endpoints] | Method | Path | Description | | ------ | --------------------------------------- | -------------------------------------------------- | | `GET` | `/api/v1/balances` | Current balance for each currency the agency holds | | `GET` | `/api/v1/balances/{balance_id}/history` | Full financial ledger for a specific currency | | `GET` | `/api/v1/history` | Booking summaries across all products | | `GET` | `/api/v1/bookings/{id}` | Single booking detail by public reference | ## Sandbox behaviour [#sandbox-behaviour] In sandbox: * `GET /api/v1/balances` returns an empty `data` array * `GET /api/v1/balances/{id}/history` returns an empty `data` array * `GET /api/v1/history` returns sandbox bookings only * `GET /api/v1/bookings/{id}` returns sandbox bookings only Sandbox bookings do not affect the live ledger. ## Error responses [#error-responses] | HTTP | `detail.error` | Cause | | ----- | ------------------- | -------------------------------------------- | | `400` | `invalid_date` | `from` or `to` is not a valid ISO-8601 value | | `400` | `invalid_product` | `product` is not one of the allowed values | | `401` | — | Missing or invalid API key | | `403` | — | Key lacks permission | | `404` | `booking_not_found` | No booking matches in this environment | | `500` | — | Internal server error | --- # eSIM bookings Source: /esims/bookings ## List bookings [#list-bookings] `GET /api/v1/esims/bookings` **Permission:** Read Paginated list of the agency's own eSIM and topup bookings, searchable by name, email, ICCID, or booking ID. ```bash curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/bookings?page=1&limit=50&search=' \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` | Query parameter | Description | | ---------------- | --------------------------------------------------------------------- | | `search` | Match against booking ID, customer name/email, package name, or ICCID | | `page` / `limit` | Pagination (`limit` max `200`) | ### Response example [#response-example] ```json { "success": true, "data": [ { "booking_id": "FX-1787835379756", "order_id": "20260827-088478", "status": "confirmed", "currency": "EUR", "total_price": 9.52, "agency_commission": 1.02, "type": "esim", "package_name": "1 GB - 7 days • France", "iccid": "8900000338153738419", "customer_email": "ana.kova@example.com", "customer_first_name": "Ana", "customer_last_name": "Kova", "customer_phone": "+34600123456", "provider_name": "Airalo", "supplier_id": "airalo", "created_at": "2026-08-27T12:56:19.000Z" } ], "pagination": { "total": 1, "page": 1, "limit": 50, "pages": 1 } } ``` Only agency-facing fields are returned — supplier source cost, FX-Port's internal platform margin, and any kiosk/partner-level commission are never included. `provider_name` / `supplier_id` identify which supplier actually fulfilled this specific booking. ## Topup history for an ICCID [#topup-history-for-an-iccid] `GET /api/v1/esims/bookings/{iccid}/topup-history` **Permission:** Read ```bash curl --request GET \ --url https://api.fx-port.com/api/v1/esims/bookings/8900000338153738419/topup-history \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` Returns the same booking shape as [list bookings](#list-bookings), filtered to `type: "topup"` rows for this ICCID. --- # eSIMs API overview Source: /esims Sell travel data eSIMs across 190+ countries from a single API surface. FX-Port sources packages from multiple eSIM suppliers and applies agency-specific pricing automatically — you never call a supplier directly, and which supplier fulfills a given package is an internal implementation detail. **Coverage and pricing grow automatically.** FX-Port continuously onboards more eSIM suppliers, and each supplier independently expands its own network of mobile operators over time — both add new countries, package sizes, and better rates/connectivity to the catalog. Your integration picks up new packages with no code changes — always read `package_id` from the catalog response rather than hard-coding it. ## eSIM concepts [#esim-concepts] Read these before integrating. What each price field means and the 12% agency commission. Why a topup can occasionally be unavailable, and how FX-Port protects your balance. ## Endpoints [#endpoints] | Method | Path | Permission | Description | | ------ | -------------------------------------------------------- | -------------- | ------------------------------------------------------------- | | `GET` | `/api/v1/esims/packages` | Read | List the package catalog, optionally filtered by country/type | | `GET` | `/api/v1/esims/compatible-devices` | Read | List devices known to support eSIM | | `POST` | `/api/v1/esims/orders` | **Read+Write** | Create a new eSIM order | | `GET` | `/api/v1/esims/topup-packages/{iccid}` | Read | List packages available to top up a specific ICCID | | `POST` | `/api/v1/esims/topups` | **Read+Write** | Top up an existing eSIM | | `GET` | `/api/v1/esims/usage/{iccid}` | Read | Data usage for a single ICCID | | `GET` | `/api/v1/esims/usage/bulk` | Read | Data usage for every eSIM owned by the agency | | `GET` | `/api/v1/esims/bookings` | Read | List the agency's eSIM/topup bookings | | `GET` | `/api/v1/esims/bookings/{iccid}/topup-history` | Read | Topup history for one ICCID | | `GET` | `/api/v1/esims/bookings/{booking_id}/setup-instructions` | Read | Setup data (QR code, APN) for a booking | | `GET` | `/api/v1/esims/instructions/{iccid}` | Read | Installation instructions for an ICCID | Only **order creation** and **topup creation** write anything (they debit your agency balance and provision the eSIM). Every other endpoint is read-only. ## Traveler contact details [#traveler-contact-details] `first_name` and `last_name` follow the same **Latin/ASCII-only** naming rule used by flights — no accents, diacritics, or non-Latin scripts. `email` and `phone` are **required** on [order](/esims/orders) and [topup](/esims/topups) creation — not optional. FX-Port asks the supplier to email the QR code and installation instructions directly to that address as soon as the eSIM is provisioned, so the traveler receives setup instructions without you relaying the QR code yourself. Only pass the traveler's/client's own contact details — never include billing, pricing, or account information in `email` or `phone`, since they are forwarded to the supplier as customer-facing contact details. ## Error responses [#error-responses] | HTTP | Cause | | ----- | -------------------------------------------------------------------------- | | `400` | Missing required field (`package_id`, `iccid`, `email`, `phone`, etc.) | | `401` | Missing or invalid API key | | `402` | Insufficient agency balance | | `403` | Key lacks write permission, or agency is not active | | `404` | Package, ICCID, or booking not found | | `409` | Duplicate order/topup detected — an identical request is already in flight | | `503` | Supplier API temporarily unavailable | ## Questions? [#questions] Email the FX-Port developer team at [dev@fx-port.com](mailto:dev@fx-port.com) for integration help, feature requests, or to report an API error. --- # Setup & installation instructions Source: /esims/instructions ## Setup instructions by booking [#setup-instructions-by-booking] `GET /api/v1/esims/bookings/{booking_id}/setup-instructions` **Permission:** Read Returns the original order/SIM payload as JSON — QR code, APN settings, manual/QR install steps, and agency-facing pricing — for display on your own confirmation or setup page. ```bash curl --request GET \ --url https://api.fx-port.com/api/v1/esims/bookings/FX-1787835379756/setup-instructions \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` ### Response example [#response-example] ```json { "success": true, "data": { "code": "20260827-088478", "currency": "EUR", "package_id": "elan-7days-1gb", "type": "sim", "validity": 7, "package": "Élan-1 GB - 7 days", "data": "1 GB", "price": 9.52, "manual_installation": "

eSIM name: Élan

Coverage: France

...", "qrcode_installation": "

eSIM name: Élan

Coverage: France

...", "installation_guides": { "en": "https://..." }, "sims": [ { "iccid": "8900000338153738419", "lpa": "lpa.example.com", "qrcode": "LPA:1$lpa.example.com$...", "qrcode_url": "https://...", "apn_type": "automatic", "apn_value": "N/A", "apn": { "ios": { "apn_type": "automatic", "apn_value": "N/A" }, "android": { "apn_type": "automatic", "apn_value": "N/A" } } } ] } } ``` ## Installation instructions by ICCID [#installation-instructions-by-iccid] `GET /api/v1/esims/instructions/{iccid}` **Permission:** Read Structured, step-by-step installation instructions per platform for a single ICCID, localized by `?locale=`. ```bash curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/instructions/8900000338153738419?locale=en' \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` ### Response example [#response-example-1] ```json { "success": true, "data": { "data": { "instructions": { "language": "EN", "ios": [ { "version": "15.0,14.0,13.0,12.0", "direct_apple_installation_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=...", "installation_via_qr_code": { "steps": { "1": "Go to \"Settings\", tap \"Cellular/Mobile Data\", then tap \"Add Cellular/Data Plan\" on your device.", "2": "Scan the QR code, then tap \"Add Cellular/Data Plan\".", "3": "Choose a label for your new eSIM plan." } } } ], "android": ["... same shape, per-manufacturer steps ..."] } } } } ``` ## Sending instructions directly to the traveler [#sending-instructions-directly-to-the-traveler] You don't have to relay the QR code yourself: pass `email` on [Create eSIM order](/esims/orders#request) or [Create eSIM topup](/esims/topups#2-create-the-topup) and the supplier emails the QR code and installation steps directly to that address as soon as the order/topup is confirmed. This is the recommended flow whenever you have the traveler's own email — use the endpoints on this page only when you need to render the QR code or instructions inside your own product instead. --- # Create eSIM order Source: /esims/orders `POST /api/v1/esims/orders` **Permission:** Read+Write Creates a new eSIM, debits your agency balance, and provisions it with the supplier. Pricing is always computed server-side — see [eSIM pricing](/esims/pricing). ## Request [#request] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/esims/orders \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "package_id": "elan-7days-1gb", "first_name": "Ana", "last_name": "Kova", "email": "ana.kova@example.com", "phone": "+34600123456", "description": "Order for Ana Kova" }' ``` ### Body fields [#body-fields] Verified directly against the running API — sending a request without `email` or without `phone` returns `400`. | Field | Required | Description | | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `package_id` | **Yes** | `id` from [the package catalog](/esims/packages) | | `email` | **Yes** | Traveler/client email. The supplier emails the QR code and installation instructions directly to this address | | `phone` | **Yes** | Traveler/client phone number | | `first_name` / `last_name` | No | Traveler name — plain Latin/ASCII letters only | | `description` | No | Free-text order note | There is no `quantity` field. Every order creates **exactly one** eSIM — pricing and the agency balance debit only ever account for a single eSIM. If a client sends `quantity` anyway, it is silently ignored; to get more eSIMs, call this endpoint once per eSIM. Only pass the traveler's/client's own email and phone number — never billing, pricing, or account information. `email` and `phone` are forwarded to the supplier purely as customer-facing contact details for eSIM delivery and installation help. ## Response example [#response-example] ```json { "success": true, "data": { "order": { "code": "20260827-088478", "package_id": "elan-7days-1gb", "type": "sim", "description": "Order for Ana Kova", "validity": 7, "price": 9.52, "currency": "EUR", "net_price": 8.50, "total_price": 9.52, "agency_currency": "EUR", "supplier_currency": "USD", "exchange_rate": 0.92, "coverage": "France" }, "sim": { "iccid": "8900000338153738419", "qrcode_url": "https://...", "direct_apple_installation_url": "https://...", "apn_type": "automatic", "apn_value": "N/A", "sharing": { "access_code": 8803, "link": "https://esims.cloud/example-share-link" } } }, "booking_id": "FX-1787835379756", "message": "eSIM order placed successfully" } ``` Save `sim.iccid` — it's required for [topups](/esims/topups), [usage](/esims/usage), and [installation instructions](/esims/instructions). Save `booking_id` for [booking lookups](/esims/bookings). ## Errors [#errors] | HTTP | Cause | | ----- | --------------------------------------------------------- | | `400` | `package_id` missing, or `email`/`phone` missing/invalid | | `402` | Insufficient agency balance | | `403` | Key lacks write permission, or agency is not active | | `404` | Package not found or has no valid price | | `409` | Duplicate order already in flight for this agency/package | | `503` | Supplier temporarily unavailable | In the **sandbox** environment, orders never debit a balance and no real eSIM is provisioned — the response includes `"balanceDebited": false`. --- # Package catalog Source: /esims/packages ## List packages [#list-packages] `GET /api/v1/esims/packages` Returns the cached package catalog with pricing computed for your agency. Results are cached server-side and refreshed periodically, so repeated calls are cheap. ```bash curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/packages?locale=en' \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` ### Query parameters [#query-parameters] | Parameter | Description | | -------------- | -------------------------------------------------------------------------------------------------- | | `country` | Filter by country slug (e.g. `france`, `turkey`) | | `filter[type]` | Filter by package type (e.g. `local`, `regional`, `global`) | | `locale` | Language for package text fields (`en`, `fr`, `ar`, …). Falls back to `Accept-Language`, then `en` | ```bash curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/packages?country=france&filter%5Btype%5D=local&locale=en' \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` ### Response example [#response-example] Example response for `GET /api/v1/esims/packages?locale=en` (one package shown): ```json { "environment": "sandbox", "success": true, "endpoint": "/packages", "data": [ { "id": "change-in-3days-unlimited", "type": "sim", "price": 1812.5, "amount": 0, "day": 3, "is_unlimited": true, "title": "Unlimited - 3 days", "is_fair_usage_policy": 1, "fair_usage_policy": "Lower speed rate of 1 Mbps after 3 GB usage per day.", "data": "Unlimited", "voice": null, "text": null, "country_name": "United States", "country_code": "US", "country_slug": "united-states", "country_image_url": "https://cdn-revamp.airalo.com/images/ad19c2a2-40b8-4dac-b95e-269703c8561b.png", "operator_name": "Change", "operator_id": 1181, "operator_style": "light", "operator_gradient_start": "#0f1b3f", "operator_gradient_end": "#194281", "operator_type": "local", "operator_is_prepaid": false, "operator_esim_type": "Prepaid", "operator_warning": null, "operator_apn_type": "manual", "operator_is_roaming": true, "operator_info": [ "5G Data-only eSIM.", "Rechargeable online with no expiry.", "Operates on T-Mobile, Verizon, and U.S. Cellular networks in the United States of America." ], "operator_image_url": "https://cdn-revamp.airalo.com/images/6bf8859b-3f43-41c7-ac54-434e2534813c.png", "operator_plan_type": "data", "operator_activation_policy": "first-usage", "operator_is_kyc_verify": false, "operator_rechargeability": true, "operator_other_info": "This eSIM is for travelers to the United States. eSIM covers the United States, including Hawaii and Puerto Rico, with limited availability in Alaska", "operator_coverages": [ { "name": "US", "code": "US", "networks": [ { "name": "T-Mobile", "types": ["5G"] }, { "name": "Verizon", "types": ["5G"] } ] } ], "operator_apn": { "ios": { "apn_type": "automatic", "apn_value": "wbdata" }, "android": { "apn_type": "manual", "apn_value": "wbdata" } }, "is_regional": false, "is_global": false, "retail_price": 2111.2, "total_price": 2111.2, "balance_debit_amount": 1885.0, "agency_commission": "12 %", "currency": "DZD", "supplier_currency": "USD", "exchange_rate": 250.0 } ], "meta": { "total_packages": 1, "filters_applied": { "country": null, "type": null }, "locale": "en", "agency_currency": "DZD", "supplier_currency": "USD", "exchange_rate": 250.0, "commission_percentage": "12 %" } } ``` Save `id` from the response — it's what you pass as `package_id` when [creating an order](/esims/orders). ### Response field reference [#response-field-reference] **Package identity and data allowance** | Field | Type | Meaning | | ---------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------- | | `id` | string | Package identifier — pass as `package_id` when [creating an order](/esims/orders) | | `type` | string | Package/product type as sold, e.g. `sim` | | `title` | string | Human-readable package label (data amount + validity) | | `data` | string | Data allowance for the package, e.g. `"1 GB"` or `"Unlimited"` | | `amount` | number | Data allowance in MB. **`0` means unlimited** — check `is_unlimited` rather than treating `0` as "no data" | | `is_unlimited` | boolean | `true` for unlimited-data packages | | `is_fair_usage_policy` | boolean/number | Truthy when an unlimited package throttles speed after a threshold — read `fair_usage_policy` for the exact terms | | `fair_usage_policy` | string \| null | Human-readable fair-usage terms (e.g. throttle speed and daily threshold) when `is_fair_usage_policy` is truthy | | `day` | number | Validity in days from activation | | `voice` | string \| null | Voice minutes included, when the package bundles voice (`null` = data-only) | | `text` | string \| null | SMS included, when the package bundles SMS (`null` = data-only) | | `is_regional` | boolean | `true` if this package covers a defined multi-country region | | `is_global` | boolean | `true` if this package covers global/worldwide destinations | **Country** | Field | Type | Meaning | | ------------------- | ------ | ---------------------------------------------------------------- | | `country_name` | string | Destination country, human-readable | | `country_code` | string | Destination country, ISO 3166-1 alpha-2 | | `country_slug` | string | Destination country slug — pass as the `country` query parameter | | `country_image_url` | string | Flag/country image URL for display | **Operator (underlying mobile network)** | Field | Type | Meaning | | ---------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------- | | `operator_name` | string | Underlying mobile network operator/brand for this package | | `operator_id` | number | Internal operator identifier | | `operator_type` | string | Package scope: `local` (one country), `regional`, or `global` | | `operator_style` / `operator_gradient_start` / `operator_gradient_end` | string | Display styling hints (light/dark theme, gradient colors) for this operator's branding | | `operator_is_prepaid` | boolean | Whether the underlying plan is prepaid | | `operator_esim_type` | string | e.g. `Prepaid` | | `operator_warning` | string \| null | Operator-specific caution to show travelers (e.g. topup window), when applicable | | `operator_apn_type` | string | `automatic` or `manual` — whether APN settings are auto-configured | | `operator_is_roaming` | boolean | Whether this package operates via roaming rather than a local SIM profile | | `operator_info` | string\[] | Short bullet facts about the operator/network (technology, rechargeability, coverage) | | `operator_image_url` | string | Operator logo URL | | `operator_plan_type` | string | e.g. `data` (data-only plan) | | `operator_activation_policy` | string | When validity starts, e.g. `first-usage` (starts on first connection, not purchase) | | `operator_is_kyc_verify` | boolean | Whether the destination country requires identity verification for this operator (rare; relevant for a few countries) | | `operator_rechargeability` | boolean | Whether this ICCID can be topped up — see [Top up an eSIM](/esims/topups) | | `operator_other_info` | string | Longer free-text coverage notes/exclusions (e.g. territories not covered) | | `operator_coverages` | array | Per-country network list: `{ name, code, networks: [{ name, types }] }` | | `operator_apn` | object | Manual APN settings per platform: `{ ios: { apn_type, apn_value }, android: { apn_type, apn_value } }` | **Pricing** — see [eSIM pricing](/esims/pricing) for the full formula | Field | Type | Meaning | | ------------------------------ | ------ | ------------------------------------------------------------------------------ | | `price` | number | FX-Port's B2B price in your agency currency | | `retail_price` / `total_price` | number | `price` + your agency commission — what you charge the traveler | | `balance_debit_amount` | number | Amount actually debited from your agency balance if you order this package | | `agency_commission` | string | Commission rate applied, as text (`"12 %"`, or `"0 %"` for corporate agencies) | | `currency` | string | Your agency's settlement currency | | `supplier_currency` | string | Currency the fulfilling supplier settles in | | `exchange_rate` | number | `supplier_currency` → `currency` rate used for this package | ### `meta` field reference [#meta-field-reference] | Field | Meaning | | ----------------------- | ---------------------------------------------------------------------- | | `total_packages` | Number of packages in `data` after filtering | | `filters_applied` | Echo of the `country` / `type` filters actually applied | | `locale` | Language used for text fields in this response | | `agency_currency` | Same as `currency` on each package — your agency's settlement currency | | `supplier_currency` | Same as `supplier_currency` on each package | | `exchange_rate` | Same as `exchange_rate` on each package | | `commission_percentage` | The commission rate applied across this response | ## Compatible devices [#compatible-devices] `GET /api/v1/esims/compatible-devices` Returns the cached list of devices known to support eSIM, useful for pre-sale device checks before a traveler purchases a package. ```bash curl --request GET \ --url https://api.fx-port.com/api/v1/esims/compatible-devices \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` ### Response example [#response-example-1] ```json { "success": true, "data": [ { "brand": "ASUS", "model": "AI2501", "name": "ASUS Zenfone 12 Ultra", "os": "android" }, { "brand": "Alcatel", "model": "T803D", "name": "V3 Ultra", "os": "android" }, { "brand": "Apple", "model": "iPhone15,2", "name": "iPhone 14 Pro", "os": "ios" } ] } ``` ### Response field reference [#response-field-reference-1] | Field | Meaning | | ------- | -------------------------------------------- | | `brand` | Device manufacturer | | `model` | Manufacturer's internal model identifier | | `name` | Human-readable marketing name for the device | | `os` | Operating system family: `ios` or `android` | This list can return `"success": false` with an empty `data` array for a brief moment while the device cache is still loading in the background right after a deployment — retry after a few seconds. --- # eSIM pricing Source: /esims/pricing Every eSIM package and topup package exposes the same set of agency-facing money fields, computed server-side in your agency's currency. Any pricing sent in a request body is ignored. ## Field meanings [#field-meanings] | Field | Meaning | | ------------------------------ | ------------------------------------------------------------------------------------------------------- | | `price` | FX-Port's B2B price for this package, in your agency currency | | `retail_price` / `total_price` | `price` + your **12% agency commission** — what you charge the traveler | | `balance_debit_amount` | **The amount debited from your agency balance** when you order or top up | | `agency_commission` | Text field showing the commission rate applied (`"12 %"`, or `"0 %"` for corporate/contracted agencies) | | `currency` | Your agency's settlement currency | | `supplier_currency` | The currency the fulfilling eSIM supplier settles in (e.g. `"USD"`) | | `exchange_rate` | `supplier_currency` → `currency` rate used to compute the above | ## The formula [#the-formula] ``` retail_price = price + (price × 12%) ``` **Example** (Euro, `EUR`, supplier settling in `USD`): ```json { "price": 8.50, "retail_price": 9.52, "total_price": 9.52, "balance_debit_amount": 8.50, "agency_commission": "12 %", "currency": "EUR", "supplier_currency": "USD", "exchange_rate": 0.92 } ``` Check: `8.50 × 1.12 = 9.52` ✓ Corporate / contracted agencies are charged **0%** commission instead of 12% — `agency_commission` reflects the rate that actually applies to your account. ## Supplier currency [#supplier-currency] FX-Port sources eSIM packages from multiple suppliers, and each supplier settles in its own currency (configured per supplier on the FX-Port side — not hard-coded). `exchange_rate` is always `supplier_currency → currency`, never assumed to be USD. If a package's `supplier_currency` already matches your agency `currency`, `exchange_rate` is `1.0`. ## Order and topup responses [#order-and-topup-responses] `POST /api/v1/esims/orders` and `POST /api/v1/esims/topups` return the same pricing shape nested under `data.order` / `data.topup` as `price`, `net_price`, `total_price`, `currency`, `agency_currency`, `supplier_currency`, and `exchange_rate`, plus a `balance` block on topups showing `previous` and `current` agency balance. --- # Supplier continuity Source: /esims/supplier-continuity eSIM data plans are provisioned on a specific mobile network operator in the destination country. FX-Port's eSIM suppliers source coverage from local/regional network operators per country, and in rare cases a supplier can **change which operator serves a given country or package**. Public eSIM partner API documentation in this space generally notes that operator and package availability can change over time, and that integrators should always fetch current package/topup data rather than caching it indefinitely. ## What this means for topups [#what-this-means-for-topups] If the underlying operator for a country changes after a traveler's eSIM was issued, the original package can be discontinued for that ICCID even though the eSIM itself is still active. When that happens, the eSIM usually keeps working until it expires, but **a topup on the old operator's package is no longer possible** — a new package from the current operator must be used instead, which typically means issuing a new eSIM order rather than a topup on the old ICCID. This is uncommon, but it is a real, documented behavior in the eSIM industry — not an FX-Port limitation. ## How FX-Port handles it [#how-fx-port-handles-it] FX-Port never assumes a topup package is still valid. The flow is designed so this situation cannot silently fail or take your money: 1. **`GET /api/v1/esims/topup-packages/{iccid}`** always queries the fulfilling supplier live, scoped to the exact ICCID. It never returns a cached assumption based on the original order — if the supplier no longer offers a topup for that ICCID (for example, because its operator changed), the response is an empty package list. 2. **`POST /api/v1/esims/topups`** only debits your agency balance and records a confirmed booking **after** the supplier accepts the topup. If the supplier rejects it (including because the ICCID's package/operator is no longer topup-eligible), FX-Port: * does **not** touch your agency balance, * records the attempt as a `failed` internal booking for support visibility, and * returns an error response so your integration can react immediately. ## Recommended integration pattern [#recommended-integration-pattern] ```bash # 1. Always check topup availability for the exact ICCID first curl --request GET \ --url https://api.fx-port.com/api/v1/esims/topup-packages/8900000000000000000 \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` ```json { "success": true, "data": { "data": [] } } ``` An empty package list means this eSIM cannot currently be topped up. In that case, place a new [eSIM order](/esims/orders) for the traveler instead of retrying the topup. If packages are returned, proceed to [Create eSIM Topup](/esims/topups) using one of the returned `package_id` values — never reuse a `package_id` from the original order without checking this endpoint first. --- # Top up an eSIM Source: /esims/topups Topping up is a two-step flow: first check what's available for the exact ICCID, then submit the topup using one of the returned `package_id` values. Always follow this order — see [Supplier continuity](/esims/supplier-continuity) for why. ## 1. Check available topup packages [#1-check-available-topup-packages] `GET /api/v1/esims/topup-packages/{iccid}` **Permission:** Read Queries the fulfilling supplier live, scoped to this ICCID, so the result always reflects current availability — including the rare case where the country's underlying network operator changed since the eSIM was issued. ```bash curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/topup-packages/8900000338153738419?locale=en&force=false' \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` | Query parameter | Description | | --------------- | ------------------------------------------------------ | | `locale` | Language for package text fields | | `force` | Bypass the short-lived cache and re-query the supplier | ### Response example [#response-example] ```json { "success": true, "data": { "data": [ { "id": "elan-7days-1gb-topup", "type": "topup", "title": "1 GB - 7 days", "data": "1 GB", "day": 7, "is_unlimited": false, "price": 8.50, "retail_price": 9.52, "total_price": 9.52, "balance_debit_amount": 8.50, "agency_commission": "12 %", "currency": "EUR", "supplier_currency": "USD" } ] } } ``` An empty `data.data` array means this eSIM currently cannot be topped up — place a [new eSIM order](/esims/orders) for the traveler instead. ## 2. Create the topup [#2-create-the-topup] `POST /api/v1/esims/topups` **Permission:** Read+Write Use `data.data[0].id` from step 1 as `package_id`, and the same ICCID you just queried. ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/esims/topups \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "package_id": "elan-7days-1gb-topup", "iccid": "8900000338153738419", "first_name": "Ana", "last_name": "Kova", "email": "ana.kova@example.com", "phone": "+34600123456" }' ``` ### Body fields [#body-fields] Verified directly against the running API — sending a request without `email` or without `phone` returns `400`. | Field | Required | Description | | -------------------------- | -------- | --------------------------------------------------------------------------------------------------- | | `package_id` | **Yes** | An `id` returned by [topup-packages](#1-check-available-topup-packages) for **this ICCID** | | `iccid` | **Yes** | The eSIM to top up | | `email` | **Yes** | Traveler/client email — the supplier emails updated installation/topup instructions to this address | | `phone` | **Yes** | Traveler/client phone number | | `first_name` / `last_name` | No | Traveler name — plain Latin/ASCII letters only | ## Response example [#response-example-1] ```json { "success": true, "data": { "topup": { "code": "20260827-088430", "validity": 7, "price": 9.52, "currency": "EUR", "net_price": 8.50, "total_price": 9.52, "agency_currency": "EUR", "supplier_currency": "USD", "exchange_rate": 0.92 }, "iccid": "8900000338153738419", "balance": { "previous": 500.00, "current": 490.48, "currency": "EUR" } }, "booking_id": "FX-1787829778186", "message": "eSIM topup completed successfully" } ``` ## Does a topup extend or replace the current plan? [#does-a-topup-extend-or-replace-the-current-plan] Topping up an active eSIM **adds** the new package's data allowance — it does not replace or shorten what the traveler already has. Depending on the specific package's own validity terms, the new validity window can extend from the topup date rather than simply stacking on the original expiry, so the exact number of remaining days after a topup can vary by package. Always confirm the traveler's actual remaining balance and expiry with [eSIM usage](/esims/usage) after a topup rather than assuming a fixed day-count formula — this mirrors general guidance published by eSIM API providers, which recommend checking usage/status rather than hard-coding validity math. **Example:** an eSIM originally issued for 3 days, topped up with a 15-day package, will show additional data and a new expiry reflecting the added validity once you re-check [usage](/esims/usage/\{iccid}) — verify the exact resulting `remaining` and `expired_at` values for your specific package rather than assuming "3 + 15" arithmetic. ## Errors [#errors] | HTTP | Cause | | ----- | --------------------------------------------------------------------------------------------------------- | | `400` | `package_id` or `iccid` missing, or `email`/`phone` missing/invalid | | `402` | Insufficient agency balance | | `403` | Key lacks write permission, or agency is not active | | `404` | Package not found for this ICCID (see [Supplier continuity](/esims/supplier-continuity)) | | `409` | Duplicate topup already in flight, or a confirmed topup for this ICCID was created in the last 60 seconds | | `503` | Supplier temporarily unavailable | If the supplier rejects the topup for any reason — including an operator change on the destination country — **your agency balance is never debited**. The attempt is recorded internally for support visibility and the response returns an error so you can react immediately. --- # eSIM usage Source: /esims/usage **Usage is cached and is not real-time.** FX-Port caches usage responses for up to **15 minutes** per ICCID to protect the supplier's rate limits. Pass `force=true` to bypass the cache and query the supplier directly when you need the freshest possible number (for example right after a topup) — but avoid doing this on every request, since suppliers rate-limit usage lookups themselves and can temporarily reject requests that poll too frequently. ## Single eSIM usage [#single-esim-usage] `GET /api/v1/esims/usage/{iccid}` **Permission:** Read ```bash curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/usage/8900000338153738419?force=false' \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` | Query parameter | Description | | --------------- | --------------------------------------------------------------- | | `force` | Bypass the cache and re-query the supplier for the latest usage | ### Response example [#response-example] ```json { "success": true, "data": { "data": { "remaining": 1024, "total": 1024, "expired_at": "2026-09-03 12:56:28", "is_unlimited": false, "status": "ACTIVE", "remaining_voice": 0, "remaining_text": 0, "total_voice": 0, "total_text": 0 }, "meta": { "message": "success" } } } ``` `remaining` and `total` are in **megabytes**. `status` is one of `ACTIVE`, `EXPIRED`, `NOT_ACTIVE`, or `FINISHED` depending on the supplier. ## Bulk usage [#bulk-usage] `GET /api/v1/esims/usage/bulk` **Permission:** Read Returns usage for every eSIM owned by the authenticated agency in one call — useful for dashboards that show all active eSIMs at once. This endpoint is also subject to the same 15-minute cache and `force` bypass described above, applied per ICCID. ```bash curl --request GET \ --url 'https://api.fx-port.com/api/v1/esims/usage/bulk?force=false' \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' ``` ```json { "success": true, "data": [ { "iccid": "8900000847275756911", "data": { "data": { "remaining": 0, "total": 0, "expired_at": "2026-05-16 09:39:02", "is_unlimited": false, "status": "EXPIRED", "remaining_voice": 0, "remaining_text": 0, "total_voice": 0, "total_text": 0 }, "meta": { "message": "success" } }, "status": "success" } ] } ``` `status` per entry is `success`, `rate_limited`, or `error` — retry `rate_limited` entries later rather than immediately re-calling bulk usage. --- # Hotels API Source: /hotels **The Hotels API is live and being documented.** FX-Port already books hotels across global suppliers (RateHawk / WorldOTA and more). Full public documentation — search, availability, room-level pricing, pre-book, and booking — is being finalised and will appear here shortly. **Check back in a few days.** ## What's coming [#whats-coming] * **Search** by destination, geo-radius, or specific hotel ID * **Room & rate availability** with per-room pricing and cancellation policies * **Pre-book** and **book** with guest details * **Booking lookup** and voucher / confirmation retrieval Hotel guest naming follows the same **Latin/ASCII-only** rule as flights (no accents or non-Latin scripts). Hotels have their own guest and occupancy requirements, which will be documented in this section. ## Want early access? [#want-early-access] If you're building a hotel integration and want early access to the docs or a walkthrough, email [dev@fx-port.com](mailto:dev@fx-port.com). --- # GET /api/v1/flights/airports Source: /flights/airports Airport autocomplete for origin/destination search inputs. Accepts an IATA code, city name, or airport name. Returns matching airports from a global database. No authentication required. **Permission:** Public (no API key needed) Designed for debounced typeahead — throttle requests to avoid overloading (recommended: 300 ms debounce). The response is a flat **array** of airport objects, not a wrapped object. ## Query parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------------------------------------------------------------------- | | `query` | string | Yes | IATA code (e.g. `ALG`), city name (e.g. `Paris`), or airport name | | `locale` | string | No | Language for airport and city names. Default `en`. See [Supported locales](#supported-locales). | ## Request examples [#request-examples] ```bash # IATA code lookup curl "https://api.fx-port.com/api/v1/flights/airports?query=CDG&locale=en" ``` ```bash # City name search, French locale curl "https://api.fx-port.com/api/v1/flights/airports?query=Paris&locale=fr" ``` ```bash # Arabic locale (airport names in Arabic) curl "https://api.fx-port.com/api/v1/flights/airports?query=ALG&locale=ar" ``` ## Response example [#response-example] The response is a **flat array** of airport objects (not a wrapped `{results:[...]}` object). ```json [ { "ap": "CDG", "airportname": "Charles de Gaulle", "cityname": "Paris, France", "cc": "FR", "country": "France", "timezone": "Europe/Paris", "utc": "+02:00", "lat": 49.01278, "lng": 2.55, "displayname": "Charles de Gaulle, Paris, France, (CDG)", "shortdisplayname": "Paris, France (CDG)" }, { "ap": "ORY", "airportname": "Orly", "cityname": "Paris, France", "cc": "FR", "country": "France", "timezone": "Europe/Paris", "utc": "+02:00", "displayname": "Orly, Paris, France, (ORY)", "shortdisplayname": "Paris, France (ORY)" } ] ``` ## Key field reference [#key-field-reference] | Field | Description | | ------------------ | ----------------------------------------------------------------------- | | `ap` | IATA airport code — use as `origin` or `destination` in search requests | | `airportname` | Airport name in the requested locale | | `cityname` | City and country in the requested locale | | `cc` | ISO 3166-1 alpha-2 country code | | `country` | Country name | | `timezone` | IANA timezone (e.g. `Europe/Paris`) | | `utc` | UTC offset at query time | | `displayname` | Full display label — suitable for showing in a result list | | `shortdisplayname` | Short display label — suitable for showing selected value | The response contains additional internal fields (coordinates, entity keys, popularity data) that are not listed here. Use only the fields documented above; internal fields may change. ## Supported locales [#supported-locales] Pass as `locale=XX` in the query string. Unrecognised values default to `en`. | `locale` | Language | | -------- | --------------------- | | `en` | English (default) | | `fr` | French | | `ar` | Arabic | | `de` | German | | `es` | Spanish | | `es-AR` | Spanish (Argentina) | | `it` | Italian | | `pt` | Portuguese | | `nl` | Dutch | | `nl-BE` | Dutch (Belgium) | | `pl` | Polish | | `cs` | Czech | | `ru` | Russian | | `uk` | Ukrainian | | `el` | Greek | | `ro` | Romanian | | `da` | Danish | | `no` | Norwegian | | `sv` | Swedish | | `fi` | Finnish | | `tr` | Turkish | | `he` | Hebrew | | `ko` | Korean | | `ja` | Japanese | | `zh-CN` | Chinese (Simplified) | | `zh-TW` | Chinese (Traditional) | | `th` | Thai | | `id` | Indonesian | | `ms` | Malay | | `vi` | Vietnamese | | `en-PH` | English (Philippines) | Locale affects airport and city name translations. IATA codes (`ap`, `cc`) are always in ASCII. --- # POST /api/v1/flights/book_flight Source: /flights/book Create a booking. Set `booking_type` to `"issue"` for instant ticket issuance, or `"hold"` to create an unpaid reservation when the selected offer supports holds. **Permission:** Read+Write Call `price_flight` before booking to ensure the offer has been re-priced and the cache is warm. Booking without pricing may result in a stale price or offer rejection. **Hold is capability-based.** Send `"booking_type": "hold"` only when the selected offer returns `supportHold: true` after `price_flight`. Otherwise use `"issue"` or choose another offer. A hold is not ticketed and can be released by the airline before the displayed payment deadline. ## Request body [#request-body] | Field | Type | Required | Description | | ----------------- | ------- | -------- | --------------------------------------------------------- | | `search_id` | string | Yes | `searchId` from the supplier entry in the search response | | `offer_id` | string | Yes | Offer ID from search results | | `booking_type` | string | Yes | `"issue"` or `"hold"` | | `passengers` | array | Yes | Passenger details (see below) | | `contact_details` | object | Yes | `{ email, phone_number }` | | `send_email` | boolean | No | `false` to suppress confirmation email. Default `true`. | ### Passenger object [#passenger-object] | Field | Type | Required | Description | | -------------------- | ------ | -------- | -------------------------------------------------------------------------------- | | `type` | string | Yes | `"adult"`, `"child"`, `"infant"`, `"seated_infant"`, `"senior"`, `"young_adult"` | | `gender` | string | Yes | `"MALE"` or `"FEMALE"` | | `first_name` | string | Yes | Given name as on passport — **Latin/ASCII only** | | `last_name` | string | Yes | Family name as on passport — **Latin/ASCII only** | | `date_of_birth` | string | Yes | `YYYY-MM-DD` — age evaluated at departure date | | `identity_documents` | array | Yes | Array with one passport object | **Name encoding**: Use plain Latin (ASCII) letters only. No diacritics (`Jose` not `José`), no non-Latin scripts. Allowed: A–Z, a–z, spaces, hyphens. Airlines and GDS systems reject anything outside this range. See [Flight passengers & ages](/flights/passengers) for full rules. ### Identity document object [#identity-document-object] | Field | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------- | | `type` | string | Yes | `"passport"` | | `number` | string | Yes | Document number | | `issuing_country` | string | Yes | ISO 3166-1 alpha-2 (e.g. `"DZ"`) | | `expiry_date` | string | Yes | `YYYY-MM-DD` | | `nationality` | string | No | ISO 3166-1 alpha-2 | ## Request examples [#request-examples] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/book_flight \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "search_id": "fs_9fa52bad-a7be-42aa-9ad6-d3f9fa642dc3", "offer_id": "fx_offer_a1b2c3d4", "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": "traveler@example.com", "phone_number": "+213554657687" } }' ``` ```bash # Use only after price_flight returns supportHold: true for this offer. curl --request POST \ --url https://api.fx-port.com/api/v1/flights/book_flight \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "search_id": "fs_9fa52bad-a7be-42aa-9ad6-d3f9fa642dc3", "offer_id": "fx_offer_a1b2c3d4", "booking_type": "hold", "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": "traveler@example.com", "phone_number": "+213554657687" } }' ``` ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/book_flight \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "search_id": "fs_9fa52bad-a7be-42aa-9ad6-d3f9fa642dc3", "offer_id": "fx_offer_a1b2c3d4", "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": "traveler@example.com", "phone_number": "+213554657687" } }' ``` ## Response example — instant issue [#response-example--instant-issue] A Duffel instant-issue booking, where the supplier prices in USD and the agency settles in DZD. `exchangeRate` appears at the **top level** when currencies differ. ```json { "environment": "sandbox", "success": true, "requestId": "e4efec38", "searchId": "fs_dfx_14923c6e-48be-4668-afe8-95446a99c8ec", "offerId": "fx_offer_56e6a88c", "processingTime": 0.51, "exchangeRate": 250.0, "booking": { "reference": "ord_0000B9o1uvvKVjUSYYrDhk", "status": "confirmed", "pnr": "4CJ5SB", "type": "instant", "createdAt": "2026-08-27T18:19:01.367831Z", "cancelledAt": null, "availableActions": ["cancel", "change", "update"], "voidableUntil": null }, "supplier": { "id": "duffel_1", "technology": "duffel", "name": "Duffel" }, "agency": { "agencyId": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "currency": "DZD" }, "pricing": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "basePrice": 16440.0, "taxesAndFees": 5238.8, "agencyCommission": 21.68, "finalPrice": 21700.48, "b2bPrice": 21678.8, "additionalServices": [] }, "payment": { "status": "paid", "paidAt": "2026-08-27T18:19:01Z", "awaitingPayment": false }, "documents": [ { "type": "electronic_ticket", "uniqueIdentifier": "1", "passengerIds": ["pas_0000B9o1tnN9Lys2jFYxi1"] } ], "bookingReferences": [ { "pnr": "4CJ5SB", "carrier": { "iataCode": "BA", "name": "British Airways" } } ], "passengers": [ { "id": "pas_0000B9o1tnN9Lys2jFYxi1", "type": "ADULT", "givenName": "Noah", "familyName": "Andersson", "ticketNumber": "1" } ], "meta": { "supportHold": false, "supportVoid": false, "voidDeadline": null }, "postProcessing": { "bookingDbId": "8649a997-b426-4735-aa38-28c0e00e0932", "bucketLink": null, "emailSent": false } } ``` `documents[n].uniqueIdentifier` is supplier-dependent — some suppliers (like Duffel in sandbox) return a short internal ticket identifier rather than a full IATA-format ticket number. Treat it as an opaque string; don't parse or validate its format. ## Response example — hold booking [#response-example--hold-booking] The same request as above but with `booking_type: "hold"` against an Amadeus Algeria offer that supports holds — same currency both sides (DZD), so there's no top-level `exchangeRate`. The order is unpaid: `booking.status` is `"pending"` and `payment.status`/`awaitingPayment` (surfaced via [`get_order_quote`](/flights/get-order-quote)) show it still needs `pay_order`. ```json { "environment": "sandbox", "success": true, "requestId": "7053291b", "searchId": "fs_df598ac9-ce7d-47a6-95ee-e62a15141a99", "offerId": "fx_offer_605e2089", "processingTime": 2.79, "booking": { "reference": "eJzTd9ePMo_0NXMEAAtYAlQ", "status": "pending", "pnr": "Z7YM6A", "type": "hold", "createdAt": "2026-08-27T18:14:00.000", "cancelledAt": null, "availableActions": ["confirm", "cancel"], "voidableUntil": null }, "supplier": { "id": "amadeus_aqc_dz_1", "technology": "amadeus_aqc", "name": "Amadeus AQC" }, "agency": { "agencyId": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "currency": "DZD" }, "pricing": { "supplierCurrency": "DZD", "agencyCurrency": "DZD", "basePrice": 3160.0, "taxesAndFees": 1921.44, "agencyCommission": 5.09, "finalPrice": 5086.53, "b2bPrice": 5081.44, "additionalServices": [], "acd": { "applied": false } }, "payment": { "status": "awaiting_payment", "paidAt": null, "paymentRequiredBy": null, "priceGuaranteeExpiresAt": null, "awaitingPayment": true }, "documents": [], "bookingReferences": [ { "pnr": "Z7YM6A", "carrier": { "iataCode": "AH", "name": "Air Algerie" } } ], "passengers": [ { "id": "1", "type": "ADULT", "givenName": "MARCO", "familyName": "ROSSI" } ] } ``` Save `booking.reference` as `order_id` and continue with [Get order quote](/flights/get-order-quote) and [Pay order](/flights/pay-order) to complete the purchase, or [Cancel order](/flights/cancel-order) to release the hold. ## Key fields [#key-fields] | Field | Description | | ------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `booking.reference` | Supplier order ID — use this as `order_id` in all subsequent calls | | `booking.pnr` | Airline record locator — **not globally unique**, use `reference` as primary ID | | `booking.status` | `"confirmed"` for instant issue; `"pending"` for an unpaid hold | | `booking.voidableUntil` | Void cutoff (midnight in airline timezone, UTC). `null` when voiding is not supported. | | `documents[n].uniqueIdentifier` | E-ticket / document identifier — format varies by supplier, treat as opaque | | `postProcessing.bookingDbId` | Internal booking UUID — same value later returned as `booking.id` by [`get_booking`](/flights/get-booking) | | `postProcessing.bucketLink` | PDF URL — `null` until generated. Poll `get_booking` to get the link. | | `exchangeRate` | Present at top level when `supplierCurrency ≠ agencyCurrency`. Never inside `pricing`. | | `pricing.acd` | Algerian Currency Declaration flag, Amadeus Algeria only — see [Flight pricing](/flights/pricing) | ## Getting the confirmation PDF [#getting-the-confirmation-pdf] After issuance, the PDF is generated asynchronously (typically 10–30 seconds). Poll `POST /api/v1/flights/get_booking` until `booking.bucket_link` is populated, or use `POST /api/v1/flights/get_ticket`. --- # Flight booking flows Source: /flights/booking-flows Every flight booking follows one of two flows. Both start with a search and a mandatory pricing step. ## Instant issue (most common) [#instant-issue-most-common] Tickets are issued immediately after booking. **Search for flights** `POST /api/v1/get_flights` — returns offers from one or all suppliers. Each supplier entry has a `searchId` — save it for the next steps. **Price the offer** *(required)* `POST /api/v1/flights/price_flight` — confirms the current fare and caches the priced offer. This step is **mandatory** — `book_flight` reads the priced offer from cache. Booking without pricing first will fail. **Book and issue tickets** `POST /api/v1/flights/book_flight` with `"booking_type": "issue"` — tickets are issued immediately. The response contains `booking.reference` (the supplier order ID) and `booking.pnr`. **Retrieve booking + PDF** `POST /api/v1/flights/get_booking` — returns the full booking record. Once the PDF is generated (typically 10–30 seconds), `booking.bucket_link` contains the download URL. ``` GET_FLIGHTS → PRICE_FLIGHT → BOOK_FLIGHT (issue) → GET_BOOKING ``` *** ## Hold then pay (suppliers supporting holds only) [#hold-then-pay-suppliers-supporting-holds-only] Reserve a seat without paying, then pay later. Use this flow only when the selected search and priced offer return `supportHold: true`. Never infer hold support from a supplier name; capability can vary by supplier, airline, fare, and offer. **Search and price** — same as instant issue (steps 1 and 2 above). **Create a hold reservation** `POST /api/v1/flights/book_flight` with `"booking_type": "hold"` — a reservation is created without ticket issuance or a balance charge. The immediate supplier response may use `booking.status: "pending"`; stored booking and quote responses use `"awaiting_payment"`. **Preview current price** *(optional)* `POST /api/v1/flights/get_order_quote` — returns the current confirmed price and whether the hold is still payable. Pass `order_id` (preferred) or `pnr`. **Pay and issue tickets** `POST /api/v1/flights/pay_order` — charges the agency balance and issues tickets. Pass `order_id`. Do **not** send `amount` or `currency` — they are resolved server-side. **A held booking is not guaranteed until payment succeeds.** `pay_order` will fail when the PNR has expired or been cancelled, the hold/payment deadline has passed, or the airline has released the reservation early. Airlines can expire a hold before the date or time originally returned. Treat the `pay_order` response as authoritative, handle failure without charging the traveler, and run a new search when a fresh booking is required. ``` GET_FLIGHTS → PRICE_FLIGHT → BOOK_FLIGHT (hold) → [GET_ORDER_QUOTE] → PAY_ORDER → GET_BOOKING ``` *** ## Cancelling a held reservation [#cancelling-a-held-reservation] **`cancel_order` cancels a HELD (unpaid) reservation — it is NOT a refund.** It deletes the reservation and releases the PNR with the airline. It does **not** apply to issued (paid) tickets and does **not** refund anything, because a held reservation was never paid. **Why this matters:** if you create a hold and then abandon it, the airline may **refuse a new booking for the same passenger** on the same route while an un-voided reservation is pending. Always cancel the old hold to release the PNR before re-booking that passenger. ``` BOOK_FLIGHT (hold) → (changed your mind) → CANCEL_ORDER → PNR released → safe to re-book ``` `POST /api/v1/flights/cancel_order` — pass `order_id` (preferred) or `booking_id`. *** ## Post-booking operations (void, exchange, refund) [#post-booking-operations-void-exchange-refund] **Self-serve void, exchange, and refund on issued tickets are not yet available through the API.** Right now, all post-booking operations on **paid/issued** bookings — **void, exchange, re-issue, and refund** — must be **submitted to the FX-Port team for manual processing**. Email [dev@fx-port.com](mailto:dev@fx-port.com), or open the booking confirmation page in the FX-Port dashboard and use **Request support**. That action is available on every booking. Requests are handled by a dedicated team over multiple channels — in-dashboard support, email, and a direct messaging channel — **24/7**, and urgent cases such as same-day voids are actioned immediately. This limitation applies to **issued flight tickets only**. Post-booking operations for **hotels, transfers, and eSIMs** are fully supported through the API. FX-Port is actively building self-serve post-booking endpoints for flights and rolling them out per supplier as airline and GDS access is granted. Until then, do **not** build a workflow that assumes automated refunds or voids on issued tickets. See [Post-booking operations](/flights/post-booking) for details. --- # POST /api/v1/flights/cancel_order Source: /flights/cancel-order Cancel a **held (unpaid)** reservation. This deletes the reservation and **releases the PNR** with the airline. **Permission:** Read+Write **This is NOT a refund and does NOT apply to issued (paid) tickets.** A held reservation was never paid, so nothing is refunded — the reservation is simply cancelled and the PNR released. For refund / void / exchange of an **issued** ticket, see [Post-booking operations](/flights/post-booking) (currently handled manually by the FX-Port team through dashboard **Request support** or [dev@fx-port.com](mailto:dev@fx-port.com)). ## Why cancel a hold [#why-cancel-a-hold] If you create a hold and then abandon it, the airline may **refuse a new booking for the same passenger** on the same route while the un-voided reservation is pending. Always cancel the old hold to release the PNR before re-booking that passenger. ## Identifying the booking [#identifying-the-booking] | Field | Description | | ------------ | ---------------------------------------------------------------- | | `order_id` | Preferred. Supplier order ID returned when the hold was created. | | `booking_id` | FX-Port booking ID or stored booking reference. | | `pnr` | Airline PNR returned when the hold was created. | Prefer `order_id`. PNRs are not globally unique across suppliers/airlines. Send only one identifier. FX-Port resolves it to the supplier order ID before cancelling the hold. ## Request example [#request-example] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/cancel_order \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "order_id": "eJzTd9ePMo-0dDQFAAsmAj8" }' ``` ## Response example [#response-example] ```json { "environment": "sandbox", "success": true, "requestId": "cc91fce4", "orderId": "eJzTd9ePMo-0dDQFAAsmAj8", "processingTime": 1.243, "booking": { "reference": "Z7Y9A5", "status": "cancelled", "pnr": "Z7Y9A5", "type": "hold", "createdAt": "2026-08-27T18:15:15.920345+00:00", "cancelledAt": "2026-08-27T18:15:23.952210", "availableActions": [], "voidableUntil": null, "cancellation": { "type": "void", "deleted": true, "deletedAt": "2026-08-27T18:15:23.952210" } }, "supplier": { "id": "amadeus_aqc_dz_1", "technology": "amadeus_aqc", "name": "Amadeus Algeria" }, "cancellation": { "status": "deleted", "deleted": true, "deletedAt": "2026-08-27T18:15:23.952210", "refundAmount": 0, "refundCurrency": "DZD", "refundTo": null, "message": "Reservation deleted successfully." }, "meta": { "supportHold": true, "supportVoid": true, "voidDeadline": "2026-08-27T23:00:00Z" } } ``` ## Field meanings [#field-meanings] | Field | Meaning | | --------------------------- | --------------------------------------------------------------------------------- | | `booking.status` | `"cancelled"` on success | | `booking.availableActions` | Empty once cancelled — no further actions apply to this order | | `cancellation.deleted` | `true` when the reservation was fully released with the supplier | | `cancellation.refundAmount` | Always `0` here — a held reservation was never paid, so there's nothing to refund | | `cancellation.message` | Human-readable outcome, useful for support/debug logs | Cancelling a hold does not touch your agency balance — nothing was ever debited for an unpaid reservation. --- # POST /api/v1/flights/flexible_search Source: /flights/flexible-search Returns a date matrix showing the lowest price for each day ±3 days around the requested departure (and return, for round trips) date, **instead of** individual offers. **Permission:** Read **Flexible date search is capability-based.** Only suppliers that support flexible-date matrices are queried by this endpoint — currently Amadeus AQC. Suppliers without this capability (e.g. Duffel) are skipped entirely by this endpoint; availability can expand as suppliers are added. Always check for the `flexPrices` key before reading it — never assume it's present. This endpoint accepts the same body as [`get_flights`](/flights/search) and internally runs the search in flexible-only mode: `flexible_only` results contain **no individual offers**, only the date matrix. If you want regular offers **and** the matrix together in one call, send `flexible: true` to `get_flights` directly instead of calling this endpoint. ## Request body [#request-body] Same fields as `POST /api/v1/get_flights`. Omit `supplier_id` to query all enabled suppliers that support flexible-date matrices, or target a supplier only after confirming that capability via [Suppliers](/suppliers). ## One-way: 7-day matrix [#one-way-7-day-matrix] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/flexible_search \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 } }' ``` For a one-way request, `matrix` has **7 entries** — one per day from `departure_date - 3` to `departure_date + 3` — each keyed only by `departure`: ```json { "success": true, "requestId": "255cfb4d", "searchType": "ONE_WAY", "flexible": true, "suppliers": [ { "success": true, "searchId": "fs_amadeus_...", "meta": { "supplierId": "amadeus_aqc_dz_1", "supplierName": "Amadeus Algeria", "supportHold": true, "supportVoid": true }, "flexPrices": { "supplierCurrency": "DZD", "agencyCurrency": "DZD", "matrix": [ { "departure": "2026-09-12", "status": "no_flights_available", "finalPrice": null, "basePrice": null, "taxesAndFees": null, "agencyCommission": null }, { "departure": "2026-09-13", "status": "available", "finalPrice": 27100.00, "basePrice": 18400.0, "taxesAndFees": 8500.0, "agencyCommission": 200.0, "agencyCurrency": "DZD" }, { "departure": "2026-09-14", "status": "available", "finalPrice": 26500.00, "basePrice": 18000.0, "taxesAndFees": 8300.0, "agencyCommission": 200.0, "agencyCurrency": "DZD" }, { "departure": "2026-09-15", "status": "available", "finalPrice": 28526.17, "basePrice": 18800.0, "taxesAndFees": 9526.17, "agencyCommission": 200.0, "agencyCurrency": "DZD" }, { "departure": "2026-09-16", "status": "no_flights_available", "finalPrice": null, "basePrice": null, "taxesAndFees": null, "agencyCommission": null }, { "departure": "2026-09-17", "status": "no_flights_available", "finalPrice": null, "basePrice": null, "taxesAndFees": null, "agencyCommission": null }, { "departure": "2026-09-18", "status": "no_flights_available", "finalPrice": null, "basePrice": null, "taxesAndFees": null, "agencyCommission": null } ] } } ] } ``` ## Round trip: 7×7 grid [#round-trip-77-grid] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/flexible_search \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "return_date": "2026-09-22", "cabin_class": "economy", "passengers": { "adults": 1 } }' ``` For a round trip (or open-jaw, using `segments`), `matrix` covers **every combination** of the two ±3-day windows — up to **49 entries** (7 × 7), each keyed by both `departure` and `return`: ```json { "flexPrices": { "supplierCurrency": "DZD", "agencyCurrency": "DZD", "matrix": [ { "departure": "2026-09-12", "return": "2026-09-19", "status": "no_flights_available", "finalPrice": null }, { "departure": "2026-09-15", "return": "2026-09-22", "status": "available", "finalPrice": 46200.00, "basePrice": 31200.0, "taxesAndFees": 14800.0, "agencyCommission": 200.0, "agencyCurrency": "DZD" }, { "departure": "2026-09-16", "return": "2026-09-21", "status": "available", "finalPrice": 44950.00, "basePrice": 30500.0, "taxesAndFees": 14250.0, "agencyCommission": 200.0, "agencyCurrency": "DZD" } ] } } ``` ## `flexPrices` field reference [#flexprices-field-reference] | Field | Type | Meaning | | ------------------------------------- | ------ | ----------------------------------------------------------------------------- | | `supplierCurrency` / `agencyCurrency` | string | Currencies for every price in `matrix` | | `matrix` | array | One entry per date (one-way) or date pair (round trip / open-jaw) — see below | ## `matrix[n]` field reference [#matrixn-field-reference] | Field | Type | Meaning | | ------------------ | -------------- | --------------------------------------------------------------------------------------------------------- | | `departure` | string | Candidate departure date, `YYYY-MM-DD` | | `return` | string | Candidate return date, `YYYY-MM-DD` — **only present for round trip / open-jaw** | | `status` | string | `available` if the supplier had a fare for this date (combination), otherwise `no_flights_available` | | `finalPrice` | number \| null | What the traveler pays for this date, in `agencyCurrency`. `null` when `status` is `no_flights_available` | | `basePrice` | number \| null | Base fare before taxes/fees, in `agencyCurrency` | | `taxesAndFees` | number \| null | Taxes and platform fees, in `agencyCurrency` | | `agencyCommission` | number \| null | Your commission included in `finalPrice` | Every date (or date pair) in the ±3-day window is always present in `matrix`, even when no fare was found — filter on `status === "available"` before reading prices, and expect `null` price fields on `no_flights_available` entries rather than treating their absence as an error. ## Using a matrix result [#using-a-matrix-result] `flexPrices` entries do not include a bookable offer ID. To book a specific date from the matrix, run a normal [search](/flights/search) for that exact `departure`/`return` date, then continue with the regular [price](/flights/price) → [book](/flights/book) flow. --- # POST /api/v1/flights/get_booking Source: /flights/get-booking Retrieve full booking details by FX-Port booking ID, supplier order ID, or PNR. **Permission:** Read ## Identifying the booking [#identifying-the-booking] The request body has one field, `booking_id`. Its value can be any of these identifiers: | `booking_id` value | Description | | ---------------------------- | ----------------------------------------------------- | | internal booking `id` (UUID) | **Recommended** — stable and unambiguous | | supplier order ID | Amadeus/Duffel order identifier stored on the booking | | PNR | Airline record locator — not globally unique | Always send the value in `booking_id`. Prefer the internal UUID or supplier order ID. PNRs can collide across suppliers/airlines. ## Getting the confirmation PDF [#getting-the-confirmation-pdf] After a booking is confirmed, the PDF is generated asynchronously (typically 10–30 seconds). Call `get_booking` and check `booking.bucket_link`. If `bucket_link` is `null`, the PDF is still generating. Retry every 5–10 seconds. Once `bucket_link` is populated, it contains the direct download URL to the PDF. Alternatively, use `POST /api/v1/flights/get_ticket` which returns a `ticket_ready` boolean and the URL once ready. ## Request example [#request-example] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/get_booking \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "booking_id": "eJzTd9ePMo_0NXMEAAtYAlQ" }' ``` ## Response example [#response-example] An Amadeus Algeria booking, already paid and ticketed: ```json { "environment": "sandbox", "success": true, "requestId": "562ec08a", "booking": { "id": "fbecce4f-022b-45c9-9bee-f82a09bcb7c3", "booking_id": "Z7YM6A", "status": "confirmed", "created_at": "2026-08-27T18:14:08.357503+00:00", "paid_at": "2026-08-27T18:14:30.784785+00:00", "total_price": 5086.53, "agency_commission": 5.09, "currency": "DZD", "departure_airport": "ALG", "arrival_airport": "ORN", "departure_date": "2026-09-20T21:20:00", "arrival_date": "2026-09-20T22:20:00", "included_checked_bags": "0", "ticketing_agreement": "DELAY_TO_CANCEL", "supplier_name": "Amadeus Algeria", "supplier_id": "amadeus_aqc_dz_1", "amadeus_id": "eJzTd9ePMo_0NXMEAAtYAlQ", "ticket_number": "124-2442997961", "bucket_link": "https://s3.example.com/flight-bookings/Z7YM6A-confirmed.pdf", "email_sent": true, "require_acd": false, "acd_submitted": null, "has_changed": false, "voided_at": null, "refund_amount": null, "environment": "sandbox", "pricingBreakdown": { "supplier_currency": "DZD", "agency_currency": "DZD", "base_price": 3160.0, "taxes_and_fees": 1921.44, "agency_commission": 5.09, "final_price": 5086.53, "b2b_price": 5081.44 }, "travelerPricing": [ { "traveler_id": "1", "traveler_type": "ADULT", "fare_option": "STANDARD", "supplier_currency": "DZD", "agency_currency": "DZD", "base": 3160.0, "total": 5086.53, "b2b_price": 5081.44 } ], "balanceOperations": [] } } ``` ## Key fields [#key-fields] | Field | Description | | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `booking.id` | Internal UUID — **use this as primary key** when storing or re-fetching a booking | | `booking.booking_id` | PNR / airline record locator. Not globally unique; prefer `id`. | | `booking.bucket_link` | Confirmation PDF URL. `null` until generated — poll until populated. | | `booking.ticket_number` | E-ticket number(s) | | `booking.status` | `confirmed`, `awaiting_payment`, `cancelled`, etc. | | `booking.ticketing_agreement` | Amadeus ticketing-time-limit agreement code, e.g. `DELAY_TO_CANCEL` | | `booking.require_acd` / `.acd_submitted` | Algerian Currency Declaration requirement/upload state for this booking, when applicable — see [Flight pricing](/flights/pricing) | | `booking.has_changed` / `.new_flight_details` | Set when the airline schedule-changed this booking after ticketing | | `booking.email_sent` | Whether the confirmation email was sent to the traveler | | `pricingBreakdown.b2b_price` | Amount debited from agency balance = `final_price − agency_commission` | | `pricingBreakdown.taxes_and_fees` | Taxes + platform fees. Excludes `agency_commission`. | | `travelerPricing[n]` | Per-traveler price breakdown, same shape as [search](/flights/search#travelerpricingn-fields) but snake\_case | | `balanceOperations` | Internal ledger entries for this booking (topups, refunds); usually empty unless a manual adjustment occurred | --- # POST /api/v1/flights/get_order_quote Source: /flights/get-order-quote Get the current quote (price, status, and available actions) for a held order. This step is **optional** — call it only when you want to preview or refresh the price before paying. **Permission:** Read ## Identifying the booking [#identifying-the-booking] Provide **one** of the following. `order_id` is strongly preferred. | Field | Description | | ------------ | ------------------------------------------------------------ | | `order_id` | Supplier order ID returned when the hold booking was created | | `booking_id` | FX-Port booking ID | | `pnr` | Airline record locator | **PNRs are not unique.** The same PNR can be issued by a different supplier, GDS, or airline. Looking a booking up **by PNR alone can be ambiguous**. Always use `order_id` when possible. ## Request example [#request-example] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/get_order_quote \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "order_id": "eJzTd9ePMo_0NXMEAAtYAlQ" }' ``` ## Response example [#response-example] Same held Amadeus order used in [Book](/flights/book) (hold), before it's paid: ```json { "environment": "sandbox", "success": true, "requestId": "256ebe7e", "orderId": "eJzTd9ePMo_0NXMEAAtYAlQ", "processingTime": 0.272, "orderQuote": { "orderId": "eJzTd9ePMo_0NXMEAAtYAlQ", "bookingReference": "Z7YM6A", "status": "awaiting_payment", "pricing": { "supplierCurrency": "DZD", "agencyCurrency": "DZD", "basePrice": 3160.0, "taxesAndFees": 1921.44, "agencyCommission": 5.09, "finalPrice": 5086.53, "b2bPrice": 5081.44, "additionalServices": [], "totalAmount": 5081.44 }, "paymentStatus": { "awaitingPayment": true, "paymentRequiredBy": null, "priceGuaranteeExpiresAt": null, "priceGuaranteed": true, "canPay": true }, "liveMode": false }, "supplier": { "id": "amadeus_aqc_dz_1", "technology": "amadeus_aqc", "name": "Amadeus Algeria" }, "agency": { "agencyId": "2e800fb6-0bbf-4fc1-b0bd-0f3be3b5bcc4", "currency": "DZD" }, "meta": { "priceGuaranteed": true, "paymentDeadline": null, "canPay": true } } ``` ## Field meanings [#field-meanings] | Field | Meaning | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `orderQuote.bookingReference` | The PNR for this order | | `orderQuote.status` | `"awaiting_payment"` while held; matches the booking's own status | | `orderQuote.pricing.b2bPrice` / `.totalAmount` | **The amount that will be debited from your agency balance** — the two fields are aliases of each other, always equal | | `orderQuote.paymentStatus.canPay` | Whether the hold can currently be paid — see warning below | | `orderQuote.paymentStatus.priceGuaranteed` | Whether the price is guaranteed not to change before `priceGuaranteeExpiresAt` | | `orderQuote.liveMode` | `false` in sandbox, `true` in live | | `agency.agencyId` / `agency.currency` | Your agency's ID and settlement currency | **Do not treat `canPay` as a hard guarantee.** Payment can fail because the PNR has expired or been cancelled, the hold/payment deadline has passed, or the airline released the reservation before the expected date or time. The only authoritative outcome is the `pay_order` response. Always handle payment failure gracefully even when `canPay` was `true` moments earlier. --- # POST /api/v1/flights/get_ticket Source: /flights/get-ticket Returns the PDF download URL for a confirmed booking. The PDF is generated asynchronously after booking — poll until `ticket_ready` is `true`. **Permission:** Read **Simpler alternative**: Call `get_booking` and read `booking.bucket_link` directly. It is populated as soon as the PDF is ready (same data, one fewer endpoint). ## Recommended polling strategy [#recommended-polling-strategy] 1. Book flight → store the returned `order_id` (or PNR). 2. After \~10 seconds, call `get_ticket`. 3. If `ticket_ready: false`, retry every 10 seconds (up to \~2 minutes). 4. Once `ticket_ready: true`, present or download the PDF from `download_url`. ## Request body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------ | | `booking_id` | string | Yes | PNR or supplier order ID | ## Request example [#request-example] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/get_ticket \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "booking_id": "eJzTd9ePMo_0NXMEAAtYAlQ" }' ``` ## Response — ticket not ready yet [#response--ticket-not-ready-yet] ```json { "success": true, "requestId": "a1b2c3d4", "ticket_ready": false, "booking_id": "eJzTd9ePMo_0NXMEAAtYAlQ", "message": "Ticket is not ready yet. Please retry in a moment." } ``` ## Response — ticket ready [#response--ticket-ready] ```json { "environment": "sandbox", "success": true, "requestId": "84e7ceac", "ticket_ready": true, "booking_id": "eJzTd9ePMo_0NXMEAAtYAlQ", "download_url": "https://s3.example.com/flight-bookings/Z7YM6A-confirmed.pdf" } ``` --- # Flights API overview Source: /flights Search, price, book, and manage flight orders across every enabled supplier from a single API surface. One request fans out to all connected GDS, NDC, LCC, and direct-airline suppliers and returns the best available fares. **New suppliers are added automatically.** FX-Port continuously onboards more GDS connections, NDC channels, low-cost carriers (LCC), direct airline deals, and additional PCC / office IDs. Your existing integration **automatically benefits** from every new supplier and improved fare — no code changes needed. As we expand, newer API versions will deliver richer content (more fare families, fare origins, and ancillary types); those ship under a new version so your current integration keeps working. ## Flight concepts [#flight-concepts] Read these before integrating — they apply to every flight endpoint. Instant issue, hold-then-pay, and how cancellation actually works. Passenger types, age rules at departure, seat vs lap infants, capacity limits. What each price field means and the b2bPrice formula. Void, exchange, and refund — how to request them today. ## Endpoints [#endpoints] | Method | Path | Permission | Description | | ------ | --------------------------------- | -------------- | -------------------------------------------------------------- | | `POST` | `/api/v1/get_flights` | Read | Search for flights | | `POST` | `/api/v1/flights/flexible_search` | Read | Search with ±3-day flexible date matrix | | `POST` | `/api/v1/flights/price_flight` | Read | Price a cached offer (required before booking) | | `POST` | `/api/v1/flights/book_flight` | **Read+Write** | Create a booking (hold or instant issue) | | `POST` | `/api/v1/flights/get_order_quote` | Read | Get current price/status for a held order | | `POST` | `/api/v1/flights/pay_order` | **Read+Write** | Pay and issue tickets for a held order | | `POST` | `/api/v1/flights/cancel_order` | **Read+Write** | Cancel a **held (unpaid)** reservation to release the PNR | | `POST` | `/api/v1/flights/get_booking` | Read | Retrieve booking details (includes PDF link) | | `POST` | `/api/v1/flights/get_ticket` | Read | Download ticket PDF | | `GET` | `/api/v1/flights/airports` | Public | Search airports by name or IATA code | ## Important notes [#important-notes] **Hold warning** — Holding a flight can prevent creating another booking for the same passenger on the same airline/route with some GDS suppliers. If you no longer need a hold, cancel it (`cancel_order`) to release the PNR before trying again. A displayed deadline is not a guarantee: an airline can release the PNR early, and only a successful `pay_order` confirms issuance. See [Cancelling a held reservation](/flights/booking-flows#cancelling-a-held-reservation). **Fee accuracy** — Exchange fees, refund fees, and penalties returned in `conditions` are indicative and may differ from what is actually charged. Communicate to travellers that a ticket is "refundable for a fee" or "changeable for a fee" rather than quoting exact amounts. ## Error responses [#error-responses] | HTTP | Cause | | ----- | ---------------------------------------------------- | | `401` | Missing or invalid API key | | `403` | Key lacks write permission for this operation | | `404` | Search ID, offer ID, or booking not found | | `410` | Offer expired and is no longer available (re-search) | | `503` | Supplier API temporarily unavailable | ## Questions? [#questions] Email the FX-Port developer team at [dev@fx-port.com](mailto:dev@fx-port.com) for integration help, feature requests, or to report an API error. For void, exchange, re-issue, or refund requests, use **Request support** on the booking confirmation page in the FX-Port dashboard or email [dev@fx-port.com](mailto:dev@fx-port.com). --- # Flight passengers & ages Source: /flights/passengers These rules are **specific to flights**. Hotels and eSIMs have their own passenger/guest requirements, documented under their own sections as those products launch. **Ages are evaluated at the departure date of the first flight segment, not the booking date.** A passenger who turns 12 on/before departure must be ticketed as an adult; an infant who turns 2 on/before departure must be ticketed as a child (and given a seat). The wrong type causes check-in rejection and possible rebooking fees. ## Passenger types [#passenger-types] | Search count field | Booking `type` | Age at departure | Seat | Age array (search) | | ------------------ | --------------- | ---------------- | --------------------- | --------------------------------------------- | | `adults` | `adult` | 18+ | own | — | | `seniors` | `senior` | 60+ | own | — | | `young_adults` | `young_adult` | 12–17 | own | `young_adult_ages` | | `children` | `child` | 2–11 | own | `child_ages` **(required)** | | `infants` | `infant` | under 2 | **lap** (on an adult) | `infant_ages` **(required)** | | `seated_infants` | `seated_infant` | under 2 | **own seat** | `seated_infant_ages` **(required)** | * **Lap infant** (`infants`): sits on an adult's lap, no seat, no separate ticket in most markets. * **Seated infant** (`seated_infants`): occupies a full seat and needs a ticket. Use this when the family wants a dedicated seat for an under-2, or when the infant will turn 2 before travel. * **Child age 2+** (`children`): is a regular seated child. One adult may travel with multiple children, and an age-2 child can travel alongside one lap infant. ## Search — `passengers` object [#search--passengers-object] Pass counts plus a parallel `_ages` array for every type that requires one. Array length must equal the count. The following is a valid six-passenger request: two adults, two children, one lap infant, and one seated infant. ```json { "adults": 2, "seniors": 0, "young_adults": 0, "children": 2, "child_ages": [10, 6], "infants": 1, "infant_ages": [1], "seated_infants": 1, "seated_infant_ages": [1] } ``` | Array | Required when | Value range (years at departure) | | -------------------- | -------------------- | -------------------------------- | | `child_ages` | `children > 0` | 2–11 | | `infant_ages` | `infants > 0` | 0–1 | | `seated_infant_ages` | `seated_infants > 0` | 0–1 | | `young_adult_ages` | `young_adults > 0` | 12–17 | **Passenger limits can be supplier-specific.** Some suppliers require one distinct adult or senior for every under-2 passenger across seated and lap infants. During automatic or multi-supplier searches, FX-Port silently skips any incompatible supplier and returns results from the compatible suppliers. HTTP `400` is returned only when no selected supplier can serve the passenger mix. This restriction does not apply to normal children aged 2–11. ## Capacity constraints [#capacity-constraints] | Rule | Limit | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Maximum **seated** passengers | **9** (adults + seniors + young\_adults + children + seated\_infants) | | Lap infants (`infants`) | Cannot exceed adults + seniors (each lap infant needs a lap) | | Under-2 seated infants (`seated_infants`) | Cannot exceed adults + seniors; children aged 2–11 are unrestricted by this rule | | Supplier-specific under-2 rule | Some suppliers require `seated_infants + infants <= adults + seniors` | | Accompanying traveler | Standard online bookings should include at least **1 adult or senior**. Unaccompanied-minor availability is airline-specific. | When no selected supplier is compatible, the API returns: ```json { "success": false, "error": "SUPPLIER_PASSENGER_COMBINATION_UNSUPPORTED", "message": "No available flight provider supports this passenger combination. Some providers require one adult or senior for each passenger under age 2.", "limitation": { "scope": "supplier_specific", "code": "UNDER_TWO_HOST_RATIO", "underTwoPassengers": 2, "adultSeniorHosts": 1 } } ``` ## Common mistakes [#common-mistakes] These are the most frequent errors that cause bookings to fail, get rejected at check-in, or misprice. ```jsonc // ❌ children count without ages → validation error / wrong price { "adults": 1, "children": 2 } // ✅ { "adults": 1, "children": 2, "child_ages": [8, 5] } // ❌ infant turning 2 before departure booked as lap "infant" // → airline rejects at check-in; they need their own seat { "adults": 1, "infants": 1 } // ✅ classify a passenger who is 2 at departure as a child { "adults": 1, "children": 1, "child_ages": [2] } // ❌ more lap infants than adults { "adults": 1, "infants": 2, "infant_ages": [0, 1] } // ❌ 10 seated passengers (exceeds the 9-seat limit) { "adults": 5, "children": 5, "child_ages": [4, 5, 6, 7, 8] } // ✅ split into two separate bookings (max 9 seated each) ``` ## Booking — passenger object [#booking--passenger-object] When building the `passengers` array for `book_flight`, each object must have: | Field | Type | Required | Description | | -------------------- | ------ | -------- | ----------------------------------------------------------------------- | | `type` | string | Yes | `adult`, `senior`, `young_adult`, `child`, `infant`, or `seated_infant` | | `gender` | string | Yes | `MALE` or `FEMALE` | | `first_name` | string | Yes | Given name as on passport | | `last_name` | string | Yes | Family name as on passport | | `date_of_birth` | string | Yes | `YYYY-MM-DD` — used to verify age at departure | | `identity_documents` | array | Yes | One passport per passenger | **Names must use plain Latin (ASCII) letters only.** Airlines and GDS systems reject accents and non-Latin scripts. Romanize before sending: `Jose` not `José`, `Muller` not `Müller`; transliterate Arabic/Cyrillic/Chinese (`Mohammed`, `Ivanov`, `Wang`). Allowed: `A–Z`, `a–z`, spaces, hyphens. This name rule applies to **all bookings** (flights, hotels, transfers, eSIMs) — the hotels and eSIMs sections will restate it as those products launch. ### Identity document object [#identity-document-object] | Field | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------ | | `type` | string | Yes | `passport` | | `number` | string | Yes | Document number | | `issuing_country` | string | Yes | ISO 3166-1 alpha-2 (e.g. `DZ`, `EG`, `FR`) | | `expiry_date` | string | Yes | `YYYY-MM-DD` | | `nationality` | string | No | ISO 3166-1 alpha-2 | Always supply a passport — it is accepted for both domestic and international travel. --- # POST /api/v1/flights/pay_order Source: /flights/pay-order Pay for a held booking and issue the tickets. Charges the agency balance by `pricing.b2bPrice`. **Permission:** Read+Write The **charge amount and currency are resolved server-side** from the stored booking. Do **not** send `amount` or `currency` — the endpoint rejects them. ## Identifying the booking [#identifying-the-booking] | Field | Description | | -------------- | ---------------------------------------------------------------- | | `order_id` | Preferred. Supplier order ID returned when the hold was created. | | `booking_id` | FX-Port booking ID | | `pnr` | Airline record locator — not unique, prefer `order_id` | | `payment_type` | Optional. `"balance"` (default), `"card"`, or `"arc_bsp_cash"` | **PNRs are not globally unique** across suppliers/GDS/airlines. Prefer `order_id` to unambiguously identify the booking you intend to pay for. **Payment cannot revive an expired hold.** `pay_order` fails if the PNR has expired or was cancelled, the payment deadline is in the past, or the airline released the hold early. Airlines can expire a PNR before the expected date or time. Treat `paymentSucceeded` and `paymentDetails.status` from this response as the source of truth; do not confirm the traveler or charge them in your system until payment succeeds. ## Request example [#request-example] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/pay_order \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "order_id": "eJzTd9ePMo_0NXMEAAtYAlQ", "payment_type": "balance" }' ``` ## Response example [#response-example] Response for the same held Amadeus order used throughout [Book](/flights/book) and [Get order quote](/flights/get-order-quote) — same currency both sides (DZD), so there is no top-level `exchangeRate` here. ```json { "environment": "sandbox", "success": true, "paymentSucceeded": true, "requestId": "debe80df", "orderId": "eJzTd9ePMo_0NXMEAAtYAlQ", "processingTime": 3.163, "paymentDetails": { "paymentId": null, "status": "succeeded", "type": "issuance", "paymentAmount": 5081.44, "currency": "DZD", "createdAt": "2026-08-27T18:14:30.624429", "message": "Payment successful! Tickets have been issued." }, "booking": { "reference": "eJzTd9ePMo_0NXMEAAtYAlQ", "status": "confirmed", "pnr": "Z7YM6A", "type": "instant", "createdAt": "2026-08-27T18:14:30.624429", "cancelledAt": null, "availableActions": [], "voidableUntil": "2026-08-27T23:00:00Z" }, "pricing": { "supplierCurrency": "DZD", "agencyCurrency": "DZD", "basePrice": 3160.0, "taxesAndFees": 1921.44, "agencyCommission": 5.09, "finalPrice": 5086.53, "b2bPrice": 5081.44, "additionalServices": [], "acd": { "applied": false } }, "payment": { "status": "paid", "paidAt": "2026-08-27T18:14:30.624549", "awaitingPayment": false }, "documents": [ { "type": "electronic_ticket", "documentType": "ETICKET", "uniqueIdentifier": "124-2442997961", "status": "ISSUED", "passengerIds": ["1"], "segmentIds": ["1"] } ], "bookingReferences": [ { "pnr": "Z7YM6A", "carrier": { "iataCode": "AH", "name": "Air Algerie" } } ] } ``` ## Field meanings [#field-meanings] | Field | Meaning | | ------------------------------- | ------------------------------------------------------------------------------------------------------- | | `paymentSucceeded` | Top-level boolean — check this **and** `paymentDetails.status` before treating payment as final | | `paymentDetails.status` | `"succeeded"`, `"pending"`, or `"failed"` — the single source of truth for payment outcome | | `paymentDetails.type` | Internal payment classification, e.g. `"issuance"` for a hold being paid and ticketed | | `paymentDetails.paymentAmount` | Amount actually debited from agency balance = `pricing.b2bPrice` | | `booking.status` | Now `"confirmed"` (was `"pending"` while held) | | `booking.voidableUntil` | Same-day void cutoff, now populated now that the ticket is issued | | `documents[n].uniqueIdentifier` | The e-ticket number | | `pricing.acd` | Algerian Currency Declaration flag on Amadeus Algeria bookings — see [Flight pricing](/flights/pricing) | | `paymentDetails.currency` | The agency currency of the debit | | `pricing.b2bPrice` | Debit amount = `finalPrice − agencyCommission` | Supplier cost and commission internals (net fare, kiosk/system commission) are **never** returned. Only agency-facing amounts are exposed. --- # Post-booking operations Source: /flights/post-booking **This page covers flight tickets only.** For **hotels**, **transfers**, **eSIMs**, and our other products, post-booking operations — cancellation, refund where the rate allows, and booking updates — are **fully supported through the API and the dashboard**. Only **issued flight tickets** still require a human in the loop, because airlines and GDSs gate those authorisations. **Post-booking operations on issued tickets are not yet self-serve.** Void, exchange, re-issue, and refund on **paid/issued** bookings currently require **manual processing by the FX-Port team**. This is *not* a slow email queue: every booking has a **Request support** action in the dashboard, and our team answers **around the clock**. See [Support and turnaround](#support-and-turnaround) below. ## Current status [#current-status] | Operation | Availability today | | -------------------------------------- | -------------------------------------------------------------------------------------- | | **Cancel a held (unpaid) reservation** | ✅ Self-serve via [`cancel_order`](/flights/cancel-order) — releases the PNR, no refund | | **Void an issued ticket** (same-day) | ✉️ Manual — email the FX-Port team | | **Exchange / re-issue** | ✉️ Manual — email the FX-Port team | | **Refund an issued ticket** | ✉️ Manual — email the FX-Port team | There is **no self-serve refund for issued (booked & paid) tickets**, and refund eligibility is **not guaranteed** — it varies by airline, fare rules, and supplier. Do not build a customer-facing flow that promises automated refunds. ## Support and turnaround [#support-and-turnaround] Manual does not mean slow. Post-booking requests are handled by a **dedicated team you can reach directly**, not by an anonymous ticket portal: * **Multiple channels** — in-dashboard **Request support** on every booking, email [dev@fx-port.com](mailto:dev@fx-port.com), plus a **direct messaging channel** and phone contact agreed with your account manager at onboarding. * **Around the clock** — coverage runs **24/7, including weekends and holidays**, because disruptions do not keep office hours. * **A named contact** — integrated partners get a dedicated account and technical contact who already knows your setup, so you never re-explain your integration. * **Fast by design** — urgent cases such as **same-day voids** and irregular-operations rebookings are prioritised and actioned immediately on receipt, well inside airline deadlines. This is deliberately different from providers where post-booking means a web form and a multi-day wait. If a request is ever not moving fast enough, escalate on the same thread — it reaches the people who can act. ## How to request a post-booking operation [#how-to-request-a-post-booking-operation] Use any support channel: * Open the booking confirmation page in the FX-Port dashboard and select **Request support**. * Email [dev@fx-port.com](mailto:dev@fx-port.com). * Message your dedicated channel or account contact for anything time-critical. Include: 1. The **order ID** (`booking.reference`) — preferred, or the internal booking **id** (UUID) 2. The **PNR** (for cross-reference) 3. The operation you need: **void**, **exchange/re-issue**, or **refund** 4. For exchanges: the new dates / itinerary requested 5. Any airline reference or context The team will confirm eligibility, quote any fees, and process the operation manually. ## Other products: already fully self-serve [#other-products-already-fully-self-serve] Flight tickets are the exception, not the rule. Across the rest of the platform, post-booking is already available end to end: | Product | Post-booking today | | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Hotels** | ✅ Cancellation and refund handling per the rate's cancellation policy, plus booking retrieval and voucher re-issue | | **Transfers** | ✅ Cancellation and booking updates | | **eSIMs** | ✅ Top-ups, usage and status retrieval | | **Flights (held, unpaid)** | ✅ Self-serve cancellation via [`cancel_order`](/flights/cancel-order) | | **Flights (issued)** | ✉️ Assisted — void, exchange, refund via the support channels above | ## What's coming [#whats-coming] **Closing this gap is a priority, and work is actively underway.** FX-Port is building self-serve post-booking endpoints for flights and enabling them **per supplier** as airline and GDS authorisations are granted — each new authorisation moves another slice of the table above from ✉️ to ✅. Releases are documented here and announced by email to API consumers. Because these are significant additions, richer post-booking capabilities may ship under a **new API version** so your current integration is never broken. Have a priority use case? Submit it from the booking confirmation page or email [dev@fx-port.com](mailto:dev@fx-port.com) — partner demand directly shapes which suppliers we unlock first. --- # POST /api/v1/flights/price_flight Source: /flights/price Price a cached offer to get the current, confirmed fare. This step is **required** before calling `book_flight`. `book_flight` reads the priced offer from cache; booking without pricing first will fail. **Permission:** Read Use the **supplier-level** `searchId` from `suppliers[n].searchId`, not the top-level `requestId`. ## Request body [#request-body] | Field | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------- | | `search_id` | string | Yes | `searchId` from the supplier entry in the search response | | `offer_id` | string | Yes | Offer `id` from the search results | ## Request example [#request-example] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/flights/price_flight \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "search_id": "fs_dfx_4a861fcd-c5d6-4a1e-a471-97db7a9f9982", "offer_id": "fx_offer_860f61e3" }' ``` ## Response example [#response-example] Example response for the request above (`exchangeRate` appears at the **top level** when supplier and agency currencies differ — the `pricing` block itself never contains an exchange rate field): ```json { "success": true, "environment": "sandbox", "requestId": "0f5b91b1", "offerId": "fx_offer_860f61e3", "offerType": "priced", "supplierCurrency": "USD", "agencyCurrency": "DZD", "exchangeRate": 250.0, "processingTime": 0.194, "results": { "offers": [ { "id": "fx_offer_860f61e3", "type": "flight_offer", "supplier": { "id": "duffel_1", "name": "Duffel", "originalId": "off_0000B9nxOvvmY4qNUnYSFL" }, "supportHold": false, "supportVoid": false, "voidDeadline": null, "bookableSeats": null, "pricing": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "basePrice": 16270.0, "taxesAndFees": 5198.2, "agencyCommission": 21.47, "finalPrice": 21489.67, "b2bPrice": 21468.2, "additionalServices": [] }, "travelerPricing": [ { "travelerId": "pas_0000B9nxOvjNICv2sJkXtl", "travelerType": "ADULT", "price": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "base": 16270.0, "total": 21489.67, "b2bPrice": 21468.2 } } ], "conditions": { "refundBeforeDeparture": { "allowed": false }, "changeBeforeDeparture": { "allowed": true } } } ] } } ``` ## Response field reference [#response-field-reference] | Field | Type | Meaning | | ------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- | | `success` | boolean | Whether pricing succeeded | | `environment` | string | `live` or `sandbox`, resolved from your API key | | `requestId` | string | FX-Port's internal ID for this pricing call — include it when contacting support | | `offerId` | string | Echoes the `offer_id` you sent | | `offerType` | string | `priced` once confirmed by the supplier | | `supplierCurrency` / `agencyCurrency` | string | Currencies used for the conversion — see [Flight pricing](/flights/pricing) | | `exchangeRate` | number | Present **only** when `supplierCurrency` ≠ `agencyCurrency`; the rate used to convert | | `processingTime` | number | Seconds FX-Port spent calling the supplier for this pricing request | | `results.offers[0].id` | string | The same offer ID — pass this and `search_id` to [book\_flight](/flights/book) | | `results.offers[0].supportHold` | boolean | **Re-check this here** — it can differ from the value at search time | | `results.offers[0].supportVoid` / `.voidDeadline` | — | Same-day void support and cutoff, confirmed at pricing time | | `results.offers[0].bookableSeats` | number \| null | Remaining bookable seats at this fare, when reported | | `results.offers[0].pricing` | object | Confirmed price — see [Flight pricing](/flights/pricing) for every field | | `results.offers[0].travelerPricing` | array | Confirmed per-traveler price breakdown, same shape as in [search](/flights/search#travelerpricingn-fields) | | `results.offers[0].conditions` | object | Confirmed refund/change eligibility for this exact priced offer | `supportHold` and `voidDeadline` are **inside the offer** in `results.offers[0]`, not at the top level. Always check them here — pricing can reveal that a previously holdable offer no longer supports a hold — before deciding whether to book as `hold` or `issue`. If the offer expired between search and pricing, you get HTTP `410` with `"error": "offer_expired"`. Re-run the search to get a fresh offer. See [Flight pricing](/flights/pricing) for what each price field means. --- # Flight pricing Source: /flights/pricing Every flight pricing block exposes the same set of agency-facing money fields. ## Field meanings [#field-meanings] | Field | Meaning | | ------------------ | --------------------------------------------------------------------------------------- | | `basePrice` | Base fare set by the airline, **before taxes** (agency currency) | | `taxesAndFees` | Taxes plus all platform-side fees, folded together. **Excludes** your agency commission | | `agencyCommission` | Your own markup — what you charge the traveller on top | | `finalPrice` | What the traveller pays = `basePrice + taxesAndFees + agencyCommission` | | `b2bPrice` | **The amount debited from your agency balance** = `finalPrice − agencyCommission` | | `totalAmount` | Alias of `b2bPrice` on endpoints that expose it — the amount debited | ## The formula [#the-formula] ``` finalPrice = basePrice + taxesAndFees + agencyCommission b2bPrice = finalPrice − agencyCommission (= debited from your balance) ``` **Example** (Egyptian Pound, `EGP`): ```json { "supplierCurrency": "EGP", "agencyCurrency": "EGP", "basePrice": 4200.00, "taxesAndFees": 1850.00, "agencyCommission": 300.00, "finalPrice": 6350.00, "b2bPrice": 6050.00 } ``` Check: `4200 + 1850 + 300 = 6350` ✓ and `6350 − 300 = 6050` ✓ ## Exchange rate [#exchange-rate] When `supplierCurrency` differs from `agencyCurrency` (for example a supplier prices in `USD` and your agency settles in `NGN`), an `exchangeRate` field appears at the **response top level** — not inside the `pricing` block. ```json { "exchangeRate": 1620.0, "pricing": { "supplierCurrency": "USD", "agencyCurrency": "NGN", "basePrice": 291600.00, "taxesAndFees": 145800.00, "agencyCommission": 8100.00, "finalPrice": 445500.00, "b2bPrice": 437400.00 } } ``` When both currencies are the same (e.g. `EUR → EUR`), no `exchangeRate` is included. ## Per-traveler pricing [#per-traveler-pricing] Search, price, and booking responses also include a `travelerPricing` array with a per-traveler `price` object (`base`, `total`, and `b2bPrice`), so you can display a breakdown per passenger. The per-traveler `b2bPrice` is that traveller's share of the amount debited. ## `acd` (Algerian Currency Declaration) [#acd-algerian-currency-declaration] Amadeus Algeria pricing blocks can include an `acd` object: ```json { "pricing": { "supplierCurrency": "DZD", "agencyCurrency": "DZD", "basePrice": 3160.0, "taxesAndFees": 1921.44, "agencyCommission": 5.09, "finalPrice": 5086.53, "b2bPrice": 5081.44, "acd": { "applied": false } } } ``` `acd.applied: true` means part of the fare was settled in foreign currency, which can require the traveler to present a currency declaration or foreign-currency exchange receipt at departure — this is specific to routes where Algeria enforces that requirement. When `applied` is `true` on a confirmed booking, [`get_booking`](/flights/get-booking) additionally exposes `require_acd` and `acd_submitted` on the stored booking so you can track whether the traveler has uploaded the required document. This only ever appears for Amadeus Algeria bookings priced in DZD; it does not apply to other suppliers. --- # GET /api/v1/suppliers Source: /suppliers Returns all enabled suppliers grouped by product. This endpoint spans **all products** (flights, hotels, eSIMs), not just flights. Disabled suppliers and unimplemented products are omitted. **Permission:** Read **The supplier list grows over time.** FX-Port continuously adds GDS connections, NDC channels, low-cost carriers, direct-airline deals, and additional PCC / office IDs. Your integration picks up new suppliers automatically — no code change needed. Treat this list as dynamic; do not hard-code it. ## Request [#request] ```bash curl --request GET \ --url https://api.fx-port.com/api/v1/suppliers \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' ``` ## Response example [#response-example] ```json { "environment": "live", "success": true, "products": [ { "product": "flights", "suppliers": [ { "id": "amadeus_aqc_dz_1", "name": "Amadeus Algeria", "technology": "amadeus_aqc", "routes": ["DZ", "*"], "description": "Dedicated Algiers GDS office with negotiated fares and an advantageous DZD rate on outbound Algeria routes. Supports hold reservations and same-day void.", "capabilities": { "hold": true, "void": true, "flexible_dates": true, "ancillaries": false }, "voidDeadline": "2026-08-17T23:00:00Z" }, { "id": "amadeus_aqc_tn_1", "name": "Amadeus Tunisia", "technology": "amadeus_aqc", "routes": ["TN", "*"], "description": "Tunis GDS office with negotiated North-Africa and European fares priced in TND. Supports hold and same-day void within the Tunis ticketing day.", "capabilities": { "hold": true, "void": true, "flexible_dates": true, "ancillaries": false }, "voidDeadline": "2026-08-17T23:00:00Z" }, { "id": "ndc_eg_1", "name": "NDC Egypt", "technology": "ndc", "routes": ["EG", "*"], "description": "Direct NDC content for Egyptian carriers with richer fare families and continuous pricing. Fares in EGP. Ancillary bundles available on supported carriers.", "capabilities": { "hold": false, "void": false, "flexible_dates": false, "ancillaries": true } }, { "id": "travelport_uk_1", "name": "TravelPort UK", "technology": "travelport", "routes": ["GB", "*"], "description": "TravelPort-connected UK office with broad European and long-haul coverage at published fares. Competitive on UK-origin itineraries. Seat and bag ancillaries on supported carriers.", "capabilities": { "hold": false, "void": false, "flexible_dates": false, "ancillaries": true } }, { "id": "travelport_ae_1", "name": "TravelPort UAE", "technology": "travelport", "routes": ["AE", "*"], "description": "Dubai TravelPort office with strong Gulf, Indian-subcontinent, and long-haul coverage. Fares in AED. Ancillaries available on supported carriers.", "capabilities": { "hold": false, "void": false, "flexible_dates": false, "ancillaries": true } }, { "id": "sabre_us_1", "name": "Sabre USA", "technology": "sabre", "routes": ["US", "*"], "description": "Sabre-connected US office with deep North-American and transatlantic inventory at published fares. Broad carrier coverage; ancillaries on supported airlines.", "capabilities": { "hold": false, "void": false, "flexible_dates": false, "ancillaries": true } }, { "id": "duffel_1", "name": "Duffel", "technology": "duffel", "routes": ["*"], "description": "Aggregated global content across hundreds of airlines at published fares. Competitive on European, Middle-Eastern, and long-haul routes. Seat and bag ancillaries on supported carriers. Holds not supported.", "capabilities": { "hold": false, "void": false, "flexible_dates": false, "ancillaries": true } } ] }, { "product": "hotels", "suppliers": [ { "id": "worldota_1", "name": "RateHawk", "technology": "worldota", "description": "Global hotel inventory from chains and independents with real-time availability and net rates." } ] }, { "product": "esims", "suppliers": [ { "id": "airalo_1", "name": "Airalo", "technology": "airalo", "description": "eSIM data plans across 190+ countries, activated instantly with QR delivery by email." } ] } ], "meta": { "environment": "live", "permissionRequired": "read" } } ``` ## Field reference [#field-reference] ### Common supplier fields [#common-supplier-fields] | Field | Type | Description | | ------------- | ------ | ---------------------------------------------------------------------- | | `id` | string | Supplier identifier — pass as `supplier_id` in search/booking requests | | `name` | string | Human-readable supplier name | | `technology` | string | Underlying integration technology (see below) | | `description` | string | Supplier characteristics, strengths, and operational notes | ### Integration technologies [#integration-technologies] The `technology` field indicates how FX-Port connects to the supplier. Current values include: | Technology | Meaning | | -------------- | ----------------------------------------------------------- | | `amadeus_aqc` | Amadeus Quick Connect (dedicated office / negotiated fares) | | `amadeus_soap` | Amadeus classic SOAP / Web Services | | `travelport` | TravelPort (Galileo / Apollo / Worldspan) | | `sabre` | Sabre GDS | | `ndc` | Direct airline NDC content | | `duffel` | Duffel aggregated content | | `worldota` | WorldOTA / RateHawk (hotels) | | `airalo` | Airalo (eSIMs) | New technologies are added as connections come online — do not hard-code this list. ### Flight-specific fields [#flight-specific-fields] | Field | Type | Description | | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- | | `routes` | array | Market scope. A country code (e.g. `"DZ"`, `"TN"`, `"GB"`) means that origin is prioritised; `"*"` means global coverage. | | `capabilities.hold` | boolean | Supports hold (reserve now, pay later) | | `capabilities.void` | boolean | Supports same-day void of issued tickets | | `capabilities.flexible_dates` | boolean | Supports the ±3-day flexible date matrix | | `capabilities.ancillaries` | boolean | Supports ancillary upsells (seats, extra bags) | | `voidDeadline` | string | Next void cutoff (UTC) — next midnight in the supplier's timezone | ## The `voidDeadline` field [#the-voiddeadline-field] `voidDeadline` is recalculated on every request — **do not cache it across days**. It is the next midnight in the supplier's operating timezone, expressed in UTC. Void eligibility varies by supplier and airline — check it immediately before attempting a void. --- # Basic route examples Source: /flights/search/basic-routes See [Search response essentials](/flights/search#response-essentials) for a full field-by-field reference of every response field shown below. All responses on this page were captured from a running sandbox request — trimmed for length, not invented. ## One-way economy [#one-way-economy] ```json { "origin": "ALG", "destination": "ORY", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 } } ``` A one-way search returns `searchType: "ONE_WAY"` and each offer has exactly one `itinerary` entry with `direction: "outbound"`. ## Round-trip economy [#round-trip-economy] ```json { "origin": "CAI", "destination": "DXB", "departure_date": "2026-09-15", "return_date": "2026-09-22", "cabin_class": "economy", "passengers": { "adults": 2 } } ``` ### Response example [#response-example] A round-trip search returns `searchType: "ROUND_TRIP"`. Each offer now has **two** `itinerary` entries (`outbound` and `return`), and `travelerPricing` has one entry per passenger — here, two adults, each with their own price share: ```json { "id": "fx_offer_2bca05ae", "pricing": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "basePrice": 81250.0, "taxesAndFees": 23098.4, "agencyCommission": 104.36, "finalPrice": 104452.76, "b2bPrice": 104348.4 }, "itinerary": [ { "direction": "outbound", "stops": 0, "duration": "PT3H48M", "segments": [{ "departure": "CAI", "arrival": "DXB", "flight": "ZZ3864", "cabin": "ECONOMY" }] }, { "direction": "return", "stops": 0, "duration": "PT3H48M", "segments": [{ "departure": "DXB", "arrival": "CAI", "flight": "ZZ3864", "cabin": "ECONOMY" }] } ], "travelerPricing": [ { "travelerId": "pas_0000B9nxSvER5dWoUBBL7Z", "travelerType": "ADULT", "price": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "base": 40625.0, "total": 52226.38, "b2bPrice": 52174.2 } }, { "travelerId": "pas_0000B9nxSvEn4JoOVHLcem", "travelerType": "ADULT", "price": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "base": 40625.0, "total": 52226.38, "b2bPrice": 52174.2 } } ] } ``` Each traveler's `price.total` (52226.38 × 2 = 104452.76) sums to the offer-level `pricing.finalPrice`. ## Direct / non-stop only [#direct--non-stop-only] `direct: true` and `nonstop: true` are aliases; both become a zero-stop supplier filter. ```json { "origin": "ALG", "destination": "ORY", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 }, "direct": true } ``` The same route without `direct` returns a mix of stop counts (`results.totalOffers` includes one-stop and multi-stop offers). With `direct: true`, every returned offer has `itinerary[n].stops: 0`, and `results.totalOffers` only counts non-stop offers — typically a smaller number than an unfiltered search on the same route. ## All enabled suppliers [#all-enabled-suppliers] Omit every supplier selector. FX-Port evaluates compatibility and calls all matching enabled suppliers in parallel. New suppliers automatically join this request as FX-Port enables them. ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "return_date": "2026-09-22", "cabin_class": "economy", "passengers": { "adults": 1 } } ``` The response's `suppliers[]` array has one entry per supplier that was actually called — check `suppliers[n].success` and `suppliers[n].meta.supplierId` to see which ones returned offers for this specific route/date/passenger combination. ## cURL [#curl] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/get_flights \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "return_date": "2026-09-22", "cabin_class": "economy", "passengers": { "adults": 1 } }' ``` --- # Open-jaw searches Source: /flights/search/complex-itineraries Use exactly two `segments` instead of `origin`, `destination`, `departure_date`, and `return_date` for an open-jaw itinerary. Multi-destination search is not currently listed in the public API documentation or Postman collection. It will be documented in a future release. Each segment requires: | Field | Description | | ------------- | ----------------------------------- | | `origin` | Departure IATA airport code | | `destination` | Arrival IATA airport code | | `date` | Local departure date (`YYYY-MM-DD`) | ## Open jaw [#open-jaw] Outbound arrives at one airport; return departs from another. ```json { "segments": [ { "origin": "ALG", "destination": "CDG", "date": "2026-09-15" }, { "origin": "ORY", "destination": "ALG", "date": "2026-09-22" } ], "cabin_class": "economy", "passengers": { "adults": 1 } } ``` FX-Port identifies this as open-jaw because the second segment is not the exact reverse of the first. ### Response example [#response-example] The top-level `searchType` is `"OPEN_JAW"` (see [Search response essentials](/flights/search#response-essentials) for the full field reference), and each offer's `itinerary` has one entry per segment you sent, in the same order, each carrying its own `direction`: ```json { "success": true, "searchType": "OPEN_JAW", "suppliers": [ { "success": true, "results": { "totalOffers": 178, "offers": [ { "itinerary": [ { "direction": "outbound", "segments": [{ "departure": { "airportCode": "ALG" }, "arrival": { "airportCode": "CDG" } }] }, { "direction": "return", "segments": [{ "departure": { "airportCode": "ORY" }, "arrival": { "airportCode": "ALG" } }] } ] } ] } } ] } ``` Note the second direction departs `ORY`, not `CDG` — that's the open jaw. ## cURL [#curl] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/get_flights \ --header 'Authorization: Bearer fxp_live_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "segments": [ { "origin": "ALG", "destination": "CDG", "date": "2026-09-15" }, { "origin": "ORY", "destination": "ALG", "date": "2026-09-22" } ], "cabin_class": "economy", "passengers": { "adults": 1 } }' ``` Each selected supplier receives both open-jaw segments. --- # Filter examples Source: /flights/search/filters All filters are optional and can be combined when their semantics do not conflict. ## Filter reference [#filter-reference] | Field | Type | Meaning | | -------------------- | --------- | ---------------------------------------------------------------------------------- | | `direct` / `nonstop` | boolean | Non-stop offers only | | `refundable` | boolean | Prefer refundable fares — see the caveat below; always confirm on the offer itself | | `checked_bags` | boolean | Offers including checked baggage | | `included_airlines` | string\[] | Restrict results to these IATA carrier codes | | `excluded_airlines` | string\[] | Remove these IATA carrier codes from the results | | `preferred_airlines` | string\[] | Alias of `included_airlines` — see the note below | | `flexible` | boolean | Request flexible-date matrix from capable suppliers while keeping regular offers | Do not send `included_airlines` and `excluded_airlines` together for the same search. Choose a whitelist or a blacklist. Amadeus rejects supplier requests that contain both. **`preferred_airlines` currently behaves exactly like `included_airlines`** — it restricts results to the listed carriers, it does not merely rank them higher. There is no separate "ranking-only, non-exclusive" preference filter today. Use `included_airlines` for clarity; `preferred_airlines` is accepted as a compatibility alias with identical behavior. Do not send both with different carrier lists in the same request. **`max_price` is not currently enforced for every supplier.** It is sent as a native filter to Amadeus, but the Duffel integration does not yet apply it — a Duffel search with `max_price` set can still return offers above that price. Treat `max_price` as best-effort and always check each offer's own `pricing.finalPrice` client-side before relying on it as a hard cap. ## Refundable only [#refundable-only] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 }, "refundable": true } ``` `refundable: true` is a hint forwarded to each supplier, not a guaranteed post-filter on every technology — a search can still return a mix of refundable and non-refundable offers. Always read `conditions.refundBeforeDeparture.allowed` on each individual offer rather than assuming every offer returned by this filter is refundable. ## Checked baggage only [#checked-baggage-only] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "return_date": "2026-09-22", "cabin_class": "economy", "passengers": { "adults": 1 }, "checked_bags": true } ``` ### Response example [#response-example] Every offer's `travelerPricing[n].fareDetailsBySegment[m].includedCheckedBags.quantity` is `>= 1`. See [Search response essentials](/flights/search#response-essentials) for the full offer shape. ```json { "id": "fx_offer_e12658c6", "pricing": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "basePrice": 16325.0, "taxesAndFees": 5210.8, "agencyCommission": 21.54, "finalPrice": 21557.34, "b2bPrice": 21535.8 }, "travelerPricing": [ { "fareDetailsBySegment": [ { "includedCheckedBags": { "quantity": 1 } } ] } ] } ``` ## Include airlines (allow-list) [#include-airlines-allow-list] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 }, "included_airlines": ["AF", "AH"] } ``` For Amadeus, FX-Port sends a native include filter. Duffel's offer request does not expose a native carrier include field, so FX-Port filters returned offers while preserving the unified response. Either way, every offer returned has `validatingAirline[0].code` equal to one of the listed codes. ## Exclude airlines (block-list) [#exclude-airlines-block-list] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 }, "excluded_airlines": ["IB"] } ``` None of the listed carrier codes appear as `validatingAirline[0].code` on any returned offer. ## Flexible dates across all suppliers [#flexible-dates-across-all-suppliers] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "return_date": "2026-09-22", "cabin_class": "economy", "passengers": { "adults": 1 }, "flexible": true } ``` Every compatible supplier still returns its regular offers under `results`; only suppliers that support the flexible-date matrix additionally add a `flexPrices` key next to `results` on their entry in `suppliers[]`. Always check for that key's presence rather than assuming it's there — see [Flexible search](/flights/flexible-search) for the full matrix shape, and use the dedicated `POST /api/v1/flights/flexible_search` endpoint when you want **only** flexible-capable suppliers queried. --- # POST /api/v1/get_flights Source: /flights/search Search available flight offers across one, several, or every enabled supplier. Results are grouped under `suppliers[]`; each supplier has its own `searchId` and offer list. **Permission:** Read **Save the supplier-level `searchId`.** Pass `suppliers[n].searchId` to `price_flight` and `book_flight` — never the top-level `requestId`. ## Request fields [#request-fields] ### Route [#route] | Field | Type | Required | Description | | ---------------- | ------ | -------- | ---------------------------------------------------------- | | `origin` | string | Yes¹ | Departure IATA code (`ALG`, `CAI`, `LHR`) | | `destination` | string | Yes¹ | Arrival IATA code | | `departure_date` | string | Yes¹ | `YYYY-MM-DD` | | `return_date` | string | No | Add for a round trip | | `segments` | array | No² | Exactly two open-jaw legs: `{ origin, destination, date }` | ¹ Simple one-way/round-trip only. ² Use exactly two `segments` for open-jaw. ### Passengers and cabin [#passengers-and-cabin] | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------------------------------------- | | `passengers` | object | Yes | Counts and age arrays; see [Flight passengers & ages](/flights/passengers) | | `cabin_class` | string | No | `economy` (default), `premium_economy`, `business`, `first`, `any` | ### Filters [#filters] | Field | Type | Description | | -------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `direct` | boolean | `true` → non-stop offers only (alias: `nonstop`) | | `refundable` | boolean | `true` → prefer refundable offers; not enforced by every supplier, always confirm on the offer | | `checked_bags` | boolean | `true` → offers that include checked baggage | | `max_price` | number | Maximum total price in the agency currency; **not enforced by every supplier yet** — see [Filter examples](/flights/search/filters) | | `included_airlines` | string\[] | Restrict results to these IATA carrier codes | | `excluded_airlines` | string\[] | Remove these IATA carrier codes from the results | | `preferred_airlines` | string\[] | Alias of `included_airlines` — currently the same strict allow-list behavior, not a soft ranking | | `flexible` | boolean | Request flexible dates from capable suppliers; regular offers still return | See [Filter examples](/flights/search/filters) for the full behavior, caveats, and example responses for each filter. ### Supplier selection [#supplier-selection] | Field | Type | Behaviour | | ------------------ | --------- | ---------------------------------------------------------------------------- | | *(omit selectors)* | — | **Recommended.** FX-Port selects compatible enabled suppliers automatically. | | `supplier_id` | string | Advanced: restrict the search to one exact supplier ID. | | `supplier_ids` | string\[] | Advanced: restrict the search to an exact supplier set. | Usually, omit all supplier fields and let FX-Port manage selection. For the special case “use exactly 2 of our 5 suppliers”, send `supplier_ids: ["supplier_a", "supplier_b"]`. The backend calls enabled suppliers in parallel. Unknown, disabled, or flexible-incompatible strict selections return HTTP 400. Traveler-incompatible suppliers are skipped silently; HTTP 400 is returned only when no selected supplier remains compatible. ## Examples by category [#examples-by-category] One-way, round-trip, direct-only, and all suppliers. Family mix, lap/seated infants, senior/young adult, business, premium economy, all cabins. Refundable, checked bags, price cap, market hints, include/exclude/preferred airlines, flexible dates. Automatic selection (recommended) or exact supplier IDs for special cases. Search two non-reversed flight segments. ## Minimal request [#minimal-request] ```bash curl --request POST \ --url https://api.fx-port.com/api/v1/get_flights \ --header 'Authorization: Bearer fxp_test_YOUR_KEY' \ --header 'Content-Type: application/json' \ --data '{ "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 } }' ``` ## Response essentials [#response-essentials] The example below is a trimmed **real sandbox response** for the request above (fields present on every offer are kept; repeated/optional fields are trimmed for length — see the field reference tables underneath for everything not shown). ```json { "success": true, "requestId": "255cfb4d", "searchType": "ONE_WAY", "flexible": false, "processingTime": 4.282, "suppliers": [ { "success": true, "searchId": "fs_dfx_4a861fcd-c5d6-4a1e-a471-97db7a9f9982", "supportHold": false, "supportVoid": false, "voidDeadline": null, "meta": { "supplierId": "duffel_1", "technologyType": "duffel", "supplierName": "Duffel", "processingTime": 1.792, "supplierCurrency": "USD", "agencyCurrency": "DZD" }, "results": { "totalOffers": 115, "supplierCurrency": "USD", "agencyCurrency": "DZD", "offers": [ { "id": "fx_offer_860f61e3", "type": "flight_offer", "supplier": { "id": "duffel_1", "name": "Duffel", "originalId": "off_0000B9nxOvvmY4qNUnYSFL" }, "bookableSeats": null, "supportHold": false, "supportVoid": false, "instantTicketingRequired": false, "lastTicketingDate": "2026-08-27", "lastTicketingDateTime": "2026-08-27T17:58:25.166703Z", "pricing": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "basePrice": 16270.0, "taxesAndFees": 5198.2, "agencyCommission": 21.47, "finalPrice": 21489.67, "b2bPrice": 21468.2, "additionalServices": [] }, "itinerary": [ { "direction": "outbound", "duration": "PT2H24M", "stops": 0, "segments": [ { "id": "seg_0000B9nxOvvmY4qNUnYSFA", "departure": { "airportCode": "ALG", "airportName": "Houari Boumediene Airport", "cityName": "Algiers", "countryCode": "DZ", "terminal": "2", "datetime": "2026-09-15T10:50:00", "timeZone": "Africa/Algiers" }, "arrival": { "airportCode": "CDG", "airportName": "Paris Charles de Gaulle Airport", "cityName": "Paris", "countryCode": "FR", "terminal": "1", "datetime": "2026-09-15T14:14:00", "timeZone": "Europe/Paris" }, "flight": { "carrierCode": "IB", "carrierName": "Iberia", "flightNumber": "3177", "operatingCarrier": "IB", "operatingCarrierName": "Iberia" }, "duration": "PT2H24M", "cabin": "ECONOMY", "fareBasis": "Y20LGTN2", "brandedFare": "BASIC", "brandedFareLabel": "Basic" } ], "conditions": { "changeBeforeDeparture": { "allowed": true }, "refundBeforeDeparture": { "allowed": false } } } ], "travelerPricing": [ { "travelerId": "pas_0000B9nxOvjNICv2sJkXtl", "travelerType": "ADULT", "fareOption": "STANDARD", "price": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "base": 16270.0, "total": 21489.67, "b2bPrice": 21468.2 }, "fareDetailsBySegment": [ { "segmentId": "seg_0000B9nxOvvmY4qNUnYSFA", "cabin": "ECONOMY", "fareBasis": "Y20LGTN2", "brandedFare": "BASIC", "brandedFareLabel": "Basic", "includedCheckedBags": { "quantity": 1 }, "includedCabinBags": { "quantity": 1 }, "amenities": [ { "description": "WIFI", "isChargeable": true, "amenityType": "WIFI" } ] } ] } ], "conditions": { "refundBeforeDeparture": { "allowed": false }, "changeBeforeDeparture": { "allowed": true } }, "fareRules": { "exchange": { "allowed": true }, "refund": { "allowed": false }, "revalidation": { "allowed": false } }, "validatingAirline": [{ "code": "IB", "name": "Iberia" }] } ] } } ] } ``` ## Top-level response fields [#top-level-response-fields] | Field | Type | Meaning | | ---------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `success` | boolean | `true` if the search executed (individual suppliers can still fail independently — see `suppliers[n].success`) | | `requestId` | string | FX-Port's internal ID for this search call — include it when contacting support | | `searchType` | string | `ONE_WAY`, `ROUND_TRIP`, or `OPEN_JAW` (two non-reversed [`segments`](/flights/search/complex-itineraries)), derived from your request. Multi-city (3+ segments) is **not supported** — do not send more than two `segments` | | `flexible` | boolean | Whether this search requested the flexible-date matrix (`flexible: true` in the request) | | `processingTime` | number | Total time in seconds FX-Port spent calling all suppliers for this request | | `suppliers` | array | One entry per supplier that was called — see below | ## `suppliers[n]` fields [#suppliersn-fields] | Field | Type | Meaning | | ----------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------- | | `success` | boolean | Whether **this specific supplier** returned usable results | | `searchId` | string | **Save this.** Required as `search_id` on [price\_flight](/flights/price) and, indirectly, on booking | | `supportHold` | boolean | Whether this supplier can create a hold (pay-later) reservation for these offers | | `supportVoid` | boolean | Whether this supplier supports same-day void of an issued ticket | | `voidDeadline` | string \| null | Next void cutoff (UTC), if `supportVoid` is `true` | | `meta.supplierId` | string | Supplier identifier, e.g. `duffel_1` — matches [suppliers](/suppliers) | | `meta.technologyType` | string | Underlying integration technology (`duffel`, `amadeus_aqc`, `ndc`, …) | | `meta.supplierName` | string | Human-readable supplier name | | `meta.processingTime` | number | Time in seconds this individual supplier took to respond | | `meta.supplierCurrency` / `meta.agencyCurrency` | string | Currencies used for conversion — see [Flight pricing](/flights/pricing) | | `results.totalOffers` | number | Total offers this supplier returned (may exceed what's shown if paginated internally) | | `results.offers` | array | The offers themselves — see below | ## `offers[n]` fields [#offersn-fields] | Field | Type | Meaning | | ---------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------ | | `id` | string | **Save this.** Required as `offer_id` on [price\_flight](/flights/price) | | `type` | string | Always `flight_offer` | | `supplier.id` / `supplier.name` | string | Which supplier this specific offer came from | | `supplier.originalId` | string | The supplier's own internal offer ID (opaque, for support/debugging only) | | `bookableSeats` | number \| null | Remaining bookable seats at this fare, when the supplier reports it | | `supportHold` / `supportVoid` / `voidDeadline` | — | Same meaning as at the supplier level, but specific to this offer | | `instantTicketingRequired` | boolean | `true` means this fare cannot be held — must book with `booking_type: "issue"` | | `lastTicketingDate` / `lastTicketingDateTime` | string | Deadline to issue a ticket for this specific offer | | `pricing` | object | See [Flight pricing](/flights/pricing) for every field's meaning | | `itinerary` | array | One entry per direction (`outbound`, and `return` for round trips) — see below | | `travelerPricing` | array | Per-traveler price breakdown — one entry per passenger, see below | | `conditions` | object | Offer-level refund/change eligibility (`allowed: true/false`); indicative, not a guarantee | | `fareRules` | object | Same refund/exchange eligibility, plus `revalidation` | | `validatingAirline` | array | The airline whose fare rules and ticket stock govern this booking | ## `itinerary[n]` and `segments[n]` fields [#itineraryn-and-segmentsn-fields] | Field | Type | Meaning | | --------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- | | `direction` | string | `outbound` or `return` | | `duration` | string | ISO-8601 duration for this direction, e.g. `PT2H24M` = 2h24m | | `stops` | number | Number of stops in this direction (`0` = non-stop) | | `segments[n].departure` / `.arrival` | object | `airportCode`, `airportName`, `cityName`, `countryCode`, `terminal`, `datetime` (local), `timeZone` | | `segments[n].flight.carrierCode` / `.carrierName` | string | Marketing carrier | | `segments[n].flight.operatingCarrier` / `.operatingCarrierName` | string | Operating carrier — differs from marketing carrier on codeshares | | `segments[n].flight.flightNumber` | string | Flight number as filed by the carrier | | `segments[n].cabin` | string | Cabin for this specific segment (can differ from the requested `cabin_class` on mixed-cabin itineraries) | | `segments[n].fareBasis` | string | Fare basis code — supplier/GDS-specific | | `segments[n].brandedFare` / `.brandedFareLabel` | string | Fare family code and its display label, when the supplier exposes branded fares | | `conditions.changeBeforeDeparture.allowed` | boolean | Whether a change is generally permitted before departure for this direction | | `conditions.refundBeforeDeparture.allowed` | boolean | Whether a refund is generally permitted before departure for this direction | ## `travelerPricing[n]` fields [#travelerpricingn-fields] | Field | Type | Meaning | | ------------------------------------------------------ | ------ | ----------------------------------------------------------------------------------------------- | | `travelerId` | string | Identifies which passenger (by search order) this price applies to | | `travelerType` | string | `ADULT`, `SENIOR`, `YOUNG_ADULT`, `CHILD`, `INFANT`, or `SEATED_INFANT` | | `fareOption` | string | Fare option selected for this traveler, e.g. `STANDARD` | | `price` | object | This traveler's own share of `base`/`total`/`b2bPrice` — see [Flight pricing](/flights/pricing) | | `fareDetailsBySegment[n].includedCheckedBags.quantity` | number | Checked bags included for this traveler on this segment | | `fareDetailsBySegment[n].includedCabinBags.quantity` | number | Cabin bags included for this traveler on this segment | | `fareDetailsBySegment[n].amenities` | array | Optional amenity list (Wi-Fi, seat pitch, entertainment, …); `isChargeable` marks paid extras | ## Save these values [#save-these-values] | Value | Location | Used by | | ------------- | ----------------------------------- | ----------------------------------------------- | | `searchId` | `suppliers[n].searchId` | Required `price_flight.search_id`, then booking | | offer `id` | `suppliers[n].results.offers[m].id` | Required `price_flight.offer_id`, then booking | | `supportHold` | Supplier and priced offer | Use hold only when still `true` after pricing | --- # Passenger and cabin examples Source: /flights/search/passenger-and-cabin See [Flight passengers & ages](/flights/passengers) for full validation rules, and [Search response essentials](/flights/search#response-essentials) for a full field-by-field reference of every response field shown below. Search counts and age arrays must match the passenger objects submitted later to `book_flight`. ## Family: adults, child, lap infant [#family-adults-child-lap-infant] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "return_date": "2026-09-22", "cabin_class": "economy", "passengers": { "adults": 2, "children": 1, "child_ages": [8], "infants": 1, "infant_ages": [1] } } ``` ### Response example [#response-example] `travelerPricing` has exactly one entry per passenger, in the same order as requested. Note the actual `travelerType` value for a lap infant is `HELD_INFANT`, not `INFANT`: ```json { "pricing": { "supplierCurrency": "USD", "agencyCurrency": "DZD", "basePrice": 49112.52, "taxesAndFees": 16969.08, "agencyCommission": 66.12, "finalPrice": 66147.72, "b2bPrice": 66081.6 }, "travelerPricing": [ { "travelerType": "ADULT", "price": { "base": 12278.13, "total": 16536.93, "b2bPrice": 16520.41 } }, { "travelerType": "ADULT", "price": { "base": 12278.13, "total": 16536.93, "b2bPrice": 16520.41 } }, { "travelerType": "CHILD", "price": { "base": 12278.13, "total": 16536.93, "b2bPrice": 16520.41 } }, { "travelerType": "HELD_INFANT", "price": { "base": 12278.13, "total": 16536.93, "b2bPrice": 16520.41 } } ] } ``` Per-traveler pricing is computed and returned by the fulfilling supplier — for this specific sandbox offer every traveler happened to price identically, but that is **not guaranteed** in general. Infants, children, and seated infants are commonly priced lower than adults on real fares. Always read each traveler's own `price.total`/`price.b2bPrice` rather than assuming a fixed ratio to the adult fare. ## Complete mix with seated infant [#complete-mix-with-seated-infant] The public payload is supplier-neutral. Passenger limits can differ by supplier, so automatic and multi-supplier searches silently skip incompatible suppliers and keep any compatible results. This example includes two adults and is compatible with suppliers that require one adult/senior host per under-2 passenger. A traveler who is truly age 2 is a regular child and does not use an infant host. ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 2, "seniors": 0, "young_adults": 0, "children": 2, "child_ages": [10, 6], "infants": 1, "infant_ages": [1], "seated_infants": 1, "seated_infant_ages": [1] }, "supplier_ids": ["amadeus_aqc_dz_1", "duffel_1"] } ``` A search like this returns `travelerPricing` with **six** entries (2 adults + 2 children + 1 lap infant + 1 seated infant), each tagged with its own `travelerType`. ## Supplier-specific fallback: one adult with two infant types [#supplier-specific-fallback-one-adult-with-two-infant-types] This payload is valid at the FX-Port API level. Do not force a supplier: FX-Port calls compatible suppliers, silently skips incompatible ones, and returns available results without a warning. If the route has no compatible supplier, the API returns HTTP `400` with `error: "SUPPLIER_PASSENGER_COMBINATION_UNSUPPORTED"` and no `suppliers[]` results. ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-29", "cabin_class": "economy", "passengers": { "adults": 1, "seated_infants": 1, "seated_infant_ages": [0], "infants": 1, "infant_ages": [0] }, "flexible": true } ``` The same traveler who is already age 2 belongs in `children`, not `seated_infants`. Children aged 2–11 do not use an infant-host slot. ## Adult, senior, and young adult [#adult-senior-and-young-adult] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1, "seniors": 1, "young_adults": 1, "young_adult_ages": [16] }, "supplier_id": "duffel_1" } ``` FX-Port preserves requested logical categories in unified responses even when a supplier prices a senior or young adult as an adult internally — expect `travelerType` values `ADULT`, `SENIOR`, and `YOUNG_ADULT` respectively in `travelerPricing`. ## Cabin classes [#cabin-classes] ```json { "origin": "DXB", "destination": "BOM", "departure_date": "2026-09-15", "cabin_class": "business", "passengers": { "adults": 1 } } ``` Response offers have `itinerary[n].segments[m].cabin: "BUSINESS"`. ```json { "origin": "LHR", "destination": "JFK", "departure_date": "2026-09-15", "cabin_class": "premium_economy", "passengers": { "adults": 1 } } ``` ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "any", "passengers": { "adults": 1 } } ``` `any` sends no cabin restriction; offers can contain different cabin classes. --- # Supplier selection Source: /flights/search/suppliers Most integrations should omit supplier selectors. FX-Port filters enabled suppliers by route, traveler mix, and requested capabilities, then queries the compatible suppliers in parallel. Each successful result remains isolated under `suppliers[]`. **Amadeus suppliers are available in the sandbox** but may return errors intermittently due to Amadeus test-environment maintenance — which is outside FX-Port's control. If an Amadeus request fails in sandbox, retry or test on live. See [Sandbox notice](/sandbox) for details. ## Automatic selection (recommended) [#automatic-selection-recommended] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 } } ``` Do not send `supplier_id` or `supplier_ids`. FX-Port handles supplier priority, compatibility, and new supplier activation without requiring client changes. If one supplier fails while another returns offers, the successful results are returned without a compatibility warning. ## Exact supplier IDs (advanced) [#exact-supplier-ids-advanced] Use exact IDs only for testing or a special commercial/operational requirement. Fetch current IDs from [`GET /api/v1/suppliers`](/suppliers) instead of hard-coding the supplier catalog. ### One exact supplier [#one-exact-supplier] ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 }, "supplier_id": "duffel_1" } ``` ### Exact subset [#exact-subset] If FX-Port has 5 enabled suppliers but you only want Amadeus Algeria and Duffel: ```json { "origin": "ALG", "destination": "CDG", "departure_date": "2026-09-15", "cabin_class": "economy", "passengers": { "adults": 1 }, "supplier_ids": ["amadeus_aqc_dz_1", "duffel_1"] } ``` Strict selection never substitutes a supplier outside the requested set. Unknown, disabled, or `flexible_only`-incompatible IDs return HTTP 400. A selected supplier can still be skipped when the traveler mix is incompatible, and upstream availability can prevent it from returning offers. HTTP 400 is returned for traveler compatibility only when no selected supplier remains compatible. ## Strict-selection error [#strict-selection-error] ```json { "environment": "sandbox", "detail": { "error": "invalid_supplier_selection", "message": "One or more requested suppliers cannot be used for this flight search.", "invalidSuppliers": ["missing_1"], "disabledSuppliers": [], "unsupportedFlexibleSuppliers": [], "availableSuppliers": ["amadeus_aqc_dz_1", "duffel_1"] } } ``` Omitting selectors avoids this integration maintenance and is therefore recommended.