Hotel Search and Booking
Real, bookable hotel inventory through LetsFG. Search is free; booking charges 5% of the price to the card on file and hands you a pay link for the balance.
Read this before your first call
The same card authorises flights and hotels. The PFS Bearer token (card-backed, from
connecting the MCP server — the consent step saves a card at https://letsfg.co/connect) is
accepted by the hotel endpoints, and so is a Developer API key (X-API-Key). Use whichever
you already hold. If you hold neither, do not register a billing account on someone's behalf
and do not fall back to scraping a hotel site — say so plainly to whoever asked.
A card on file is required for search, not just booking. A hotel search opens a real
session at the supplier and booking blocks a real rate, so every hotel endpoint returns
402 without a payment method. This is deliberate: better to refuse up front than to let you
reach the point of commitment and discover you cannot pay.
Only free-cancellation, pay-later rates are sold. Those are the rates where the guest's balance can safely be settled with the supplier after booking, which is what makes the 5%-now/rest-later model work at all. You will see fewer results than a metasearch shows. Every one of them can actually be booked.
How the money works
| Step | Who is charged | Amount |
|---|---|---|
| Search | nobody | free |
| Book | the agent's card, immediately | 5% of the price, non-refundable |
| Balance | the guest pays the supplier directly, via pay_link |
the rest, by balance_due_by |
balance_due_by is the supplier's own auto-cancellation date, not a date LetsFG invents.
Miss it and the room is released.
price in the search response is what the guest pays. There is no wholesale figure in the
response to quote by mistake.
Workflow
from letsfg import LetsFG
lfg = LetsFG(api_key="trav_...") # NOT a Bearer token
# 1. Resolve the place name to a supplier city id
city = lfg.hotel_destinations("Warsaw")[0]
# 2. Search
stays = lfg.search_hotels(
city_id=city["Id"], city_name=city["Name"],
check_in="2026-11-10", check_out="2026-11-12", adults=2,
)
hotel = stays["hotels"][0]
offer = hotel["offers"][0]
# offer: price, reservation_fee_now, balance_to_supplier, balance_due_by,
# free_cancellation_until, combination_id_v2
# 3. Book — asynchronous. book_hotel() returns a job; this helper polls for you.
booking = lfg.book_hotel_and_wait(
session_id=stays["session_id"],
hotel_code=hotel["hotel_code"],
combination_id_v2=offer["combination_id_v2"],
expected_price=offer["price"],
expected_balance=offer["balance_to_supplier"],
city_id=city["Id"], city_name=city["Name"],
check_in="2026-11-10", check_out="2026-11-12",
guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
email="guest@example.com", phone="512345678",
)
print(booking["confirmation"], booking["pay_link"], booking["balance_due_by"])
# 4. Cancel if needed — free until balance_due_by
lfg.cancel_hotel(booking["confirmation"])
MCP tools, in call order: resolve_hotel_city → search_hotels → book_hotel →
get_hotel_booking → cancel_hotel_booking.
Critical rules
- Never call
book_hoteltwice for the same rate. It is not idempotent: two calls book the room twice and charge two reservation fees. If a call times out, poll the job. - Booking is asynchronous.
book_hotelreturns abooking_job_id, not a booking. Pollget_hotel_bookingevery ~20s untilstatusissucceededorfailed. This is what makes it impossible to charge a card and then lose the confirmation to a timeout. - Send
expected_priceandexpected_balanceback verbatim from the search response. The booking is refused if the supplier's price has moved, so a guest is never charged a price they did not agree to. - Use the guest's real email. The voucher and the pay link go there; a typo loses the booking. It is validated before anything is charged.
- Tell the guest the fee is non-refundable before you book. Cancelling returns the balance obligation, never the 5%.
- A cancellation timeout is not a failure. It drives a browser at the supplier and takes over a minute. Re-check before retrying.
Error handling
| Status | Meaning | What to do |
|---|---|---|
401 |
Credential invalid, expired or revoked | Reconnect at https://letsfg.co/connect, or check the API key |
402 |
No payment method on file | Attach a card; required for search too |
409 |
The chosen rate is gone | Search again and pick another |
504 |
Supplier did not answer in time | If booking, poll the job — do NOT re-book |
job failed |
Card declined, or no rate left | Read error; nothing was charged |
Links
- Hotels guide: https://letsfg.co/developers/docs/hotels/
- Agent guide: https://letsfg.co/for-agents
- Repo: https://github.com/LetsFG/LetsFG