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 +283 -221
- package/dist/{chunk-GO3FXQXC.mjs → chunk-F5BBI6XX.mjs} +134 -20
- package/dist/cli.js +381 -216
- package/dist/cli.mjs +248 -61
- package/dist/index.d.mts +92 -14
- package/dist/index.d.ts +92 -14
- package/dist/index.js +134 -20
- package/dist/index.mjs +1 -1
- package/package.json +56 -56
- package/dist/auth-SYZ5AYZ6.mjs +0 -16
- package/dist/chunk-XNO2W77S.mjs +0 -126
|
@@ -1671,13 +1671,23 @@ var LetsFG = class {
|
|
|
1671
1671
|
return Array.isArray(data) ? data : data.locations || [];
|
|
1672
1672
|
}
|
|
1673
1673
|
/**
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1674
|
+
* RETIRED 2026-09-08. Throws instead of calling the server.
|
|
1675
|
+
*
|
|
1676
|
+
* There is no unlock step on any lane. Unlock existed to confirm a live price
|
|
1677
|
+
* before charging; booking now HOLDS the fare on the connected payment method
|
|
1678
|
+
* and captures only once a real airline PNR exists, so a fare that moved
|
|
1679
|
+
* cannot become a charge for a ticket you did not get. If it moves at
|
|
1680
|
+
* checkout you get a `price_change` question to accept or decline instead.
|
|
1681
|
+
*
|
|
1682
|
+
* Kept as a method, and throwing locally rather than making the request, so an
|
|
1683
|
+
* older caller gets one clear sentence at the line that is actually wrong —
|
|
1684
|
+
* not a 410 body to decode, and not a TypeError somewhere else.
|
|
1677
1685
|
*/
|
|
1678
|
-
async unlock(
|
|
1679
|
-
|
|
1680
|
-
|
|
1686
|
+
async unlock(_offerId) {
|
|
1687
|
+
throw new LetsFGError(
|
|
1688
|
+
"unlock() was retired on 2026-09-08 and the endpoint answers 410 Gone. There is no unlock step: call book() directly. The fare is held on the connected payment method and captured only against a real airline PNR. See https://letsfg.co/developers/api/docs",
|
|
1689
|
+
410
|
|
1690
|
+
);
|
|
1681
1691
|
}
|
|
1682
1692
|
/**
|
|
1683
1693
|
* Book a flight.
|
|
@@ -1688,9 +1698,14 @@ var LetsFG = class {
|
|
|
1688
1698
|
* complete, { ok, booked: false, booking_url } — hand the link to the user,
|
|
1689
1699
|
* nothing was charged.
|
|
1690
1700
|
*
|
|
1691
|
-
* Developer API (X-API-Key):
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1701
|
+
* Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
|
|
1702
|
+
* REQUIRED — an offer is bookable only inside the search that produced it.
|
|
1703
|
+
* The connected Revolut method is HELD, not charged; a LetsFG booking agent
|
|
1704
|
+
* buys the ticket and the hold is captured only against a real airline PNR.
|
|
1705
|
+
* Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
|
|
1706
|
+
* poll getBooking(bookingId) until `terminal`, or use bookAndWait().
|
|
1707
|
+
* Always provide idempotencyKey: a retry with the same key returns the
|
|
1708
|
+
* existing booking instead of opening a second hold on the card.
|
|
1694
1709
|
*/
|
|
1695
1710
|
async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
|
|
1696
1711
|
this.requireAuth();
|
|
@@ -1711,15 +1726,92 @@ var LetsFG = class {
|
|
|
1711
1726
|
});
|
|
1712
1727
|
}
|
|
1713
1728
|
this.requireApiKey();
|
|
1729
|
+
if (!searchId) {
|
|
1730
|
+
throw new LetsFGError(
|
|
1731
|
+
"searchId is required to book on the Developer API \u2014 pass the search_id from search()'s result. An offer can only be booked inside the search that produced it. (Before 2026-09-08 this argument was ignored on this path.)",
|
|
1732
|
+
400
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
const pax = passengers.map((p) => ({ ...p }));
|
|
1736
|
+
if (contactPhone && pax.length && !pax[0].phone_number) pax[0].phone_number = contactPhone;
|
|
1714
1737
|
const body = {
|
|
1738
|
+
search_id: searchId,
|
|
1715
1739
|
offer_id: offerId,
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
contact_email: contactEmail,
|
|
1719
|
-
contact_phone: contactPhone
|
|
1740
|
+
passengers: pax,
|
|
1741
|
+
contact_email: contactEmail
|
|
1720
1742
|
};
|
|
1721
1743
|
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
1722
|
-
return this.post("/developers/api/v1/
|
|
1744
|
+
return this.post("/developers/api/v1/flights/book", body);
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Poll a Developer API flight booking.
|
|
1748
|
+
*
|
|
1749
|
+
* Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
|
|
1750
|
+
* knows you are still there, which is what keeps a booking paused on a
|
|
1751
|
+
* question alive — so do not back off to minutes.
|
|
1752
|
+
*
|
|
1753
|
+
* States: authorised, card_issued, booking_in_progress, awaiting_settlement,
|
|
1754
|
+
* then completed (with `pnr` and `charged_amount`), failed (hold released,
|
|
1755
|
+
* nothing charged) or needs_attention (a human at LetsFG is on it — do not
|
|
1756
|
+
* book again).
|
|
1757
|
+
*/
|
|
1758
|
+
async getBooking(bookingId) {
|
|
1759
|
+
this.requireApiKey();
|
|
1760
|
+
return this.getWithAuth(
|
|
1761
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Answer the open `question` on a booking.
|
|
1766
|
+
*
|
|
1767
|
+
* Echo the question's `round`. A stale round is refused with 409 rather than
|
|
1768
|
+
* guessed at, so an answer to an old question can never be applied to a new
|
|
1769
|
+
* one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
|
|
1770
|
+
* { confirm: true } or { skip: true } — declining an extra still completes
|
|
1771
|
+
* the booking, without it.
|
|
1772
|
+
*/
|
|
1773
|
+
async answerBooking(bookingId, round, answer = {}) {
|
|
1774
|
+
this.requireApiKey();
|
|
1775
|
+
return this.post(
|
|
1776
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}/answer`,
|
|
1777
|
+
{ round, ...answer }
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Book and poll to a terminal state. Mirrors bookHotelAndWait().
|
|
1782
|
+
*
|
|
1783
|
+
* Blocks for as long as the booking takes (4–11 minutes typically), so use
|
|
1784
|
+
* book() + getBooking() instead if your caller has a request timeout.
|
|
1785
|
+
*
|
|
1786
|
+
* `onQuestion` returns the answer for answerBooking(). Without it, a fare
|
|
1787
|
+
* increase is ACCEPTED and a paid extra is DECLINED — the conservative
|
|
1788
|
+
* reading of "the traveller asked for this flight".
|
|
1789
|
+
*/
|
|
1790
|
+
async bookAndWait(offerId, passengers, contactEmail, searchId, opts = {}) {
|
|
1791
|
+
const { contactPhone = "", idempotencyKey = "", pollMs = 5e3, timeoutMs = 9e5, onQuestion } = opts;
|
|
1792
|
+
const started = await this.book(
|
|
1793
|
+
offerId,
|
|
1794
|
+
passengers,
|
|
1795
|
+
contactEmail,
|
|
1796
|
+
contactPhone,
|
|
1797
|
+
idempotencyKey,
|
|
1798
|
+
searchId
|
|
1799
|
+
);
|
|
1800
|
+
if (!started || started.ok !== true) return started;
|
|
1801
|
+
const bookingId = String(started.booking_id);
|
|
1802
|
+
const deadline = Date.now() + timeoutMs;
|
|
1803
|
+
while (Date.now() < deadline) {
|
|
1804
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1805
|
+
const state = await this.getBooking(bookingId);
|
|
1806
|
+
const question = state.question;
|
|
1807
|
+
if (question) {
|
|
1808
|
+
const answer = onQuestion ? onQuestion(question) : question.kind === "extra" ? { skip: true } : { confirm: true };
|
|
1809
|
+
await this.answerBooking(bookingId, Number(question.round), answer);
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (state.terminal) return state;
|
|
1813
|
+
}
|
|
1814
|
+
return this.getBooking(bookingId);
|
|
1723
1815
|
}
|
|
1724
1816
|
// ── Hotels ──────────────────────────────────────────────────────────
|
|
1725
1817
|
//
|
|
@@ -1877,15 +1969,37 @@ var LetsFG = class {
|
|
|
1877
1969
|
);
|
|
1878
1970
|
}
|
|
1879
1971
|
/**
|
|
1880
|
-
* [Developer API
|
|
1972
|
+
* [Developer API] Mint a one-time link for connecting a Revolut payment method.
|
|
1973
|
+
*
|
|
1974
|
+
* This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
|
|
1975
|
+
* card details never touch LetsFG: the returned `connect_url` opens a hosted page
|
|
1976
|
+
* where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
|
|
1977
|
+
* it in a browser — there is no endpoint that takes card details, so do not ask a
|
|
1978
|
+
* user for a card number and do not try to automate this step.
|
|
1881
1979
|
*
|
|
1882
|
-
* Most agents should NOT
|
|
1883
|
-
* search and booking
|
|
1884
|
-
* through a zero-amount setup and creates no billing account.
|
|
1980
|
+
* Most agents should NOT need a Developer API account at all. To authenticate for
|
|
1981
|
+
* search and booking, run `letsfg auth`, which creates no billing account.
|
|
1885
1982
|
*/
|
|
1886
|
-
async
|
|
1983
|
+
async connectPayment() {
|
|
1887
1984
|
this.requireApiKey();
|
|
1888
|
-
return this.post("/developers/api/v1/agents/
|
|
1985
|
+
return this.post("/developers/api/v1/agents/connect-payment", {});
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
|
|
1989
|
+
*
|
|
1990
|
+
* `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
|
|
1991
|
+
* Revolut rail as the rest of the product: call connectPayment() and open the
|
|
1992
|
+
* `connect_url` it returns.
|
|
1993
|
+
*
|
|
1994
|
+
* Kept as a method, and throwing locally rather than making the request, for the
|
|
1995
|
+
* same reason as unlock() — an older caller gets one clear sentence at the line that
|
|
1996
|
+
* is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
|
|
1997
|
+
*/
|
|
1998
|
+
async setupPayment(_token) {
|
|
1999
|
+
throw new LetsFGError(
|
|
2000
|
+
"setupPayment() was retired on 2026-09-08 with Stripe and the endpoint answers 410 Gone. Call connectPayment() instead and open the connect_url it returns; nothing is charged to connect. See https://letsfg.co/developers/api/docs",
|
|
2001
|
+
410
|
|
2002
|
+
);
|
|
1889
2003
|
}
|
|
1890
2004
|
/**
|
|
1891
2005
|
* Get current agent profile and usage stats.
|