letsfg 2026.5.71 → 2026.5.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,221 +1,283 @@
1
- # LetsFG — Your AI agent just learned to book flights. (Node.js)
2
-
3
- **Server-side search engine. Real prices. One function call.** Search hundreds of airlines at raw airline prices — **$20–$50 cheaper** than Booking.com, Kayak, and other OTAs. Zero dependencies. Built for AI agents.
4
-
5
- [![GitHub stars](https://img.shields.io/github/stars/LetsFG/LetsFG?style=social)](https://github.com/LetsFG/LetsFG)
6
- [![npm](https://img.shields.io/npm/v/letsfg)](https://www.npmjs.com/package/letsfg)
7
-
8
- ## Two ways to use LetsFG
9
-
10
- | | **CLI / SDK** (this package) | **Developer API** |
11
- |---|---|---|
12
- | **Search cost** | Free (Bearer token via `letsfg auth` — zero-amount card setup) | Prepaid credits |
13
- | **Booking** | `POST /api/agent-book` — confirmed order or a booking link, no LetsFG fee | Direct airline URL (unlock required first) |
14
- | **Speed** | 8–10 s to first results; longer on a split | 2–5 s (discover) · 8–10 s to first results (full) |
15
- | **Setup** | `npm install letsfg` then `letsfg auth` | [letsfg.co/developers](https://letsfg.co/developers) |
16
-
17
- > **Want direct airline URLs without any per-booking fee?** Use the [Developer API](https://letsfg.co/developers) — prepaid credits, results in seconds, no checkout step.
18
-
19
- ## Install
20
-
21
- ```bash
22
- npm install letsfg
23
- ```
24
-
25
- ## Quick Start (SDK)
26
-
27
- ```typescript
28
- import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
29
-
30
- // PFS — free. Get a Bearer token once with `letsfg auth` (zero-amount card
31
- // setup, nothing charged), then pass it here.
32
- const bt = new LetsFG({ bearerToken: 'eyJ...' });
33
-
34
- // Search — FREE
35
- const flights = await bt.search('GDN', 'BER', '2026-03-03');
36
- const best = cheapestOffer(flights);
37
- console.log(offerSummary(best));
38
-
39
- // Book — free, ticket price only, no LetsFG fee. No unlock step.
40
- const result = await bt.book(
41
- best.id,
42
- [{ given_name: 'John', family_name: 'Doe', born_on: '1990-01-15', gender: 'm' }],
43
- 'john@example.com',
44
- '',
45
- '',
46
- flights.search_id,
47
- );
48
- if (result.booked) {
49
- console.log(`Order: ${result.order_id}`);
50
- } else {
51
- console.log(`Booking link (nothing charged): ${result.booking_url}`);
52
- }
53
- ```
54
-
55
- Prefer the paid Developer API instead? Pass `apiKey` instead of `bearerToken` —
56
- `search()`/`book()` dispatch automatically. That path requires `unlock()`
57
- (1% fee, min $3) before `book()`.
58
-
59
- ## Quick Start (CLI)
60
-
61
- ```bash
62
- export LETSFG_BEARER_TOKEN=<your-bearer-token> # from `letsfg auth`
63
-
64
- letsfg search GDN BER 2026-03-03 --sort price
65
- letsfg search LON BCN 2026-04-01 --json # Machine-readable
66
- letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m"}' -e john@example.com
67
- ```
68
-
69
- ## API
70
-
71
- ### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
72
-
73
- ### `bt.search(origin, destination, dateFrom, options?)`
74
- ### `bt.resolveLocation(query)`
75
- ### `bt.unlock(offerId)` — Developer API only
76
- ### `bt.book(offerId, passengers, contactEmail, contactPhone?, idempotencyKey?, searchId?)`
77
- Dispatches on which credential is set: `bearerToken` → free PFS booking via
78
- `POST /api/agent-book` (pass `searchId`, one passenger). `apiKey` → paid
79
- Developer API `book` (requires `unlock()` first, supports multiple passengers
80
- and `idempotencyKey`).
81
- ### `bt.setupPayment(token?)` — Developer API only
82
- ### `bt.me()`
83
- ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
84
-
85
- ### Helpers
86
- - `offerSummary(offer)` — One-line string summary
87
- - `cheapestOffer(result)` — Get cheapest offer from search
88
-
89
- ## Starlink Wi-Fi
90
-
91
- Offers may carry `starlink`: `confirmed_all` / `confirmed_some` mean the carrier
92
- has **fully** fitted that aircraft type; `likely_all` / `likely_some` mean the
93
- rollout on that type is underway but incomplete. Segments carry `confirmed` or
94
- `likely`.
95
-
96
- Only `confirmed_*` is safe to state as fact — `likely_*` is a signal, not a
97
- promise. Anything ending `_some` has at least one leg without it. An **absent**
98
- field means no information, **not** an absence of Wi-Fi.
99
-
100
- Full semantics: [docs/api-search.md](https://github.com/LetsFG/LetsFG/blob/main/docs/api-search.md#starlink-wi-fi).
101
-
102
- ## Zero Dependencies
103
-
104
- Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
105
-
106
- ## Also Available As
107
-
108
- - **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
109
- - **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
110
- - **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
111
- - **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
112
-
113
- > ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
114
-
115
- ## License
116
-
117
- MIT
118
-
119
- ## 🏨 Hotels — new, and live
120
-
121
- Your agent can now book hotels, not just flights. Same API key, same card on file.
122
-
123
- ```python
124
- from letsfg import LetsFG
125
- lfg = LetsFG()
126
-
127
- city = lfg.hotel_destinations("Warsaw")[0]
128
- stays = lfg.search_hotels(
129
- city_id=city["Id"], city_name=city["Name"],
130
- check_in="2026-11-10", check_out="2026-11-12", adults=2,
131
- )
132
-
133
- hotel = stays["hotels"][0]
134
- offer = hotel["offers"][0]
135
- print(hotel["name"], offer["price"], stays["currency"])
136
- # Hotel Gromada Warszawa Centrum 669.86 PLN
137
-
138
- booking = lfg.book_hotel_and_wait(
139
- session_id=stays["session_id"],
140
- hotel_code=hotel["hotel_code"],
141
- combination_id_v2=offer["combination_id_v2"],
142
- expected_price=offer["price"],
143
- expected_balance=offer["balance_to_supplier"],
144
- city_id=city["Id"], city_name=city["Name"],
145
- check_in="2026-11-10", check_out="2026-11-12",
146
- guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
147
- email="guest@example.com", phone="512345678",
148
- )
149
- print(booking["confirmation"], booking["pay_link"])
150
- ```
151
-
152
- ### How you pay
153
-
154
- **5% now, the rest to the hotel later.** At booking we charge 5% of the price
155
- to your card as a reservation fee. The remaining balance is paid **directly to
156
- the supplier** through a `pay_link` we return — we never hold it.
157
-
158
- `balance_due_by` is the supplier's own auto-cancellation date, not a date we
159
- invent. Miss it and the room is released.
160
-
161
- The 5% is **non-refundable**. Cancelling before `balance_due_by` costs nothing
162
- else; after it, the hotel's own cancellation ladder applies and can reach 100%.
163
- That ladder ships in the booking's `terms`, so you can always see the cost before
164
- you cancel.
165
-
166
- ### What search costs
167
-
168
- Search is metered separately from booking, on **either** auth path (free PFS
169
- Bearer token or Developer API key — both count against the same agent):
170
- **the first 1,000 `search_hotels` calls since your last hotel booking are
171
- free.** Past that, searches are billed in blocks of 1,000 for **$5**
172
- (~$0.005/search) from your prepaid balance — refused with a 402 if the
173
- balance can't cover the next block, never silently allowed. Book a hotel
174
- and the count resets to zero. Resolving a city name (`hotel_destinations`)
175
- is not metered, only the search call itself.
176
-
177
- ### Things worth knowing before you build
178
-
179
- - **A card on file is required for every hotel call, including search.** That is
180
- unusual and it is deliberate: a hotel search opens a real session at the
181
- supplier, and booking blocks a real rate. We would rather refuse up front than
182
- let you reach the point of commitment and discover you cannot pay. The same
183
- card that authorises flight booking authorises hotels — there is no separate
184
- hotel signup.
185
- - **Only free-cancellation, pay-later rates are sold.** Those are the rates where
186
- the balance can safely be settled with the supplier after booking, which is
187
- what makes 5%-now/rest-later work at all. You will see fewer results than a
188
- metasearch shows you. Every one of them can actually be booked.
189
- - **Booking is asynchronous.** `book_hotel` returns a `booking_job_id`, not a
190
- booking — the real thing takes minutes. Poll `hotel_booking(job_id)` until
191
- `status` is `succeeded` or `failed`, or call `book_hotel_and_wait` and let the
192
- SDK do it. This is not ceremony: it is what makes it impossible to charge a
193
- card and then lose the confirmation to a timeout.
194
- - **The fee is charged before the room is committed.** A declined card therefore
195
- costs nothing to unwind — no reservation exists and nothing is charged.
196
- - **Do not retry a booking blindly.** Calling `book_hotel` twice for the same
197
- rate books the room twice and charges two reservation fees.
198
- - `price` is what the guest pays. There is no wholesale figure in the response to
199
- quote by mistake.
200
-
201
- ### JavaScript
202
-
203
- ```javascript
204
- import { LetsFG } from 'letsfg';
205
- const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
206
-
207
- const [city] = await lfg.hotelDestinations('Warsaw');
208
- const stays = await lfg.searchHotels({
209
- cityId: city.Id, cityName: city.Name,
210
- checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
211
- });
212
-
213
- const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
214
- console.log(booking.confirmation, booking.pay_link);
215
- ```
216
-
217
- ### MCP
218
-
219
- Five new tools, in the order you call them: `resolve_hotel_city` →
220
- `search_hotels` → `book_hotel` → `get_hotel_booking` → `cancel_hotel_booking`.
221
-
1
+ # LetsFG — Your AI agent just learned to book flights. (Node.js)
2
+
3
+ **Server-side search engine. Real prices. One function call.** Search hundreds of airlines at raw airline prices — **$20–$50 cheaper** than Booking.com, Kayak, and other OTAs. Zero dependencies. Built for AI agents.
4
+
5
+ [![GitHub stars](https://img.shields.io/github/stars/LetsFG/LetsFG?style=social)](https://github.com/LetsFG/LetsFG)
6
+ [![npm](https://img.shields.io/npm/v/letsfg)](https://www.npmjs.com/package/letsfg)
7
+
8
+ ## Two ways to use LetsFG
9
+
10
+ | | **CLI / SDK** (this package) | **Developer API** |
11
+ |---|---|---|
12
+ | **Search cost** | Free (card-backed token from [letsfg.co/connect](https://letsfg.co/connect), nothing charged) | Prepaid credits |
13
+ | **Booking** | `POST /api/agent-book` — fare held on your card, a LetsFG agent buys the ticket, captured only on a real PNR. Every offer. | Direct airline URL (unlock required first) |
14
+ | **Speed** | 8–10 s to first results; longer on a split | 2–5 s (discover) · 8–10 s to first results (full) |
15
+ | **Setup** | `npm install letsfg`, then connect at [letsfg.co/developers/api/mcp](https://letsfg.co/developers/api/mcp) | [letsfg.co/developers](https://letsfg.co/developers) |
16
+
17
+ > **Building a product, or need hotels?** Use the [Developer API](https://letsfg.co/developers) — look-to-book search (200 free after every booking, then $0.01), booking through `POST /flights/book`, no booking fee and no transaction fee.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install letsfg
23
+ ```
24
+
25
+ ## Getting a token
26
+
27
+ Connect LetsFG as an MCP server at `https://letsfg.co/developers/api/mcp` and
28
+ approve the connection — in Claude, ChatGPT, Cursor, Windsurf, or Claude Code
29
+ (`claude mcp add --transport http letsfg https://letsfg.co/developers/api/mcp`).
30
+ The consent step opens [letsfg.co/connect](https://letsfg.co/connect), where you
31
+ add a card (any card, or Revolut Pay / Google Pay) in a 0.00 Revolut setup.
32
+ Nothing is charged, no Revolut account is needed, and the card details go to
33
+ Revolut, never to LetsFG. The token you get back is card-backed: it searches
34
+ and it books. One card = one account; quotas are per card (10 searches per
35
+ 10 min, 30 per hour, 100 per day — polling never counts).
36
+
37
+ Pass it as `bearerToken`, or set `LETSFG_BEARER_TOKEN` for the CLI.
38
+
39
+ > `letsfg auth` still runs the Stripe card setup that was retired on
40
+ > 2026-09-02 and cannot get a token today; every token issued that way was
41
+ > revoked (401 `TOKEN_REVOKED`). A connect-flow login for the CLI and SDKs is
42
+ > coming — until then, connect through the MCP.
43
+
44
+ ## Quick Start (SDK)
45
+
46
+ ```typescript
47
+ import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
48
+
49
+ // PFS — free. The card-backed token from the connect flow (see above).
50
+ const bt = new LetsFG({ bearerToken: 'eyJ...' });
51
+
52
+ // Search — FREE
53
+ const flights = await bt.search('GDN', 'BER', '2026-03-03');
54
+ const best = cheapestOffer(flights);
55
+ console.log(offerSummary(best));
56
+
57
+ // Book — no booking fee, no transaction fee — our margin is already in the price you saw; no unlock step. Starts the booking:
58
+ // the fare is HELD on your card and a LetsFG agent buys the ticket (4-11 min).
59
+ const result = await bt.book(
60
+ best.id,
61
+ [{
62
+ given_name: 'John', family_name: 'Doe', born_on: '1990-01-15', gender: 'm',
63
+ nationality: 'GB', phone_number: '+447700900123', phone_country: 'GB',
64
+ address_line1: '1 Analytical Way', address_city: 'London',
65
+ address_postal: 'N1 9GU', address_country: 'GB',
66
+ }],
67
+ 'john@example.com',
68
+ '',
69
+ '',
70
+ flights.search_id,
71
+ );
72
+ const bookingRef = result.booking_ref as string;
73
+
74
+ // Poll until it lands (every 20-30 s): completed | failed | needs_attention
75
+ let status: Record<string, unknown>;
76
+ do {
77
+ await new Promise(r => setTimeout(r, 25_000));
78
+ status = await (await fetch('https://letsfg.co/api/agent-book/status', {
79
+ method: 'POST',
80
+ headers: { Authorization: 'Bearer eyJ...', 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ booking_ref: bookingRef }),
82
+ })).json();
83
+ } while (status.state === 'booking_in_progress');
84
+ console.log(status); // { state: 'completed', pnr: 'ABC123', charged_amount: 93, currency: 'EUR' }
85
+ ```
86
+
87
+ ### How booking works
88
+
89
+ `bt.book()` posts to `POST /api/agent-book` and does exactly what the website
90
+ checkout does: the fare plus LetsFG's markup is **held** on the connected card
91
+ (not taken), a LetsFG booking agent buys the ticket from the seller, and the
92
+ hold is captured only once a real airline PNR exists. If the booking fails the
93
+ hold is released and nothing is charged. Every offer can be booked this way —
94
+ no unlock step, no booking-link fallback, no separate LetsFG fee.
95
+
96
+ The call returns within seconds with a `booking_ref`; the booking itself takes
97
+ 4–11 minutes. Poll `POST /api/agent-book/status` with `{"booking_ref": ...}`
98
+ every 20–30 s (the SDK has no helper for this yet):
99
+
100
+ | `state` | Meaning |
101
+ |---|---|
102
+ | `booking_in_progress` | the agent is at the seller's checkout — keep waiting |
103
+ | `completed` | booked — `pnr`, `charged_amount`, `currency` are in the answer |
104
+ | `failed` | not booked — the hold was released, nothing charged; see `failure_reason` |
105
+ | `needs_attention` | a human at LetsFG is checking it — do **not** book again |
106
+
107
+ One traveller per call, with the details an airline checkout asks for: name,
108
+ date of birth, gender, nationality, email, phone with its country, residence
109
+ address (passport optional). A missing detail returns `missing_details` with
110
+ `missing_fields` and charges nothing. Never start a second booking for the
111
+ same trip while one is in progress — that would place a second hold.
112
+
113
+ Prefer the paid Developer API instead? Pass `apiKey` instead of `bearerToken` —
114
+ `search()`/`book()` dispatch automatically. That path needs a `searchId` (an offer
115
+ is bookable only inside the search that produced it) and has no unlock step:
116
+ `unlock()` was retired 2026-09-08, the route answers `410 Gone`, and there is no fee.
117
+
118
+ ## Quick Start (CLI)
119
+
120
+ ```bash
121
+ export LETSFG_BEARER_TOKEN=<your-bearer-token> # card-backed, from the connect flow
122
+
123
+ letsfg search GDN BER 2026-03-03 --sort price
124
+ letsfg search LON BCN 2026-04-01 --json # Machine-readable
125
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m","nationality":"GB","phone_number":"+447700900123","phone_country":"GB","address_line1":"1 Analytical Way","address_city":"London","address_postal":"N1 9GU","address_country":"GB"}' -e john@example.com
126
+ # prints the booking_ref — poll POST /api/agent-book/status until completed
127
+ ```
128
+
129
+ ## API
130
+
131
+ ### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
132
+
133
+ ### `bt.search(origin, destination, dateFrom, options?)`
134
+ ### `bt.resolveLocation(query)`
135
+ ### `bt.book(offerId, passengers, contactEmail, contactPhone?, idempotencyKey?, searchId?)`
136
+ Dispatches on which credential is set: `bearerToken` → PFS booking via
137
+ `POST /api/agent-book` (pass `searchId`, one passenger with full details;
138
+ returns `booking_ref` — poll `POST /api/agent-book/status`). `apiKey` → paid
139
+ Developer API `book` (requires `searchId`, no unlock step, supports multiple
140
+ passengers and `idempotencyKey`; returns a `booking_id` to poll).
141
+ ### `bt.getBooking(bookingId)` / `bt.answerBooking(...)` / `bt.bookAndWait(...)` — Developer API only
142
+ ### `bt.connectPayment()` — Developer API only. Returns `connect_url`; nothing is charged
143
+ ### `bt.setupPayment()` / `bt.unlock()` — **retired 2026-09-08, both throw locally**
144
+ ### `bt.me()`
145
+ ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
146
+
147
+ ### Helpers
148
+ - `offerSummary(offer)` — One-line string summary
149
+ - `cheapestOffer(result)` — Get cheapest offer from search
150
+
151
+ ## Starlink Wi-Fi
152
+
153
+ Offers may carry `starlink`: `confirmed_all` / `confirmed_some` mean the carrier
154
+ has **fully** fitted that aircraft type; `likely_all` / `likely_some` mean the
155
+ rollout on that type is underway but incomplete. Segments carry `confirmed` or
156
+ `likely`.
157
+
158
+ Only `confirmed_*` is safe to state as fact — `likely_*` is a signal, not a
159
+ promise. Anything ending `_some` has at least one leg without it. An **absent**
160
+ field means no information, **not** an absence of Wi-Fi.
161
+
162
+ Full semantics: [docs/api-search.md](https://github.com/LetsFG/LetsFG/blob/main/docs/api-search.md#starlink-wi-fi).
163
+
164
+ ## Zero Dependencies
165
+
166
+ Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
167
+
168
+ ## Also Available As
169
+
170
+ - **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
171
+ - **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
172
+ - **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
173
+ - **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
174
+
175
+ > ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
176
+
177
+ ## License
178
+
179
+ MIT
180
+
181
+ ## 🏨 Hotels — new, and live
182
+
183
+ Your agent can book hotels as well as flights. Same card-backed token or API key, same card on file.
184
+
185
+ ```python
186
+ from letsfg import LetsFG
187
+ lfg = LetsFG()
188
+
189
+ city = lfg.hotel_destinations("Warsaw")[0]
190
+ stays = lfg.search_hotels(
191
+ city_id=city["Id"], city_name=city["Name"],
192
+ check_in="2026-11-10", check_out="2026-11-12", adults=2,
193
+ )
194
+
195
+ hotel = stays["hotels"][0]
196
+ offer = hotel["offers"][0]
197
+ print(hotel["name"], offer["price"], stays["currency"])
198
+ # Hotel Gromada Warszawa Centrum 669.86 PLN
199
+
200
+ booking = lfg.book_hotel_and_wait(
201
+ session_id=stays["session_id"],
202
+ hotel_code=hotel["hotel_code"],
203
+ combination_id_v2=offer["combination_id_v2"],
204
+ expected_price=offer["price"],
205
+ expected_balance=offer["balance_to_supplier"],
206
+ city_id=city["Id"], city_name=city["Name"],
207
+ check_in="2026-11-10", check_out="2026-11-12",
208
+ guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
209
+ email="guest@example.com", phone="512345678",
210
+ )
211
+ print(booking["confirmation"], booking["pay_link"])
212
+ ```
213
+
214
+ ### How you pay
215
+
216
+ **5% now, the rest to the hotel later.** At booking we charge 5% of the price
217
+ to your card as a reservation fee. The remaining balance is paid **directly to
218
+ the supplier** through a `pay_link` we return — we never hold it.
219
+
220
+ `balance_due_by` is the supplier's own auto-cancellation date, not a date we
221
+ invent. Miss it and the room is released.
222
+
223
+ The 5% is **non-refundable**. Cancelling before `balance_due_by` costs nothing
224
+ else; after it, the hotel's own cancellation ladder applies and can reach 100%.
225
+ That ladder ships in the booking's `terms`, so you can always see the cost before
226
+ you cancel.
227
+
228
+ ### What search costs
229
+
230
+ Search is metered separately from booking, on **either** auth path (free PFS
231
+ Bearer token or Developer API key — both count against the same agent):
232
+ **the first 1,000 `search_hotels` calls since your last hotel booking are
233
+ free.** Past that, searches are billed in blocks of 1,000 for **$5**
234
+ (~$0.005/search) from your prepaid balance — refused with a 402 if the
235
+ balance can't cover the next block, never silently allowed. Book a hotel
236
+ and the count resets to zero. Resolving a city name (`hotel_destinations`)
237
+ is not metered, only the search call itself.
238
+
239
+ ### Things worth knowing before you build
240
+
241
+ - **A card on file is required for every hotel call, including search.** That is
242
+ unusual and it is deliberate: a hotel search opens a real session at the
243
+ supplier, and booking blocks a real rate. We would rather refuse up front than
244
+ let you reach the point of commitment and discover you cannot pay. The same
245
+ card that authorises flight booking authorises hotels — there is no separate
246
+ hotel signup.
247
+ - **Only free-cancellation, pay-later rates are sold.** Those are the rates where
248
+ the balance can safely be settled with the supplier after booking, which is
249
+ what makes 5%-now/rest-later work at all. You will see fewer results than a
250
+ metasearch shows you. Every one of them can actually be booked.
251
+ - **Booking is asynchronous.** `book_hotel` returns a `booking_job_id`, not a
252
+ booking — the real thing takes minutes. Poll `hotel_booking(job_id)` until
253
+ `status` is `succeeded` or `failed`, or call `book_hotel_and_wait` and let the
254
+ SDK do it. This is not ceremony: it is what makes it impossible to charge a
255
+ card and then lose the confirmation to a timeout.
256
+ - **The fee is charged before the room is committed.** A declined card therefore
257
+ costs nothing to unwind — no reservation exists and nothing is charged.
258
+ - **Do not retry a booking blindly.** Calling `book_hotel` twice for the same
259
+ rate books the room twice and charges two reservation fees.
260
+ - `price` is what the guest pays. There is no wholesale figure in the response to
261
+ quote by mistake.
262
+
263
+ ### JavaScript
264
+
265
+ ```javascript
266
+ import { LetsFG } from 'letsfg';
267
+ const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
268
+
269
+ const [city] = await lfg.hotelDestinations('Warsaw');
270
+ const stays = await lfg.searchHotels({
271
+ cityId: city.Id, cityName: city.Name,
272
+ checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
273
+ });
274
+
275
+ const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
276
+ console.log(booking.confirmation, booking.pay_link);
277
+ ```
278
+
279
+ ### MCP
280
+
281
+ Five new tools, in the order you call them: `resolve_hotel_city` →
282
+ `search_hotels` → `book_hotel` → `get_hotel_booking` → `cancel_hotel_booking`.
283
+