
Air Travel API
OperationalOne REST API sells every airline we carry: search, book, ticket, refund and reissue, paid from your FlightOTA deposit.
Version v1 · 21 endpoints
Examples use the sandbox base URL https://sandbox.flightota.com/api/v1. Sandbox bookings go to airline test systems: tickets are test tickets and the deposit is test money. Test systems are slower and sometimes refuse what live accepts; a refusal there is a real code your integration must handle. Sandbox holds you do not issue are released automatically.
Partner API — Flights
One REST API sells every airline we carry: search, book, ticket, refund and reissue, paid from your FlightOTA deposit. Every example below is a real call made on the sandbox on 22 Sep 2026.
Getting started
| Environment | Base URL | Key prefix | Use |
|---|---|---|---|
| Sandbox | https://sandbox.flightota.com/api/v1 | ak_test_ | Build and certify. Test airline systems, test tickets, test deposit. |
| Production | https://api.flightota.com/api/v1 | ak_live_ | Real bookings and real money. Switched on after certification. |
1. Get a key. In the agent portal, open API access, buy the API plan and issue a sandbox key. The secret is shown once; store it server-side only.
2. Whitelist your server. A key only works from the public IP addresses you add under API access (up to 5 on a live key, 20 on a sandbox key). Never call the API from a browser or mobile app.
3. Send two headers on every request. No login call, no token.
X-Api-Key: ak_test_your_key_id
X-Api-Secret: <your secret>
Accept: application/json
Content-Type: application/json4. Call ping first. It tells you which agency and key you are, and the IP we saw, which is the address to whitelist if you get a 403.
GET /api/v1/ping{
"ok": true,
"partner": "PTR-000020",
"key_id": "ak_test_your_key_id",
"environment": "test",
"from_ip": "203.0.113.10",
"server_time": "2026-09-22T13:25:02+00:00"
}Conventions
- Money is always
{"amount": <integer minor units>, "currency": "BDT"}.6863200BDT is 68,632.00 BDT. Never a float, never a string. A key can take them in currency units instead (15161.94): switch Amounts on the key on the API access page; prices you send back (expected_total,accept_total) are then read in currency units too. - Currency is your account's billing currency. Every offer, booking and refund is in it; there is no currency parameter.
- Flight times (
departs_at,arrives_at) are local to the airport,YYYY-MM-DDTHH:MM:SSwith no offset.departs_at_utcis given when the airline supplies it, elsenull. All other times (expires_at,created_at,hold_expires_at) are UTC, RFC 3339. - Unknown is null.
refundable: nullmeans the airline did not say, not "no".pieces: 0means no bag;pieces: nullmeans not stated. conditions_on_request: trueon an offer means its refund and change terms are read from the airline's filed rules when you price it: the offer thatPOST /flights/offers/{offer_id}/pricereturns carries them inconditions, and its fare rules give them in full.- IDs are ours:
srh_…search,ofr_…offer,TFL-XXXXXXXXbooking (bookings made before 28 Sep 2026 keep theirFL-reference). The airline's own record locator is returned asairline_reference(on an NDC booking the airline's order reference, which is its record locator;"NDC"until there is one); for a booking held in a GDS, its record locator there isgds_reference(elsenull). - Ignore fields you do not know. New fields are added without notice; nothing is removed or changes meaning within v1.
- Errors always look like
{"error": {"code": "offer_expired", "message": "…"}}. Branch oncode;messageis for people. - Rate limit is per key and set by your plan. Over it, you get
429 rate_limited; wait a minute.
Booking flow
A booking takes five calls: search, confirm the price, read what to send, book, issue. Most fares are held first and issued later from your deposit. Fares marked instant_ticket: true are paid and ticketed in the booking call itself.
| Step | Call | Required | What you get |
|---|---|---|---|
| 1 | POST /flights/search | Yes | Offers for one-way, round trip or multi-city |
| 2 | POST /flights/offers/{offer_id}/price | Optional (booking re-prices anyway) | The airline's price now |
| 3 | GET /flights/offers/{offer_id}/extras | Recommended | Who to send, in what order; seats, bags, meals |
| 4 | POST /flights/bookings | Yes | A held booking (or a ticketed one for instant fares) |
| 5 | POST /flights/bookings/{booking_ref}/issue | Yes, before hold_expires_at | Tickets, paid from your deposit |
| After | /refund, /void, /reissue, /cancel | When needed | See After-sales |
Booking states
| state | Meaning | What you can do |
|---|---|---|
pending | Accepted, the airline has not confirmed yet | Read it again |
held | Seats confirmed, not paid; hold_expires_at set | Issue, or cancel |
ticketing | Issue requested, the airline is still issuing | Read it again; it becomes ticketed or rejected |
ticketed | Tickets issued, tickets[] filled | Refund, void, reissue |
rejected | The airline declined to issue; nothing charged | Book again |
expired | The hold lapsed unpaid. We release holds ourselves a few minutes before hold_expires_at (5 by default) | Book again |
cancelled | Cancelled or voided | Nothing |
There are no webhooks yet. GET /flights/bookings/{booking_ref} is always the source of truth: read it after an issue that returned ticketing, and before acting on a booking you have not touched for a while.
Search
POST /flights/search shops every airline source at once and answers in 5–30 seconds. The trip type is simply the number of slices: 1 is one-way, 2 is round trip, 3 to 6 is multi-city. Offers stay bookable for 15 minutes (expires_at).
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
slices[] | array, 1–6 | Yes | One per flight direction, in travel order |
slices[].origin / destination | IATA airport or city code | Yes | DAC, DXB |
slices[].departure_date | YYYY-MM-DD | Yes | Today or later |
passengers.adults | integer 1–9 | Yes | 12 years and over |
passengers.children | integer 0–8 | No | 2–11 years on the travel date |
passengers.infants | integer 0–8 | No | Under 2, on an adult's lap; not more than adults |
cabin | economy | premium_economy | business | first | No | Default economy |
fares | standard | branded | No | standard (default): the cheapest fare per itinerary. branded: every fare family (Saver, Classic, Flex…) as its own offer |
filters.nonstop | boolean | No | Direct flights only |
filters.carriers[] | airline codes | No | Only these airlines |
filters.exclude_carriers[] | airline codes | No | Never these airlines |
filters.max_offers | integer 1–200 | No | Default 50. Caps each source's results, so the combined list can be longer |
One-way (1 adult, 1 child, 1 infant)
{
"slices": [{"origin": "DAC", "destination": "DXB", "departure_date": "2026-10-22"}],
"passengers": {"adults": 1, "children": 1, "infants": 1},
"cabin": "economy",
"fares": "standard",
"filters": {"max_offers": 20}
}Sandbox result: 48 offers in 30 s.
Round trip (2 adults, 1 child, all fare families)
{
"slices": [
{"origin": "DAC", "destination": "DXB", "departure_date": "2026-10-22"},
{"origin": "DXB", "destination": "DAC", "departure_date": "2026-10-29"}
],
"passengers": {"adults": 2, "children": 1, "infants": 0},
"cabin": "economy",
"fares": "branded"
}Sandbox result: 121 offers in 18 s. Each offer holds both directions, one slices[] entry each, at one price.
Multi-city (1 adult)
{
"slices": [
{"origin": "DAC", "destination": "DXB", "departure_date": "2026-10-22"},
{"origin": "DXB", "destination": "DOH", "departure_date": "2026-10-29"},
{"origin": "DOH", "destination": "DAC", "departure_date": "2026-11-02"}
],
"passengers": {"adults": 1},
"cabin": "economy"
}Sandbox result: 59 offers in 7 s, each ticketed as one itinerary. Some sources sell one-way and round trip only, so multi-city returns fewer airlines.
Response
A real sandbox offer (one-way above, Qatar Airways via Doha). Long arrays are cut to one entry where marked.
{
"search_id": "srh_01m34mpemkpjehfke5rdpwrd8q",
"expires_at": "2026-09-22T13:40:45Z",
"currency": "BDT",
"fares": "standard",
"passengers": {"adults": 1, "children": 1, "infants": 1},
"offers": [
{
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"total": {"amount": 9440600, "currency": "BDT"},
"price": {
"base": {"amount": 6417600, "currency": "BDT"},
"taxes": {"amount": 3023000, "currency": "BDT"},
"fees": {"amount": 0, "currency": "BDT"},
"service_fee": {"amount": 0, "currency": "BDT"}
},
"passenger_prices": [
{
"type": "adult",
"code": "ADT",
"count": 1,
"base": {"amount": 3375700, "currency": "BDT"},
"taxes": {"amount": 1469200, "currency": "BDT"},
"fees": {"amount": 0, "currency": "BDT"},
"each": {"amount": 4844900, "currency": "BDT"},
"subtotal": {"amount": 4844900, "currency": "BDT"}
},
{
"type": "child",
"code": "CHD",
"count": 1,
"each": {"amount": 3804100, "currency": "BDT"},
"subtotal": {"amount": 3804100, "currency": "BDT"}
},
{
"type": "infant",
"code": "INF",
"count": 1,
"each": {"amount": 791600, "currency": "BDT"},
"subtotal": {"amount": 791600, "currency": "BDT"}
}
],
"passengers": {"adults": 1, "children": 1, "infants": 1},
"brand": {
"code": "ECONOMY_CLASSIC",
"name": "ECONOMY CLASSIC",
"tier": null,
"attributes": [
{"classification": "CheckedBag", "inclusion": "Included"},
{"classification": "Refund", "inclusion": "Chargeable"}
]
},
"baggage": {"checked": {"pieces": null, "weight_kg": 25}, "cabin": {"pieces": 1, "weight_kg": 7}},
"baggage_by_passenger": {
"adult": {
"checked": {"pieces": null, "weight_kg": 25},
"cabin": {"pieces": 1, "weight_kg": 7}
},
"child": {
"checked": {"pieces": null, "weight_kg": 25},
"cabin": {"pieces": 1, "weight_kg": 7}
}
},
"conditions": {
"refundable": true,
"changeable": true,
"refund_fee": {"amount": 1731100, "currency": "BDT"},
"change_fee": {"amount": 989200, "currency": "BDT"}
},
"ticketing_deadline": null,
"instant_ticket": false,
"carrier": "QR",
"seats_available": 9,
"itinerary_key": "itn_4267f9d93e7870fb",
"slices": [
{
"origin": "DAC",
"destination": "DXB",
"duration_minutes": 1095,
"baggage": {
"checked": {"pieces": null, "weight_kg": 25},
"cabin": {"pieces": 1, "weight_kg": 7}
},
"segments": [
{
"origin": "DAC",
"destination": "DOH",
"origin_terminal": "1",
"destination_terminal": null,
"departs_at": "2026-10-22T11:10:00",
"departs_at_utc": "2026-10-22T05:10:00Z",
"arrives_at": "2026-10-22T13:50:00",
"arrives_at_utc": "2026-10-22T10:50:00Z",
"marketing_carrier": "QR",
"operating_carrier": "QR",
"flight_number": "641",
"aircraft": "77W",
"cabin": "economy",
"booking_class": "O",
"duration_minutes": 340,
"seats_available": 9
},
{
"origin": "DOH",
"destination": "DXB",
"departs_at": "2026-10-23T01:05:00",
"arrives_at": "2026-10-23T03:25:00",
"marketing_carrier": "QR",
"operating_carrier": "QR",
"flight_number": "1002",
"aircraft": "77W",
"cabin": "economy",
"booking_class": "O",
"duration_minutes": 80
}
]
}
]
}
]
}Reading the offer
totalis what your deposit pays.sum(passenger_prices[].subtotal) + price.service_fee == total.sellappears only if you set a markup or commission rule in the portal:sell.totalis your selling price,totalstays your cost.instant_ticket: truemeans the airline will not hold this fare: booking it pays and tickets at once, so your deposit must cover it.itinerary_keyis shared by offers on the same flights. Withfares: branded, group on it to show one row per itinerary with its fare families behind it.brandisnullfor unbranded fares.brand.attributeslists what the family includes; depending on the airline source each entry is either an object (classification,inclusion, sometimesname) or a plain text line, so accept both.- Baggage is given per segment, per slice and per offer. The slice and offer figures are the smallest allowance across the legs below them. Show
weight_kgwhenpiecesisnull; most allowances are weight-only. ticketing_deadlineis a forecast at search time; the real deadline is the booking'shold_expires_at.- Zero offers means nothing was found for these dates; a search again later may find more. When no fares can be fetched at all the answer is
503 supplier_unavailable.
GET /flights/search/{search_id} returns the same search again while it is valid. An expired or unknown id answers 410 offer_expired.
Offer details
Four read-only calls on one offer_id before you book. None of them reserves anything.
| Call | Purpose |
|---|---|
GET /flights/offers/{offer_id} | The offer again, same shape as in search, as {"offer": {…}} |
POST /flights/offers/{offer_id}/price | The airline confirms the price now |
GET /flights/offers/{offer_id}/rules | The fare rules as text |
GET /flights/offers/{offer_id}/extras | Who the booking must name, and the seats, bags and meals on sale |
Confirm the price
POST /flights/offers/{offer_id}/price, no body. Optional: booking re-prices anyway, but this lets you show a confirmed total before collecting passenger details.
{
"status": "confirmed",
"offer": {
"offer_id": "ofr_01m34mpemkpjehfke5rdpwrd8r",
"total": {"amount": 6863200, "currency": "BDT"},
"…": "the full offer"
}
}If the airline's price moved, you get the offer at its new price under the same offer_id. Booking that id books the new price.
{
"error": {
"code": "price_changed",
"message": "The airline has changed the price. The offer below carries the new price; book it to accept."
},
"offer": {"offer_id": "ofr_…", "total": {"amount": 6990000, "currency": "BDT"}, "…": "…"}
}410 offer_expired means search again; 409 offer_unavailable means the airline no longer sells the fare.
Fare rules
GET /flights/offers/ofr_01m34mpemkpjehfke5rdpwrd8r/rules{
"rules": [
{
"title": "Changes",
"text": "Before departure: not permitted.\nAfter departure: not permitted."
},
{
"title": "Refunds",
"text": "Before departure: not permitted.\nAfter departure: not permitted."
},
{
"title": "Fare family — Economy Basic",
"text": "Included: 05 Percent Miles Earned, Hand Bag, Food And Beverage.\nFor a fee: Standard Seat Selection, …"
}
]
}An empty rules list means the airline gave none; conditions on the offer still applies.
Extras and what to send
GET /flights/offers/{offer_id}/extras. Call it before building the passenger form: travellers is the exact list the booking must send, in this order and of these types.
{
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"travellers": [
{"position": 1, "type": "adult"},
{"position": 2, "type": "child"},
{"position": 3, "type": "infant"}
],
"travel_dates": {"first": "2026-10-22", "last": "2026-10-23"},
"infant_min_days": 7,
"instant_ticket": false,
"passenger_requests": {"frequent_flyer": false, "wheelchair": true, "wheelchair_price": null},
"extras": {
"baggage": [
{
"id": "anc_01m34…",
"label": "Extra bag 20 kg (DAC–DXB)",
"weight_kg": 20,
"price": {"amount": 450000, "currency": "BDT"}
}
],
"seats": [
{
"origin": "DAC",
"destination": "DOH",
"carrier": "QR",
"flight_number": "641",
"all_or_none": false,
"columns": ["A", "B", "C", "D", "E", "F", "G", "H", "J", "K"],
"rows": [
{
"row": "10",
"seats": [
{
"id": "anc_01m34n6ehrp4y2xd2b88xznwej",
"name": "10A",
"row": "10",
"column": "A",
"available": true,
"characteristics": ["chargeable", "bulkhead", "window"],
"price": {"amount": 0, "currency": "BDT"}
}
]
}
]
}
],
"meals": [
{
"origin": "DAC",
"destination": "DOH",
"carrier": "QR",
"flight_number": "all flights",
"options": [
{
"id": "anc_01m34mqcj8cxr9j4xv1drv4q9a",
"code": "BBML",
"label": "Baby meal",
"price": {"amount": 0, "currency": "BDT"}
}
]
}
]
}
}(The baggage entry is illustrative; the others are the sandbox's real answer for this Qatar Airways offer.)
travel_datesandinfant_min_daysset the age rules: a child must be 2–11 and an infant under 2 (and at leastinfant_min_daysold) on the travel dates.passenger_requestssays whether the airline takes a frequent-flyer number and a wheelchair request.wheelchair_priceis set when the airline charges for it; the charge is added to the booking total.- To buy an extra, send
"<id>:<position>"in the booking'sextras[], e.g."anc_01m34n6ehrp4y2xd2b88xznwej:1"gives seat 10A to traveller 1. Extra ids belong to this offer only. all_or_none: truemeans the airline seats everyone on that flight or no one: choose a seat for every non-infant traveller, or none.- An empty list means nothing is on sale for that kind, which is common.
Book
POST /flights/bookings reserves the seats. The airline re-prices the offer first; your request names the offer, never a price. Most fares come back held with a hold_expires_at; an instant_ticket fare is paid from your deposit and comes back ticketed.
Send an Idempotency-Key header, unique per booking attempt (your order number works). If your call times out, send the same request with the same key: you get the booking that was made, never a second one. Reusing a key with a different body answers 409 duplicate_request.
Request fields
| Field | Required | Notes |
|---|---|---|
offer_id | Yes | From search, within 15 minutes |
travellers[] | Yes | Exactly the list from /extras, same order and type |
travellers[].type | Yes | adult | child | infant |
travellers[].title | Yes | MR, MS, MRS for adults; MSTR, MISS for children and infants |
travellers[].given_name, family_name | Yes | As on the passport; letters, spaces and hyphens only |
travellers[].gender | Yes | M | F |
travellers[].date_of_birth | Yes | YYYY-MM-DD; must fit the type on the travel dates |
travellers[].nationality | Yes | ISO country code, BD |
travellers[].document.number, issuing_country, expires_on | Yes | Passport; must not have expired |
travellers[].frequent_flyer_number | No | Sent to the airline only when /extras says it takes one; otherwise the booking goes ahead without it and says so in warnings |
travellers[].frequent_flyer_carrier | No | The programme's airline code (e.g. EK) when it is not the flight's airline. loyalty: {"carrier": "EK", "number": "…"} is accepted as well |
travellers[].wheelchair | No | { "requested": true, "remark": "Cannot walk long distances" }. Same rule as the frequent-flyer number |
contact.email, contact.dial_code, contact.mobile | Yes | The passenger contact the airline receives. dial_code without + or with it |
extras[] | No | "<extra id>:<traveller position>" from /extras. Every one is checked before anything is booked: see extras_invalid below |
client_reference | No | Your own reference, up to 64 characters. Returned on the booking and searchable |
Request
The sandbox call that made booking TFL-Y678QNFB:
POST /api/v1/flights/bookings
Idempotency-Key: docs-demo-003{
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"travellers": [
{
"type": "adult",
"title": "MR",
"given_name": "ARIF",
"family_name": "RAHMAN",
"gender": "M",
"date_of_birth": "1988-05-12",
"nationality": "BD",
"document": {
"type": "passport",
"number": "A01234567",
"issuing_country": "BD",
"expires_on": "2032-09-22"
}
},
{
"type": "child",
"title": "MSTR",
"given_name": "AYAN",
"family_name": "RAHMAN",
"gender": "M",
"date_of_birth": "2018-09-22",
"nationality": "BD",
"document": {
"type": "passport",
"number": "A07654321",
"issuing_country": "BD",
"expires_on": "2032-09-22"
}
},
{
"type": "infant",
"title": "MISS",
"given_name": "ANAYA",
"family_name": "RAHMAN",
"gender": "F",
"date_of_birth": "2025-10-22",
"nationality": "BD",
"document": {
"type": "passport",
"number": "A09876543",
"issuing_country": "BD",
"expires_on": "2032-09-22"
}
}
],
"contact": {"email": "ops@agency.example", "dial_code": "880", "mobile": "1711000000"},
"client_reference": "DOCS-DEMO-003"
}Response 201 Created
The same shape is returned by every booking call below.
{
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "held",
"created_at": "2026-09-22T13:27:52Z",
"hold_expires_at": "2026-09-22T21:27:00Z",
"ticketed_at": null,
"client_reference": "DOCS-DEMO-003",
"airline_reference": "7EAQKO",
"gds_reference": "HN8TYB",
"validating_carrier": "QR",
"route": "DAC → DXB",
"total": {"amount": 9440600, "currency": "BDT"},
"price": {
"base": {"amount": 6417600, "currency": "BDT"},
"taxes": {"amount": 3023000, "currency": "BDT"},
"fees": {"amount": 0, "currency": "BDT"}
},
"travellers": [
{
"position": 1,
"type": "adult",
"title": "MR",
"given_name": "ARIF",
"family_name": "RAHMAN",
"date_of_birth": "1988-05-12",
"ticket_number": null,
"frequent_flyer": {"number": "EK123456", "carrier": "EK"},
"wheelchair": null
},
{
"position": 2,
"type": "child",
"title": "MSTR",
"given_name": "AYAN",
"family_name": "RAHMAN",
"date_of_birth": "2018-09-22",
"ticket_number": null,
"frequent_flyer": null,
"wheelchair": {"requested": true, "remark": "Cannot climb stairs"}
},
{
"position": 3,
"type": "infant",
"title": "MISS",
"given_name": "ANAYA",
"family_name": "RAHMAN",
"date_of_birth": "2025-10-22",
"ticket_number": null,
"frequent_flyer": null,
"wheelchair": null
}
],
"extras": [
{
"kind": "seat",
"label": "Seat 10A",
"traveller": 1,
"price": {"amount": 0, "currency": "BDT"},
"added_after_booking": false
},
{
"kind": "baggage",
"label": "Checked bag 20 kg · DAC–DXB",
"traveller": 1,
"price": {"amount": 450000, "currency": "BDT"},
"added_after_booking": false
}
],
"tickets": [],
"slices": [
{
"origin": "DAC",
"destination": "DXB",
"duration_minutes": 1095,
"segments": ["… as in the offer …"]
}
],
"rejection_reason": null,
"after_sales": []
},
"warnings": []
}travellers[].frequent_flyerandtravellers[].wheelchairare what the airline was sent;nullmeans nothing was.extras[]lists the seats, bags and meals on the booking, and a charged wheelchair (kind: "assistance"), each with its traveller and price. Their total is insideprice.fees.added_after_bookingmarks those bought later.warnings[](on the 201 only) names anything in your request the airline does not take, which was therefore not sent:{ "traveller": 2, "field": "wheelchair", "code": "not_sent", "message": "…" }. The booking itself went ahead.
The same request with the same Idempotency-Key answered 200 OK with this booking, and nothing more was reserved.
When booking is refused
Nothing is reserved or charged in any of these.
Duplicate and repeated bookings. Airlines charge the ticketing agency for a passenger booked twice on the same flight, or booked and cancelled over and over. Before a booking reaches the airline, it is checked against the bookings already made for the same passenger. The same passenger means any two of name, date of birth and passport number agree. Bookings made through other channels count too. A passenger who already holds a booking on the flight (or on the same route and day) gets duplicate_booking; cancel that booking first. Some airlines also cap how many times one passenger may be booked on a flight, cancellations included, and past that cap the answer is booking_limit_reached. Sending date of birth and passport number for every traveller keeps these checks accurate. Where most of an account's recent bookings with an airline ended without a ticket, new holds with that airline may be paused for the rest of the day (churn_limit_reached); fares ticketed at once are still sold.
Made-up names, repeated passengers and daily limits. Airlines also charge for bookings in names no passport carries, for the same person twice on one booking, and for too many bookings in a day. A live booking with a name such as TEST, DUMMY, XXX, one letter repeated (AAA) or initials only (A B) is refused with passenger_name_refused; initials beside a name (K M RAHMAN) and short names (MD) are fine. Sandbox bookings, made on airline test systems, are not checked for names. Two travellers in the same request who are the same person (any two of name, date of birth and passport number agree) get duplicate_passenger. Some airlines limit how many bookings an agency may make with them in one calendar day (Asia/Dhaka); past it the answer is hold_limit_reached until the next day. error.message says which passenger or airline it is.
| HTTP | error.code | What to do |
|---|---|---|
| 400 | idempotency_key_required | Add the header |
| 422 | validation_failed | Fix the fields listed in errors |
| 410 | offer_expired | Search again |
| 409 | price_changed | The response carries offer at the new price; book again to accept |
| 409 | offer_unavailable | The fare is gone; pick another offer |
| 409 | hold_unavailable | The airline will not hold this fare; it is now instant_ticket: true. Booking again pays and tickets at once |
| 402 | insufficient_funds | An instant fare your deposit does not cover; balance and shortfall are included |
| 409 | duplicate_booking | A passenger already holds a booking on this flight (or the same route and day), with us or refused by the airline. Cancel it before booking again; booking_ref names it when it is yours |
| 409 | booking_limit_reached | A passenger has been booked (and cancelled) on this flight as many times as the airline allows. Nothing was reserved |
| 409 | churn_limit_reached | Most of your recent bookings with this airline ended without a ticket, so new holds with it are paused until tomorrow. Nothing was reserved; an instant-ticket fare can still be booked |
| 422 | passenger_name_refused | A passenger name looks made up (TEST, AAA, initials only). Send every name exactly as printed on the passport |
| 422 | duplicate_passenger | The same person is on the booking twice. Send each passenger once |
| 409 | hold_limit_reached | Your agency has made as many bookings with this airline today as it allows. Book again tomorrow (Asia/Dhaka day) |
| 422 | frequent_flyer_invalid | The airline rejected a frequent-flyer number |
| 422 | document_required | The route needs complete passport details |
| 422 | extras_invalid | An extra cannot be booked as sent. problems[] names each: { "extra": "anc_…:2", "reason": "…", "message": "…" }. Reasons: unknown (not an id from this offer's /extras), no_such_traveller, not_for_traveller (e.g. a seat for an infant), seat_unavailable, seat_taken_twice, two_seats_one_flight, all_or_none |
| 410 | extras_expired | The extras you read have expired. Call /extras again and book with the new ids |
| 422 | wheelchair_unavailable | The airline offers no wheelchair assistance on these flights. Book without it and ask the airline, or pick another offer |
| 409 | request_in_progress | The same key is still being booked; read it back shortly |
| 409 | booking_failed | The airline declined; try again or pick another offer |
Extras after booking
Bags, meals and seats the airline still sells on a booking that is held or ticketed. Not every airline sells extras after booking; available: false says so.
| Call | Purpose |
|---|---|
GET /flights/bookings/{booking_ref}/extras | What can be added now, each with its id and price |
POST /flights/bookings/{booking_ref}/extras | Buy them from your deposit. Needs an Idempotency-Key |
GET /flights/bookings/TFL-Y678QNFB/extras{
"booking_ref": "TFL-Y678QNFB",
"available": true,
"travellers": [{"position": 1, "type": "adult"},{"position": 2, "type": "child"}],
"extras": {
"baggage": [
{
"id": "anc_01m3…",
"label": "Checked bag 10 kg",
"weight_kg": 10,
"price": {"amount": 250000, "currency": "BDT"}
}
],
"seats": [],
"meals": []
}
}POST /api/v1/flights/bookings/TFL-Y678QNFB/extras
Idempotency-Key: po-4471-extras-1{"extras": ["anc_01m3…:1"]}201 Created answers { "status": "added", "charged": { "amount": 250000, "currency": "BDT" }, "added_to_total": { "amount": 250000, "currency": "BDT" }, "pay_at_issue": false, "booking": { … } } with the extras in booking.extras. The same request with the same key answers 200 with the same body and buys nothing more.
On a ticketed booking the extras are paid from your deposit at once (charged). On a held booking nothing is taken now: charged is zero, pay_at_issue is true, and added_to_total is added to booking.total, which you pay once when you issue.
| HTTP | error.code | What to do |
|---|---|---|
| 422 | extras_invalid | An id is not on sale for this booking now (unknown[] lists them); read the extras again |
| 402 | insufficient_funds | Top up; balance and shortfall are included |
| 409 | extras_refused | The airline did not add them; nothing was charged |
| 409 | extras_not_available | Not held or ticketed, a request is open on it, or the airline sells none after booking |
Ticket, read and cancel
Issue tickets
POST /flights/bookings/{booking_ref}/issue pays a held booking from your deposit and tickets it. Send an Idempotency-Key. Issue before hold_expires_at; we release unpaid holds a few minutes before it (5 by default), so the airline never lets one lapse unticketed.
We ask the airline first whether the reservation still stands at the agreed price. If the fare changed, the call is refused with the new price and nothing is charged; to accept, call again with that exact figure:
{"accept_fare_change": true, "expected_total": {"amount": 9512300, "currency": "BDT"}}The sandbox call that ticketed TFL-Y678QNFB, body {}, 21 seconds:
{
"status": "ticketed",
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "ticketed",
"ticketed_at": "2026-09-22T13:28:45Z",
"airline_reference": "7EAQKO",
"gds_reference": "HN8TYB",
"total": {"amount": 9440600, "currency": "BDT"},
"tickets": [
{"position": 1, "number": "1572134359166", "issued_at": "2026-09-22T13:28:45Z"},
{"position": 2, "number": "1572134359165", "issued_at": "2026-09-22T13:28:45Z"},
{"position": 3, "number": "1572134359167", "issued_at": "2026-09-22T13:28:45Z"}
],
"…": "the rest of the booking, with ticket_number on each traveller"
}
}| HTTP | Answer | Meaning |
|---|---|---|
| 200 | status: ticketed | Tickets issued; tickets[] filled. Also the answer when you repeat the call |
| 202 | status: ticketing | Paid; the airline is still issuing. Read the booking until ticketed or rejected |
| 409 | fare_changed | Carries new_total; send accept_fare_change with expected_total to accept |
| 402 | insufficient_funds | Top up; balance and shortfall included. Nothing charged |
| 409 | hold_invalid | The airline no longer holds the seats; book again |
| 409 | ticketing_rejected | The airline declined; the amount is returned to your deposit |
| 409 | invalid_state | Not a held booking (for example expired or cancelled) |
| 503 | supplier_unavailable | Nothing charged; retry |
Read one booking
GET /flights/bookings/{booking_ref} returns {"booking": {…}} in the shape shown under Book. In place of the booking_ref you may send the booking's airline_reference (the airline PNR) or gds_reference; if a PNR was booked again after a release, the live booking is returned, else the newest. A reference that is not yours answers 404 booking_not_found. Issuing, cancelling and after-sales take the booking_ref only.
List bookings
GET /flights/bookings, newest first, 25 a page.
| Query parameter | Notes |
|---|---|
state | held, ticketed, cancelled… |
client_reference | Your own reference |
created_from, created_to | YYYY-MM-DD |
page, per_page | per_page up to 100 |
GET /api/v1/flights/bookings?state=held&per_page=5{
"bookings": [{"booking_ref": "TFL-Y678QNFB", "state": "held", "…": "…"}],
"page": 1,
"per_page": 5,
"total": 1,
"last_page": 1
}Cancel a hold
POST /flights/bookings/{booking_ref}/cancel releases held seats; nothing is charged. Body optional: {"reason": "Customer changed plans"}. A ticketed booking is not cancelled; void or refund it.
{"status": "cancelled", "booking": {"booking_ref": "TFL-…", "state": "cancelled", "…": "…"}}If the airline does not confirm the release, the booking stays held and you get 503 supplier_unavailable; retry, or let it lapse. Some airline test systems never release unpaid holds; on the sandbox those lapse on their own.
After-sales: void, refund, reissue
Each is a request on a ticketed booking, one open at a time. Where the airline answers by itself, it quotes at once and an accepted quote is settled on the spot. Where it does not, our desk handles it and quotes by hand. Either way the request sits in the booking's after_sales[]; read the booking to follow it.
The request moves through these states:
| From | To | When |
|---|---|---|
requested | quoted | The airline or our desk quotes |
requested | in_progress | Our desk is working on it |
in_progress | quoted | Our desk quotes |
quoted | quote_approved | You accept with POST /quote |
quoted | quote_rejected | You decline with POST /quote |
quoted | quote_expired | Not answered in time |
quote_approved | completed | Settled with the airline |
requested | completed | A void done at once or by our desk |
requested | rejected | Not possible |
The request object, as it appears in responses and in booking.after_sales[]:
{
"request_id": "01a0c94e-618a-71c6-81bc-19d403d07a93",
"kind": "void",
"status": "requested",
"mode": "manual",
"created_at": "2026-09-22T13:29:12Z",
"quote": null,
"quote_note": null,
"quote_expires_at": null,
"settled": null
}kind is void, refund or reissue. mode is auto (the airline answered) or manual (our desk). quote is what reaches your deposit for a refund, or what is charged for a reissue. settled is the final figure once completed.
Refund quote (changes nothing)
GET /flights/bookings/{booking_ref}/refund-quote
{
"available": true,
"refund": {"amount": 2200000, "currency": "BDT"},
"penalties": {"amount": 800000, "currency": "BDT"},
"service_charge": {"amount": 100000, "currency": "BDT"},
"net": {"amount": 2100000, "currency": "BDT"},
"tickets": ["1572134359166"]
}refund is what the airline returns, penalties what it keeps, service_charge our handling fee, net what reaches your deposit. When the airline cannot quote by itself you get {"available": false}, and the refund goes to our desk, which quotes it by hand.
Refund
POST /flights/bookings/{booking_ref}/refund
{"mode": "auto", "reason": "Trip cancelled"}auto(default): the airline quotes, we accept for you and settle; the answer'srequest.statusiscompletedandnetis in your deposit. If the airline will not answer, the request falls back to our desk (mode: manual,status: requested).manual: straight to our desk. We quote; you accept or decline withPOST /quote.
Answer 202 Accepted: { "request": {…}, "booking": {…} }.
Void
POST /flights/bookings/{booking_ref}/void, body optional {"reason": "…"}. A void cancels the tickets inside the airline's void window, which ends at 23:59 on the day of issue (Asia/Dhaka, UTC+06:00 time). The option closes 5 minutes before that; a later void answers 409 void_window_closed, and a refund is the way instead.
- Voided at once. Where the airline voids by itself, the tickets are voided in the same call and the fare less our void charge returns to your deposit:
request.modeisauto,request.statusiscompletedand the booking iscancelled. - Voided by our desk. Otherwise our desk voids the tickets with the airline and returns the amount:
request.modeismanualandrequest.statusisrequesteduntil it is done.
The sandbox void on TFL-Y678QNFB, handled by the desk:
{
"request": {
"request_id": "01a0c94e-618a-71c6-81bc-19d403d07a93",
"kind": "void",
"status": "requested",
"mode": "manual",
"…": "…"
},
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "ticketed",
"after_sales": [{"kind": "void", "status": "requested", "…": "…"}],
"…": "…"
}
}When the desk finishes, the booking becomes cancelled and the request completed, with settled set.
Reissue (date change)
1. Search new flights. POST /flights/bookings/{booking_ref}/reissue/search: the traveller positions moving and one date per leg of the booking (a round trip needs two dates).
{"travellers": [1,2,3], "dates": ["2026-10-29"]}{
"exchanges": false,
"travellers": [1,2,3],
"dates": ["2026-10-29"],
"offers": [
{
"offer_id": "ofr_01m34mwr1a0zywbzyex186g74n",
"carrier": "QR",
"total": {"amount": 9242700, "currency": "BDT"},
"slices": ["…"],
"…": "offer shape as in search"
}
]
}Sandbox: 15 offers for TFL-Y678QNFB. exchanges: true means the airline reissues by itself and the offers are its exchange fares; false means the offers are the airline's current fares and our desk does the reissue.
2. Ask for the reissue within 30 minutes: POST /flights/bookings/{booking_ref}/reissue
{
"offer_id": "ofr_01m34mwr1a0zywbzyex186g74n",
"mode": "auto",
"remark": "Passenger moves to 29 Oct"
}With exchanges: true the airline quotes at once: request.status is quoted and request.quote is the total charge (fare difference, airline change fee and our service charge; quote_note itemises it). Otherwise our desk quotes.
3. Accept or decline before quote_expires_at: POST /flights/bookings/{booking_ref}/quote
{"accept": true}Accepting a reissue charges your deposit and, when the airline quoted it, reissues on the spot: request.status becomes completed and the booking carries the new ticket numbers and flights. {"accept": false, "reason": "…"} declines and the tickets stay as they are. 402 insufficient_funds means top up and accept again before the quote expires.
After-sales errors
| HTTP | error.code | Meaning |
|---|---|---|
| 409 | invalid_state | Only a ticketed booking can be voided, refunded or reissued |
| 409 | void_window_closed | The void window has closed; request a refund instead |
| 409 | request_open | One request at a time; wait for the open one |
| 422 | special_fare | This fare cannot be refunded or changed (for example a group fare) |
| 409 | no_quote | Nothing is waiting for an answer |
| 409 | quote_expired | Ask again for a fresh quote |
| 409 | not_settled | The airline quoted but settlement failed; our desk has it |
Deposit balance and updates
Tickets, instant fares, accepted reissue quotes and paid extras are taken from your FlightOTA deposit; refunds and voids are paid back into it. Top up in the agent portal under Deposit.
GET /account/balance
{
"partner_code": "PTR-000020",
"currency": "BDT",
"balance": {"amount": 20000000, "currency": "BDT"}
}Check it before issuing large bookings; an issue your deposit cannot cover answers 402 insufficient_funds and charges nothing.
Webhooks are not available yet. Poll GET /flights/bookings/{booking_ref}:
- after an issue that answered
202 ticketing: every 30 seconds untilticketedorrejected; - for an open after-sales request: every few minutes, or when your user opens the booking;
- for held bookings: before
hold_expires_at, so your users can issue in time.
Errors
Every error has the same envelope. Branch on error.code; show error.message to people if you like, but it may be reworded. Validation errors add errors, keyed by field.
{
"error": {
"code": "validation_failed",
"message": "The request is malformed; see errors for the fields."
},
"errors": {
"travellers.1.type": ["This traveller must be of the type the offer lists at this position."]
}
}Access
| HTTP | error.code | Fix |
|---|---|---|
| 401 | unauthenticated | Headers missing, or key or secret wrong |
| 401 | credential_inactive | The key was revoked or expired; issue a new one |
| 403 | ip_whitelist_required | Add your server's public IP to the key |
| 403 | ip_not_whitelisted | This server's IP is not on the key; GET /ping from it shows the IP |
| 403 | product_not_allowed | The key is not enabled for flights |
| 403 | partner_inactive | Your agency account is suspended or pending |
| 403 | subscription_required, subscription_pending, subscription_suspended, subscription_cancelled | API plan not active; see API access in the portal |
| 403 | environment_not_activated | Live keys work after certification |
| 429 | rate_limited | Slow down; wait a minute |
All flight codes
| HTTP | error.code | Where | Meaning |
|---|---|---|---|
| 400 | idempotency_key_required | book, issue, extras | Send the header |
| 422 | validation_failed | any | See errors |
| 410 | offer_expired | offers, book | Search again |
| 409 | offer_unavailable | offers, book | The airline no longer sells the fare |
| 409 | price_changed | price, book | New price attached as offer |
| 409 | hold_unavailable | book | Fare is now instant-ticket only |
| 402 | insufficient_funds | book, issue, quote | Deposit too low; nothing charged |
| 409 | duplicate_booking | book | Same passenger already booked on the flight (or route and day); booking_ref when it is yours |
| 409 | booking_limit_reached | book | Same passenger booked on the flight too many times (cancellations count) |
| 409 | churn_limit_reached | book | Holds with this airline paused for the day after repeated bookings ended without a ticket |
| 422 | passenger_name_refused | book | A passenger name looks made up (live only) |
| 422 | duplicate_passenger | book | The same passenger twice in one booking |
| 409 | hold_limit_reached | book | The airline's daily limit of bookings for your agency is reached |
| 422 | frequent_flyer_invalid, document_required | book | Passenger data refused by the airline |
| 422 | wheelchair_unavailable | book | No wheelchair assistance on these flights |
| 422 | extras_invalid | book, extras | An extra cannot be booked as sent; see problems or unknown |
| 410 | extras_expired | book | Read /extras again and use the new ids |
| 409 | extras_refused, extras_not_available | extras | The airline did not add them, or the booking takes none now |
| 409 | booking_failed | book | Airline declined |
| 409 | duplicate_request | book | Idempotency-Key reused with a different body |
| 409 | request_in_progress | book, issue | The same request is still running |
| 404 | booking_not_found | bookings | Unknown, or not your agency's |
| 409 | fare_changed | issue | Accept with accept_fare_change + expected_total |
| 409 | hold_invalid | issue | The airline dropped the reservation |
| 409 | ticketing_rejected | issue | Airline declined; money returned |
| 409 | invalid_state | issue, cancel, after-sales | Not allowed in the booking's current state |
| 409 | request_open, no_quote, quote_expired, not_settled | after-sales | See After-sales |
| 409 | void_window_closed | void | Too late to void; refund instead |
| 422 | special_fare | after-sales | Fare cannot be refunded or changed |
| 503 | supplier_unavailable | any | The airline did not answer; nothing changed; retry |
| 503 | pricing_unavailable | search, price | Our pricing is unavailable; retry |
Retries. 503, 429 and network timeouts are safe to retry. Retry booking and issuing only with the same Idempotency-Key.
Go-live checklist
Certification is run on the sandbox. Send us the booking_ref of each item below; when they check out we switch on your live key.
GET /pingreturns your agency code from your server's whitelisted IP- One-way search with an adult, a child and an infant
- Round-trip search with
fares: branded, shown grouped byitinerary_key - Multi-city search with three slices
POST /offers/{id}/pricehandled for bothconfirmedand409 price_changed- Passenger form built from
/offers/{id}/extras(order, types, age rules, frequent flyer and wheelchair only when offered) - Hold booked with an
Idempotency-Key; the same request repeated returns the same booking warningson the booking response shown to your user;extras_invalidproblems shown per extra- Held booking issued; tickets shown per traveller
- A hold cancelled
402 insufficient_fundsand410 offer_expiredshown to your user in plain words- Refund quote read and a refund requested on a ticketed booking
- Reissue search and request on a ticketed booking; quote accepted or declined
- Money displayed from minor units with the right decimals (BDT and USD 2)
- Flight times displayed as airport local time
- Your system polls
GET /bookings/{ref}after a202 ticketing
Sandbox notes. Sandbox bookings go to airline test systems: tickets are test tickets and the deposit is test money. Test systems are slower and sometimes refuse what live accepts; a refusal there is a real code your integration must handle. Sandbox holds you do not issue are released automatically.
Questions and certification: your account manager, or support from the agent portal.
Endpoint reference
Getting started
Ping
/pingCall ping first. It tells you which agency and key you are, and the IP we saw, which is the address to whitelist if you get a 403.
Parameters
This endpoint takes no parameters.
Errors
- 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/ping" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"ok": true,
"partner": "PTR-000020",
"key_id": "ak_test_your_key_id",
"environment": "test",
"from_ip": "203.0.113.10",
"server_time": "2026-09-22T13:25:02+00:00"
}{
"error": {
"code": "unauthenticated",
"message": "Headers missing, or key or secret wrong."
}
}Search
Search flights
/flights/searchShops every airline source at once and answers in 5–30 seconds. The trip type is simply the number of slices: 1 is one-way, 2 is round trip, 3 to 6 is multi-city. Offers stay bookable for 15 minutes (expires_at).
Parameters
| Parameter | In | Description |
|---|---|---|
slices[]array, 1–6required | body | One per flight direction, in travel order. |
slices[].originIATA airport or city coderequired | body | Origin airport or city. Example: DAC |
slices[].destinationIATA airport or city coderequired | body | Destination airport or city. Example: DXB |
slices[].departure_dateYYYY-MM-DDrequired | body | Today or later. Example: 2026-10-22 |
passengers.adultsinteger 1–9required | body | 12 years and over. Example: 1 |
passengers.childreninteger 0–8optional | body | 2–11 years on the travel date. Example: 1 |
passengers.infantsinteger 0–8optional | body | Under 2, on an adult's lap; not more than adults. Example: 1 |
cabineconomy | premium_economy | business | firstoptional | body | Default economy. Example: economy |
faresstandard | brandedoptional | body | standard (default): the cheapest fare per itinerary. branded: every fare family (Saver, Classic, Flex…) as its own offer. Example: standard |
filters.nonstopbooleanoptional | body | Direct flights only. |
filters.carriers[]airline codesoptional | body | Only these airlines. |
filters.exclude_carriers[]airline codesoptional | body | Never these airlines. |
filters.max_offersinteger 1–200optional | body | Default 50. Caps each source's results, so the combined list can be longer. Example: 20 |
Errors
- 422
validation_failedSee errors, keyed by field. - 503
supplier_unavailableThe airline did not answer; nothing changed; retry. - 503
pricing_unavailableOur pricing is unavailable; retry. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/search" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"slices": [
{
"origin": "DAC",
"destination": "DXB",
"departure_date": "2026-10-22"
}
],
"passengers": {
"adults": 1,
"children": 1,
"infants": 1
},
"cabin": "economy",
"fares": "standard",
"filters": {
"max_offers": 20
}
}'{
"search_id": "srh_01m34mpemkpjehfke5rdpwrd8q",
"expires_at": "2026-09-22T13:40:45Z",
"currency": "BDT",
"fares": "standard",
"passengers": {
"adults": 1,
"children": 1,
"infants": 1
},
"offers": [
{
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"total": {
"amount": 9440600,
"currency": "BDT"
},
"price": {
"base": {
"amount": 6417600,
"currency": "BDT"
},
"taxes": {
"amount": 3023000,
"currency": "BDT"
},
"fees": {
"amount": 0,
"currency": "BDT"
},
"service_fee": {
"amount": 0,
"currency": "BDT"
}
},
"passenger_prices": [
{
"type": "adult",
"code": "ADT",
"count": 1,
"base": {
"amount": 3375700,
"currency": "BDT"
},
"taxes": {
"amount": 1469200,
"currency": "BDT"
},
"fees": {
"amount": 0,
"currency": "BDT"
},
"each": {
"amount": 4844900,
"currency": "BDT"
},
"subtotal": {
"amount": 4844900,
"currency": "BDT"
}
},
{
"type": "child",
"code": "CHD",
"count": 1,
"each": {
"amount": 3804100,
"currency": "BDT"
},
"subtotal": {
"amount": 3804100,
"currency": "BDT"
}
},
{
"type": "infant",
"code": "INF",
"count": 1,
"each": {
"amount": 791600,
"currency": "BDT"
},
"subtotal": {
"amount": 791600,
"currency": "BDT"
}
}
],
"passengers": {
"adults": 1,
"children": 1,
"infants": 1
},
"brand": {
"code": "ECONOMY_CLASSIC",
"name": "ECONOMY CLASSIC",
"tier": null,
"attributes": [
{
"classification": "CheckedBag",
"inclusion": "Included"
},
{
"classification": "Refund",
"inclusion": "Chargeable"
}
]
},
"baggage": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
},
"baggage_by_passenger": {
"adult": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
},
"child": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
}
},
"conditions": {
"refundable": true,
"changeable": true,
"refund_fee": {
"amount": 1731100,
"currency": "BDT"
},
"change_fee": {
"amount": 989200,
"currency": "BDT"
}
},
"ticketing_deadline": null,
"instant_ticket": false,
"carrier": "QR",
"seats_available": 9,
"itinerary_key": "itn_4267f9d93e7870fb",
"slices": [
{
"origin": "DAC",
"destination": "DXB",
"duration_minutes": 1095,
"baggage": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
},
"segments": [
{
"origin": "DAC",
"destination": "DOH",
"origin_terminal": "1",
"destination_terminal": null,
"departs_at": "2026-10-22T11:10:00",
"departs_at_utc": "2026-10-22T05:10:00Z",
"arrives_at": "2026-10-22T13:50:00",
"arrives_at_utc": "2026-10-22T10:50:00Z",
"marketing_carrier": "QR",
"operating_carrier": "QR",
"flight_number": "641",
"aircraft": "77W",
"cabin": "economy",
"booking_class": "O",
"duration_minutes": 340,
"seats_available": 9
},
{
"origin": "DOH",
"destination": "DXB",
"departs_at": "2026-10-23T01:05:00",
"arrives_at": "2026-10-23T03:25:00",
"marketing_carrier": "QR",
"operating_carrier": "QR",
"flight_number": "1002",
"aircraft": "77W",
"cabin": "economy",
"booking_class": "O",
"duration_minutes": 80
}
]
}
]
}
]
}{
"error": {
"code": "validation_failed",
"message": "See errors, keyed by field."
}
}Search
Read a search again
/flights/search/{search_id}Returns the same search again while it is valid. An expired or unknown id answers 410 offer_expired.
Parameters
| Parameter | In | Description |
|---|---|---|
search_idstringrequired | path | The search id. Example: srh_01m34mpemkpjehfke5rdpwrd8q |
Errors
- 410
offer_expiredSearch again. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/search/srh_01m34mpemkpjehfke5rdpwrd8q" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"search_id": "srh_01m34mpemkpjehfke5rdpwrd8q",
"expires_at": "2026-09-22T13:40:45Z",
"currency": "BDT",
"fares": "standard",
"passengers": {
"adults": 1,
"children": 1,
"infants": 1
},
"offers": [
{
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"total": {
"amount": 9440600,
"currency": "BDT"
},
"price": {
"base": {
"amount": 6417600,
"currency": "BDT"
},
"taxes": {
"amount": 3023000,
"currency": "BDT"
},
"fees": {
"amount": 0,
"currency": "BDT"
},
"service_fee": {
"amount": 0,
"currency": "BDT"
}
},
"passenger_prices": [
{
"type": "adult",
"code": "ADT",
"count": 1,
"base": {
"amount": 3375700,
"currency": "BDT"
},
"taxes": {
"amount": 1469200,
"currency": "BDT"
},
"fees": {
"amount": 0,
"currency": "BDT"
},
"each": {
"amount": 4844900,
"currency": "BDT"
},
"subtotal": {
"amount": 4844900,
"currency": "BDT"
}
},
{
"type": "child",
"code": "CHD",
"count": 1,
"each": {
"amount": 3804100,
"currency": "BDT"
},
"subtotal": {
"amount": 3804100,
"currency": "BDT"
}
},
{
"type": "infant",
"code": "INF",
"count": 1,
"each": {
"amount": 791600,
"currency": "BDT"
},
"subtotal": {
"amount": 791600,
"currency": "BDT"
}
}
],
"passengers": {
"adults": 1,
"children": 1,
"infants": 1
},
"brand": {
"code": "ECONOMY_CLASSIC",
"name": "ECONOMY CLASSIC",
"tier": null,
"attributes": [
{
"classification": "CheckedBag",
"inclusion": "Included"
},
{
"classification": "Refund",
"inclusion": "Chargeable"
}
]
},
"baggage": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
},
"baggage_by_passenger": {
"adult": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
},
"child": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
}
},
"conditions": {
"refundable": true,
"changeable": true,
"refund_fee": {
"amount": 1731100,
"currency": "BDT"
},
"change_fee": {
"amount": 989200,
"currency": "BDT"
}
},
"ticketing_deadline": null,
"instant_ticket": false,
"carrier": "QR",
"seats_available": 9,
"itinerary_key": "itn_4267f9d93e7870fb",
"slices": [
{
"origin": "DAC",
"destination": "DXB",
"duration_minutes": 1095,
"baggage": {
"checked": {
"pieces": null,
"weight_kg": 25
},
"cabin": {
"pieces": 1,
"weight_kg": 7
}
},
"segments": [
{
"origin": "DAC",
"destination": "DOH",
"origin_terminal": "1",
"destination_terminal": null,
"departs_at": "2026-10-22T11:10:00",
"departs_at_utc": "2026-10-22T05:10:00Z",
"arrives_at": "2026-10-22T13:50:00",
"arrives_at_utc": "2026-10-22T10:50:00Z",
"marketing_carrier": "QR",
"operating_carrier": "QR",
"flight_number": "641",
"aircraft": "77W",
"cabin": "economy",
"booking_class": "O",
"duration_minutes": 340,
"seats_available": 9
},
{
"origin": "DOH",
"destination": "DXB",
"departs_at": "2026-10-23T01:05:00",
"arrives_at": "2026-10-23T03:25:00",
"marketing_carrier": "QR",
"operating_carrier": "QR",
"flight_number": "1002",
"aircraft": "77W",
"cabin": "economy",
"booking_class": "O",
"duration_minutes": 80
}
]
}
]
}
]
}{
"error": {
"code": "offer_expired",
"message": "Search again."
}
}Offer details
Read an offer
/flights/offers/{offer_id}The offer again, same shape as in search. Read-only; it reserves nothing.
Parameters
| Parameter | In | Description |
|---|---|---|
offer_idstringrequired | path | The offer from search, within 15 minutes. Example: ofr_01m34mpempz46w64zzecwd3p6z |
Errors
- 410
offer_expiredSearch again. - 409
offer_unavailableThe airline no longer sells the fare. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/offers/ofr_01m34mpempz46w64zzecwd3p6z" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"offer": {
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"…": "same shape as in search"
}
}{
"error": {
"code": "offer_expired",
"message": "Search again."
}
}Offer details
Confirm the price
/flights/offers/{offer_id}/priceThe airline confirms the price now. No body. Optional: booking re-prices anyway, but this lets you show a confirmed total before collecting passenger details. If the airline's price moved, you get the offer at its new price under the same offer_id. Booking that id books the new price.
Parameters
| Parameter | In | Description |
|---|---|---|
offer_idstringrequired | path | The offer from search, within 15 minutes. Example: ofr_01m34mpemkpjehfke5rdpwrd8r |
Errors
- 409
price_changedNew price attached as offer. - 410
offer_expiredMeans search again. - 409
offer_unavailableMeans the airline no longer sells the fare. - 503
pricing_unavailableOur pricing is unavailable; retry. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/offers/ofr_01m34mpemkpjehfke5rdpwrd8r/price" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"status": "confirmed",
"offer": {
"offer_id": "ofr_01m34mpemkpjehfke5rdpwrd8r",
"total": {
"amount": 6863200,
"currency": "BDT"
},
"…": "the full offer"
}
}{
"error": {
"code": "price_changed",
"message": "The airline has changed the price. The offer below carries the new price; book it to accept."
},
"offer": {
"offer_id": "ofr_…",
"total": {
"amount": 6990000,
"currency": "BDT"
},
"…": "…"
}
}{
"error": {
"code": "price_changed",
"message": "New price attached as offer."
}
}Offer details
Read fare rules
/flights/offers/{offer_id}/rulesThe fare rules as text. An empty rules list means the airline gave none; conditions on the offer still applies.
Parameters
| Parameter | In | Description |
|---|---|---|
offer_idstringrequired | path | The offer from search, within 15 minutes. Example: ofr_01m34mpemkpjehfke5rdpwrd8r |
Errors
- 410
offer_expiredSearch again. - 409
offer_unavailableThe airline no longer sells the fare. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/offers/ofr_01m34mpemkpjehfke5rdpwrd8r/rules" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"rules": [
{
"title": "Changes",
"text": "Before departure: not permitted.\nAfter departure: not permitted."
},
{
"title": "Refunds",
"text": "Before departure: not permitted.\nAfter departure: not permitted."
},
{
"title": "Fare family — Economy Basic",
"text": "Included: 05 Percent Miles Earned, Hand Bag, Food And Beverage.\nFor a fee: Standard Seat Selection, …"
}
]
}{
"error": {
"code": "offer_expired",
"message": "Search again."
}
}Offer details
Read extras and what to send
/flights/offers/{offer_id}/extrasWho the booking must name, and the seats, bags and meals on sale. Call it before building the passenger form: travellers is the exact list the booking must send, in this order and of these types. An empty list means nothing is on sale for that kind, which is common.
Parameters
| Parameter | In | Description |
|---|---|---|
offer_idstringrequired | path | The offer from search, within 15 minutes. Example: ofr_01m34mpempz46w64zzecwd3p6z |
Errors
- 410
offer_expiredSearch again. - 409
offer_unavailableThe airline no longer sells the fare. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/offers/ofr_01m34mpempz46w64zzecwd3p6z/extras" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"travellers": [
{
"position": 1,
"type": "adult"
},
{
"position": 2,
"type": "child"
},
{
"position": 3,
"type": "infant"
}
],
"travel_dates": {
"first": "2026-10-22",
"last": "2026-10-23"
},
"infant_min_days": 7,
"instant_ticket": false,
"passenger_requests": {
"frequent_flyer": false,
"wheelchair": true,
"wheelchair_price": null
},
"extras": {
"baggage": [
{
"id": "anc_01m34…",
"label": "Extra bag 20 kg (DAC–DXB)",
"weight_kg": 20,
"price": {
"amount": 450000,
"currency": "BDT"
}
}
],
"seats": [
{
"origin": "DAC",
"destination": "DOH",
"carrier": "QR",
"flight_number": "641",
"all_or_none": false,
"columns": [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"J",
"K"
],
"rows": [
{
"row": "10",
"seats": [
{
"id": "anc_01m34n6ehrp4y2xd2b88xznwej",
"name": "10A",
"row": "10",
"column": "A",
"available": true,
"characteristics": [
"chargeable",
"bulkhead",
"window"
],
"price": {
"amount": 0,
"currency": "BDT"
}
}
]
}
]
}
],
"meals": [
{
"origin": "DAC",
"destination": "DOH",
"carrier": "QR",
"flight_number": "all flights",
"options": [
{
"id": "anc_01m34mqcj8cxr9j4xv1drv4q9a",
"code": "BBML",
"label": "Baby meal",
"price": {
"amount": 0,
"currency": "BDT"
}
}
]
}
]
}
}{
"error": {
"code": "offer_expired",
"message": "Search again."
}
}Book
Book
/flights/bookingsReserves the seats. The airline re-prices the offer first; your request names the offer, never a price. Most fares come back held with a hold_expires_at; an instant_ticket fare is paid from your deposit and comes back ticketed.
Send an Idempotency-Key header, unique per booking attempt (your order number works). If your call times out, send the same request with the same key: you get the booking that was made, never a second one.
Parameters
| Parameter | In | Description |
|---|---|---|
Idempotency-Keystringrequired | header | Unique per booking attempt (your order number works). Reusing a key with a different body answers 409 duplicate_request. Example: docs-demo-003 |
offer_idstringrequired | body | From search, within 15 minutes. |
travellers[]arrayrequired | body | Exactly the list from /extras, same order and type. |
travellers[].typeadult | child | infantrequired | body | The traveller type. |
travellers[].titlestringrequired | body | MR, MS, MRS for adults; MSTR, MISS for children and infants. |
travellers[].given_namestringrequired | body | As on the passport; letters, spaces and hyphens only. |
travellers[].family_namestringrequired | body | As on the passport; letters, spaces and hyphens only. |
travellers[].genderM | Frequired | body | The traveller's gender. |
travellers[].date_of_birthYYYY-MM-DDrequired | body | Must fit the type on the travel dates. |
travellers[].nationalitystringrequired | body | ISO country code. Example: BD |
travellers[].document.numberstringrequired | body | Passport; must not have expired. |
travellers[].document.issuing_countrystringrequired | body | Passport; must not have expired. |
travellers[].document.expires_onYYYY-MM-DDrequired | body | Passport; must not have expired. |
travellers[].frequent_flyer_numberstringoptional | body | Sent to the airline only when /extras says it takes one; otherwise the booking goes ahead without it and says so in warnings. |
travellers[].frequent_flyer_carrierstringoptional | body | The programme's airline code (e.g. EK) when it is not the flight's airline. loyalty: {"carrier": "EK", "number": "…"} is accepted as well. |
travellers[].wheelchairobjectoptional | body | { "requested": true, "remark": "Cannot walk long distances" }. Same rule as the frequent-flyer number. |
contact.emailstringrequired | body | The passenger contact the airline receives. |
contact.dial_codestringrequired | body | The passenger contact the airline receives. dial_code without + or with it. |
contact.mobilestringrequired | body | The passenger contact the airline receives. |
extras[]string[]optional | body | "<extra id>:<traveller position>" from /extras. Every one is checked before anything is booked: see extras_invalid. |
client_referencestringoptional | body | Your own reference, up to 64 characters. Returned on the booking and searchable. |
Errors
- 400
idempotency_key_requiredSend the header. - 422
validation_failedSee errors, keyed by field. - 410
offer_expiredSearch again. - 409
price_changedThe response carries offer at the new price; book again to accept. - 409
offer_unavailableThe fare is gone; pick another offer. - 409
hold_unavailableThe airline will not hold this fare; it is now instant_ticket: true. Booking again pays and tickets at once. - 402
insufficient_fundsAn instant fare your deposit does not cover; balance and shortfall are included. - 409
duplicate_bookingA passenger already holds a booking on this flight (or the same route and day), with us or refused by the airline. Cancel it before booking again; booking_ref names it when it is yours. - 409
booking_limit_reachedA passenger has been booked (and cancelled) on this flight as many times as the airline allows. Nothing was reserved. - 409
churn_limit_reachedMost of your recent bookings with this airline ended without a ticket, so new holds with it are paused until tomorrow. Nothing was reserved; an instant-ticket fare can still be booked. - 422
passenger_name_refusedA passenger name looks made up (TEST, AAA, initials only). Send every name exactly as printed on the passport. - 422
duplicate_passengerThe same person is on the booking twice. Send each passenger once. - 409
hold_limit_reachedYour agency has made as many bookings with this airline today as it allows. Book again tomorrow (Asia/Dhaka day). - 422
frequent_flyer_invalidThe airline rejected a frequent-flyer number. - 422
document_requiredThe route needs complete passport details. - 422
extras_invalidAn extra cannot be booked as sent. problems[] names each. Reasons: unknown, no_such_traveller, not_for_traveller, seat_unavailable, seat_taken_twice, two_seats_one_flight, all_or_none. - 410
extras_expiredThe extras you read have expired. Call /extras again and book with the new ids. - 422
wheelchair_unavailableThe airline offers no wheelchair assistance on these flights. Book without it and ask the airline, or pick another offer. - 409
duplicate_requestIdempotency-Key reused with a different body. - 409
request_in_progressThe same key is still being booked; read it back shortly. - 409
booking_failedThe airline declined; try again or pick another offer. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Idempotency-Key: docs-demo-003" \
-H "Content-Type: application/json" \
-d '{
"offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
"travellers": [
{
"type": "adult",
"title": "MR",
"given_name": "ARIF",
"family_name": "RAHMAN",
"gender": "M",
"date_of_birth": "1988-05-12",
"nationality": "BD",
"document": {
"type": "passport",
"number": "A01234567",
"issuing_country": "BD",
"expires_on": "2032-09-22"
}
},
{
"type": "child",
"title": "MSTR",
"given_name": "AYAN",
"family_name": "RAHMAN",
"gender": "M",
"date_of_birth": "2018-09-22",
"nationality": "BD",
"document": {
"type": "passport",
"number": "A07654321",
"issuing_country": "BD",
"expires_on": "2032-09-22"
}
},
{
"type": "infant",
"title": "MISS",
"given_name": "ANAYA",
"family_name": "RAHMAN",
"gender": "F",
"date_of_birth": "2025-10-22",
"nationality": "BD",
"document": {
"type": "passport",
"number": "A09876543",
"issuing_country": "BD",
"expires_on": "2032-09-22"
}
}
],
"contact": {
"email": "ops@agency.example",
"dial_code": "880",
"mobile": "1711000000"
},
"client_reference": "DOCS-DEMO-003"
}'{
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "held",
"created_at": "2026-09-22T13:27:52Z",
"hold_expires_at": "2026-09-22T21:27:00Z",
"ticketed_at": null,
"client_reference": "DOCS-DEMO-003",
"airline_reference": "7EAQKO",
"gds_reference": "HN8TYB",
"validating_carrier": "QR",
"route": "DAC → DXB",
"total": {
"amount": 9440600,
"currency": "BDT"
},
"price": {
"base": {
"amount": 6417600,
"currency": "BDT"
},
"taxes": {
"amount": 3023000,
"currency": "BDT"
},
"fees": {
"amount": 0,
"currency": "BDT"
}
},
"travellers": [
{
"position": 1,
"type": "adult",
"title": "MR",
"given_name": "ARIF",
"family_name": "RAHMAN",
"date_of_birth": "1988-05-12",
"ticket_number": null,
"frequent_flyer": {
"number": "EK123456",
"carrier": "EK"
},
"wheelchair": null
},
{
"position": 2,
"type": "child",
"title": "MSTR",
"given_name": "AYAN",
"family_name": "RAHMAN",
"date_of_birth": "2018-09-22",
"ticket_number": null,
"frequent_flyer": null,
"wheelchair": {
"requested": true,
"remark": "Cannot climb stairs"
}
},
{
"position": 3,
"type": "infant",
"title": "MISS",
"given_name": "ANAYA",
"family_name": "RAHMAN",
"date_of_birth": "2025-10-22",
"ticket_number": null,
"frequent_flyer": null,
"wheelchair": null
}
],
"extras": [
{
"kind": "seat",
"label": "Seat 10A",
"traveller": 1,
"price": {
"amount": 0,
"currency": "BDT"
},
"added_after_booking": false
},
{
"kind": "baggage",
"label": "Checked bag 20 kg · DAC–DXB",
"traveller": 1,
"price": {
"amount": 450000,
"currency": "BDT"
},
"added_after_booking": false
}
],
"tickets": [],
"slices": [
{
"origin": "DAC",
"destination": "DXB",
"duration_minutes": 1095,
"segments": [
"… as in the offer …"
]
}
],
"rejection_reason": null,
"after_sales": []
},
"warnings": []
}{
"error": {
"code": "idempotency_key_required",
"message": "Send the header."
}
}Ticket, read and cancel
Read one booking
/flights/bookings/{booking_ref}Returns the booking in the shape shown under Book, and is always the source of truth. In place of the booking_ref you may send the booking's airline_reference (the airline PNR) or gds_reference; if a PNR was booked again after a release, the live booking is returned, else the newest. A reference that is not yours answers 404 booking_not_found.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking_ref, airline_reference or gds_reference. Example: TFL-Y678QNFB |
Errors
- 404
booking_not_foundUnknown, or not your agency's. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "held",
"…": "the shape shown under Book"
}
}{
"error": {
"code": "booking_not_found",
"message": "Unknown, or not your agency's."
}
}Ticket, read and cancel
List bookings
/flights/bookingsNewest first, 25 a page.
Parameters
| Parameter | In | Description |
|---|---|---|
statestringoptional | query | held, ticketed, cancelled… Example: held |
client_referencestringoptional | query | Your own reference. |
created_fromYYYY-MM-DDoptional | query | Created on or after this date. |
created_toYYYY-MM-DDoptional | query | Created on or before this date. |
pageintegeroptional | query | The page. |
per_pageintegeroptional | query | per_page up to 100. Example: 5 |
Errors
- 422
validation_failedSee errors, keyed by field. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/bookings?state=held&per_page=5" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"bookings": [
{
"booking_ref": "TFL-Y678QNFB",
"state": "held",
"…": "…"
}
],
"page": 1,
"per_page": 5,
"total": 1,
"last_page": 1
}{
"error": {
"code": "validation_failed",
"message": "See errors, keyed by field."
}
}Ticket, read and cancel
Issue tickets
/flights/bookings/{booking_ref}/issuePays a held booking from your deposit and tickets it. Issue before hold_expires_at; we release unpaid holds a few minutes before it (5 by default). We ask the airline first whether the reservation still stands at the agreed price. If the fare changed, the call is refused with the new price and nothing is charged; to accept, call again with that exact figure. A 202 status: ticketing means paid, the airline is still issuing: read the booking every 30 seconds until ticketed or rejected.
This call pays from your deposit. Send an Idempotency-Key, and retry only with the same key.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
Idempotency-Keystringrequired | header | Retry issuing only with the same Idempotency-Key. Example: docs-demo-003-issue |
accept_fare_changebooleanoptional | body | To accept a changed fare, call again with this set to true. |
expected_totalMoneyoptional | body | The exact new_total from fare_changed. |
Errors
- 400
idempotency_key_requiredSend the header. - 409
fare_changedCarries new_total; send accept_fare_change with expected_total to accept. - 402
insufficient_fundsTop up; balance and shortfall included. Nothing charged. - 409
hold_invalidThe airline no longer holds the seats; book again. - 409
ticketing_rejectedThe airline declined; the amount is returned to your deposit. - 409
invalid_stateNot a held booking (for example expired or cancelled). - 409
request_in_progressThe same request is still running. - 503
supplier_unavailableNothing charged; retry. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/issue" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Idempotency-Key: docs-demo-003-issue" \
-H "Content-Type: application/json" \
-d '{}'{
"status": "ticketed",
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "ticketed",
"ticketed_at": "2026-09-22T13:28:45Z",
"airline_reference": "7EAQKO",
"gds_reference": "HN8TYB",
"total": {
"amount": 9440600,
"currency": "BDT"
},
"tickets": [
{
"position": 1,
"number": "1572134359166",
"issued_at": "2026-09-22T13:28:45Z"
},
{
"position": 2,
"number": "1572134359165",
"issued_at": "2026-09-22T13:28:45Z"
},
{
"position": 3,
"number": "1572134359167",
"issued_at": "2026-09-22T13:28:45Z"
}
],
"…": "the rest of the booking, with ticket_number on each traveller"
}
}{
"status": "ticketing",
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "ticketing",
"…": "…"
}
}{
"error": {
"code": "idempotency_key_required",
"message": "Send the header."
}
}Ticket, read and cancel
Cancel a hold
/flights/bookings/{booking_ref}/cancelReleases held seats; nothing is charged. A ticketed booking is not cancelled; void or refund it. If the airline does not confirm the release, the booking stays held and you get 503 supplier_unavailable; retry, or let it lapse. Some airline test systems never release unpaid holds; on the sandbox those lapse on their own.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
reasonstringoptional | body | Body optional. Example: Customer changed plans |
Errors
- 409
invalid_stateNot allowed in the booking's current state. - 404
booking_not_foundUnknown, or not your agency's. - 503
supplier_unavailableThe airline did not answer; nothing changed; retry. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/cancel" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"reason": "Customer changed plans"
}'{
"status": "cancelled",
"booking": {
"booking_ref": "TFL-…",
"state": "cancelled",
"…": "…"
}
}{
"error": {
"code": "invalid_state",
"message": "Not allowed in the booking's current state."
}
}Extras after booking
Read extras after booking
/flights/bookings/{booking_ref}/extrasWhat can be added now, each with its id and price. Bags, meals and seats the airline still sells on a booking that is held or ticketed. Not every airline sells extras after booking; available: false says so.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
Errors
- 404
booking_not_foundUnknown, or not your agency's. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/extras" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"booking_ref": "TFL-Y678QNFB",
"available": true,
"travellers": [
{
"position": 1,
"type": "adult"
},
{
"position": 2,
"type": "child"
}
],
"extras": {
"baggage": [
{
"id": "anc_01m3…",
"label": "Checked bag 10 kg",
"weight_kg": 10,
"price": {
"amount": 250000,
"currency": "BDT"
}
}
],
"seats": [],
"meals": []
}
}{
"error": {
"code": "booking_not_found",
"message": "Unknown, or not your agency's."
}
}Extras after booking
Buy extras after booking
/flights/bookings/{booking_ref}/extrasBuy them from your deposit. On a ticketed booking the extras are paid from your deposit at once (charged). On a held booking nothing is taken now: charged is zero, pay_at_issue is true, and added_to_total is added to booking.total, which you pay once when you issue. The same request with the same key answers 200 with the same body and buys nothing more.
Needs an Idempotency-Key. On a ticketed booking this call pays from your deposit at once.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
Idempotency-Keystringrequired | header | The same request with the same key answers 200 with the same body and buys nothing more. Example: po-4471-extras-1 |
extras[]string[]required | body | "<extra id>:<traveller position>" from the extras read above. |
Errors
- 400
idempotency_key_requiredSend the header. - 422
extras_invalidAn id is not on sale for this booking now (unknown[] lists them); read the extras again. - 402
insufficient_fundsTop up; balance and shortfall are included. - 409
extras_refusedThe airline did not add them; nothing was charged. - 409
extras_not_availableNot held or ticketed, a request is open on it, or the airline sells none after booking. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/extras" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Idempotency-Key: po-4471-extras-1" \
-H "Content-Type: application/json" \
-d '{
"extras": [
"anc_01m3…:1"
]
}'{
"status": "added",
"charged": {
"amount": 250000,
"currency": "BDT"
},
"added_to_total": {
"amount": 250000,
"currency": "BDT"
},
"pay_at_issue": false,
"booking": {
"…": "…"
}
}{
"error": {
"code": "idempotency_key_required",
"message": "Send the header."
}
}After-sales
Read a refund quote
/flights/bookings/{booking_ref}/refund-quoteChanges nothing. refund is what the airline returns, penalties what it keeps, service_charge our handling fee, net what reaches your deposit. When the airline cannot quote by itself you get {"available": false}, and the refund goes to our desk, which quotes it by hand.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
Errors
- 409
invalid_stateOnly a ticketed booking can be voided, refunded or reissued. - 404
booking_not_foundUnknown, or not your agency's. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/refund-quote" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"available": true,
"refund": {
"amount": 2200000,
"currency": "BDT"
},
"penalties": {
"amount": 800000,
"currency": "BDT"
},
"service_charge": {
"amount": 100000,
"currency": "BDT"
},
"net": {
"amount": 2100000,
"currency": "BDT"
},
"tickets": [
"1572134359166"
]
}{
"error": {
"code": "invalid_state",
"message": "Only a ticketed booking can be voided, refunded or reissued."
}
}After-sales
Request a refund
/flights/bookings/{booking_ref}/refundauto (default): the airline quotes, we accept for you and settle; the answer's request.status is completed and net is in your deposit. If the airline will not answer, the request falls back to our desk (mode: manual, status: requested). manual: straight to our desk. We quote; you accept or decline with POST /quote.
This is a request on a ticketed booking, one open at a time. Read the booking's after_sales[] to follow it.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
modeauto | manualoptional | body | Default auto. Example: auto |
reasonstringoptional | body | Why the refund is requested. Example: Trip cancelled |
Errors
- 409
invalid_stateOnly a ticketed booking can be voided, refunded or reissued. - 409
request_openOne request at a time; wait for the open one. - 422
special_fareThis fare cannot be refunded or changed (for example a group fare). - 409
not_settledThe airline quoted but settlement failed; our desk has it. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/refund" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"mode": "auto",
"reason": "Trip cancelled"
}'{
"request": {
"…": "…"
},
"booking": {
"…": "…"
}
}{
"error": {
"code": "invalid_state",
"message": "Only a ticketed booking can be voided, refunded or reissued."
}
}After-sales
Void tickets
/flights/bookings/{booking_ref}/voidA void cancels the tickets inside the airline's void window, which ends at 23:59 on the day of issue (Asia/Dhaka, UTC+06:00 time). The option closes 5 minutes before that; a later void answers 409 void_window_closed, and a refund is the way instead. Voided at once: request.mode is auto, request.status is completed and the booking is cancelled, with the fare less our void charge back in your deposit. Voided by our desk: request.mode is manual and request.status is requested until it is done.
This is a request on a ticketed booking, one open at a time. Read the booking's after_sales[] to follow it.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
reasonstringoptional | body | Body optional. |
Errors
- 409
invalid_stateOnly a ticketed booking can be voided, refunded or reissued. - 409
request_openOne request at a time; wait for the open one. - 422
special_fareThis fare cannot be refunded or changed (for example a group fare). - 409
void_window_closedThe void window has closed; request a refund instead. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/void" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{}'{
"request": {
"request_id": "01a0c94e-618a-71c6-81bc-19d403d07a93",
"kind": "void",
"status": "requested",
"mode": "manual",
"…": "…"
},
"booking": {
"booking_ref": "TFL-Y678QNFB",
"state": "ticketed",
"after_sales": [
{
"kind": "void",
"status": "requested",
"…": "…"
}
],
"…": "…"
}
}{
"error": {
"code": "invalid_state",
"message": "Only a ticketed booking can be voided, refunded or reissued."
}
}After-sales
Search reissue flights
/flights/bookings/{booking_ref}/reissue/searchThe traveller positions moving and one date per leg of the booking (a round trip needs two dates). exchanges: true means the airline reissues by itself and the offers are its exchange fares; false means the offers are the airline's current fares and our desk does the reissue.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
travellersinteger[]required | body | The traveller positions moving. |
datesstring[]required | body | One date per leg of the booking (a round trip needs two dates). |
Errors
- 409
invalid_stateOnly a ticketed booking can be voided, refunded or reissued. - 409
request_openOne request at a time; wait for the open one. - 422
special_fareThis fare cannot be refunded or changed (for example a group fare). - 503
supplier_unavailableThe airline did not answer; nothing changed; retry. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/reissue/search" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"travellers": [
1,
2,
3
],
"dates": [
"2026-10-29"
]
}'{
"exchanges": false,
"travellers": [
1,
2,
3
],
"dates": [
"2026-10-29"
],
"offers": [
{
"offer_id": "ofr_01m34mwr1a0zywbzyex186g74n",
"carrier": "QR",
"total": {
"amount": 9242700,
"currency": "BDT"
},
"slices": [
"…"
],
"…": "offer shape as in search"
}
]
}{
"error": {
"code": "invalid_state",
"message": "Only a ticketed booking can be voided, refunded or reissued."
}
}After-sales
Ask for a reissue
/flights/bookings/{booking_ref}/reissueWithin 30 minutes of the reissue search. With exchanges: true the airline quotes at once: request.status is quoted and request.quote is the total charge (fare difference, airline change fee and our service charge; quote_note itemises it). Otherwise our desk quotes.
This is a request on a ticketed booking, one open at a time. Read the booking's after_sales[] to follow it.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
offer_idstringrequired | body | An offer from the reissue search. Example: ofr_01m34mwr1a0zywbzyex186g74n |
modeauto | manualoptional | body | How the reissue is handled. Example: auto |
remarkstringoptional | body | A note on the change. Example: Passenger moves to 29 Oct |
Errors
- 409
invalid_stateOnly a ticketed booking can be voided, refunded or reissued. - 409
request_openOne request at a time; wait for the open one. - 422
special_fareThis fare cannot be refunded or changed (for example a group fare). - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/reissue" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"offer_id": "ofr_01m34mwr1a0zywbzyex186g74n",
"mode": "auto",
"remark": "Passenger moves to 29 Oct"
}'{
"request": {
"…": "…"
},
"booking": {
"…": "…"
}
}{
"error": {
"code": "invalid_state",
"message": "Only a ticketed booking can be voided, refunded or reissued."
}
}After-sales
Accept or decline a quote
/flights/bookings/{booking_ref}/quoteBefore quote_expires_at. Accepting a reissue charges your deposit and, when the airline quoted it, reissues on the spot: request.status becomes completed and the booking carries the new ticket numbers and flights. {"accept": false, "reason": "…"} declines and the tickets stay as they are. 402 insufficient_funds means top up and accept again before the quote expires.
Accepting a reissue quote charges your deposit.
Parameters
| Parameter | In | Description |
|---|---|---|
booking_refstringrequired | path | The booking reference, TFL-XXXXXXXX. Example: TFL-Y678QNFB |
acceptbooleanrequired | body | true accepts, false declines. Example: true |
reasonstringoptional | body | Why the quote is declined. |
Errors
- 409
no_quoteNothing is waiting for an answer. - 409
quote_expiredAsk again for a fresh quote. - 409
not_settledThe airline quoted but settlement failed; our desk has it. - 402
insufficient_fundsDeposit too low; nothing charged. balance and shortfall are included. - 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/flights/bookings/TFL-Y678QNFB/quote" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"accept": true
}'{
"request": {
"…": "…"
},
"booking": {
"…": "…"
}
}{
"error": {
"code": "no_quote",
"message": "Nothing is waiting for an answer."
}
}Deposit
Read the deposit balance
/account/balanceCheck it before issuing large bookings; an issue your deposit cannot cover answers 402 insufficient_funds and charges nothing. Top up in the agent portal under Deposit.
Parameters
This endpoint takes no parameters.
Errors
- 401
unauthenticatedHeaders missing, or key or secret wrong. - 401
credential_inactiveThe key was revoked or expired; issue a new one. - 403
ip_whitelist_requiredAdd your server's public IP to the key. - 403
ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP. - 403
product_not_allowedThe key is not enabled for flights. - 403
partner_inactiveYour agency account is suspended or pending. - 403
subscription_requiredAPI plan not active; see API access in the portal. - 403
subscription_pendingAPI plan not active; see API access in the portal. - 403
subscription_suspendedAPI plan not active; see API access in the portal. - 403
subscription_cancelledAPI plan not active; see API access in the portal. - 403
environment_not_activatedLive keys work after certification. - 429
rate_limitedSlow down; wait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/account/balance" \
-H "X-Api-Key: $FLIGHTOTA_API_KEY" \
-H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
-H "Accept: application/json"{
"partner_code": "PTR-000020",
"currency": "BDT",
"balance": {
"amount": 20000000,
"currency": "BDT"
}
}{
"error": {
"code": "unauthenticated",
"message": "Headers missing, or key or secret wrong."
}
}