letsfg 2026.5.62 → 2026.5.64

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,104 +1,197 @@
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 (Twitter/X Bearer token via `letsfg auth`) | Prepaid credits |
13
- | **Booking** | `POST /api/agent-book` | Direct airline URL |
14
- | **Speed** | 60–90 s | 2–5 s (discover) · 60–90 s (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
- // Register (one-time)
31
- const creds = await LetsFG.register('my-agent', 'agent@example.com');
32
- console.log(creds.api_key); // Save this
33
-
34
- // Use
35
- const bt = new LetsFG({ apiKey: 'trav_...' });
36
-
37
- // Search — FREE
38
- const flights = await bt.search('GDN', 'BER', '2026-03-03');
39
- const best = cheapestOffer(flights);
40
- console.log(offerSummary(best));
41
-
42
- // Unlock
43
- const unlock = await bt.unlock(best.id);
44
-
45
- // Book
46
- const booking = await bt.book(
47
- best.id,
48
- [{
49
- id: flights.passenger_ids[0],
50
- given_name: 'John',
51
- family_name: 'Doe',
52
- born_on: '1990-01-15',
53
- gender: 'm',
54
- title: 'mr',
55
- email: 'john@example.com',
56
- }],
57
- 'john@example.com'
58
- );
59
- console.log(`PNR: ${booking.booking_reference}`);
60
- ```
61
-
62
- ## Quick Start (CLI)
63
-
64
- ```bash
65
- export LETSFG_BEARER_TOKEN=<your-bearer-token>
66
-
67
- letsfg search GDN BER 2026-03-03 --sort price
68
- letsfg search LON BCN 2026-04-01 --json # Machine-readable
69
- letsfg unlock off_xxx
70
- letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
71
- ```
72
-
73
- ## API
74
-
75
- ### `new LetsFG({ apiKey, baseUrl?, timeout? })`
76
-
77
- ### `bt.search(origin, destination, dateFrom, options?)`
78
- ### `bt.resolveLocation(query)`
79
- ### `bt.unlock(offerId)`
80
- ### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
81
- ### `bt.setupPayment(token?)`
82
- ### `bt.me()`
83
- ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)`
84
-
85
- ### Helpers
86
- - `offerSummary(offer)` — One-line string summary
87
- - `cheapestOffer(result)` — Get cheapest offer from search
88
-
89
- ## Zero Dependencies
90
-
91
- Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
92
-
93
- ## Also Available As
94
-
95
- - **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
96
- - **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
97
- - **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
98
- - **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
99
-
100
- > ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
101
-
102
- ## License
103
-
104
- MIT
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 (Twitter/X Bearer token via `letsfg auth`) | Prepaid credits |
13
+ | **Booking** | `POST /api/agent-book` | Direct airline URL |
14
+ | **Speed** | 60–90 s | 2–5 s (discover) · 60–90 s (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
+ // Register (one-time)
31
+ const creds = await LetsFG.register('my-agent', 'agent@example.com');
32
+ console.log(creds.api_key); // Save this
33
+
34
+ // Use
35
+ const bt = new LetsFG({ apiKey: 'trav_...' });
36
+
37
+ // Search — FREE
38
+ const flights = await bt.search('GDN', 'BER', '2026-03-03');
39
+ const best = cheapestOffer(flights);
40
+ console.log(offerSummary(best));
41
+
42
+ // Unlock
43
+ const unlock = await bt.unlock(best.id);
44
+
45
+ // Book
46
+ const booking = await bt.book(
47
+ best.id,
48
+ [{
49
+ id: flights.passenger_ids[0],
50
+ given_name: 'John',
51
+ family_name: 'Doe',
52
+ born_on: '1990-01-15',
53
+ gender: 'm',
54
+ title: 'mr',
55
+ email: 'john@example.com',
56
+ }],
57
+ 'john@example.com'
58
+ );
59
+ console.log(`PNR: ${booking.booking_reference}`);
60
+ ```
61
+
62
+ ## Quick Start (CLI)
63
+
64
+ ```bash
65
+ export LETSFG_BEARER_TOKEN=<your-bearer-token>
66
+
67
+ letsfg search GDN BER 2026-03-03 --sort price
68
+ letsfg search LON BCN 2026-04-01 --json # Machine-readable
69
+ letsfg unlock off_xxx
70
+ letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
71
+ ```
72
+
73
+ ## API
74
+
75
+ ### `new LetsFG({ apiKey, baseUrl?, timeout? })`
76
+
77
+ ### `bt.search(origin, destination, dateFrom, options?)`
78
+ ### `bt.resolveLocation(query)`
79
+ ### `bt.unlock(offerId)`
80
+ ### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
81
+ ### `bt.setupPayment(token?)`
82
+ ### `bt.me()`
83
+ ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)`
84
+
85
+ ### Helpers
86
+ - `offerSummary(offer)` — One-line string summary
87
+ - `cheapestOffer(result)` — Get cheapest offer from search
88
+
89
+ ## Zero Dependencies
90
+
91
+ Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
92
+
93
+ ## Also Available As
94
+
95
+ - **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
96
+ - **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
97
+ - **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
98
+ - **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
99
+
100
+ > ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
101
+
102
+ ## License
103
+
104
+ MIT
105
+
106
+ ## 🏨 Hotels — new, and live
107
+
108
+ Your agent can now book hotels, not just flights. Same API key, same card on file.
109
+
110
+ ```python
111
+ from letsfg import LetsFG
112
+ lfg = LetsFG()
113
+
114
+ city = lfg.hotel_destinations("Warsaw")[0]
115
+ stays = lfg.search_hotels(
116
+ city_id=city["Id"], city_name=city["Name"],
117
+ check_in="2026-11-10", check_out="2026-11-12", adults=2,
118
+ )
119
+
120
+ hotel = stays["hotels"][0]
121
+ offer = hotel["offers"][0]
122
+ print(hotel["name"], offer["price"], stays["currency"])
123
+ # Hotel Gromada Warszawa Centrum 669.86 PLN
124
+
125
+ booking = lfg.book_hotel_and_wait(
126
+ session_id=stays["session_id"],
127
+ hotel_code=hotel["hotel_code"],
128
+ combination_id_v2=offer["combination_id_v2"],
129
+ expected_price=offer["price"],
130
+ expected_balance=offer["balance_to_supplier"],
131
+ city_id=city["Id"], city_name=city["Name"],
132
+ check_in="2026-11-10", check_out="2026-11-12",
133
+ guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
134
+ email="guest@example.com", phone="512345678",
135
+ )
136
+ print(booking["confirmation"], booking["pay_link"])
137
+ ```
138
+
139
+ ### How you pay
140
+
141
+ **10% now, the rest to the hotel later.** At booking we charge 10% of the price
142
+ to your card as a reservation fee. The remaining balance is paid **directly to
143
+ the supplier** through a `pay_link` we return — we never hold it.
144
+
145
+ `balance_due_by` is the supplier's own auto-cancellation date, not a date we
146
+ invent. Miss it and the room is released.
147
+
148
+ The 10% is **non-refundable**. Cancelling before `balance_due_by` costs nothing
149
+ else; after it, the hotel's own cancellation ladder applies and can reach 100%.
150
+ That ladder ships in the booking's `terms`, so you can always see the cost before
151
+ you cancel.
152
+
153
+ ### Things worth knowing before you build
154
+
155
+ - **A card on file is required for every hotel call, including search.** That is
156
+ unusual and it is deliberate: a hotel search opens a real session at the
157
+ supplier, and booking blocks a real rate. We would rather refuse up front than
158
+ let you reach the point of commitment and discover you cannot pay. The same
159
+ card that authorises flight booking authorises hotels — there is no separate
160
+ hotel signup.
161
+ - **Only free-cancellation, pay-later rates are sold.** Those are the rates where
162
+ the balance can safely be settled with the supplier after booking, which is
163
+ what makes 10%-now/rest-later work at all. You will see fewer results than a
164
+ metasearch shows you. Every one of them can actually be booked.
165
+ - **Booking is asynchronous.** `book_hotel` returns a `booking_job_id`, not a
166
+ booking — the real thing takes minutes. Poll `hotel_booking(job_id)` until
167
+ `status` is `succeeded` or `failed`, or call `book_hotel_and_wait` and let the
168
+ SDK do it. This is not ceremony: it is what makes it impossible to charge a
169
+ card and then lose the confirmation to a timeout.
170
+ - **The fee is charged before the room is committed.** A declined card therefore
171
+ costs nothing to unwind — no reservation exists and nothing is charged.
172
+ - **Do not retry a booking blindly.** Calling `book_hotel` twice for the same
173
+ rate books the room twice and charges two reservation fees.
174
+ - `price` is what the guest pays. There is no wholesale figure in the response to
175
+ quote by mistake.
176
+
177
+ ### JavaScript
178
+
179
+ ```javascript
180
+ import { LetsFG } from 'letsfg';
181
+ const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
182
+
183
+ const [city] = await lfg.hotelDestinations('Warsaw');
184
+ const stays = await lfg.searchHotels({
185
+ cityId: city.Id, cityName: city.Name,
186
+ checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
187
+ });
188
+
189
+ const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
190
+ console.log(booking.confirmation, booking.pay_link);
191
+ ```
192
+
193
+ ### MCP
194
+
195
+ Five new tools, in the order you call them: `resolve_hotel_city` →
196
+ `search_hotels` → `book_hotel` → `get_hotel_booking` → `cancel_hotel_booking`.
197
+
@@ -1652,6 +1652,161 @@ var LetsFG = class {
1652
1652
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
1653
1653
  return this.post("/developers/api/v1/bookings/book", body);
1654
1654
  }
1655
+ // ── Hotels ──────────────────────────────────────────────────────────
1656
+ //
1657
+ // A card on file is required for EVERY hotel call, search included. That is
1658
+ // deliberate: a hotel search opens a real session at the supplier and booking
1659
+ // blocks a real rate, so a caller is never allowed to reach the point of
1660
+ // commitment only to discover it cannot pay. The same card that authorises
1661
+ // flight booking authorises hotels — there is no separate hotel enrolment.
1662
+ //
1663
+ // Only free-cancellation, pay-later rates are sold. Those are the rates where
1664
+ // the guest's balance can safely be settled with the supplier after booking,
1665
+ // which is what makes 10%-now/rest-later work. The result set is smaller than
1666
+ // a metasearch's, and every row in it can actually be booked.
1667
+ /**
1668
+ * Resolve a place name to the city id that searchHotels() needs.
1669
+ *
1670
+ * Use `Id` from the first result as `cityId` and `Name` as `cityName`.
1671
+ */
1672
+ async hotelDestinations(text) {
1673
+ this.requireApiKey();
1674
+ const data = await this.post(
1675
+ "/developers/api/v1/hotels/destinations",
1676
+ { text },
1677
+ 6e4
1678
+ );
1679
+ return data.results ?? [];
1680
+ }
1681
+ /**
1682
+ * Search real, bookable hotel inventory.
1683
+ *
1684
+ * Slow by nature — the supplier streams a whole city and every rate is priced
1685
+ * — so this gets its own generous timeout rather than the client default.
1686
+ *
1687
+ * Each offer carries `price` (what the guest pays), `reservation_fee_now`
1688
+ * (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
1689
+ * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
1690
+ *
1691
+ * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
1692
+ * identify the exact rate, and booking needs both.
1693
+ */
1694
+ async searchHotels(params) {
1695
+ this.requireApiKey();
1696
+ const body = {
1697
+ city_id: params.cityId,
1698
+ city_name: params.cityName,
1699
+ check_in: params.checkIn,
1700
+ check_out: params.checkOut,
1701
+ adults: params.adults ?? 2,
1702
+ children: params.children ?? 0,
1703
+ nationality: params.nationality ?? "PL",
1704
+ limit: params.limit ?? 40,
1705
+ with_images: params.withImages ?? true
1706
+ };
1707
+ if (params.childAges?.length) body.child_ages = params.childAges;
1708
+ return this.post("/developers/api/v1/hotels/search", body, 24e4);
1709
+ }
1710
+ /**
1711
+ * Start a booking. Returns a job immediately — it does NOT book inline.
1712
+ *
1713
+ * A booking takes minutes: the rate is re-blocked at the supplier, every
1714
+ * price and date rail is checked, the 10% reservation fee is charged to your
1715
+ * card, and only then is the room committed. No proxy holds a connection that
1716
+ * long, so this returns at once and you poll hotelBooking() for the outcome.
1717
+ * Use bookHotelAndWait() if you would rather block.
1718
+ *
1719
+ * Because the fee is taken BEFORE the commit, a declined card costs nothing
1720
+ * to unwind: no reservation exists and nothing is charged.
1721
+ *
1722
+ * Send `expectedPrice` and `expectedBalance` back exactly as search returned
1723
+ * them — the booking is refused if the supplier has moved beyond tolerance,
1724
+ * so a guest is never charged a price they did not agree to.
1725
+ *
1726
+ * Do NOT call this again for the same rate while a job is running: that books
1727
+ * the room twice and charges two reservation fees.
1728
+ */
1729
+ async bookHotel(params) {
1730
+ this.requireApiKey();
1731
+ const body = {
1732
+ session_id: params.sessionId,
1733
+ hotel_code: params.hotelCode,
1734
+ combination_id_v2: params.combinationIdV2,
1735
+ expected_price: params.expectedPrice,
1736
+ expected_balance: params.expectedBalance,
1737
+ city_id: params.cityId,
1738
+ city_name: params.cityName,
1739
+ check_in: params.checkIn,
1740
+ check_out: params.checkOut,
1741
+ adults: params.adults ?? 2,
1742
+ guests: params.guests,
1743
+ email: params.email,
1744
+ phone: params.phone,
1745
+ phone_country_code: params.phoneCountryCode ?? "48",
1746
+ special_requests: params.specialRequests ?? []
1747
+ };
1748
+ if (params.combinationId != null) body.combination_id = params.combinationId;
1749
+ if (params.hotelName) body.hotel_name = params.hotelName;
1750
+ return this.post("/developers/api/v1/hotels/book", body, 9e4);
1751
+ }
1752
+ /**
1753
+ * Collect the result of a booking started with bookHotel().
1754
+ *
1755
+ * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
1756
+ * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
1757
+ * `balance_due_by` and `terms` (including the full cancellation ladder).
1758
+ */
1759
+ async hotelBooking(bookingJobId) {
1760
+ this.requireApiKey();
1761
+ return this.get(
1762
+ `/developers/api/v1/hotels/booking/${encodeURIComponent(bookingJobId)}`,
1763
+ 6e4
1764
+ );
1765
+ }
1766
+ /**
1767
+ * bookHotel(), then poll until the booking settles. Convenience only.
1768
+ *
1769
+ * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
1770
+ * still complete. The returned object carries `booking_job_id` so you can
1771
+ * keep polling, and the confirmation is emailed to the guest regardless.
1772
+ */
1773
+ async bookHotelAndWait(params) {
1774
+ const pollIntervalMs = params.pollIntervalMs ?? 2e4;
1775
+ const maxWaitMs = params.maxWaitMs ?? 6e5;
1776
+ const job = await this.bookHotel(params);
1777
+ const jobId = job.booking_job_id;
1778
+ if (!jobId) return job;
1779
+ let waited = 0;
1780
+ let result = job;
1781
+ while (waited < maxWaitMs) {
1782
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1783
+ waited += pollIntervalMs;
1784
+ result = await this.hotelBooking(jobId);
1785
+ const st = result.status;
1786
+ if (st === "succeeded" || st === "failed") return result;
1787
+ }
1788
+ if (result.booking_job_id == null) result.booking_job_id = jobId;
1789
+ return result;
1790
+ }
1791
+ /**
1792
+ * Release a reservation at the supplier.
1793
+ *
1794
+ * Free until `balance_due_by`; after that the hotel's own cancellation ladder
1795
+ * applies and can reach 100%. The ladder ships in the booking's `terms`, so
1796
+ * you can see the cost before calling this. The 10% reservation fee is NOT
1797
+ * refunded.
1798
+ *
1799
+ * This drives a browser at the supplier and takes over a minute. If it times
1800
+ * out, do not assume it failed — re-check before retrying.
1801
+ */
1802
+ async cancelHotel(confirmation) {
1803
+ this.requireApiKey();
1804
+ return this.post(
1805
+ "/developers/api/v1/hotels/cancel",
1806
+ { confirmation },
1807
+ 3e5
1808
+ );
1809
+ }
1655
1810
  /**
1656
1811
  * [Developer API only] Attach a card to a PAID prepaid Developer API account.
1657
1812
  *
@@ -1709,15 +1864,15 @@ var LetsFG = class {
1709
1864
  const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
1710
1865
  return this.requestWithHeaders(path, "POST", headers, body);
1711
1866
  }
1712
- async post(path, body) {
1713
- return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
1867
+ async post(path, body, timeoutMs) {
1868
+ return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body, timeoutMs);
1714
1869
  }
1715
- async get(path) {
1716
- return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
1870
+ async get(path, timeoutMs) {
1871
+ return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey }, void 0, timeoutMs);
1717
1872
  }
1718
- async requestWithHeaders(path, method, extraHeaders, body) {
1873
+ async requestWithHeaders(path, method, extraHeaders, body, timeoutMs) {
1719
1874
  const controller = new AbortController();
1720
- const timer = setTimeout(() => controller.abort(), this.timeout);
1875
+ const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
1721
1876
  try {
1722
1877
  const resp = await fetch(`${this.baseUrl}${path}`, {
1723
1878
  method,
package/dist/cli.js CHANGED
@@ -407,6 +407,161 @@ var LetsFG = class {
407
407
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
408
408
  return this.post("/developers/api/v1/bookings/book", body);
409
409
  }
410
+ // ── Hotels ──────────────────────────────────────────────────────────
411
+ //
412
+ // A card on file is required for EVERY hotel call, search included. That is
413
+ // deliberate: a hotel search opens a real session at the supplier and booking
414
+ // blocks a real rate, so a caller is never allowed to reach the point of
415
+ // commitment only to discover it cannot pay. The same card that authorises
416
+ // flight booking authorises hotels — there is no separate hotel enrolment.
417
+ //
418
+ // Only free-cancellation, pay-later rates are sold. Those are the rates where
419
+ // the guest's balance can safely be settled with the supplier after booking,
420
+ // which is what makes 10%-now/rest-later work. The result set is smaller than
421
+ // a metasearch's, and every row in it can actually be booked.
422
+ /**
423
+ * Resolve a place name to the city id that searchHotels() needs.
424
+ *
425
+ * Use `Id` from the first result as `cityId` and `Name` as `cityName`.
426
+ */
427
+ async hotelDestinations(text) {
428
+ this.requireApiKey();
429
+ const data = await this.post(
430
+ "/developers/api/v1/hotels/destinations",
431
+ { text },
432
+ 6e4
433
+ );
434
+ return data.results ?? [];
435
+ }
436
+ /**
437
+ * Search real, bookable hotel inventory.
438
+ *
439
+ * Slow by nature — the supplier streams a whole city and every rate is priced
440
+ * — so this gets its own generous timeout rather than the client default.
441
+ *
442
+ * Each offer carries `price` (what the guest pays), `reservation_fee_now`
443
+ * (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
444
+ * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
445
+ *
446
+ * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
447
+ * identify the exact rate, and booking needs both.
448
+ */
449
+ async searchHotels(params) {
450
+ this.requireApiKey();
451
+ const body = {
452
+ city_id: params.cityId,
453
+ city_name: params.cityName,
454
+ check_in: params.checkIn,
455
+ check_out: params.checkOut,
456
+ adults: params.adults ?? 2,
457
+ children: params.children ?? 0,
458
+ nationality: params.nationality ?? "PL",
459
+ limit: params.limit ?? 40,
460
+ with_images: params.withImages ?? true
461
+ };
462
+ if (params.childAges?.length) body.child_ages = params.childAges;
463
+ return this.post("/developers/api/v1/hotels/search", body, 24e4);
464
+ }
465
+ /**
466
+ * Start a booking. Returns a job immediately — it does NOT book inline.
467
+ *
468
+ * A booking takes minutes: the rate is re-blocked at the supplier, every
469
+ * price and date rail is checked, the 10% reservation fee is charged to your
470
+ * card, and only then is the room committed. No proxy holds a connection that
471
+ * long, so this returns at once and you poll hotelBooking() for the outcome.
472
+ * Use bookHotelAndWait() if you would rather block.
473
+ *
474
+ * Because the fee is taken BEFORE the commit, a declined card costs nothing
475
+ * to unwind: no reservation exists and nothing is charged.
476
+ *
477
+ * Send `expectedPrice` and `expectedBalance` back exactly as search returned
478
+ * them — the booking is refused if the supplier has moved beyond tolerance,
479
+ * so a guest is never charged a price they did not agree to.
480
+ *
481
+ * Do NOT call this again for the same rate while a job is running: that books
482
+ * the room twice and charges two reservation fees.
483
+ */
484
+ async bookHotel(params) {
485
+ this.requireApiKey();
486
+ const body = {
487
+ session_id: params.sessionId,
488
+ hotel_code: params.hotelCode,
489
+ combination_id_v2: params.combinationIdV2,
490
+ expected_price: params.expectedPrice,
491
+ expected_balance: params.expectedBalance,
492
+ city_id: params.cityId,
493
+ city_name: params.cityName,
494
+ check_in: params.checkIn,
495
+ check_out: params.checkOut,
496
+ adults: params.adults ?? 2,
497
+ guests: params.guests,
498
+ email: params.email,
499
+ phone: params.phone,
500
+ phone_country_code: params.phoneCountryCode ?? "48",
501
+ special_requests: params.specialRequests ?? []
502
+ };
503
+ if (params.combinationId != null) body.combination_id = params.combinationId;
504
+ if (params.hotelName) body.hotel_name = params.hotelName;
505
+ return this.post("/developers/api/v1/hotels/book", body, 9e4);
506
+ }
507
+ /**
508
+ * Collect the result of a booking started with bookHotel().
509
+ *
510
+ * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
511
+ * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
512
+ * `balance_due_by` and `terms` (including the full cancellation ladder).
513
+ */
514
+ async hotelBooking(bookingJobId) {
515
+ this.requireApiKey();
516
+ return this.get(
517
+ `/developers/api/v1/hotels/booking/${encodeURIComponent(bookingJobId)}`,
518
+ 6e4
519
+ );
520
+ }
521
+ /**
522
+ * bookHotel(), then poll until the booking settles. Convenience only.
523
+ *
524
+ * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
525
+ * still complete. The returned object carries `booking_job_id` so you can
526
+ * keep polling, and the confirmation is emailed to the guest regardless.
527
+ */
528
+ async bookHotelAndWait(params) {
529
+ const pollIntervalMs = params.pollIntervalMs ?? 2e4;
530
+ const maxWaitMs = params.maxWaitMs ?? 6e5;
531
+ const job = await this.bookHotel(params);
532
+ const jobId = job.booking_job_id;
533
+ if (!jobId) return job;
534
+ let waited = 0;
535
+ let result = job;
536
+ while (waited < maxWaitMs) {
537
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
538
+ waited += pollIntervalMs;
539
+ result = await this.hotelBooking(jobId);
540
+ const st = result.status;
541
+ if (st === "succeeded" || st === "failed") return result;
542
+ }
543
+ if (result.booking_job_id == null) result.booking_job_id = jobId;
544
+ return result;
545
+ }
546
+ /**
547
+ * Release a reservation at the supplier.
548
+ *
549
+ * Free until `balance_due_by`; after that the hotel's own cancellation ladder
550
+ * applies and can reach 100%. The ladder ships in the booking's `terms`, so
551
+ * you can see the cost before calling this. The 10% reservation fee is NOT
552
+ * refunded.
553
+ *
554
+ * This drives a browser at the supplier and takes over a minute. If it times
555
+ * out, do not assume it failed — re-check before retrying.
556
+ */
557
+ async cancelHotel(confirmation) {
558
+ this.requireApiKey();
559
+ return this.post(
560
+ "/developers/api/v1/hotels/cancel",
561
+ { confirmation },
562
+ 3e5
563
+ );
564
+ }
410
565
  /**
411
566
  * [Developer API only] Attach a card to a PAID prepaid Developer API account.
412
567
  *
@@ -464,15 +619,15 @@ var LetsFG = class {
464
619
  const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
465
620
  return this.requestWithHeaders(path, "POST", headers, body);
466
621
  }
467
- async post(path, body) {
468
- return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
622
+ async post(path, body, timeoutMs) {
623
+ return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body, timeoutMs);
469
624
  }
470
- async get(path) {
471
- return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
625
+ async get(path, timeoutMs) {
626
+ return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey }, void 0, timeoutMs);
472
627
  }
473
- async requestWithHeaders(path, method, extraHeaders, body) {
628
+ async requestWithHeaders(path, method, extraHeaders, body, timeoutMs) {
474
629
  const controller = new AbortController();
475
- const timer = setTimeout(() => controller.abort(), this.timeout);
630
+ const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
476
631
  try {
477
632
  const resp = await fetch(`${this.baseUrl}${path}`, {
478
633
  method,
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-AYF7QMP4.mjs";
6
+ } from "./chunk-ZX2EIEN7.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  function getFlag(args, flag, alias) {
package/dist/index.d.mts CHANGED
@@ -480,6 +480,112 @@ declare class LetsFG {
480
480
  * Always provide idempotencyKey to prevent double-bookings on retry.
481
481
  */
482
482
  book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
483
+ /**
484
+ * Resolve a place name to the city id that searchHotels() needs.
485
+ *
486
+ * Use `Id` from the first result as `cityId` and `Name` as `cityName`.
487
+ */
488
+ hotelDestinations(text: string): Promise<Array<Record<string, unknown>>>;
489
+ /**
490
+ * Search real, bookable hotel inventory.
491
+ *
492
+ * Slow by nature — the supplier streams a whole city and every rate is priced
493
+ * — so this gets its own generous timeout rather than the client default.
494
+ *
495
+ * Each offer carries `price` (what the guest pays), `reservation_fee_now`
496
+ * (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
497
+ * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
498
+ *
499
+ * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
500
+ * identify the exact rate, and booking needs both.
501
+ */
502
+ searchHotels(params: {
503
+ cityId: number;
504
+ cityName: string;
505
+ checkIn: string;
506
+ checkOut: string;
507
+ adults?: number;
508
+ children?: number;
509
+ childAges?: number[];
510
+ /** Two-letter code. Rates and taxes genuinely differ by nationality. */
511
+ nationality?: string;
512
+ limit?: number;
513
+ withImages?: boolean;
514
+ }): Promise<Record<string, unknown>>;
515
+ /**
516
+ * Start a booking. Returns a job immediately — it does NOT book inline.
517
+ *
518
+ * A booking takes minutes: the rate is re-blocked at the supplier, every
519
+ * price and date rail is checked, the 10% reservation fee is charged to your
520
+ * card, and only then is the room committed. No proxy holds a connection that
521
+ * long, so this returns at once and you poll hotelBooking() for the outcome.
522
+ * Use bookHotelAndWait() if you would rather block.
523
+ *
524
+ * Because the fee is taken BEFORE the commit, a declined card costs nothing
525
+ * to unwind: no reservation exists and nothing is charged.
526
+ *
527
+ * Send `expectedPrice` and `expectedBalance` back exactly as search returned
528
+ * them — the booking is refused if the supplier has moved beyond tolerance,
529
+ * so a guest is never charged a price they did not agree to.
530
+ *
531
+ * Do NOT call this again for the same rate while a job is running: that books
532
+ * the room twice and charges two reservation fees.
533
+ */
534
+ bookHotel(params: {
535
+ sessionId: string;
536
+ hotelCode: number;
537
+ combinationIdV2: string;
538
+ expectedPrice: number;
539
+ expectedBalance: number;
540
+ cityId: number;
541
+ cityName: string;
542
+ checkIn: string;
543
+ checkOut: string;
544
+ guests: Array<{
545
+ title: string;
546
+ first_name: string;
547
+ last_name: string;
548
+ }>;
549
+ /** The voucher and the pay link go here. A typo loses the booking. */
550
+ email: string;
551
+ phone: string;
552
+ adults?: number;
553
+ combinationId?: number;
554
+ hotelName?: string;
555
+ phoneCountryCode?: string;
556
+ specialRequests?: string[];
557
+ }): Promise<Record<string, unknown>>;
558
+ /**
559
+ * Collect the result of a booking started with bookHotel().
560
+ *
561
+ * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
562
+ * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
563
+ * `balance_due_by` and `terms` (including the full cancellation ladder).
564
+ */
565
+ hotelBooking(bookingJobId: string): Promise<Record<string, unknown>>;
566
+ /**
567
+ * bookHotel(), then poll until the booking settles. Convenience only.
568
+ *
569
+ * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
570
+ * still complete. The returned object carries `booking_job_id` so you can
571
+ * keep polling, and the confirmation is emailed to the guest regardless.
572
+ */
573
+ bookHotelAndWait(params: Parameters<LetsFG['bookHotel']>[0] & {
574
+ pollIntervalMs?: number;
575
+ maxWaitMs?: number;
576
+ }): Promise<Record<string, unknown>>;
577
+ /**
578
+ * Release a reservation at the supplier.
579
+ *
580
+ * Free until `balance_due_by`; after that the hotel's own cancellation ladder
581
+ * applies and can reach 100%. The ladder ships in the booking's `terms`, so
582
+ * you can see the cost before calling this. The 10% reservation fee is NOT
583
+ * refunded.
584
+ *
585
+ * This drives a browser at the supplier and takes over a minute. If it times
586
+ * out, do not assume it failed — re-check before retrying.
587
+ */
588
+ cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
483
589
  /**
484
590
  * [Developer API only] Attach a card to a PAID prepaid Developer API account.
485
591
  *
package/dist/index.d.ts CHANGED
@@ -480,6 +480,112 @@ declare class LetsFG {
480
480
  * Always provide idempotencyKey to prevent double-bookings on retry.
481
481
  */
482
482
  book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
483
+ /**
484
+ * Resolve a place name to the city id that searchHotels() needs.
485
+ *
486
+ * Use `Id` from the first result as `cityId` and `Name` as `cityName`.
487
+ */
488
+ hotelDestinations(text: string): Promise<Array<Record<string, unknown>>>;
489
+ /**
490
+ * Search real, bookable hotel inventory.
491
+ *
492
+ * Slow by nature — the supplier streams a whole city and every rate is priced
493
+ * — so this gets its own generous timeout rather than the client default.
494
+ *
495
+ * Each offer carries `price` (what the guest pays), `reservation_fee_now`
496
+ * (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
497
+ * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
498
+ *
499
+ * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
500
+ * identify the exact rate, and booking needs both.
501
+ */
502
+ searchHotels(params: {
503
+ cityId: number;
504
+ cityName: string;
505
+ checkIn: string;
506
+ checkOut: string;
507
+ adults?: number;
508
+ children?: number;
509
+ childAges?: number[];
510
+ /** Two-letter code. Rates and taxes genuinely differ by nationality. */
511
+ nationality?: string;
512
+ limit?: number;
513
+ withImages?: boolean;
514
+ }): Promise<Record<string, unknown>>;
515
+ /**
516
+ * Start a booking. Returns a job immediately — it does NOT book inline.
517
+ *
518
+ * A booking takes minutes: the rate is re-blocked at the supplier, every
519
+ * price and date rail is checked, the 10% reservation fee is charged to your
520
+ * card, and only then is the room committed. No proxy holds a connection that
521
+ * long, so this returns at once and you poll hotelBooking() for the outcome.
522
+ * Use bookHotelAndWait() if you would rather block.
523
+ *
524
+ * Because the fee is taken BEFORE the commit, a declined card costs nothing
525
+ * to unwind: no reservation exists and nothing is charged.
526
+ *
527
+ * Send `expectedPrice` and `expectedBalance` back exactly as search returned
528
+ * them — the booking is refused if the supplier has moved beyond tolerance,
529
+ * so a guest is never charged a price they did not agree to.
530
+ *
531
+ * Do NOT call this again for the same rate while a job is running: that books
532
+ * the room twice and charges two reservation fees.
533
+ */
534
+ bookHotel(params: {
535
+ sessionId: string;
536
+ hotelCode: number;
537
+ combinationIdV2: string;
538
+ expectedPrice: number;
539
+ expectedBalance: number;
540
+ cityId: number;
541
+ cityName: string;
542
+ checkIn: string;
543
+ checkOut: string;
544
+ guests: Array<{
545
+ title: string;
546
+ first_name: string;
547
+ last_name: string;
548
+ }>;
549
+ /** The voucher and the pay link go here. A typo loses the booking. */
550
+ email: string;
551
+ phone: string;
552
+ adults?: number;
553
+ combinationId?: number;
554
+ hotelName?: string;
555
+ phoneCountryCode?: string;
556
+ specialRequests?: string[];
557
+ }): Promise<Record<string, unknown>>;
558
+ /**
559
+ * Collect the result of a booking started with bookHotel().
560
+ *
561
+ * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
562
+ * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
563
+ * `balance_due_by` and `terms` (including the full cancellation ladder).
564
+ */
565
+ hotelBooking(bookingJobId: string): Promise<Record<string, unknown>>;
566
+ /**
567
+ * bookHotel(), then poll until the booking settles. Convenience only.
568
+ *
569
+ * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
570
+ * still complete. The returned object carries `booking_job_id` so you can
571
+ * keep polling, and the confirmation is emailed to the guest regardless.
572
+ */
573
+ bookHotelAndWait(params: Parameters<LetsFG['bookHotel']>[0] & {
574
+ pollIntervalMs?: number;
575
+ maxWaitMs?: number;
576
+ }): Promise<Record<string, unknown>>;
577
+ /**
578
+ * Release a reservation at the supplier.
579
+ *
580
+ * Free until `balance_due_by`; after that the hotel's own cancellation ladder
581
+ * applies and can reach 100%. The ladder ships in the booking's `terms`, so
582
+ * you can see the cost before calling this. The 10% reservation fee is NOT
583
+ * refunded.
584
+ *
585
+ * This drives a browser at the supplier and takes over a minute. If it times
586
+ * out, do not assume it failed — re-check before retrying.
587
+ */
588
+ cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
483
589
  /**
484
590
  * [Developer API only] Attach a card to a PAID prepaid Developer API account.
485
591
  *
package/dist/index.js CHANGED
@@ -1700,6 +1700,161 @@ var LetsFG = class {
1700
1700
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
1701
1701
  return this.post("/developers/api/v1/bookings/book", body);
1702
1702
  }
1703
+ // ── Hotels ──────────────────────────────────────────────────────────
1704
+ //
1705
+ // A card on file is required for EVERY hotel call, search included. That is
1706
+ // deliberate: a hotel search opens a real session at the supplier and booking
1707
+ // blocks a real rate, so a caller is never allowed to reach the point of
1708
+ // commitment only to discover it cannot pay. The same card that authorises
1709
+ // flight booking authorises hotels — there is no separate hotel enrolment.
1710
+ //
1711
+ // Only free-cancellation, pay-later rates are sold. Those are the rates where
1712
+ // the guest's balance can safely be settled with the supplier after booking,
1713
+ // which is what makes 10%-now/rest-later work. The result set is smaller than
1714
+ // a metasearch's, and every row in it can actually be booked.
1715
+ /**
1716
+ * Resolve a place name to the city id that searchHotels() needs.
1717
+ *
1718
+ * Use `Id` from the first result as `cityId` and `Name` as `cityName`.
1719
+ */
1720
+ async hotelDestinations(text) {
1721
+ this.requireApiKey();
1722
+ const data = await this.post(
1723
+ "/developers/api/v1/hotels/destinations",
1724
+ { text },
1725
+ 6e4
1726
+ );
1727
+ return data.results ?? [];
1728
+ }
1729
+ /**
1730
+ * Search real, bookable hotel inventory.
1731
+ *
1732
+ * Slow by nature — the supplier streams a whole city and every rate is priced
1733
+ * — so this gets its own generous timeout rather than the client default.
1734
+ *
1735
+ * Each offer carries `price` (what the guest pays), `reservation_fee_now`
1736
+ * (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
1737
+ * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
1738
+ *
1739
+ * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
1740
+ * identify the exact rate, and booking needs both.
1741
+ */
1742
+ async searchHotels(params) {
1743
+ this.requireApiKey();
1744
+ const body = {
1745
+ city_id: params.cityId,
1746
+ city_name: params.cityName,
1747
+ check_in: params.checkIn,
1748
+ check_out: params.checkOut,
1749
+ adults: params.adults ?? 2,
1750
+ children: params.children ?? 0,
1751
+ nationality: params.nationality ?? "PL",
1752
+ limit: params.limit ?? 40,
1753
+ with_images: params.withImages ?? true
1754
+ };
1755
+ if (params.childAges?.length) body.child_ages = params.childAges;
1756
+ return this.post("/developers/api/v1/hotels/search", body, 24e4);
1757
+ }
1758
+ /**
1759
+ * Start a booking. Returns a job immediately — it does NOT book inline.
1760
+ *
1761
+ * A booking takes minutes: the rate is re-blocked at the supplier, every
1762
+ * price and date rail is checked, the 10% reservation fee is charged to your
1763
+ * card, and only then is the room committed. No proxy holds a connection that
1764
+ * long, so this returns at once and you poll hotelBooking() for the outcome.
1765
+ * Use bookHotelAndWait() if you would rather block.
1766
+ *
1767
+ * Because the fee is taken BEFORE the commit, a declined card costs nothing
1768
+ * to unwind: no reservation exists and nothing is charged.
1769
+ *
1770
+ * Send `expectedPrice` and `expectedBalance` back exactly as search returned
1771
+ * them — the booking is refused if the supplier has moved beyond tolerance,
1772
+ * so a guest is never charged a price they did not agree to.
1773
+ *
1774
+ * Do NOT call this again for the same rate while a job is running: that books
1775
+ * the room twice and charges two reservation fees.
1776
+ */
1777
+ async bookHotel(params) {
1778
+ this.requireApiKey();
1779
+ const body = {
1780
+ session_id: params.sessionId,
1781
+ hotel_code: params.hotelCode,
1782
+ combination_id_v2: params.combinationIdV2,
1783
+ expected_price: params.expectedPrice,
1784
+ expected_balance: params.expectedBalance,
1785
+ city_id: params.cityId,
1786
+ city_name: params.cityName,
1787
+ check_in: params.checkIn,
1788
+ check_out: params.checkOut,
1789
+ adults: params.adults ?? 2,
1790
+ guests: params.guests,
1791
+ email: params.email,
1792
+ phone: params.phone,
1793
+ phone_country_code: params.phoneCountryCode ?? "48",
1794
+ special_requests: params.specialRequests ?? []
1795
+ };
1796
+ if (params.combinationId != null) body.combination_id = params.combinationId;
1797
+ if (params.hotelName) body.hotel_name = params.hotelName;
1798
+ return this.post("/developers/api/v1/hotels/book", body, 9e4);
1799
+ }
1800
+ /**
1801
+ * Collect the result of a booking started with bookHotel().
1802
+ *
1803
+ * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
1804
+ * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
1805
+ * `balance_due_by` and `terms` (including the full cancellation ladder).
1806
+ */
1807
+ async hotelBooking(bookingJobId) {
1808
+ this.requireApiKey();
1809
+ return this.get(
1810
+ `/developers/api/v1/hotels/booking/${encodeURIComponent(bookingJobId)}`,
1811
+ 6e4
1812
+ );
1813
+ }
1814
+ /**
1815
+ * bookHotel(), then poll until the booking settles. Convenience only.
1816
+ *
1817
+ * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
1818
+ * still complete. The returned object carries `booking_job_id` so you can
1819
+ * keep polling, and the confirmation is emailed to the guest regardless.
1820
+ */
1821
+ async bookHotelAndWait(params) {
1822
+ const pollIntervalMs = params.pollIntervalMs ?? 2e4;
1823
+ const maxWaitMs = params.maxWaitMs ?? 6e5;
1824
+ const job = await this.bookHotel(params);
1825
+ const jobId = job.booking_job_id;
1826
+ if (!jobId) return job;
1827
+ let waited = 0;
1828
+ let result = job;
1829
+ while (waited < maxWaitMs) {
1830
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1831
+ waited += pollIntervalMs;
1832
+ result = await this.hotelBooking(jobId);
1833
+ const st = result.status;
1834
+ if (st === "succeeded" || st === "failed") return result;
1835
+ }
1836
+ if (result.booking_job_id == null) result.booking_job_id = jobId;
1837
+ return result;
1838
+ }
1839
+ /**
1840
+ * Release a reservation at the supplier.
1841
+ *
1842
+ * Free until `balance_due_by`; after that the hotel's own cancellation ladder
1843
+ * applies and can reach 100%. The ladder ships in the booking's `terms`, so
1844
+ * you can see the cost before calling this. The 10% reservation fee is NOT
1845
+ * refunded.
1846
+ *
1847
+ * This drives a browser at the supplier and takes over a minute. If it times
1848
+ * out, do not assume it failed — re-check before retrying.
1849
+ */
1850
+ async cancelHotel(confirmation) {
1851
+ this.requireApiKey();
1852
+ return this.post(
1853
+ "/developers/api/v1/hotels/cancel",
1854
+ { confirmation },
1855
+ 3e5
1856
+ );
1857
+ }
1703
1858
  /**
1704
1859
  * [Developer API only] Attach a card to a PAID prepaid Developer API account.
1705
1860
  *
@@ -1757,15 +1912,15 @@ var LetsFG = class {
1757
1912
  const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
1758
1913
  return this.requestWithHeaders(path, "POST", headers, body);
1759
1914
  }
1760
- async post(path, body) {
1761
- return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
1915
+ async post(path, body, timeoutMs) {
1916
+ return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body, timeoutMs);
1762
1917
  }
1763
- async get(path) {
1764
- return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
1918
+ async get(path, timeoutMs) {
1919
+ return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey }, void 0, timeoutMs);
1765
1920
  }
1766
- async requestWithHeaders(path, method, extraHeaders, body) {
1921
+ async requestWithHeaders(path, method, extraHeaders, body, timeoutMs) {
1767
1922
  const controller = new AbortController();
1768
- const timer = setTimeout(() => controller.abort(), this.timeout);
1923
+ const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
1769
1924
  try {
1770
1925
  const resp = await fetch(`${this.baseUrl}${path}`, {
1771
1926
  method,
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  offerSummary,
23
23
  rankOffers,
24
24
  selectDiverseTop
25
- } from "./chunk-AYF7QMP4.mjs";
25
+ } from "./chunk-ZX2EIEN7.mjs";
26
26
  export {
27
27
  AuthenticationError,
28
28
  BoostedTravel,
package/package.json CHANGED
@@ -1,52 +1,56 @@
1
- {
2
- "name": "letsfg",
3
- "version": "2026.5.62",
4
- "description": "Flight search & booking for AI agents. Server-side engine covers hundreds of airlines. Free search via Bearer token or prepaid Developer API. Includes open-source ranking engine.",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
7
- "types": "dist/index.d.ts",
8
- "bin": {
9
- "letsfg": "dist/cli.js"
10
- },
11
- "files": [
12
- "dist/",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "scripts": {
17
- "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
18
- "test": "tsx --test src/index.test.ts",
19
- "prepublishOnly": "npm run build"
20
- },
21
- "keywords": [
22
- "flights",
23
- "travel",
24
- "booking",
25
- "agent",
26
- "ai",
27
- "cli",
28
- "autonomous",
29
- "mcp",
30
- "openai",
31
- "airline",
32
- "ndc",
33
- "search",
34
- "ndc"
35
- ],
36
- "author": "LetsFG",
37
- "license": "MIT",
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/LetsFG/LetsFG.git"
41
- },
42
- "homepage": "https://letsfg.co",
43
- "devDependencies": {
44
- "@types/node": "^25.3.3",
45
- "tsup": "^8.0.0",
46
- "tsx": "^4.19.0",
47
- "typescript": "^5.3.0"
48
- },
49
- "engines": {
50
- "node": ">=18.0.0"
51
- }
52
- }
1
+ {
2
+ "name": "letsfg",
3
+ "version": "2026.5.64",
4
+ "description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with free cancellation and pay-later terms. Includes open-source ranking engine.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "letsfg": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
18
+ "test": "tsx --test src/index.test.ts",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "flights",
23
+ "travel",
24
+ "booking",
25
+ "agent",
26
+ "ai",
27
+ "cli",
28
+ "autonomous",
29
+ "mcp",
30
+ "openai",
31
+ "airline",
32
+ "ndc",
33
+ "search",
34
+ "ndc",
35
+ "hotels",
36
+ "hotel-booking",
37
+ "hotel-search",
38
+ "accommodation"
39
+ ],
40
+ "author": "LetsFG",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/LetsFG/LetsFG.git"
45
+ },
46
+ "homepage": "https://letsfg.co",
47
+ "devDependencies": {
48
+ "@types/node": "^25.3.3",
49
+ "tsup": "^8.0.0",
50
+ "tsx": "^4.19.0",
51
+ "typescript": "^5.3.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ }
56
+ }