Developer Docs

REST API Reference

Everything you need to integrate FDFares into your platform: live flight and hotel search, booking, and management, over a clean JSON REST API.

Base URL

https://fdfares.com/api/v1

Environments

Production API URL: https://fdfares.com/api/v1
UAT Environment (Sandbox) API URL: https://sandbox.fdfares.com/api/v1

• All endpoints require HTTPS• API version: v1• Format: JSON

Quick Start

You'll need an API key from the Partner Portal. A flight booking always follows the same three steps — search for fares, review one to lock in a price and get a booking_session, then book using that booking_session directly. Nothing about pricing is ever supplied by the client — the server always resolves and re-validates it server-side.

quickstart.js
const API_KEY = 'FD_lv_your_key_here'
const BASE = 'https://fdfares.com/api/v1'

// 1. Search
const search = await fetch(`${BASE}/flights/search`, {
  method: 'POST',
  headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    origin: 'DEL', destination: 'BOM',
    travel_date: '2026-09-20', trip_type: 'one_way', adults: 1
  })
}).then(r => r.json())

const flight = search.data.results.onward.results[0]

// 2. Review — locks in the fare and returns a booking_session
const review = await fetch(`${BASE}/flights/review`, {
  method: 'POST',
  headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ inventory_ids: [flight.inventory_id], adults: 1 })
}).then(r => r.json())

// 3. Book — pass the booking_session straight through, never a raw booking id
const booking = await fetch(`${BASE}/bookings/instant`, {
  method: 'POST',
  headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    booking_session: review.data.booking_session,
    passengers: [{ type: 'adult', title: 'Mr', first_name: 'Jane', last_name: 'Doe' }],
    delivery_emails: ['jane@example.com'],
    delivery_contacts: ['9999999999'],
    contact_info: { emails: ['jane@example.com'], contacts: ['9999999999'], name: 'Jane Doe' }
  })
}).then(r => r.json())

console.log(booking.data.booking_no, booking.data.status)

Or with curl:

quickstart.sh
curl -X POST https://fdfares.com/api/v1/flights/search \
  -H "x-api-key: FD_lv_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "origin": "DEL",
    "destination": "BOM",
    "travel_date": "2026-09-20",
    "trip_type": "one_way",
    "adults": 1
  }'

Authentication

Every request under /api/v1 requires an API key, sent as an x-api-key header — not a Bearer token. Keys look like FD_lv_<32 hex characters>. Only a hash of your key is stored on our side; the raw key is shown once, at creation time, in the Partner Portal — copy it then, it can't be retrieved again.

Header
x-api-key: FD_lv_d7ffc8a91b2e4f0c9a8d7e6f5c4b3a2d

Module scoping

Each key is provisioned with access to specific modules. A request outside a key's granted modules is rejected with MODULE_NOT_ALLOWED.

flights_domesticSearch, review and book flights within India.
flights_internationalSearch, review and book flights that cross an international border.
hotelsEvery endpoint under /hotels.
busesReserved for a future bus API.
packagesReserved for a future holiday-packages API.

IP allow-listing

Optionally restrict a key to specific IPs or CIDR ranges in the Partner Portal. A request from outside the allow-list is rejected with IP_NOT_ALLOWED.

Auth error responses

401MISSING_API_KEYx-api-key header is required
401INVALID_API_KEYAPI key is invalid, inactive, or expired
403IP_NOT_ALLOWEDCaller IP is not in the whitelist configured for this key
403MODULE_NOT_ALLOWEDThis API key does not have access to the module this request needs

Rate Limits

Every request is rate-limited per API key (falling back to IP if no key is present). Booking endpoints carry an additional, tighter limit on top of the general one. Every response includes RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers.

ScopeWindowMax requests
All of /api/v160s100
/bookings/* (on top of the above)60s20
429 Too Many Requests
{
  "success": false,
  "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests — see Retry-After / RateLimit-* headers" }
}

Errors

Every error — auth failures, validation errors, and booking-system failures alike — is JSON in the same envelope. Validation errors use a messages array instead of a single message.

Standard error
{
  "success": false,
  "error": {
    "code": "STRING_CODE",
    "message": "Human-readable description",
    "reason": "optional — a stable machine identifier",
    "details": [ /* optional — present when the airline or hotel system reported multiple errors at once */ ]
  }
}
Validation error
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "messages": [
      "origin is required",
      "travel_date must be in YYYY-MM-DD format"
    ]
  }
}

How status codes are chosen

TriggerStatuserror.code
Search result or review session expired422SEARCH_RESULT_EXPIRED
Flight/room no longer available410FLIGHT_UNAVAILABLE
Fare or price expired400FARE_EXPIRED
Other rejection from the airline or hotel system (400)400see error reference table below
Internal authentication failure401PROVIDER_AUTH_ERROR
Other failure from the airline or hotel system502see error reference table below
The airline or hotel system did not respond in time504PROVIDER_TIMEOUT
Request body failed validation400VALIDATION_ERROR
Anything unhandled500INTERNAL_ERROR

Error code reference

When a search or booking is rejected by the airline or hotel system itself, the response carries one of these stable codes in error.code (with the numeric id in error.reason). Anything not listed here falls back to PROVIDER_ERROR.

Availability & session — usually resolved by searching again

1001FLIGHT_UNAVAILABLEFlight is no longer available. Please search again.
1002FARE_EXPIREDFare has expired. Please search again.
1003SESSION_NOT_FOUNDBooking session not found. Please search again.
1004HOLD_EXPIREDHold time limit has expired. Please re-book.
1005FARE_UNAVAILABLEFare is no longer available. Please search again.
1006FARE_MISMATCHPayment amount does not match the fare total. Please retry.
1007DUPLICATE_BOOKINGA booking for this flight already exists.
1008SEAT_UNAVAILABLESeat selection is not available for this flight.
1009INVALID_FARE_TYPEChildren and infants cannot be booked on a student/senior-citizen fare.

Passenger data

2001INVALID_PAX_COUNTInfants/children exceed adults, or total passengers exceed 9.
2002INVALID_PASSENGERName formatting issue or missing document id.
2003DUPLICATE_PASSENGERTwo passengers share the same name.
2004INVALID_PASSENGER_AGEAge is out of the allowed range for this passenger type / fare.
2005MISSING_DOBDate of birth is required and was not supplied.
2006INVALID_TITLETitle must be Mr/Mrs/Ms/Master/Miss as applicable.
2007INVALID_PANThe PAN number entered is invalid.

Travel documents

3001MISSING_PASSPORTA passport is required for this booking.
3002INVALID_PASSPORTPassport number, expiry, or issue date is invalid (includes expiry-within-6-months checks).

Route & dates

4001INVALID_TRAVEL_DATES
4002INVALID_ROUTE

Booking system

5001PROVIDER_ERRORGeneric fallback for anything unmapped.
5002PROVIDER_AUTH_ERROR
5003PROVIDER_INACTIVE
5004PROVIDER_ACCESS_DENIED
5005PROVIDER_BAD_REQUEST
5006INVALID_ORDER_STATEThis booking cannot be processed in its current state. Please contact support.

Contact & GST

6001INVALID_GST
6002INVALID_CONTACT
6003MISSING_CONTACT

Amendment

7001AMENDMENT_UNSUPPORTED
7002AMENDMENT_EXISTS
7003INVALID_AMENDMENT
7004AMENDMENT_EXPIRED

Flights

Search live domestic and international fares, review a fare before booking, and pull fare rules or seat maps for a specific flight.

POST/flights/review

Review a fare before booking

Locks in the exact fare and passenger conditions for one or two inventory_ids from a search response — one for a one-way itinerary, two (onward first, then return) for a round trip. Returns a short-lived booking_session — this is the only handle you pass into every booking endpoint, never a raw inventory_id or fare id.

Body parameters

inventory_idsrequiredstring[]One id for a one-way itinerary. Two ids for a round trip — the first is always treated as onward, the second as return, regardless of which leg is cheaper or which airline operates it; onward and return may come from entirely different fares or airlines.
adultsinteger ≥ 1Defaults to 1.
childreninteger ≥ 0Defaults to 0.
infantsinteger ≥ 0Defaults to 0.

Response

200 OK
{
  "success": true,
  "data": {
    "booking_session": "a13f3869-32af-474d-9e37-522bf781762a",
    "session_expires_at": "2026-08-27T10:15:00.000Z",
    "onward": {
      "inventory_id": "0af7304d-d7cd-506d-82af-aef2a20f5070",
      "journey": {
        "origin": "DEL",
        "destination": "BOM",
        "departure_time": "2026-09-15T08:10:00+05:30",
        "arrival_time": "2026-09-15T10:20:00+05:30",
        "duration_minutes": 130,
        "stops": 0,
        "segments": [ "… identical shape to a /flights/search result's journey.segments …" ]
      },
      "fare_product": {
        "fare_basis": "Y26ID", "booking_class": "Y", "cabin_class": "ECONOMY",
        "branded_fare": null, "branded_fare_label": null,
        "refundable": 1, "meal_included": false, "seat_selection": null,
        "seats_remaining": 6,
        "baggage": { "check_in_kg": 15, "check_in_pieces": null, "cabin_kg": 7, "cabin_pieces": null, "raw": "15Kg" },
        "infant_baggage": null
      },
      "seats": { "adults": 1, "children": 0, "infants": 0 },
      "price": {
        "currency": "INR",
        "per_adult":  { "base_fare": 4200, "taxes": 1259, "total": 5459 },
        "per_child":  null,
        "per_infant": null
      },
      "conditions": {
        "hold_allowed": true,
        "passport": { "mandatory": false, "expiry_required": false, "issue_date_required": false },
        "dob": { "adult_required": false, "child_required": true, "infant_required": true },
        "gst": { "applicable": true, "mandatory": false },
        "document_id": { "applicable": false, "mandatory": false }
      },
      "fare_rules": {
        "cancellation": [
          { "amount": 3500, "additional_fee": 115, "policy_info": null, "start_hours": 4, "end_hours": 96, "policy_period": null, "fare_components": null }
        ],
        "date_change": [
          { "amount": 3000, "additional_fee": 100, "policy_info": "+ Fare Difference if any", "start_hours": 4, "end_hours": 8760, "policy_period": null, "fare_components": null }
        ],
        "no_show": [
          { "amount": 0, "additional_fee": 0, "policy_info": "If Cancelled within 4 hrs of scheduled departure only statutory taxes will be Refunded.", "start_hours": 0, "end_hours": 4, "policy_period": null, "fare_components": null }
        ],
        "seat_chargeable": [
          { "amount": 0, "additional_fee": 0, "policy_info": "Paid Seat", "start_hours": 0, "end_hours": 8760, "policy_period": null, "fare_components": null }
        ],
        "misc_info": null
      }
    },
    "return": { "…": "same shape as onward — present only when a second (return) inventory_id was included in the request" },
    "total_price": {
      "currency": "INR",
      "net_total": 5459,
      "base_fare": 4200,
      "taxes": 1259,
      "adults":   { "traveller": 1, "base_fare": 4200, "taxes": 1259, "net_total": 5459 },
      "children": null,
      "infants":  null
    }
  }
}
  • booking_session is opaque and valid for roughly 15 minutes (see session_expires_at — the shorter of the two legs' own expiry, for a round trip; the same expiry covers both legs, so it's reported once, not per leg). Pass booking_session alone into /bookings/instant or /bookings/hold — no inventory_id or per-leg ids needed.
  • "return" is present only for a round trip. Onward and return are reviewed independently, so they can each end up on a different fare or airline without you doing anything differently.
  • onward.conditions.hold_allowed tells you whether book_type: "hold" is available for this specific fare — not every fare supports holding, and round trips book instantly regardless (see /bookings/hold below).
  • conditions.dob / conditions.passport tell you which passenger fields are mandatory for this fare before you call the booking endpoint.
  • fare_rules is the same shape as GET /flights/fare-rules's per-route entry — saves that extra call right after reviewing, since the fare data is already available at this point. Can be null on rare fares with no rules on file.
  • inventory_id/journey/fare_product are the same full flight details a /flights/search result already carried for this fare — repeated here so you can render/confirm the itinerary from the review response alone, without holding onto the original search result.
  • fare_product.seats_remaining reflects the review's own fresher count (from re-checking availability), which can differ from what search showed — this is the one to trust.
  • onward.seats / return.seats echo the adults/children/infants this review actually covers.
  • onward.price / return.price are per-ONE-passenger figures for that leg only — never qty-multiplied. Don't sum these across legs yourself.
  • The top-level total_price is the one figure to actually charge/display as "Total Payable" — already combined across both legs (if a round trip) and scaled by pax count. adults/children/infants each carry traveller (the count) plus that ONE passenger's price (same convention as onward.price); net_total is already the full combined total, no math needed on top of it.
  • "taxes" (both onward.price and total_price) already includes any service fee configured on your account — no separate service_fee/gst_on_service_fee field to add on top.

Possible errors

400VALIDATION_ERRORinventory_ids missing, empty, over 6 entries, or invalid pax counts.
404REQUEST_ERROROne or more inventory_ids were not found or have expired — search again.
410FLIGHT_UNAVAILABLEThe flight could no longer be reviewed even after a refresh attempt — search again.
GET/flights/fare-rules/{inventoryId}

Get fare rules

Cancellation, date-change and no-show rules for a specific fare. If you've already called POST /flights/review for this fare, you don't need this — the same data is included there as fare_rules, no extra call required.

Path parameters

inventoryIdrequiredstringThe inventory_id from a /flights/search result.

Response

200 OK
{
  "success": true,
  "data": {
    "routes": {
      "DEL-BOM": {
        "cancellation": [
          { "amount": 3000, "additional_fee": 0, "policy_info": "Non-refundable within 24h of departure", "start_hours": 0, "end_hours": 24, "policy_period": "Departure -24h to 0h", "fare_components": null }
        ],
        "date_change": [
          { "amount": 2000, "additional_fee": 0, "policy_info": "Subject to fare difference", "start_hours": 0, "end_hours": 9999, "policy_period": "Any time before departure", "fare_components": null }
        ],
        "no_show": [
          { "amount": 5432, "additional_fee": 0, "policy_info": "Full fare forfeited on no-show", "start_hours": 0, "end_hours": 0, "policy_period": "At departure", "fare_components": null }
        ],
        "seat_chargeable": null,
        "misc_info": "Fare rules are subject to airline discretion and may change without notice."
      }
    }
  }
}
  • Cached for 30 minutes — repeated calls for the same inventory_id may return a slightly stale (but still valid) snapshot.
  • routes can rarely come back empty ({}) if there's genuinely no fare-rule data on file for this specific fare — not an error, just nothing to show.

Possible errors

404REQUEST_ERRORinventory_id not found or expired — search again.
GET/flights/seat-map/{bookingId}

Get seat map

Seat availability and pricing for a flight tied to an existing booking.

Path parameters

bookingIdrequiredstringYour own booking_id (the UUID from /bookings/instant or /bookings/hold's response).

Response

200 OK
{
  "success": true,
  "data": {
    "onward": {
      "segments": {
        "DEL-BOM-6E123": {
          "rows": 30, "columns": 6, "notes": null,
          "seats": [
            { "seat_no": "12A", "row": 12, "column": "A", "is_booked": false, "is_legroom": false, "is_aisle": true, "is_exit_row": false, "code": "STD", "amount": 250 }
          ]
        }
      }
    },
    "return": {
      "segments": {
        "BOM-DEL-6E124": {
          "rows": 30, "columns": 6, "notes": null,
          "seats": [
            { "seat_no": "14C", "row": 14, "column": "C", "is_booked": false, "is_legroom": false, "is_aisle": true, "is_exit_row": false, "code": "STD", "amount": 250 }
          ]
        }
      }
    }
  }
}
  • A one-way booking only ever has "onward" — "return" is present only for a round trip.
  • Each leg is fetched independently, so if one leg simply doesn't offer seat selection while the other does, you still get the leg that's available rather than the whole call failing.
  • Cached for 2 minutes per leg.

Possible errors

404REQUEST_ERRORNo booking found for that booking_id.
400SEAT_UNAVAILABLESeat selection isn't offered on any leg of this booking — a normal, expected response for some flights/fares, not an error on your part.

Flight Bookings

Book a reviewed fare instantly or on hold, then ticket, confirm, retrieve, or cancel it.

Every endpoint below operates on your own FDFares booking_id. For /instant and /hold, the exact amount charged is always calculated server-side — never trust or send a client-side price.
POST/bookings/instant

Book instantly

Confirms and tickets a reviewed fare immediately. Takes the booking_session from /flights/review — never a raw booking id or inventory_id. The amount charged is always resolved and validated server-side, never trusted from the client. For a round trip, booking_session already carries both legs, so this one call books both.

Body parameters

booking_sessionrequiredstringFrom the matching /flights/review response. Carries both legs for a round trip — nothing else identifies the flight(s).
passengersrequiredPassengerInput[]One entry per traveller — see below. No two passengers may share the same first + last name.
passengers[].typerequired"adult" | "child" | "infant"
passengers[].titlerequired"Mr" | "Mrs" | "Ms" (adult) or "Ms" | "Master" (child/infant)Enforced per passenger type.
passengers[].first_namerequiredstring
passengers[].last_namerequiredstring
passengers[].date_of_birthstring (YYYY-MM-DD)Required when the review response's conditions.dob says so for this passenger type.
passengers[].passport_numberstringRequired when conditions.passport.mandatory is true.
passengers[].passport_expirystringRequired when conditions.passport.expiry_required is true.
passengers[].passport_nationalitystring
passengers[].passport_issue_datestringRequired when conditions.passport.issue_date_required is true.
passengers[].pan_numberstring
passengers[].document_idstringRequired when conditions.document_id.mandatory is true.
passengers[].frequent_flyerobject (carrier code → number)
passengers[].ssr_baggage / ssr_meals / ssr_seats / ssr_extra{ segment_key, code }[]Ancillary selections, matched against the codes returned in search/review.
delivery_emailsrequiredstring[]At least one. Used for the booking confirmation email.
delivery_contactsrequiredstring[]At least one. Falls back to contact_info.contacts if omitted.
contact_info{ emails: string[], contacts: string[], name: string }Required if delivery_emails is omitted — its emails are used instead.
gst_info{ gst_number, registered_name, mobile?, email?, address? }Attaches a business GST number to the booking. gst_number must be exactly 15 characters.

Response

200 OK
{
  "success": true,
  "data": {
    "booking_id": "df1c5416-7dea-4db8-94e2-b7b21064c23f",
    "booking_no": "TH-MTANQRD3-U2WR",
    "status": "confirmed"
  }
}
  • status is one of "confirmed" or "pending" (rarely returned as a final status — the endpoint waits for ticketing to complete before responding) — a hard failure is returned as an error instead, never as status: "failed".
  • booking_no (the "TH-…" reference) is a human-readable reference for this booking — for anything else in the API that needs to identify this booking (seat map, confirm, cancel, etc.), use booking_id instead, not booking_no.
  • For a round trip, the response instead carries "onward": { "status": "confirmed" } and "return": { "status": "confirmed" } alongside booking_id/booking_no — one call books both legs. If the return leg fails after the onward leg is already ticketed, you get "partial_failure": true and a message instead — a real partial outcome, not an error response.
  • The wallet is only debited once the booking is confirmed.

Possible errors

400VALIDATION_ERRORMissing/invalid booking_session, passengers, or contact fields — see error.messages[].
402INSUFFICIENT_WALLET_BALANCEWallet balance (or available credit) is lower than the reviewed fare total.
409ALREADY_BOOKEDThis exact fare has already been booked — check your bookings list instead of retrying.
410SEARCH_RESULT_EXPIREDThe booking_session has expired — call /flights/review again.
400(mapped, see Errors reference)A booking rejection from the airline — fare changed, invalid passenger data, etc.
POST/bookings/hold

Book on hold

Same request shape as instant booking, but places the fare on hold instead of ticketing immediately — only when the reviewed fare's conditions.hold_allowed was true. Confirm and ticket it later with /bookings/confirm-fare followed by /bookings/confirm, before the hold deadline.

Round-trip bookings always book instantly regardless of book_type — not every fare on a two-leg itinerary supports hold, so round trips skip it entirely.

Body parameters

(all fields)requiredIdentical request body to POST /bookings/instant — see above.

Response

200 OK
{
  "success": true,
  "data": {
    "booking_id": "…",
    "booking_no": "TH-…",
    "status": "on_hold"
  }
}

Possible errors

409HOLD_NOT_ALLOWEDThe reviewed fare does not support holding — book instantly instead.
402INSUFFICIENT_WALLET_BALANCEThe hold amount is charged (refundable) at hold time, same as an instant booking.
POST/bookings/confirm-fare

Re-validate a held fare

Checks whether a held fare is still valid (and at what price) before ticketing it. A read-only step — nothing is charged here.

Body parameters

booking_idrequiredstringYour FDFares booking_id from /bookings/hold.

Response

200 OK
{
  "success": true,
  "data": {
    "booking_id": "…",
    "success": true,
    "alerts": [],
    "errors": [],
    "conditions": { "is_hold_allowed": true, "session_valid_seconds": 900 },
    "total_price": { "base_fare": 4200, "taxes": 1259, "total": 5459, "net_fare": 4100, "commission": 100, "currency": "INR" }
  }
}
  • total_price is only included when the fare actually changed since the hold was placed.
  • The response's own "booking_id" field is a separate, internal value — not the booking_id you send as input.

Possible errors

404REQUEST_ERRORNo booking found for that booking_id.
POST/bookings/confirm

Confirm and ticket a held booking

Pays and finalises a held booking. The payment amount is never taken from the client — the server always re-validates the fare first and pays exactly what's quoted at that moment (falling back to the amount recorded at hold time only if no change is reported).

Body parameters

booking_idrequiredstringYour FDFares booking_id from /bookings/hold.

Response

200 OK
{
  "success": true,
  "data": {
    "bookingRef": "TH-…",
    "bookingId": "…",
    "status": "confirmed",
    "errors": [],
    "message": "Ticketing submitted."
  }
}
  • The response's own "bookingRef"/"bookingId" fields describe the ticketing result — not the booking_id you send as input.

Possible errors

404REQUEST_ERRORNo booking found for that booking_id.
409REQUEST_ERRORFare changed or is no longer available for this booking.
502PROVIDER_ERRORNo usable fare figure was returned for confirmation.
GET/bookings/{bookingId}/details

Get booking details / PNR

Retrieves the full booking record, including the PNR and ticket numbers. Also syncs FDFares's own stored booking status from the result.

Path parameters

bookingIdrequiredstringYour FDFares booking_id (the UUID from /bookings/instant or /bookings/hold's response).

Query parameters

leg"onward" | "return"Which leg of a round-trip booking to fetch. Defaults to onward.
paxPricing"true" | omittedInclude a per-passenger price breakdown.

Response

200 OK
{
  "success": true,
  "data": {
    "booking_id": "…",
    "order_status": "SUCCESS",
    "amount_charged": 5459,
    "order_note": null,
    "delivery_info": { "emails": ["a@b.com"], "contacts": ["+919999999999"] },
    "trip_infos": [
      {
        "segments": [ "… same shape as a search result's journey.segments …" ],
        "price_list": [
          {
            "price_id": "…", "fare_identifier": "STANDARD", "account_code": null,
            "adult_fare": { "base_fare": 4200, "taxes": 1259, "total": 5459, "net_fare": 4100, "commission": 100 },
            "child_fare": null, "infant_fare": null,
            "seats_remaining": 6, "plating_carrier": "6E",
            "fare_rule_info": { "…": "same shape as fare_rules on POST /flights/review — null if there's no fare-rule data on file for this booking" }
          }
        ]
      }
    ],
    "total_price": { "base_fare": 4200, "taxes": 1259, "total": 5459, "net_fare": 4100, "commission": 100, "currency": "INR" },
    "travellers": [
      {
        "title": "Mr", "first_name": "John", "last_name": "Doe", "pax_type": "ADULT",
        "pnr_details": { "6E": "ABCDEF" }, "gds_pnrs": {}, "ticket_numbers": { "6E": "1234567890" },
        "status_map": {}, "ssr_baggage": {}, "ssr_meals": {}, "ssr_seats": {}
      }
    ],
    "gst_info": null,
    "time_limit": null
  }
}
  • pnr_details / ticket_numbers are keyed by carrier code — this is where the actual PNR lives.

Possible errors

404REQUEST_ERRORNo booking found for that booking_id, or leg=return was requested on a booking with no return leg.
POST/bookings/release-pnr

Release a held PNR

Body parameters

booking_idrequiredstringYour FDFares booking_id.
pnrsrequiredstring[]PNR codes to release, from the booking details response.

Response

200 OK
{
  "success": true,
  "data": { "…": "raw response from the airline system — shape varies by carrier, treat as opaque" }
}
  • On success, the corresponding booking leg is marked cancelled in your booking history.

Possible errors

404REQUEST_ERRORNo booking found for that booking_id.
GET/bookings/{bookingId}/amendment-charges

Get cancellation / amendment charges

Path parameters

bookingIdrequiredstringYour FDFares booking_id.

Query parameters

remarksrequiredstringA short reason for the amendment — required by the airline.
leg"onward" | "return"Which leg of a round-trip booking to get charges for. Defaults to onward.

Response

200 OK
{
  "success": true,
  "data": {
    "booking_id": "…",
    "errors": [],
    "trips": [
      {
        "origin": "DEL", "destination": "BOM", "departure_date": "2026-09-15",
        "flight_numbers": ["6E123"], "airlines": ["6E"],
        "pax_charges": { "adult": { "amendment_charges": 500, "refund_amount": 3700, "total_fare": 5459 } },
        "traveller_charges": [ { "first_name": "John", "last_name": "Doe", "amendment_charges": 500, "refund_amount": 3700, "total_fare": 5459 } ]
      }
    ]
  }
}

Possible errors

404REQUEST_ERRORNo booking found for that booking_id, or leg=return was requested on a booking with no return leg.
POST/bookings/cancel

Cancel a booking

Body parameters

booking_idrequiredstringYour FDFares booking_id.
remarksrequiredstringReason for cancellation.
tripsarrayScope the cancellation to specific legs of a multi-leg itinerary. Passed through as-is.
leg"onward" | "return"Which leg of a round-trip booking to cancel. Defaults to onward — cancelling both legs of a round trip currently takes two separate calls.

Response

200 OK
{
  "success": true,
  "data": { "…": "raw response from the airline system — treat as opaque" }
}
  • If the wallet was actually debited for this booking, the fare minus the cancellation fee is automatically refunded to your wallet on a successful cancellation.
  • This submits a cancellation request to the airline — it is not necessarily instant. Poll GET /bookings/{bookingId}/details or the amendment endpoints for the final outcome.

Possible errors

404REQUEST_ERRORNo booking found for that booking_id, or leg=return was requested on a booking with no return leg.
GET/bookings/amendment/{amendmentId}

Get amendment status

Path parameters

amendmentIdrequiredstringReturned by the cancel call.

Response

200 OK
{
  "success": true,
  "data": {
    "booking_id": "…",
    "amendment_id": "…",
    "status": "PROCESSED",
    "amendment_charges": 500,
    "refundable_amount": 3700,
    "total_fare": 5459,
    "trips": [
      {
        "origin": "DEL", "destination": "BOM", "departure_date": "2026-09-15",
        "travellers": [ { "first_name": "John", "last_name": "Doe", "amendment_charges": 500, "refund_amount": 3700, "total_fare": 5459 } ]
      }
    ]
  }
}

Possible errors

404REQUEST_ERRORNo amendment found for that id.

Hotels

Search hotel availability by city, drill into room-level pricing for one property, then book, hold, or cancel a room.

GET/hotels/cities

Search destinations

Free-text city autocomplete, used to obtain a city_id for /hotels/search.

Query parameters

qrequiredstringFree-text city name.
countrystringNarrow results to one country — a 2-letter ISO 3166-1 code ("IN") or a full country name ("India").
limitnumberDefaults to 20, capped at 50.

Response

200 OK
{
  "success": true,
  "data": {
    "results": [
      { "id": "c05ac8fa-8474-4775-87d3-3ebda2c21b51", "city": "Mumbai", "country": "IN", "full_name": "Mumbai, Maharashtra, India" }
    ]
  }
}
  • id is what you pass into /hotels/search as city_id.
  • country is an ISO 3166-1 alpha-2 code — /hotels/search's own country parameter also accepts the full country name if you prefer.

Possible errors

400VALIDATION_ERRORcountry isn't a recognized ISO 3166-1 code or country name.
POST/hotels/detail

Get room options for a hotel

Body parameters

search_idrequiredstringFrom the matching /hotels/search response.
listing_idrequiredstringA listing_id from that same search response — not a raw hotel id.

Response

200 OK
{
  "success": true,
  "data": {
    "hotel_id": "3f9e2c1a-8b7d-4e6f-9a2c-1b8d7e6f9a2c",
    "hotel_name": "Grand Regency Palace",
    "review_hash": "opaque-hash-pass-through-as-is",
    "options": [
      {
        "option_id": "opt_9f3a2c1e",
        "option_type": "STANDARD",
        "room_info": [ { "id": "rm_1", "name": "Deluxe Room", "adults": 2, "children": 0 } ],
        "inclusions": ["Free WiFi", "Breakfast included"],
        "meal_basis": "Room Only",
        "booking_notes": null,
        "pricing": {
          "total": 24500, "base": 20000, "discount": 0, "taxes": 4500,
          "management_fee": 0, "management_fee_tax": 0,
          "service_fee": 0, "gst_on_service_fee": 0,
          "currency": "INR", "strikethrough": 27000, "gst_claimable": 0
        },
        "commercial": { "type": "STANDARD", "commission": 0 },
        "compliance": { "gst_type": "REGULAR", "pan_required": false, "passport_required": false },
        "cancellation": { "isRefundable": true, "penalties": [ { "from": "2026-09-18", "to": "2026-09-20", "amount": 5000 } ] },
        "rooms_left": 3,
        "deadline": "2026-09-19T18:00:00Z"
      }
    ]
  }
}
  • hotel_id is FDFares's own internal hotel identifier — use it in /hotels/review, never a raw property id.
  • If there's no availability at all for the requested dates/rooms, this still returns 200 with options: [] rather than an error.

Possible errors

400VALIDATION_ERRORsearch_id or listing_id missing.
410REQUEST_ERRORThe search session expired, or this listing_id is no longer part of your search — search again.
502(mapped, see Errors reference)A live error on this specific hotel.
POST/hotels/review

Review a room option

Locks in a specific room option for booking and returns a real booking_id — unlike the flights side, the hotel booking flow gives you this id directly, so /hotels/book can be called right away with it.

Body parameters

search_idrequiredstring
hotel_idrequiredstringThe hotel_id from /hotels/detail — FDFares's own id, not the property's id.
option_idrequiredstringFrom a specific entry in /hotels/detail's options[].
review_hashrequiredstringFrom /hotels/detail.

Response

200 OK
{
  "success": true,
  "data": {
    "hotel_id": "3f9e2c1a-8b7d-4e6f-9a2c-1b8d7e6f9a2c",
    "hotel_name": "Grand Regency Palace",
    "booking_id": "HTL-9F3A2C1E-BOOK",
    "hold_allowed": true,
    "option": { "…": "same shape as a /hotels/detail options[] entry, re-priced" }
  }
}
  • This booking_id is what you pass into POST /hotels/book. It's tied to a 10-minute booking session — book before it expires.

Possible errors

400VALIDATION_ERRORsearch_id, hotel_id, option_id, or review_hash missing.
400REQUEST_ERRORhotel_id did not resolve — search again.
409OPTION_SOLD_OUTThe room option sold out between detail and review.
410REQUEST_ERRORThe search session expired.
POST/hotels/book

Book a room

Submits the booking. This call polls for a terminal status before responding — allow up to 3 minutes.

delivery_codes is validated as required, but delivery numbers are currently always sent with the India country code regardless of what you pass — international delivery contact numbers aren't supported yet for hotels.

Body parameters

booking_idrequiredstringFrom /hotels/review.
book_typerequired"instant" | "hold""hold" is rejected if the reviewed option doesn't support it.
roomsrequired{ travellers: Traveller[] }[]One entry per room, matching the room count from review.
rooms[].travellers[].titlerequiredstring
rooms[].travellers[].typerequired"ADULT" | "CHILD"
rooms[].travellers[].first_namerequiredstring
rooms[].travellers[].last_namerequiredstring
rooms[].travellers[].panstringRequired for the lead guest only, when the reviewed option's compliance.pan_required is true.
rooms[].travellers[].passport_numberstringRequired for every guest when compliance.passport_required is true.
delivery_emailsrequiredstring[]At least one.
delivery_contactsrequiredstring[]At least one.
delivery_codesrequiredstring[]Country dial codes for delivery_contacts.

Response

200 OK
{
  "success": true,
  "data": {
    "id": "internal-uuid",
    "booking_ref": "HT-LX3F9A2-K7QZ",
    "status": "confirmed"
  }
}
  • status is "confirmed", "on_hold", or "pending" — a hard failure comes back as an error instead.
  • id is FDFares's own booking id — pass it as booking_id into /hotels/confirm-book, /hotels/booking-details, and /hotels/cancel/{bookingId}.
  • booking_ref is a human-readable reference for this booking (shown in your bookings list) — it isn't an input to any endpoint.

Possible errors

400GUEST_INFO_INCOMPLETEA required PAN or passport field is missing.
402INSUFFICIENT_WALLET_BALANCEInstant bookings only — hold bookings aren't charged at booking time.
409HOLD_NOT_ALLOWEDbook_type was "hold" but the reviewed option doesn't support it.
409REQUEST_ERRORThe booking failed after submission — nothing was charged.
410REQUEST_ERRORThe 10-minute review/booking session expired — call /hotels/review again.
POST/hotels/confirm-book

Confirm a held booking

Pays and finalises a booking made with book_type: "hold", before its hold deadline. Also polls for a terminal status — allow up to 3 minutes.

Body parameters

booking_idrequiredstringThe id from /hotels/book's response — not booking_ref.

Response

200 OK
{
  "success": true,
  "data": { "id": "internal-uuid", "booking_ref": "HT-…", "status": "confirmed" }
}

Possible errors

402INSUFFICIENT_WALLET_BALANCE
404REQUEST_ERRORbooking_id not found.
409REQUEST_ERRORThe booking isn't currently on_hold, or confirmation didn't complete successfully — nothing was charged.
POST/hotels/booking-details

Get booking details

Body parameters

booking_idrequiredstringThe id from /hotels/book's response — not booking_ref.

Response

200 OK
{
  "success": true,
  "data": { "…": "raw booking-details response from the property/booking system — shape not fixed, treat as opaque and inspect a live response for the fields you need" }
}
POST/hotels/cancel/{bookingId}

Cancel a booking

Unlike flight cancellation, this endpoint does not automatically refund the wallet. If a refund is owed, confirm the amount and timing with your account manager until that's wired up.

Path parameters

bookingIdrequiredstringThis is FDFares's own booking id (the "id" from /hotels/book's response) — not booking_ref.

Response

200 OK
{
  "success": true,
  "data": { "id": "internal-uuid", "booking_ref": "HT-…", "status": "cancelled" }
}

Possible errors

404REQUEST_ERRORNo booking found for that booking_id.
POST/hotels/static-detail

Get static hotel content

Property-level content — name, star rating, address, images — independent of live pricing.

This endpoint currently expects a raw property id, unlike every other hotel endpoint (which uses FDFares's internal hotel_id). Static content by our own hotel_id isn't available through the public API yet — treat this endpoint as provisional.

Body parameters

hotel_idrequiredstring

Response

200 OK
{
  "success": true,
  "data": { "…": "raw static content — name, star rating, address, coordinates, images", "fromCache": true }
}
  • Cached for 24 hours server-side.

Changelog

v1.0.02026-08-27

Initial public reference for flight search, review and booking, and the full hotel search-to-booking flow.

Need an API key?

Become a partner to get access to your API keys and a dedicated test environment.

Become a Partner