Skip to content
Hotel API

Hotel API

Operational

One REST API finds, books and cancels hotel stays worldwide, paid from your FlightOTA deposit.

Version v1 · 12 endpoints

Examples use the sandbox base URL https://sandbox.flightota.com/api/v1. Sandbox stays go to hotel test systems: confirmations such as TestBooking are not real, the deposit is test money, and test systems are slower (paying a hold took about a minute). Unpaid sandbox holds lapse on their own.

Partner API — Hotels

One REST API finds, books and cancels hotel stays worldwide, paid from your FlightOTA deposit. Every example below is a real call made on the sandbox on 22 Sep 2026. The flights API uses the same keys and conventions.

Getting started

EnvironmentBase URLKey prefixUse
Sandboxhttps://sandbox.flightota.com/api/v1ak_test_Build and certify. Hotel test systems, test deposit
Productionhttps://api.flightota.com/api/v1ak_live_Real stays, real money. Switched on after certification
  1. Get a key in the agent portal under API access, enabled for hotels. The secret is shown once.
  2. Whitelist your server's public IP on the key. Never call the API from a browser or app.
  3. Send the two headers on every request:
http
X-Api-Key: ak_test_your_key_id
X-Api-Secret: <your secret>
Accept: application/json
Content-Type: application/json

4. Call GET /ping first. It returns your agency code and the IP we saw.

Conventions

  • Money is {"amount": <integer minor units>, "currency": "BDT"}: 1900946 is 19,009.46 BDT. Always your billing currency. 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.
  • Dates are YYYY-MM-DD; check-in and check-out are hotel-local days. Other times (expires_at, hold_expires_at, cancellation windows) are UTC, RFC 3339.
  • IDs are ours: loc_… destination, srh_… search, ofr_… hotel offer or room rate, TFH-XXXXXXXX booking (stays booked before 28 Sep 2026 keep their HT- reference). The hotel's own confirmation number is hotel_confirmation.
  • Unknown is null. refundable: null means the hotel did not say.
  • Ignore fields you do not know; new ones are added without notice.
  • Errors are {"error": {"code": "…", "message": "…"}}. Branch on code.

Booking flow

A stay takes five calls: find the destination, search hotels, list one hotel's rooms, re-check the chosen rate, book. A booking is either paid now (confirmed with the hotel at once) or held where the rate allows it, then paid before its deadline.

StepCallRequired
1GET /hotels/destinations?term=Recommended (or search by name)
2POST /hotels/searchYes
3GET /hotels/offers/{offer_id}/roomsYes
4GET /hotels/rates/{rate_id}/priceRecommended: tells you if the room can be held and what the guests must give
5POST /hotels/bookingsYes
6POST /hotels/bookings/{ref}/payFor a hold, before hold_expires_at
AfterGET …/cancellation, POST …/cancelWhen needed

Stay states

stateMeaningWhat you can do
heldRoom held, not paid; hold_expires_at setPay, or cancel (free)
pendingPaid; the hotel has not confirmed yetRead it again
confirmedThe hotel confirmed; hotel_confirmation setCancel under the hotel's rules
cancelledCancelled; cancellation shows charge and refundNothing
expiredA hold that was not paid in timeBook again
rejectedThe hotel declinedBook again

If a paid booking is not confirmed by the hotel straight away, the answer is requested and needs_attention: true: the money stays with us and our team confirms the stay or finds an alternative. It is never refunded automatically. There are no webhooks yet; read GET /hotels/bookings/{ref} to follow a stay.

Rooms and rate check

Rooms and rates of one hotel

GET /hotels/offers/{offer_id}/rooms. Each rate has its own offer_id: that is the rate_id you book. Sandbox: 171 rates for Royal Tulip in 10 s.

json
{
  "rooms_id": "srh_01m34q8qy2jd0tvbw50j887d1k",
  "expires_at": "2026-09-22T14:25:42Z",
  "currency": "BDT",
  "rates": [
    {
      "offer_id": "ofr_01m34qg0zz8d40ew1ab159b48f",
      "hotel_country": "AE",
      "total": {"amount": 1900946, "currency": "BDT"},
      "price": {
        "base": {"amount": 1900946, "currency": "BDT"},
        "per_night": {"amount": 950473, "currency": "BDT"},
        "taxes": {"amount": 0, "currency": "BDT"},
        "fees": {"amount": 0, "currency": "BDT"}
      },
      "room": {
        "name": "Standard Room",
        "image": "https://…/3e7bcff9_b.jpg",
        "beds": ["1 Double or 1 Twin"],
        "max_adults": null
      },
      "board_basis": "Room Only",
      "includes": [],
      "notes": [
        "Upon your arrival please provide valid government-issued ID to the hotel front desk…"
      ],
      "occupancy": {"adults": 2, "children": 1},
      "refundable": true,
      "free_cancellation": false,
      "pay_at_hotel": false,
      "deposit_required": false,
      "cancellation": [
        {
          "from": "2026-09-22T00:00:00Z",
          "until": "2026-10-24T19:59:00Z",
          "amount": {"amount": 0, "currency": "BDT"},
          "percent": null
        },
        {
          "from": "2026-10-24T20:00:00Z",
          "until": "2026-10-29T00:00:00Z",
          "amount": {"amount": 1900946, "currency": "BDT"},
          "percent": null
        }
      ],
      "booking_modes": ["hold", "confirm"],
      "hold_deadline": null
    }
  ]
}
  • board_basis: Room Only, Bed & Breakfast, Half Board, Full Board, All Inclusive.
  • cancellation lists what cancelling costs in each window: amount (or percent) charged if you cancel between from and until. An amount of 0 is free cancellation.
  • booking_modes: confirm means pay now and confirm; hold means the room may be held and paid later. Whether a hold is really possible is decided by the rate check below.
  • notes are the hotel's own conditions (tourism fees paid at the hotel, ID at check-in). Show them before booking.

Re-check a rate

GET /hotels/rates/{rate_id}/price asks the hotel again. Call it before collecting guest details.

json
{
  "status": "confirmed",
  "quoted": {"amount": 1900946, "currency": "BDT"},
  "rate": {
    "offer_id": "ofr_01m34qg0zz8d40ew1ab159b48f",
    "total": {"amount": 1900946, "currency": "BDT"},
    "…": "the full rate"
  },
  "hold": {"available": true, "deadline": "2026-10-23T14:29:00Z"},
  "requirements": {"passport_required": false, "pan_required": false, "pan_for_all_guests": false}
}
  • status: price_changed means rate.total is the new price and quoted the old one. Book with accept_total set to the new total to accept it.
  • hold.available: true means you may book with hold: true and pay by hold.deadline.
  • requirements.passport_required: the lead guest's passport number is needed. pan_required: an Indian lead guest must give a PAN; pan_for_all_guests: every Indian guest must.
  • 410 offer_expired means search again; 409 unavailable means the room is gone.

Book a stay

POST /hotels/bookings books one rate. The rate is priced again first; your request never names a price. Without hold, the stay is paid from your deposit and confirmed with the hotel in the same call (up to about a minute). With "hold": true (only where the rate check said hold.available), nothing is charged until you pay.

Send an Idempotency-Key header, unique per booking attempt. Repeating the request with the same key returns the booking already made. A rate can be booked only once: to book a second room of the same kind, fetch the rooms again for fresh rate ids.

Request fields

FieldRequiredNotes
hotel_offer_idYesThe hotel's offer_id from search
rate_idYesThe rate's offer_id from rooms
holdNotrue to hold and pay later, where allowed
rooms[]YesOne entry per searched room, in the same order
rooms[].guests[]YesEvery guest of that room: exactly the adults and children it was searched for
guests[].typeYesadult (12 or over on check-in) or child; as many of each as the room was searched for
guests[].titleYesMR, MRS, MS, MISS, MSTR
guests[].first_name, last_nameYesLetters, spaces, hyphens, apostrophes
guests[].nationalityNoISO code; defaults to the search nationality
guests[].date_of_birthNoYYYY-MM-DD; must fit adult or child on check-in. A child's must fit the age it was searched at: the room's first child is its first child_ages, and so on
guests[].passport_numberLead guest, when requirements.passport_required
guests[].passport_expiryNoMust be after check-out
guests[].pan_numberIndian lead guest when pan_required; every Indian guest when pan_for_all_guestsFormat ABCDE1234F. Never needed for other nationalities
contact.email, dial_code, mobileYesFor the hotel and the voucher. mobile digits only
contact.nameNoDefaults to the lead guest
special_requestsNoUp to 500 characters; passed to the hotel, not guaranteed
arrival_timeNoe.g. 23:00
accept_totalNoInteger minor units (currency units on a key with Amounts switched): accept a changed price up to this total
client_referenceNoYour own reference, up to 64 characters

Request

The sandbox call that made TFH-WD5BD2P4 (a hold):

http
POST /api/v1/hotels/bookings
Idempotency-Key: docs-hotel-001
json
{
  "hotel_offer_id": "ofr_01m34q7w1fmxw6tgp917fwnqrv",
  "rate_id": "ofr_01m34q8qy88vwza13395cqh6bn",
  "hold": true,
  "rooms": [
    {
      "guests": [
        {
          "type": "adult",
          "title": "MR",
          "first_name": "Arif",
          "last_name": "Rahman",
          "nationality": "BD",
          "passport_number": "A01234567"
        },
        {
          "type": "adult",
          "title": "MRS",
          "first_name": "Nusrat",
          "last_name": "Rahman",
          "nationality": "BD"
        },
        {
          "type": "child",
          "title": "MSTR",
          "first_name": "Ayan",
          "last_name": "Rahman",
          "nationality": "BD",
          "date_of_birth": "2019-09-22"
        }
      ]
    }
  ],
  "contact": {"email": "ops@agency.example", "dial_code": "880", "mobile": "1711000000"},
  "special_requests": "Late arrival, around 23:00",
  "client_reference": "DOCS-HOTEL-001"
}

Response 201 Created

Every stay call below returns this booking shape.

json
{
  "status": "held",
  "booking": {
    "booking_ref": "TFH-WD5BD2P4",
    "state": "held",
    "created_at": "2026-09-22T14:11:36Z",
    "confirmed_at": null,
    "hold_expires_at": "2026-10-23T14:29:00Z",
    "client_reference": "DOCS-HOTEL-001",
    "hotel_confirmation": null,
    "needs_attention": false,
    "total": {"amount": 1900946, "currency": "BDT"},
    "price": {
      "room_rate": {"amount": 1900946, "currency": "BDT"},
      "taxes": {"amount": 0, "currency": "BDT"}
    },
    "hotel": {
      "name": "Royal Tulip Hotel",
      "star_rating": 3,
      "address": "Opposite Delhi Resturant, Al Murar area Deira 116957 Dubai United Arab Emirates",
      "city": "Dubai",
      "country_code": "AE",
      "phone": null,
      "check_in_time": null,
      "check_out_time": null
    },
    "stay": {
      "check_in": "2026-10-27",
      "check_out": "2026-10-29",
      "nights": 2,
      "rooms": [{"adults": 2, "children": 1, "child_ages": [7]}],
      "nationality": "BD"
    },
    "room": {
      "name": "Standard Room",
      "board_basis": "Room Only",
      "refundable": true,
      "cancellation": [
        "Free cancellation until 24 Oct 2026",
        "From 24 Oct 2026 to 29 Oct 2026: 19,009.46 BDT charge"
      ]
    },
    "guests": [
      {
        "position": 1,
        "type": "adult",
        "title": "MR",
        "first_name": "ARIF",
        "last_name": "RAHMAN"
      },
      {
        "position": 2,
        "type": "adult",
        "title": "MRS",
        "first_name": "NUSRAT",
        "last_name": "RAHMAN"
      },
      {
        "position": 3,
        "type": "child",
        "title": "MSTR",
        "first_name": "AYAN",
        "last_name": "RAHMAN"
      }
    ],
    "special_requests": "Late arrival, around 23:00",
    "cancellation": null
  }
}
HTTPstatusMeaning
201confirmedPaid and confirmed by the hotel; hotel_confirmation set
201pendingPaid; confirmation is on its way. Read the booking
201heldHeld, nothing charged; pay before hold_expires_at
202requestedPaid, but the hotel did not confirm at once. Our team is on it (needs_attention: true)
200(no status)The same request repeated with the same key

When booking is refused

Nothing is charged in any of these.

HTTPerror.codeWhat to do
400idempotency_key_requiredAdd the header
422validation_failedSee errors; for example rooms.0.guests.0.pan_number
410offer_expiredSearch again
409price_changedtotal is the new price; send accept_total with it to accept
409room_unavailableSold out; choose another rate
409hold_unavailableThis rate cannot be held; book without hold
409hold_refusedThe hotel would not hold it now; book without hold or retry
402insufficient_fundsTop up your deposit
409rate_already_bookedThis rate id was already booked (as booking_ref); fetch rooms again
409duplicate_requestThe key was used with a different body
409offer_unavailableThe room cannot be booked now; choose another

Read, pay, cancel

Read one stay

GET /hotels/bookings/{booking_ref} returns {"booking": {…}} in the shape shown under Book. Another agency's reference answers 404 booking_not_found.

List stays

GET /hotels/bookings, newest first, 25 a page.

Query parameterNotes
stateheld, pending, confirmed, cancelled, expired, rejected
client_referenceYour own reference
created_from, created_toYYYY-MM-DD
page, per_pageper_page up to 100
json
{
  "bookings": [{"booking_ref": "TFH-WD5BD2P4", "state": "cancelled", "…": "…"}],
  "page": 1,
  "per_page": 25,
  "total": 1,
  "last_page": 1
}

Pay a held stay

POST /hotels/bookings/{booking_ref}/pay, no body, before hold_expires_at. The deposit pays and the hotel confirms. The sandbox payment of TFH-WD5BD2P4 (60 s):

200 OK
{
  "status": "confirmed",
  "booking": {
    "booking_ref": "TFH-WD5BD2P4",
    "state": "confirmed",
    "confirmed_at": "2026-09-22T14:12:57Z",
    "hotel_confirmation": "TestBooking",
    "…": "…"
  }
}

(TestBooking is the hotel test system's confirmation; live stays carry the hotel's real number.)

HTTPAnswerMeaning
200confirmed or pendingPaid
202requestedPaid; our team is confirming with the hotel
409hold_expiredToo late; search again
402insufficient_fundsTop up; nothing charged
409invalid_stateNot a held stay

What cancelling would cost

GET /hotels/bookings/{booking_ref}/cancellation changes nothing. For TFH-WD5BD2P4 after payment:

json
{
  "cancellable": true,
  "reason": null,
  "refundable": true,
  "rules": [
    "Free cancellation until 24 Oct 2026",
    "From 24 Oct 2026 to 29 Oct 2026: 19,009.46 BDT charge"
  ],
  "free_until": "2026-10-24T19:59:00+00:00",
  "total": {"amount": 1900946, "currency": "BDT"},
  "charge": {"amount": 0, "currency": "BDT"},
  "service_charge": {"amount": 0, "currency": "BDT"},
  "refund": {"amount": 1900946, "currency": "BDT"}
}

charge is what the hotel keeps if you cancel now, service_charge our cancellation fee, and refund what returns to your deposit. cancellable: false comes with a reason.

Cancel

POST /hotels/bookings/{booking_ref}/cancel, body optional {"reason": "Guest cancelled the trip"}.

  • A held stay is released; nothing was charged, refund is 0.
  • A confirmed stay is cancelled with the hotel under its rules, and refund is paid back to your deposit at once.

The sandbox cancellation of TFH-WD5BD2P4:

200 OK
{
  "status": "cancelled",
  "refund": {"amount": 1900946, "currency": "BDT"},
  "booking": {
    "booking_ref": "TFH-WD5BD2P4",
    "state": "cancelled",
    "cancellation": {
      "cancelled_at": "2026-09-22T14:13:17+00:00",
      "charge": {"amount": 0, "currency": "BDT"},
      "refund": {"amount": 1900946, "currency": "BDT"}
    },
    "…": "…"
  }
}

409 not_cancellable carries the reason (for example, a non-refundable rate the hotel will not cancel). 503 supplier_unavailable means the hotel did not confirm the cancellation; the stay is unchanged, so retry.

Errors

Every error has the same envelope; validation errors add errors by field.

json
{
  "error": {
    "code": "validation_failed",
    "message": "The request is malformed; see errors for the fields."
  },
  "errors": {"rooms.0.guests.0.pan_number": ["An Indian lead guest needs to give a PAN card number."]}
}

Access

HTTPerror.codeFix
401unauthenticatedHeaders missing, or key or secret wrong
401credential_inactiveKey revoked or expired
403ip_whitelist_required, ip_not_whitelistedWhitelist your server's IP on the key
403product_not_allowedThe key is not enabled for hotels
403partner_inactive, subscription_*, environment_not_activatedAccount or API plan not active; live keys work after certification
429rate_limitedWait a minute

Hotel codes

HTTPerror.codeWhereMeaning
422validation_failedanySee errors
410offer_expiredrooms, price, bookSearch again
409offer_unavailablerooms, bookHotel or room not bookable now
409unavailablepriceRate gone
409price_changedbookNew total attached; send accept_total
409room_unavailablebookSold out
409hold_unavailable, hold_refusedbookBook without hold
409rate_already_bookedbookFetch rooms again for fresh rate ids
400idempotency_key_requiredbookAdd the header
409duplicate_request, request_in_progressbook, payKey reused, or still running
402insufficient_fundsbook, payDeposit too low; nothing charged
404booking_not_foundbookingsUnknown, or not your agency's
409hold_expiredpayHold lapsed
409invalid_state, not_cancellablepay, cancelNot allowed in the current state; see message
503supplier_unavailable, pricing_unavailableanyNothing changed; retry

Retries. 503, 429 and timeouts are safe to retry. Retry booking only with the same Idempotency-Key.

Go-live checklist

Run these on the sandbox and send us the booking_refs; we then switch on your live key.

  • GET /ping from your whitelisted server
  • Destination lookup while the user types, and a search by location_id
  • Search with two rooms, one of them with a child and its age
  • Rooms list shown with board basis, cancellation windows and hotel notes
  • Rate re-checked before the guest form; price_changed handled with accept_total
  • Guest form follows requirements (passport, PAN only for Indian guests)
  • A stay booked and paid now, with an Idempotency-Key; the repeat returns the same booking
  • A stay held (where offered), then paid with /pay
  • A held stay cancelled
  • A confirmed stay's cancellation previewed, then cancelled, and the refund shown
  • 402 insufficient_funds, 410 offer_expired and 409 room_unavailable shown in plain words
  • requested / needs_attention shown to your user as "being confirmed by FlightOTA"
  • Money shown from minor units with the right decimals

Sandbox notes. Sandbox stays go to hotel test systems: confirmations such as TestBooking are not real, the deposit is test money, and test systems are slower (paying a hold took about a minute). Unpaid sandbox holds lapse on their own.

Questions and certification: your account manager, or support from the agent portal.

Endpoint reference

Getting started

Ping

GET/ping

Call GET /ping first. It returns your agency code and the IP we saw.

Parameters

This endpoint takes no parameters.

Errors

  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait 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."
  }
}

Destinations and search

Find a destination

GET/hotels/destinations

Use the id in the search; ids are valid for about an hour, so look them up as the user types.

Parameters

ParameterInDescription
term
stringrequired
queryAt least 2 characters.
Example: Dubai

Errors

  • 422validation_failedSee errors.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/hotels/destinations?term=Dubai" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "term": "Dubai",
  "destinations": [
    {
      "id": "loc_01m34q6gd14nc2ea9q39tbq8z0",
      "name": "Dubai",
      "full_name": "Dubai, Dubai, United Arab Emirates",
      "type": "city",
      "country": "AE"
    },
    {
      "id": "loc_01m34q6gd14nc2ea9q39tbq8z1",
      "name": "Dubai (and vicinity)",
      "full_name": "Dubai (and vicinity), Dubai, United Arab Emirates",
      "type": "multiCity",
      "country": "AE"
    },
    {
      "id": "loc_01m34q6gd14nc2ea9q39tbq8z2",
      "name": "Dubai (DXB-Dubai Intl.)",
      "full_name": "Dubai, United Arab Emirates (DXB-Dubai Intl.)",
      "type": "airport",
      "country": "AE"
    }
  ]
}
Error · 422
{
  "error": {
    "code": "validation_failed",
    "message": "See errors."
  }
}

Destinations and search

Read a search again

GET/hotels/search/{search_id}

Returns the same results again while valid; an expired one answers 410 offer_expired.

Parameters

ParameterInDescription
search_id
stringrequired
pathThe search id.
Example: srh_01m34q7w1fmxw6tgp917fwnqrt

Errors

  • 410offer_expiredSearch again.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/hotels/search/srh_01m34q7w1fmxw6tgp917fwnqrt" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "search_id": "srh_01m34q7w1fmxw6tgp917fwnqrt",
  "expires_at": "2026-09-22T14:25:13Z",
  "currency": "BDT",
  "offers": [
    {
      "offer_id": "ofr_01m34q7w1fmxw6tgp917fwnqrv",
      "hotel_key": "htl_3e344d6c950e8809",
      "hotel_country": "AE",
      "total": {
        "amount": 900792,
        "currency": "BDT"
      },
      "price": {
        "base": {
          "amount": 900792,
          "currency": "BDT"
        },
        "taxes": {
          "amount": 0,
          "currency": "BDT"
        },
        "fees": {
          "amount": 0,
          "currency": "BDT"
        },
        "per_night": {
          "amount": 450396,
          "currency": "BDT"
        }
      },
      "hotel": {
        "name": "Royal Tulip Hotel",
        "star_rating": 3,
        "address": "Opposite Delhi Resturant, Al Murar area Deira 116957 Dubai United Arab Emirates",
        "city": "Dubai",
        "country_code": "AE",
        "coordinates": {
          "lat": 25.276138,
          "long": 55.31023
        },
        "thumbnail": "https://…/hotelImage.aspx?id=88054978",
        "images": [
          "https://…",
          "…"
        ],
        "description": "With a stay at Royal Tulip Hotel in Dubai (Deira), you'll be convenient to Naif Souq and Gold Souk…",
        "check_in_time": null,
        "check_out_time": null,
        "property_type": "Hotel",
        "amenities": [
          "…"
        ],
        "review": {
          "…": "…"
        }
      },
      "refundable": true,
      "free_cancellation": true,
      "free_breakfast": false,
      "pay_at_hotel": false,
      "more_rates": true,
      "sold_out": false
    }
  ]
}
Error · 410
{
  "error": {
    "code": "offer_expired",
    "message": "Search again."
  }
}

Rooms and rate check

List rooms and rates

GET/hotels/offers/{offer_id}/rooms

Each rate has its own offer_id: that is the rate_id you book. Sandbox: 171 rates for Royal Tulip in 10 s. notes are the hotel's own conditions (tourism fees paid at the hotel, ID at check-in). Show them before booking.

Parameters

ParameterInDescription
offer_id
stringrequired
pathThe hotel's offer_id from search.
Example: ofr_01m34q7w1fmxw6tgp917fwnqrv

Errors

  • 410offer_expiredSearch again.
  • 409offer_unavailableHotel or room not bookable now.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/hotels/offers/ofr_01m34q7w1fmxw6tgp917fwnqrv/rooms" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "rooms_id": "srh_01m34q8qy2jd0tvbw50j887d1k",
  "expires_at": "2026-09-22T14:25:42Z",
  "currency": "BDT",
  "rates": [
    {
      "offer_id": "ofr_01m34qg0zz8d40ew1ab159b48f",
      "hotel_country": "AE",
      "total": {
        "amount": 1900946,
        "currency": "BDT"
      },
      "price": {
        "base": {
          "amount": 1900946,
          "currency": "BDT"
        },
        "per_night": {
          "amount": 950473,
          "currency": "BDT"
        },
        "taxes": {
          "amount": 0,
          "currency": "BDT"
        },
        "fees": {
          "amount": 0,
          "currency": "BDT"
        }
      },
      "room": {
        "name": "Standard Room",
        "image": "https://…/3e7bcff9_b.jpg",
        "beds": [
          "1 Double or 1 Twin"
        ],
        "max_adults": null
      },
      "board_basis": "Room Only",
      "includes": [],
      "notes": [
        "Upon your arrival please provide valid government-issued ID to the hotel front desk…"
      ],
      "occupancy": {
        "adults": 2,
        "children": 1
      },
      "refundable": true,
      "free_cancellation": false,
      "pay_at_hotel": false,
      "deposit_required": false,
      "cancellation": [
        {
          "from": "2026-09-22T00:00:00Z",
          "until": "2026-10-24T19:59:00Z",
          "amount": {
            "amount": 0,
            "currency": "BDT"
          },
          "percent": null
        },
        {
          "from": "2026-10-24T20:00:00Z",
          "until": "2026-10-29T00:00:00Z",
          "amount": {
            "amount": 1900946,
            "currency": "BDT"
          },
          "percent": null
        }
      ],
      "booking_modes": [
        "hold",
        "confirm"
      ],
      "hold_deadline": null
    }
  ]
}
Error · 410
{
  "error": {
    "code": "offer_expired",
    "message": "Search again."
  }
}

Rooms and rate check

Re-check a rate

GET/hotels/rates/{rate_id}/price

Asks the hotel again. Call it before collecting guest details. status: price_changed means rate.total is the new price and quoted the old one. Book with accept_total set to the new total to accept it. hold.available: true means you may book with hold: true and pay by hold.deadline.

Parameters

ParameterInDescription
rate_id
stringrequired
pathThe rate's offer_id from rooms.
Example: ofr_01m34qg0zz8d40ew1ab159b48f

Errors

  • 410offer_expiredMeans search again.
  • 409unavailableMeans the room is gone.
  • 503supplier_unavailableNothing changed; retry.
  • 503pricing_unavailableNothing changed; retry.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/hotels/rates/ofr_01m34qg0zz8d40ew1ab159b48f/price" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "status": "confirmed",
  "quoted": {
    "amount": 1900946,
    "currency": "BDT"
  },
  "rate": {
    "offer_id": "ofr_01m34qg0zz8d40ew1ab159b48f",
    "total": {
      "amount": 1900946,
      "currency": "BDT"
    },
    "…": "the full rate"
  },
  "hold": {
    "available": true,
    "deadline": "2026-10-23T14:29:00Z"
  },
  "requirements": {
    "passport_required": false,
    "pan_required": false,
    "pan_for_all_guests": false
  }
}
Error · 410
{
  "error": {
    "code": "offer_expired",
    "message": "Means search again."
  }
}

Book a stay

Book a stay

POST/hotels/bookings

Books one rate. The rate is priced again first; your request never names a price. Without hold, the stay is paid from your deposit and confirmed with the hotel in the same call (up to about a minute). With "hold": true (only where the rate check said hold.available), nothing is charged until you pay. A rate can be booked only once: to book a second room of the same kind, fetch the rooms again for fresh rate ids.

Send an Idempotency-Key header, unique per booking attempt. Repeating the request with the same key returns the booking already made. Without hold, this call pays from your deposit.

Parameters

ParameterInDescription
Idempotency-Key
stringrequired
headerUnique per booking attempt.
Example: docs-hotel-001
hotel_offer_id
stringrequired
bodyThe hotel's offer_id from search.
rate_id
stringrequired
bodyThe rate's offer_id from rooms.
hold
booleanoptional
bodytrue to hold and pay later, where allowed.
rooms[]
arrayrequired
bodyOne entry per searched room, in the same order.
rooms[].guests[]
arrayrequired
bodyEvery guest of that room: exactly the adults and children it was searched for.
guests[].type
adult | childrequired
bodyadult (12 or over on check-in) or child; as many of each as the room was searched for.
guests[].title
stringrequired
bodyMR, MRS, MS, MISS, MSTR.
guests[].first_name
stringrequired
bodyLetters, spaces, hyphens, apostrophes.
guests[].last_name
stringrequired
bodyLetters, spaces, hyphens, apostrophes.
guests[].nationality
stringoptional
bodyISO code; defaults to the search nationality.
guests[].date_of_birth
YYYY-MM-DDoptional
bodyMust fit adult or child on check-in. A child's must fit the age it was searched at: the room's first child is its first child_ages, and so on.
guests[].passport_number
stringoptional
bodyLead guest, when requirements.passport_required.
guests[].passport_expiry
YYYY-MM-DDoptional
bodyMust be after check-out.
guests[].pan_number
stringoptional
bodyIndian lead guest when pan_required; every Indian guest when pan_for_all_guests. Format ABCDE1234F. Never needed for other nationalities.
contact.email
stringrequired
bodyFor the hotel and the voucher.
contact.dial_code
stringrequired
bodyFor the hotel and the voucher.
contact.mobile
stringrequired
bodyFor the hotel and the voucher. mobile digits only.
contact.name
stringoptional
bodyDefaults to the lead guest.
special_requests
stringoptional
bodyUp to 500 characters; passed to the hotel, not guaranteed.
arrival_time
stringoptional
bodye.g. 23:00.
accept_total
integeroptional
bodyInteger minor units (currency units on a key with Amounts switched): accept a changed price up to this total.
client_reference
stringoptional
bodyYour own reference, up to 64 characters.

Errors

  • 400idempotency_key_requiredAdd the header.
  • 422validation_failedSee errors; for example rooms.0.guests.0.pan_number.
  • 410offer_expiredSearch again.
  • 409price_changedtotal is the new price; send accept_total with it to accept.
  • 409room_unavailableSold out; choose another rate.
  • 409hold_unavailableThis rate cannot be held; book without hold.
  • 409hold_refusedThe hotel would not hold it now; book without hold or retry.
  • 402insufficient_fundsTop up your deposit.
  • 409rate_already_bookedThis rate id was already booked (as booking_ref); fetch rooms again.
  • 409duplicate_requestThe key was used with a different body.
  • 409request_in_progressStill running.
  • 409offer_unavailableThe room cannot be booked now; choose another.
  • 503supplier_unavailableNothing changed; retry.
  • 503pricing_unavailableNothing changed; retry.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/hotels/bookings" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: docs-hotel-001" \
  -H "Content-Type: application/json" \
  -d '{
  "hotel_offer_id": "ofr_01m34q7w1fmxw6tgp917fwnqrv",
  "rate_id": "ofr_01m34q8qy88vwza13395cqh6bn",
  "hold": true,
  "rooms": [
    {
      "guests": [
        {
          "type": "adult",
          "title": "MR",
          "first_name": "Arif",
          "last_name": "Rahman",
          "nationality": "BD",
          "passport_number": "A01234567"
        },
        {
          "type": "adult",
          "title": "MRS",
          "first_name": "Nusrat",
          "last_name": "Rahman",
          "nationality": "BD"
        },
        {
          "type": "child",
          "title": "MSTR",
          "first_name": "Ayan",
          "last_name": "Rahman",
          "nationality": "BD",
          "date_of_birth": "2019-09-22"
        }
      ]
    }
  ],
  "contact": {
    "email": "ops@agency.example",
    "dial_code": "880",
    "mobile": "1711000000"
  },
  "special_requests": "Late arrival, around 23:00",
  "client_reference": "DOCS-HOTEL-001"
}'
Response · 201
{
  "status": "held",
  "booking": {
    "booking_ref": "TFH-WD5BD2P4",
    "state": "held",
    "created_at": "2026-09-22T14:11:36Z",
    "confirmed_at": null,
    "hold_expires_at": "2026-10-23T14:29:00Z",
    "client_reference": "DOCS-HOTEL-001",
    "hotel_confirmation": null,
    "needs_attention": false,
    "total": {
      "amount": 1900946,
      "currency": "BDT"
    },
    "price": {
      "room_rate": {
        "amount": 1900946,
        "currency": "BDT"
      },
      "taxes": {
        "amount": 0,
        "currency": "BDT"
      }
    },
    "hotel": {
      "name": "Royal Tulip Hotel",
      "star_rating": 3,
      "address": "Opposite Delhi Resturant, Al Murar area Deira 116957 Dubai United Arab Emirates",
      "city": "Dubai",
      "country_code": "AE",
      "phone": null,
      "check_in_time": null,
      "check_out_time": null
    },
    "stay": {
      "check_in": "2026-10-27",
      "check_out": "2026-10-29",
      "nights": 2,
      "rooms": [
        {
          "adults": 2,
          "children": 1,
          "child_ages": [
            7
          ]
        }
      ],
      "nationality": "BD"
    },
    "room": {
      "name": "Standard Room",
      "board_basis": "Room Only",
      "refundable": true,
      "cancellation": [
        "Free cancellation until 24 Oct 2026",
        "From 24 Oct 2026 to 29 Oct 2026: 19,009.46 BDT charge"
      ]
    },
    "guests": [
      {
        "position": 1,
        "type": "adult",
        "title": "MR",
        "first_name": "ARIF",
        "last_name": "RAHMAN"
      },
      {
        "position": 2,
        "type": "adult",
        "title": "MRS",
        "first_name": "NUSRAT",
        "last_name": "RAHMAN"
      },
      {
        "position": 3,
        "type": "child",
        "title": "MSTR",
        "first_name": "AYAN",
        "last_name": "RAHMAN"
      }
    ],
    "special_requests": "Late arrival, around 23:00",
    "cancellation": null
  }
}
Error · 400
{
  "error": {
    "code": "idempotency_key_required",
    "message": "Add the header."
  }
}

Read, pay, cancel

Read one stay

GET/hotels/bookings/{booking_ref}

Returns the booking in the shape shown under Book. Another agency's reference answers 404 booking_not_found.

Parameters

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFH-XXXXXXXX.
Example: TFH-WD5BD2P4

Errors

  • 404booking_not_foundUnknown, or not your agency's.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/hotels/bookings/TFH-WD5BD2P4" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "booking": {
    "booking_ref": "TFH-WD5BD2P4",
    "state": "held",
    "…": "the shape shown under Book"
  }
}
Error · 404
{
  "error": {
    "code": "booking_not_found",
    "message": "Unknown, or not your agency's."
  }
}

Read, pay, cancel

List stays

GET/hotels/bookings

Newest first, 25 a page.

Parameters

ParameterInDescription
state
stringoptional
queryheld, pending, confirmed, cancelled, expired, rejected.
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.

Errors

  • 422validation_failedSee errors.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/hotels/bookings" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "bookings": [
    {
      "booking_ref": "TFH-WD5BD2P4",
      "state": "cancelled",
      "…": "…"
    }
  ],
  "page": 1,
  "per_page": 25,
  "total": 1,
  "last_page": 1
}
Error · 422
{
  "error": {
    "code": "validation_failed",
    "message": "See errors."
  }
}

Read, pay, cancel

Pay a held stay

POST/hotels/bookings/{booking_ref}/pay

No body, before hold_expires_at. The deposit pays and the hotel confirms. 202 requested means paid; our team is confirming with the hotel. (TestBooking is the hotel test system's confirmation; live stays carry the hotel's real number.)

This call pays from your deposit.

Parameters

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFH-XXXXXXXX.
Example: TFH-WD5BD2P4

Errors

  • 409hold_expiredToo late; search again.
  • 402insufficient_fundsTop up; nothing charged.
  • 409invalid_stateNot a held stay.
  • 409duplicate_requestKey reused.
  • 409request_in_progressStill running.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/hotels/bookings/TFH-WD5BD2P4/pay" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "status": "confirmed",
  "booking": {
    "booking_ref": "TFH-WD5BD2P4",
    "state": "confirmed",
    "confirmed_at": "2026-09-22T14:12:57Z",
    "hotel_confirmation": "TestBooking",
    "…": "…"
  }
}
Error · 409
{
  "error": {
    "code": "hold_expired",
    "message": "Too late; search again."
  }
}

Read, pay, cancel

What cancelling would cost

GET/hotels/bookings/{booking_ref}/cancellation

Changes nothing. charge is what the hotel keeps if you cancel now, service_charge our cancellation fee, and refund what returns to your deposit. cancellable: false comes with a reason.

Parameters

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFH-XXXXXXXX.
Example: TFH-WD5BD2P4

Errors

  • 404booking_not_foundUnknown, or not your agency's.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X GET "https://sandbox.flightota.com/api/v1/hotels/bookings/TFH-WD5BD2P4/cancellation" \
  -H "X-Api-Key: $FLIGHTOTA_API_KEY" \
  -H "X-Api-Secret: $FLIGHTOTA_API_SECRET" \
  -H "Accept: application/json"
Response · 200
{
  "cancellable": true,
  "reason": null,
  "refundable": true,
  "rules": [
    "Free cancellation until 24 Oct 2026",
    "From 24 Oct 2026 to 29 Oct 2026: 19,009.46 BDT charge"
  ],
  "free_until": "2026-10-24T19:59:00+00:00",
  "total": {
    "amount": 1900946,
    "currency": "BDT"
  },
  "charge": {
    "amount": 0,
    "currency": "BDT"
  },
  "service_charge": {
    "amount": 0,
    "currency": "BDT"
  },
  "refund": {
    "amount": 1900946,
    "currency": "BDT"
  }
}
Error · 404
{
  "error": {
    "code": "booking_not_found",
    "message": "Unknown, or not your agency's."
  }
}

Read, pay, cancel

Cancel

POST/hotels/bookings/{booking_ref}/cancel

A held stay is released; nothing was charged, refund is 0. A confirmed stay is cancelled with the hotel under its rules, and refund is paid back to your deposit at once.

A confirmed stay is cancelled with the hotel under its rules.

Parameters

ParameterInDescription
booking_ref
stringrequired
pathThe booking reference, TFH-XXXXXXXX.
Example: TFH-WD5BD2P4
reason
stringoptional
bodyBody optional.
Example: Guest cancelled the trip

Errors

  • 409not_cancellableCarries the reason (for example, a non-refundable rate the hotel will not cancel).
  • 409invalid_stateNot allowed in the current state; see message.
  • 503supplier_unavailableThe hotel did not confirm the cancellation; the stay is unchanged, so retry.
  • 401unauthenticatedHeaders missing, or key or secret wrong.
  • 401credential_inactiveKey revoked or expired.
  • 403ip_whitelist_requiredWhitelist your server's IP on the key.
  • 403ip_not_whitelistedWhitelist your server's IP on the key.
  • 403product_not_allowedThe key is not enabled for hotels.
  • 403partner_inactiveAccount or API plan not active; live keys work after certification.
  • 403subscription_*Account or API plan not active; live keys work after certification.
  • 403environment_not_activatedAccount or API plan not active; live keys work after certification.
  • 429rate_limitedWait a minute.
curl -X POST "https://sandbox.flightota.com/api/v1/hotels/bookings/TFH-WD5BD2P4/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": "Guest cancelled the trip"
}'
Response · 200
{
  "status": "cancelled",
  "refund": {
    "amount": 1900946,
    "currency": "BDT"
  },
  "booking": {
    "booking_ref": "TFH-WD5BD2P4",
    "state": "cancelled",
    "cancellation": {
      "cancelled_at": "2026-09-22T14:13:17+00:00",
      "charge": {
        "amount": 0,
        "currency": "BDT"
      },
      "refund": {
        "amount": 1900946,
        "currency": "BDT"
      }
    },
    "…": "…"
  }
}
Error · 409
{
  "error": {
    "code": "not_cancellable",
    "message": "Carries the reason (for example, a non-refundable rate the hotel will not cancel)."
  }
}