letsfg 2026.5.73 → 2026.5.75
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 +325 -283
- package/dist/{chunk-F5BBI6XX.mjs → chunk-6IWYHW62.mjs} +93 -47
- package/dist/cli.js +92 -47
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +80 -32
- package/dist/index.d.ts +80 -32
- package/dist/index.js +94 -47
- package/dist/index.mjs +3 -1
- package/package.json +56 -56
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(index_exports, {
|
|
|
25
25
|
BoostedTravelError: () => BoostedTravelError,
|
|
26
26
|
ErrorCategory: () => ErrorCategory,
|
|
27
27
|
ErrorCode: () => ErrorCode,
|
|
28
|
+
HOTEL_BOOKING_FINAL_STATUSES: () => HOTEL_BOOKING_FINAL_STATUSES,
|
|
28
29
|
LetsFG: () => LetsFG,
|
|
29
30
|
LetsFGError: () => LetsFGError,
|
|
30
31
|
OfferExpiredError: () => OfferExpiredError,
|
|
@@ -1612,6 +1613,7 @@ var LATE_MERGE_POLL_MS = 3e3;
|
|
|
1612
1613
|
var LATE_MERGE_GRACE_MS = 9e4;
|
|
1613
1614
|
var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
|
|
1614
1615
|
var NON_TERMINAL = ["pending", "searching"];
|
|
1616
|
+
var HOTEL_BOOKING_FINAL_STATUSES = ["succeeded", "failed", "attention"];
|
|
1615
1617
|
var LetsFG = class {
|
|
1616
1618
|
bearerToken;
|
|
1617
1619
|
apiKey;
|
|
@@ -1665,15 +1667,23 @@ var LetsFG = class {
|
|
|
1665
1667
|
currency: options.currency ?? "EUR",
|
|
1666
1668
|
limit: options.limit ?? 50
|
|
1667
1669
|
};
|
|
1668
|
-
if (options.returnDate) body.return_date = options.returnDate;
|
|
1669
|
-
if (options.cabinClass) body.cabin_class = options.cabinClass;
|
|
1670
|
-
if (options.maxStopovers != null) body.max_stopovers = options.maxStopovers;
|
|
1671
|
-
if (options.sort) body.sort = options.sort;
|
|
1672
1670
|
if (options.departureTimeFrom) body.departure_time_from = options.departureTimeFrom;
|
|
1673
1671
|
if (options.departureTimeTo) body.departure_time_to = options.departureTimeTo;
|
|
1674
1672
|
if (this.usingPFS) {
|
|
1675
|
-
|
|
1673
|
+
if (options.returnDate) body.return_date = options.returnDate;
|
|
1674
|
+
if (options.cabinClass) body.cabin = options.cabinClass;
|
|
1675
|
+
if (options.maxStopovers != null) body.max_stops = options.maxStopovers;
|
|
1676
|
+
if (options.sort) body.sort_by = options.sort;
|
|
1677
|
+
const result = await this.searchPFS(body);
|
|
1678
|
+
if (options.limit != null && Array.isArray(result.offers)) {
|
|
1679
|
+
result.offers = result.offers.slice(0, Math.max(0, options.limit));
|
|
1680
|
+
}
|
|
1681
|
+
return result;
|
|
1676
1682
|
}
|
|
1683
|
+
if (options.returnDate) body.return_from = options.returnDate;
|
|
1684
|
+
if (options.cabinClass) body.cabin_class = options.cabinClass;
|
|
1685
|
+
if (options.maxStopovers != null) body.max_stopovers = options.maxStopovers;
|
|
1686
|
+
if (options.sort) body.sort = options.sort;
|
|
1677
1687
|
return this.post("/developers/api/v1/flights/search", body);
|
|
1678
1688
|
}
|
|
1679
1689
|
/** PFS path: POST /api/search -> poll /api/results/<id> */
|
|
@@ -1863,16 +1873,18 @@ var LetsFG = class {
|
|
|
1863
1873
|
}
|
|
1864
1874
|
// ── Hotels ──────────────────────────────────────────────────────────
|
|
1865
1875
|
//
|
|
1866
|
-
// A
|
|
1867
|
-
// deliberate: a hotel search opens a real session at the
|
|
1868
|
-
// blocks a real rate, so a caller is never allowed to
|
|
1869
|
-
// commitment only to discover it cannot pay. The same
|
|
1870
|
-
// flight booking authorises hotels
|
|
1876
|
+
// A connected payment method is required for EVERY hotel call, search
|
|
1877
|
+
// included. That is deliberate: a hotel search opens a real session at the
|
|
1878
|
+
// supplier and booking blocks a real rate, so a caller is never allowed to
|
|
1879
|
+
// reach the point of commitment only to discover it cannot pay. The same
|
|
1880
|
+
// method that authorises flight booking authorises hotels.
|
|
1871
1881
|
//
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1874
|
-
//
|
|
1875
|
-
//
|
|
1882
|
+
// How a hotel is paid (since 2026-09-11): the full `price` is HELD on the
|
|
1883
|
+
// connected Revolut method, LetsFG books and pays the supplier itself, and the
|
|
1884
|
+
// hold is captured only once the supplier has confirmed. A booking that fails
|
|
1885
|
+
// releases the hold. There is no reservation fee, no deposit and no pay link —
|
|
1886
|
+
// those belonged to the process retired on 2026-09-11. Every rate type is
|
|
1887
|
+
// sold, refundable and non-refundable.
|
|
1876
1888
|
/**
|
|
1877
1889
|
* Resolve a place name to the city id that searchHotels() needs.
|
|
1878
1890
|
*
|
|
@@ -1893,12 +1905,17 @@ var LetsFG = class {
|
|
|
1893
1905
|
* Slow by nature — the supplier streams a whole city and every rate is priced
|
|
1894
1906
|
* — so this gets its own generous timeout rather than the client default.
|
|
1895
1907
|
*
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
1898
|
-
* `
|
|
1908
|
+
* The response carries `session_id`, `currency`, `supplier_currency`,
|
|
1909
|
+
* `markup_rate`, `fx_rate`, `fx_as_of`, `count`, `hotels`, `terms` and
|
|
1910
|
+
* `caveats`. Each offer carries `price` (what the guest pays, in `currency`),
|
|
1911
|
+
* `currency`, `fx_rate`, `expected_cost` (the supplier's cost, in PLN),
|
|
1912
|
+
* `refundable`, `free_cancellation_until` (refundable rates only),
|
|
1913
|
+
* `cancellation_policy` and its own `session_id`.
|
|
1899
1914
|
*
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1915
|
+
* `price` is the supplier's cost plus `markup_rate` (6.4% for Revolut Pay or an
|
|
1916
|
+
* EEA-issued card, 8.3% for a card issued outside the EEA); nothing is added at
|
|
1917
|
+
* booking. Keep the chosen offer whole: bookHotel() needs its `session_id`,
|
|
1918
|
+
* `combination_id_v2`, `price`, `expected_cost`, `currency` and `fx_rate`.
|
|
1902
1919
|
*/
|
|
1903
1920
|
async searchHotels(params) {
|
|
1904
1921
|
this.requireApiKey();
|
|
@@ -1911,7 +1928,8 @@ var LetsFG = class {
|
|
|
1911
1928
|
children: params.children ?? 0,
|
|
1912
1929
|
nationality: params.nationality ?? "PL",
|
|
1913
1930
|
limit: params.limit ?? 40,
|
|
1914
|
-
with_images: params.withImages ?? true
|
|
1931
|
+
with_images: params.withImages ?? true,
|
|
1932
|
+
currency: params.currency ?? "USD"
|
|
1915
1933
|
};
|
|
1916
1934
|
if (params.childAges?.length) body.child_ages = params.childAges;
|
|
1917
1935
|
return this.post("/developers/api/v1/hotels/search", body, 24e4);
|
|
@@ -1919,30 +1937,42 @@ var LetsFG = class {
|
|
|
1919
1937
|
/**
|
|
1920
1938
|
* Start a booking. Returns a job immediately — it does NOT book inline.
|
|
1921
1939
|
*
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
1940
|
+
* What happens, in order: the offer's full `price` is HELD on the Revolut
|
|
1941
|
+
* payment method connected to this account (authorised, not taken); LetsFG
|
|
1942
|
+
* books the room with the supplier and pays the supplier itself; the hold is
|
|
1943
|
+
* captured only once the supplier has confirmed. If the booking fails for any
|
|
1944
|
+
* reason, the hold is released and nothing is charged. There is no
|
|
1945
|
+
* reservation fee, no deposit and no pay link.
|
|
1927
1946
|
*
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1947
|
+
* A booking takes minutes and no proxy holds a connection that long, so this
|
|
1948
|
+
* returns at once and you poll hotelBooking() for the outcome. Use
|
|
1949
|
+
* bookHotelAndWait() if you would rather block.
|
|
1930
1950
|
*
|
|
1931
|
-
* Send `expectedPrice`
|
|
1932
|
-
* them
|
|
1933
|
-
*
|
|
1951
|
+
* Send `expectedPrice` (the offer's `price`), `expectedCost`, `currency` and
|
|
1952
|
+
* `fxRate` exactly as search returned them. The booking is refused if the
|
|
1953
|
+
* supplier's live cost is above `expectedCost`, and a USD offer sent without
|
|
1954
|
+
* its `currency` is refused with 400 price_mismatch (the API assumes PLN).
|
|
1955
|
+
* Guest names, phone and e-mail are checked before anything is held; a problem
|
|
1956
|
+
* returns 400 invalid_details naming the fields.
|
|
1934
1957
|
*
|
|
1935
|
-
* Do NOT call this again for
|
|
1936
|
-
* the
|
|
1958
|
+
* Do NOT call this again for a booking whose job is still running: poll it.
|
|
1959
|
+
* A retry of the same booking returns the job already under way
|
|
1960
|
+
* (`duplicate: true`) rather than holding the money twice.
|
|
1937
1961
|
*/
|
|
1938
1962
|
async bookHotel(params) {
|
|
1939
1963
|
this.requireApiKey();
|
|
1964
|
+
if (typeof params.expectedCost !== "number") {
|
|
1965
|
+
throw new LetsFGError(
|
|
1966
|
+
"bookHotel() needs expectedCost: copy the offer's expected_cost, currency and fx_rate. " + ("expectedBalance" in params ? "expectedBalance belonged to the reservation-fee process retired on 2026-09-11 and is not sent. " : "") + "See https://letsfg.co/developers/api/docs",
|
|
1967
|
+
400
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1940
1970
|
const body = {
|
|
1941
1971
|
session_id: params.sessionId,
|
|
1942
1972
|
hotel_code: params.hotelCode,
|
|
1943
1973
|
combination_id_v2: params.combinationIdV2,
|
|
1944
1974
|
expected_price: params.expectedPrice,
|
|
1945
|
-
|
|
1975
|
+
expected_cost: params.expectedCost,
|
|
1946
1976
|
city_id: params.cityId,
|
|
1947
1977
|
city_name: params.cityName,
|
|
1948
1978
|
check_in: params.checkIn,
|
|
@@ -1954,16 +1984,30 @@ var LetsFG = class {
|
|
|
1954
1984
|
phone_country_code: params.phoneCountryCode ?? "48",
|
|
1955
1985
|
special_requests: params.specialRequests ?? []
|
|
1956
1986
|
};
|
|
1987
|
+
if (params.currency) body.currency = params.currency;
|
|
1988
|
+
if (params.fxRate != null) body.fx_rate = params.fxRate;
|
|
1957
1989
|
if (params.combinationId != null) body.combination_id = params.combinationId;
|
|
1958
1990
|
if (params.hotelName) body.hotel_name = params.hotelName;
|
|
1991
|
+
if (params.idempotencyKey) body.idempotency_key = params.idempotencyKey;
|
|
1959
1992
|
return this.post("/developers/api/v1/hotels/book", body, 9e4);
|
|
1960
1993
|
}
|
|
1961
1994
|
/**
|
|
1962
1995
|
* Collect the result of a booking started with bookHotel().
|
|
1963
1996
|
*
|
|
1964
|
-
* `status` is 'in_progress', 'succeeded'
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1997
|
+
* `status` is 'in_progress', 'succeeded', 'failed' or 'attention'; the last
|
|
1998
|
+
* three are final (HOTEL_BOOKING_FINAL_STATUSES).
|
|
1999
|
+
*
|
|
2000
|
+
* - succeeded: `confirmation`, `booking_id`, `hotel`, `room`, `total_price` +
|
|
2001
|
+
* `currency` (what the guest is charged), `supplier_paid` +
|
|
2002
|
+
* `supplier_currency` (what the supplier was paid), `payment_status`,
|
|
2003
|
+
* `refundable`, `free_cancellation_until`, `cancellation_ladder`, `terms`.
|
|
2004
|
+
* - failed: `error`, written for the guest. The hold has been released and
|
|
2005
|
+
* nothing was charged.
|
|
2006
|
+
* - attention: `error` (and `confirmation` when known). The outcome could not
|
|
2007
|
+
* be settled automatically; the hold is kept — nothing is charged — while a
|
|
2008
|
+
* person checks with the supplier. Do not book again.
|
|
2009
|
+
*
|
|
2010
|
+
* The guest is e-mailed in every case.
|
|
1967
2011
|
*/
|
|
1968
2012
|
async hotelBooking(bookingJobId) {
|
|
1969
2013
|
this.requireApiKey();
|
|
@@ -1975,13 +2019,15 @@ var LetsFG = class {
|
|
|
1975
2019
|
/**
|
|
1976
2020
|
* bookHotel(), then poll until the booking settles. Convenience only.
|
|
1977
2021
|
*
|
|
1978
|
-
*
|
|
1979
|
-
*
|
|
1980
|
-
*
|
|
2022
|
+
* Stops at 'succeeded', 'failed' or 'attention' and never re-books. Giving up
|
|
2023
|
+
* after `maxWaitMs` (default 30 minutes: a booking usually takes 5-10, and
|
|
2024
|
+
* hotel bookings run one at a time) does NOT cancel anything — the booking may
|
|
2025
|
+
* still complete. The returned object carries `booking_job_id` so you can keep
|
|
2026
|
+
* polling, and the guest is e-mailed the outcome regardless.
|
|
1981
2027
|
*/
|
|
1982
2028
|
async bookHotelAndWait(params) {
|
|
1983
2029
|
const pollIntervalMs = params.pollIntervalMs ?? 2e4;
|
|
1984
|
-
const maxWaitMs = params.maxWaitMs ??
|
|
2030
|
+
const maxWaitMs = params.maxWaitMs ?? 18e5;
|
|
1985
2031
|
const job = await this.bookHotel(params);
|
|
1986
2032
|
const jobId = job.booking_job_id;
|
|
1987
2033
|
if (!jobId) return job;
|
|
@@ -1991,19 +2037,19 @@ var LetsFG = class {
|
|
|
1991
2037
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1992
2038
|
waited += pollIntervalMs;
|
|
1993
2039
|
result = await this.hotelBooking(jobId);
|
|
1994
|
-
|
|
1995
|
-
if (st === "succeeded" || st === "failed") return result;
|
|
2040
|
+
if (HOTEL_BOOKING_FINAL_STATUSES.includes(result.status)) return result;
|
|
1996
2041
|
}
|
|
1997
2042
|
if (result.booking_job_id == null) result.booking_job_id = jobId;
|
|
1998
2043
|
return result;
|
|
1999
2044
|
}
|
|
2000
2045
|
/**
|
|
2001
|
-
* Release a reservation at the supplier.
|
|
2046
|
+
* Release a reservation at the supplier and refund the guest.
|
|
2002
2047
|
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2048
|
+
* Only this account's own bookings can be cancelled (anything else is 404). A
|
|
2049
|
+
* zero-charge cancellation — a refundable rate before its
|
|
2050
|
+
* `free_cancellation_until` — refunds the charge in full, or releases a hold
|
|
2051
|
+
* not yet captured. A cancellation that would cost money is refused with 409;
|
|
2052
|
+
* the hotel's own ladder is in the booking's `terms`.
|
|
2007
2053
|
*
|
|
2008
2054
|
* This drives a browser at the supplier and takes over a minute. If it times
|
|
2009
2055
|
* out, do not assume it failed — re-check before retrying.
|
|
@@ -2142,6 +2188,7 @@ var BoostedTravelError = LetsFGError;
|
|
|
2142
2188
|
BoostedTravelError,
|
|
2143
2189
|
ErrorCategory,
|
|
2144
2190
|
ErrorCode,
|
|
2191
|
+
HOTEL_BOOKING_FINAL_STATUSES,
|
|
2145
2192
|
LetsFG,
|
|
2146
2193
|
LetsFGError,
|
|
2147
2194
|
OfferExpiredError,
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
BoostedTravelError,
|
|
5
5
|
ErrorCategory,
|
|
6
6
|
ErrorCode,
|
|
7
|
+
HOTEL_BOOKING_FINAL_STATUSES,
|
|
7
8
|
LetsFG,
|
|
8
9
|
LetsFGError,
|
|
9
10
|
OfferExpiredError,
|
|
@@ -22,13 +23,14 @@ import {
|
|
|
22
23
|
offerSummary,
|
|
23
24
|
rankOffers,
|
|
24
25
|
selectDiverseTop
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-6IWYHW62.mjs";
|
|
26
27
|
export {
|
|
27
28
|
AuthenticationError,
|
|
28
29
|
BoostedTravel,
|
|
29
30
|
BoostedTravelError,
|
|
30
31
|
ErrorCategory,
|
|
31
32
|
ErrorCode,
|
|
33
|
+
HOTEL_BOOKING_FINAL_STATUSES,
|
|
32
34
|
LetsFG,
|
|
33
35
|
LetsFGError,
|
|
34
36
|
OfferExpiredError,
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "letsfg",
|
|
3
|
-
"version": "2026.5.
|
|
4
|
-
"description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with
|
|
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 src/auth.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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "letsfg",
|
|
3
|
+
"version": "2026.5.75",
|
|
4
|
+
"description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with pay-later terms, and every rate says whether it is refundable. 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 src/auth.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
|
+
}
|