Skip to content
Air Travel API

Air Travel API

Operational

One 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

EnvironmentBase URLKey prefixUse
Sandboxhttps://sandbox.flightota.com/api/v1ak_test_Build and certify. Test airline systems, test tickets, test deposit.
Productionhttps://api.flightota.com/api/v1ak_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.

http
X-Api-Key: ak_test_your_key_id
X-Api-Secret: <your secret>
Accept: application/json
Content-Type: application/json

4. 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.

http
GET /api/v1/ping
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"
}

Conventions

  • Money is always {"amount": <integer minor units>, "currency": "BDT"}. 6863200 BDT 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:SS with no offset. departs_at_utc is given when the airline supplies it, else null. All other times (expires_at, created_at, hold_expires_at) are UTC, RFC 3339.
  • Unknown is null. refundable: null means the airline did not say, not "no". pieces: 0 means no bag; pieces: null means not stated.
  • conditions_on_request: true on an offer means its refund and change terms are read from the airline's filed rules when you price it: the offer that POST /flights/offers/{offer_id}/price returns carries them in conditions, and its fare rules give them in full.
  • IDs are ours: srh_… search, ofr_… offer, TFL-XXXXXXXX booking (bookings made before 28 Sep 2026 keep their FL- reference). The airline's own record locator is returned as airline_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 is gds_reference (else null).
  • 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 on code; message is 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.

StepCallRequiredWhat you get
1POST /flights/searchYesOffers for one-way, round trip or multi-city
2POST /flights/offers/{offer_id}/priceOptional (booking re-prices anyway)The airline's price now
3GET /flights/offers/{offer_id}/extrasRecommendedWho to send, in what order; seats, bags, meals
4POST /flights/bookingsYesA held booking (or a ticketed one for instant fares)
5POST /flights/bookings/{booking_ref}/issueYes, before hold_expires_atTickets, paid from your deposit
After/refund, /void, /reissue, /cancelWhen neededSee After-sales

Booking states

stateMeaningWhat you can do
pendingAccepted, the airline has not confirmed yetRead it again
heldSeats confirmed, not paid; hold_expires_at setIssue, or cancel
ticketingIssue requested, the airline is still issuingRead it again; it becomes ticketed or rejected
ticketedTickets issued, tickets[] filledRefund, void, reissue
rejectedThe airline declined to issue; nothing chargedBook again
expiredThe hold lapsed unpaid. We release holds ourselves a few minutes before hold_expires_at (5 by default)Book again
cancelledCancelled or voidedNothing

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.

Offer details

Four read-only calls on one offer_id before you book. None of them reserves anything.

CallPurpose
GET /flights/offers/{offer_id}The offer again, same shape as in search, as {"offer": {…}}
POST /flights/offers/{offer_id}/priceThe airline confirms the price now
GET /flights/offers/{offer_id}/rulesThe fare rules as text
GET /flights/offers/{offer_id}/extrasWho 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.

200 OK
{
  "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.

409 Conflict
{
  "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

http
GET /flights/offers/ofr_01m34mpemkpjehfke5rdpwrd8r/rules
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, …"
    }
  ]
}

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.

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"}
          }
        ]
      }
    ]
  }
}

(The baggage entry is illustrative; the others are the sandbox's real answer for this Qatar Airways offer.)

  • travel_dates and infant_min_days set the age rules: a child must be 2–11 and an infant under 2 (and at least infant_min_days old) on the travel dates.
  • passenger_requests says whether the airline takes a frequent-flyer number and a wheelchair request. wheelchair_price is 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's extras[], e.g. "anc_01m34n6ehrp4y2xd2b88xznwej:1" gives seat 10A to traveller 1. Extra ids belong to this offer only.
  • all_or_none: true means 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

FieldRequiredNotes
offer_idYesFrom search, within 15 minutes
travellers[]YesExactly the list from /extras, same order and type
travellers[].typeYesadult | child | infant
travellers[].titleYesMR, MS, MRS for adults; MSTR, MISS for children and infants
travellers[].given_name, family_nameYesAs on the passport; letters, spaces and hyphens only
travellers[].genderYesM | F
travellers[].date_of_birthYesYYYY-MM-DD; must fit the type on the travel dates
travellers[].nationalityYesISO country code, BD
travellers[].document.number, issuing_country, expires_onYesPassport; must not have expired
travellers[].frequent_flyer_numberNoSent 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_carrierNoThe programme's airline code (e.g. EK) when it is not the flight's airline. loyalty: {"carrier": "EK", "number": "…"} is accepted as well
travellers[].wheelchairNo{ "requested": true, "remark": "Cannot walk long distances" }. Same rule as the frequent-flyer number
contact.email, contact.dial_code, contact.mobileYesThe 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_referenceNoYour own reference, up to 64 characters. Returned on the booking and searchable

Request

The sandbox call that made booking TFL-Y678QNFB:

http
POST /api/v1/flights/bookings
Idempotency-Key: docs-demo-003
json
{
  "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.

json
{
  "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_flyer and travellers[].wheelchair are what the airline was sent; null means 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 inside price.fees. added_after_booking marks 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.

HTTPerror.codeWhat to do
400idempotency_key_requiredAdd the header
422validation_failedFix the fields listed in errors
410offer_expiredSearch again
409price_changedThe response carries offer at the new price; book again to accept
409offer_unavailableThe fare is gone; pick another offer
409hold_unavailableThe airline will not hold this fare; it is now instant_ticket: true. Booking again pays and tickets at once
402insufficient_fundsAn instant fare your deposit does not cover; balance and shortfall are included
409duplicate_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
409booking_limit_reachedA passenger has been booked (and cancelled) on this flight as many times as the airline allows. Nothing was reserved
409churn_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
422passenger_name_refusedA passenger name looks made up (TEST, AAA, initials only). Send every name exactly as printed on the passport
422duplicate_passengerThe same person is on the booking twice. Send each passenger once
409hold_limit_reachedYour agency has made as many bookings with this airline today as it allows. Book again tomorrow (Asia/Dhaka day)
422frequent_flyer_invalidThe airline rejected a frequent-flyer number
422document_requiredThe route needs complete passport details
422extras_invalidAn 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
410extras_expiredThe extras you read have expired. Call /extras again and book with the new ids
422wheelchair_unavailableThe airline offers no wheelchair assistance on these flights. Book without it and ask the airline, or pick another offer
409request_in_progressThe same key is still being booked; read it back shortly
409booking_failedThe 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.

CallPurpose
GET /flights/bookings/{booking_ref}/extrasWhat can be added now, each with its id and price
POST /flights/bookings/{booking_ref}/extrasBuy them from your deposit. Needs an Idempotency-Key
http
GET /flights/bookings/TFL-Y678QNFB/extras
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": []
  }
}
http
POST /api/v1/flights/bookings/TFL-Y678QNFB/extras
Idempotency-Key: po-4471-extras-1
json
{"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.

HTTPerror.codeWhat to do
422extras_invalidAn id is not on sale for this booking now (unknown[] lists them); read the extras again
402insufficient_fundsTop up; balance and shortfall are included
409extras_refusedThe airline did not add them; nothing was charged
409extras_not_availableNot 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:

json
{"accept_fare_change": true, "expected_total": {"amount": 9512300, "currency": "BDT"}}

The sandbox call that ticketed TFL-Y678QNFB, body {}, 21 seconds:

200 OK
{
  "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"
  }
}
HTTPAnswerMeaning
200status: ticketedTickets issued; tickets[] filled. Also the answer when you repeat the call
202status: ticketingPaid; the airline is still issuing. Read the booking until ticketed or rejected
409fare_changedCarries new_total; send accept_fare_change with expected_total to accept
402insufficient_fundsTop up; balance and shortfall included. Nothing charged
409hold_invalidThe airline no longer holds the seats; book again
409ticketing_rejectedThe airline declined; the amount is returned to your deposit
409invalid_stateNot a held booking (for example expired or cancelled)
503supplier_unavailableNothing 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 parameterNotes
stateheld, ticketed, cancelled…
client_referenceYour own reference
created_from, created_toYYYY-MM-DD
page, per_pageper_page up to 100
http
GET /api/v1/flights/bookings?state=held&per_page=5
json
{
  "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.

200 OK
{"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:

FromToWhen
requestedquotedThe airline or our desk quotes
requestedin_progressOur desk is working on it
in_progressquotedOur desk quotes
quotedquote_approvedYou accept with POST /quote
quotedquote_rejectedYou decline with POST /quote
quotedquote_expiredNot answered in time
quote_approvedcompletedSettled with the airline
requestedcompletedA void done at once or by our desk
requestedrejectedNot possible

The request object, as it appears in responses and in booking.after_sales[]:

json
{
  "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

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"]
}

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

json
{"mode": "auto", "reason": "Trip cancelled"}
  • auto (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.

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.mode is auto, request.status is completed and the booking is cancelled.
  • Voided by our desk. Otherwise our desk voids the tickets with the airline and returns the amount: request.mode is manual and request.status is requested until it is done.

The sandbox void on TFL-Y678QNFB, handled by the desk:

202 Accepted
{
  "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).

json
{"travellers": [1,2,3], "dates": ["2026-10-29"]}
json
{
  "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

json
{
  "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

json
{"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

HTTPerror.codeMeaning
409invalid_stateOnly a ticketed booking can be voided, refunded or reissued
409void_window_closedThe void window has closed; request a refund instead
409request_openOne request at a time; wait for the open one
422special_fareThis fare cannot be refunded or changed (for example a group fare)
409no_quoteNothing is waiting for an answer
409quote_expiredAsk again for a fresh quote
409not_settledThe 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

json
{
  "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 until ticketed or rejected;
  • 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.

json
{
  "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

HTTPerror.codeFix
401unauthenticatedHeaders missing, or key or secret wrong
401credential_inactiveThe key was revoked or expired; issue a new one
403ip_whitelist_requiredAdd your server's public IP to the key
403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP
403product_not_allowedThe key is not enabled for flights
403partner_inactiveYour agency account is suspended or pending
403subscription_required, subscription_pending, subscription_suspended, subscription_cancelledAPI plan not active; see API access in the portal
403environment_not_activatedLive keys work after certification
429rate_limitedSlow down; wait a minute

All flight codes

HTTPerror.codeWhereMeaning
400idempotency_key_requiredbook, issue, extrasSend the header
422validation_failedanySee errors
410offer_expiredoffers, bookSearch again
409offer_unavailableoffers, bookThe airline no longer sells the fare
409price_changedprice, bookNew price attached as offer
409hold_unavailablebookFare is now instant-ticket only
402insufficient_fundsbook, issue, quoteDeposit too low; nothing charged
409duplicate_bookingbookSame passenger already booked on the flight (or route and day); booking_ref when it is yours
409booking_limit_reachedbookSame passenger booked on the flight too many times (cancellations count)
409churn_limit_reachedbookHolds with this airline paused for the day after repeated bookings ended without a ticket
422passenger_name_refusedbookA passenger name looks made up (live only)
422duplicate_passengerbookThe same passenger twice in one booking
409hold_limit_reachedbookThe airline's daily limit of bookings for your agency is reached
422frequent_flyer_invalid, document_requiredbookPassenger data refused by the airline
422wheelchair_unavailablebookNo wheelchair assistance on these flights
422extras_invalidbook, extrasAn extra cannot be booked as sent; see problems or unknown
410extras_expiredbookRead /extras again and use the new ids
409extras_refused, extras_not_availableextrasThe airline did not add them, or the booking takes none now
409booking_failedbookAirline declined
409duplicate_requestbookIdempotency-Key reused with a different body
409request_in_progressbook, issueThe same request is still running
404booking_not_foundbookingsUnknown, or not your agency's
409fare_changedissueAccept with accept_fare_change + expected_total
409hold_invalidissueThe airline dropped the reservation
409ticketing_rejectedissueAirline declined; money returned
409invalid_stateissue, cancel, after-salesNot allowed in the booking's current state
409request_open, no_quote, quote_expired, not_settledafter-salesSee After-sales
409void_window_closedvoidToo late to void; refund instead
422special_fareafter-salesFare cannot be refunded or changed
503supplier_unavailableanyThe airline did not answer; nothing changed; retry
503pricing_unavailablesearch, priceOur 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 /ping returns 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 by itinerary_key
  • Multi-city search with three slices
  • POST /offers/{id}/price handled for both confirmed and 409 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
  • warnings on the booking response shown to your user; extras_invalid problems shown per extra
  • Held booking issued; tickets shown per traveller
  • A hold cancelled
  • 402 insufficient_funds and 410 offer_expired shown 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 a 202 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

GET/ping

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.

Parameters

This endpoint takes no parameters.

Errors

  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "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 · 401
{
  "error": {
    "code": "unauthenticated",
    "message": "Headers missing, or key or secret wrong."
  }
}

Search

Read a search again

GET/flights/search/{search_id}

Returns the same search again while it is valid. An expired or unknown id answers 410 offer_expired.

Parameters

ParameterInDescription
search_id
stringrequired
pathThe search id.
Example: srh_01m34mpemkpjehfke5rdpwrd8q

Errors

  • 410offer_expiredSearch again.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "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 · 410
{
  "error": {
    "code": "offer_expired",
    "message": "Search again."
  }
}

Offer details

Read an offer

GET/flights/offers/{offer_id}

The offer again, same shape as in search. Read-only; it reserves nothing.

Parameters

ParameterInDescription
offer_id
stringrequired
pathThe offer from search, within 15 minutes.
Example: ofr_01m34mpempz46w64zzecwd3p6z

Errors

  • 410offer_expiredSearch again.
  • 409offer_unavailableThe airline no longer sells the fare.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "offer": {
    "offer_id": "ofr_01m34mpempz46w64zzecwd3p6z",
    "…": "same shape as in search"
  }
}
Error · 410
{
  "error": {
    "code": "offer_expired",
    "message": "Search again."
  }
}

Offer details

Confirm the price

POST/flights/offers/{offer_id}/price

The 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

ParameterInDescription
offer_id
stringrequired
pathThe offer from search, within 15 minutes.
Example: ofr_01m34mpemkpjehfke5rdpwrd8r

Errors

  • 409price_changedNew price attached as offer.
  • 410offer_expiredMeans search again.
  • 409offer_unavailableMeans the airline no longer sells the fare.
  • 503pricing_unavailableOur pricing is unavailable; retry.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "status": "confirmed",
  "offer": {
    "offer_id": "ofr_01m34mpemkpjehfke5rdpwrd8r",
    "total": {
      "amount": 6863200,
      "currency": "BDT"
    },
    "…": "the full offer"
  }
}
Response · 409 price_changed
{
  "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 · 409
{
  "error": {
    "code": "price_changed",
    "message": "New price attached as offer."
  }
}

Offer details

Read fare rules

GET/flights/offers/{offer_id}/rules

The fare rules as text. An empty rules list means the airline gave none; conditions on the offer still applies.

Parameters

ParameterInDescription
offer_id
stringrequired
pathThe offer from search, within 15 minutes.
Example: ofr_01m34mpemkpjehfke5rdpwrd8r

Errors

  • 410offer_expiredSearch again.
  • 409offer_unavailableThe airline no longer sells the fare.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "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 · 410
{
  "error": {
    "code": "offer_expired",
    "message": "Search again."
  }
}

Offer details

Read extras and what to send

GET/flights/offers/{offer_id}/extras

Who 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

ParameterInDescription
offer_id
stringrequired
pathThe offer from search, within 15 minutes.
Example: ofr_01m34mpempz46w64zzecwd3p6z

Errors

  • 410offer_expiredSearch again.
  • 409offer_unavailableThe airline no longer sells the fare.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "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 · 410
{
  "error": {
    "code": "offer_expired",
    "message": "Search again."
  }
}

Book

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.

Parameters

ParameterInDescription
Idempotency-Key
stringrequired
headerUnique per booking attempt (your order number works). Reusing a key with a different body answers 409 duplicate_request.
Example: docs-demo-003
offer_id
stringrequired
bodyFrom search, within 15 minutes.
travellers[]
arrayrequired
bodyExactly the list from /extras, same order and type.
travellers[].type
adult | child | infantrequired
bodyThe traveller type.
travellers[].title
stringrequired
bodyMR, MS, MRS for adults; MSTR, MISS for children and infants.
travellers[].given_name
stringrequired
bodyAs on the passport; letters, spaces and hyphens only.
travellers[].family_name
stringrequired
bodyAs on the passport; letters, spaces and hyphens only.
travellers[].gender
M | Frequired
bodyThe traveller's gender.
travellers[].date_of_birth
YYYY-MM-DDrequired
bodyMust fit the type on the travel dates.
travellers[].nationality
stringrequired
bodyISO country code.
Example: BD
travellers[].document.number
stringrequired
bodyPassport; must not have expired.
travellers[].document.issuing_country
stringrequired
bodyPassport; must not have expired.
travellers[].document.expires_on
YYYY-MM-DDrequired
bodyPassport; must not have expired.
travellers[].frequent_flyer_number
stringoptional
bodySent 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
stringoptional
bodyThe 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
objectoptional
body{ "requested": true, "remark": "Cannot walk long distances" }. Same rule as the frequent-flyer number.
contact.email
stringrequired
bodyThe passenger contact the airline receives.
contact.dial_code
stringrequired
bodyThe passenger contact the airline receives. dial_code without + or with it.
contact.mobile
stringrequired
bodyThe 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_reference
stringoptional
bodyYour own reference, up to 64 characters. Returned on the booking and searchable.

Errors

  • 400idempotency_key_requiredSend the header.
  • 422validation_failedSee errors, keyed by field.
  • 410offer_expiredSearch again.
  • 409price_changedThe response carries offer at the new price; book again to accept.
  • 409offer_unavailableThe fare is gone; pick another offer.
  • 409hold_unavailableThe airline will not hold this fare; it is now instant_ticket: true. Booking again pays and tickets at once.
  • 402insufficient_fundsAn instant fare your deposit does not cover; balance and shortfall are included.
  • 409duplicate_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.
  • 409booking_limit_reachedA passenger has been booked (and cancelled) on this flight as many times as the airline allows. Nothing was reserved.
  • 409churn_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.
  • 422passenger_name_refusedA passenger name looks made up (TEST, AAA, initials only). Send every name exactly as printed on the passport.
  • 422duplicate_passengerThe same person is on the booking twice. Send each passenger once.
  • 409hold_limit_reachedYour agency has made as many bookings with this airline today as it allows. Book again tomorrow (Asia/Dhaka day).
  • 422frequent_flyer_invalidThe airline rejected a frequent-flyer number.
  • 422document_requiredThe route needs complete passport details.
  • 422extras_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.
  • 410extras_expiredThe extras you read have expired. Call /extras again and book with the new ids.
  • 422wheelchair_unavailableThe airline offers no wheelchair assistance on these flights. Book without it and ask the airline, or pick another offer.
  • 409duplicate_requestIdempotency-Key reused with a different body.
  • 409request_in_progressThe same key is still being booked; read it back shortly.
  • 409booking_failedThe airline declined; try again or pick another offer.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
}'
Response · 201
{
  "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 · 400
{
  "error": {
    "code": "idempotency_key_required",
    "message": "Send the header."
  }
}

Ticket, read and cancel

Read one booking

GET/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

ParameterInDescription
booking_ref
stringrequired
pathThe booking_ref, airline_reference or gds_reference.
Example: TFL-Y678QNFB

Errors

  • 404booking_not_foundUnknown, or not your agency's.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "booking": {
    "booking_ref": "TFL-Y678QNFB",
    "state": "held",
    "…": "the shape shown under Book"
  }
}
Error · 404
{
  "error": {
    "code": "booking_not_found",
    "message": "Unknown, or not your agency's."
  }
}

Ticket, read and cancel

List bookings

GET/flights/bookings

Newest first, 25 a page.

Parameters

ParameterInDescription
state
stringoptional
queryheld, ticketed, cancelled…
Example: held
client_reference
stringoptional
queryYour own reference.
created_from
YYYY-MM-DDoptional
queryCreated on or after this date.
created_to
YYYY-MM-DDoptional
queryCreated on or before this date.
page
integeroptional
queryThe page.
per_page
integeroptional
queryper_page up to 100.
Example: 5

Errors

  • 422validation_failedSee errors, keyed by field.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "bookings": [
    {
      "booking_ref": "TFL-Y678QNFB",
      "state": "held",
      "…": "…"
    }
  ],
  "page": 1,
  "per_page": 5,
  "total": 1,
  "last_page": 1
}
Error · 422
{
  "error": {
    "code": "validation_failed",
    "message": "See errors, keyed by field."
  }
}

Ticket, read and cancel

Issue tickets

POST/flights/bookings/{booking_ref}/issue

Pays 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB
Idempotency-Key
stringrequired
headerRetry issuing only with the same Idempotency-Key.
Example: docs-demo-003-issue
accept_fare_change
booleanoptional
bodyTo accept a changed fare, call again with this set to true.
expected_total
Moneyoptional
bodyThe exact new_total from fare_changed.

Errors

  • 400idempotency_key_requiredSend the header.
  • 409fare_changedCarries new_total; send accept_fare_change with expected_total to accept.
  • 402insufficient_fundsTop up; balance and shortfall included. Nothing charged.
  • 409hold_invalidThe airline no longer holds the seats; book again.
  • 409ticketing_rejectedThe airline declined; the amount is returned to your deposit.
  • 409invalid_stateNot a held booking (for example expired or cancelled).
  • 409request_in_progressThe same request is still running.
  • 503supplier_unavailableNothing charged; retry.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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 '{}'
Response · 200
{
  "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"
  }
}
Response · 202 ticketing
{
  "status": "ticketing",
  "booking": {
    "booking_ref": "TFL-Y678QNFB",
    "state": "ticketing",
    "…": "…"
  }
}
Error · 400
{
  "error": {
    "code": "idempotency_key_required",
    "message": "Send the header."
  }
}

Ticket, read and cancel

Cancel a hold

POST/flights/bookings/{booking_ref}/cancel

Releases 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB
reason
stringoptional
bodyBody optional.
Example: Customer changed plans

Errors

  • 409invalid_stateNot allowed in the booking's current state.
  • 404booking_not_foundUnknown, or not your agency's.
  • 503supplier_unavailableThe airline did not answer; nothing changed; retry.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
}'
Response · 200
{
  "status": "cancelled",
  "booking": {
    "booking_ref": "TFL-…",
    "state": "cancelled",
    "…": "…"
  }
}
Error · 409
{
  "error": {
    "code": "invalid_state",
    "message": "Not allowed in the booking's current state."
  }
}

Extras after booking

Read extras after booking

GET/flights/bookings/{booking_ref}/extras

What 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB

Errors

  • 404booking_not_foundUnknown, or not your agency's.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "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 · 404
{
  "error": {
    "code": "booking_not_found",
    "message": "Unknown, or not your agency's."
  }
}

Extras after booking

Buy extras after booking

POST/flights/bookings/{booking_ref}/extras

Buy 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB
Idempotency-Key
stringrequired
headerThe 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

  • 400idempotency_key_requiredSend the header.
  • 422extras_invalidAn id is not on sale for this booking now (unknown[] lists them); read the extras again.
  • 402insufficient_fundsTop up; balance and shortfall are included.
  • 409extras_refusedThe airline did not add them; nothing was charged.
  • 409extras_not_availableNot held or ticketed, a request is open on it, or the airline sells none after booking.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
  ]
}'
Response · 201
{
  "status": "added",
  "charged": {
    "amount": 250000,
    "currency": "BDT"
  },
  "added_to_total": {
    "amount": 250000,
    "currency": "BDT"
  },
  "pay_at_issue": false,
  "booking": {
    "…": "…"
  }
}
Error · 400
{
  "error": {
    "code": "idempotency_key_required",
    "message": "Send the header."
  }
}

After-sales

Read a refund quote

GET/flights/bookings/{booking_ref}/refund-quote

Changes 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB

Errors

  • 409invalid_stateOnly a ticketed booking can be voided, refunded or reissued.
  • 404booking_not_foundUnknown, or not your agency's.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "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 · 409
{
  "error": {
    "code": "invalid_state",
    "message": "Only a ticketed booking can be voided, refunded or reissued."
  }
}

After-sales

Request a refund

POST/flights/bookings/{booking_ref}/refund

auto (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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB
mode
auto | manualoptional
bodyDefault auto.
Example: auto
reason
stringoptional
bodyWhy the refund is requested.
Example: Trip cancelled

Errors

  • 409invalid_stateOnly a ticketed booking can be voided, refunded or reissued.
  • 409request_openOne request at a time; wait for the open one.
  • 422special_fareThis fare cannot be refunded or changed (for example a group fare).
  • 409not_settledThe airline quoted but settlement failed; our desk has it.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
}'
Response · 202
{
  "request": {
    "…": "…"
  },
  "booking": {
    "…": "…"
  }
}
Error · 409
{
  "error": {
    "code": "invalid_state",
    "message": "Only a ticketed booking can be voided, refunded or reissued."
  }
}

After-sales

Void tickets

POST/flights/bookings/{booking_ref}/void

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: 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB
reason
stringoptional
bodyBody optional.

Errors

  • 409invalid_stateOnly a ticketed booking can be voided, refunded or reissued.
  • 409request_openOne request at a time; wait for the open one.
  • 422special_fareThis fare cannot be refunded or changed (for example a group fare).
  • 409void_window_closedThe void window has closed; request a refund instead.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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 '{}'
Response · 202
{
  "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 · 409
{
  "error": {
    "code": "invalid_state",
    "message": "Only a ticketed booking can be voided, refunded or reissued."
  }
}

After-sales

Ask for a reissue

POST/flights/bookings/{booking_ref}/reissue

Within 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB
offer_id
stringrequired
bodyAn offer from the reissue search.
Example: ofr_01m34mwr1a0zywbzyex186g74n
mode
auto | manualoptional
bodyHow the reissue is handled.
Example: auto
remark
stringoptional
bodyA note on the change.
Example: Passenger moves to 29 Oct

Errors

  • 409invalid_stateOnly a ticketed booking can be voided, refunded or reissued.
  • 409request_openOne request at a time; wait for the open one.
  • 422special_fareThis fare cannot be refunded or changed (for example a group fare).
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
}'
Response · 202
{
  "request": {
    "…": "…"
  },
  "booking": {
    "…": "…"
  }
}
Error · 409
{
  "error": {
    "code": "invalid_state",
    "message": "Only a ticketed booking can be voided, refunded or reissued."
  }
}

After-sales

Accept or decline a quote

POST/flights/bookings/{booking_ref}/quote

Before 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

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFL-XXXXXXXX.
Example: TFL-Y678QNFB
accept
booleanrequired
bodytrue accepts, false declines.
Example: true
reason
stringoptional
bodyWhy the quote is declined.

Errors

  • 409no_quoteNothing is waiting for an answer.
  • 409quote_expiredAsk again for a fresh quote.
  • 409not_settledThe airline quoted but settlement failed; our desk has it.
  • 402insufficient_fundsDeposit too low; nothing charged. balance and shortfall are included.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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
}'
Response · 200
{
  "request": {
    "…": "…"
  },
  "booking": {
    "…": "…"
  }
}
Error · 409
{
  "error": {
    "code": "no_quote",
    "message": "Nothing is waiting for an answer."
  }
}

Deposit

Read the deposit balance

GET/account/balance

Check 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

  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveThe key was revoked or expired; issue a new one.
  • 403ip_whitelist_requiredAdd your server's public IP to the key.
  • 403ip_not_whitelistedThis server's IP is not on the key; GET /ping from it shows the IP.
  • 403product_not_allowedThe key is not enabled for flights.
  • 403partner_inactiveYour agency account is suspended or pending.
  • 403subscription_requiredAPI plan not active; see API access in the portal.
  • 403subscription_pendingAPI plan not active; see API access in the portal.
  • 403subscription_suspendedAPI plan not active; see API access in the portal.
  • 403subscription_cancelledAPI plan not active; see API access in the portal.
  • 403environment_not_activatedLive keys work after certification.
  • 429rate_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"
Response · 200
{
  "partner_code": "PTR-000020",
  "currency": "BDT",
  "balance": {
    "amount": 20000000,
    "currency": "BDT"
  }
}
Error · 401
{
  "error": {
    "code": "unauthenticated",
    "message": "Headers missing, or key or secret wrong."
  }
}