letsfg 2026.5.72 → 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.
@@ -1671,13 +1671,23 @@ var LetsFG = class {
1671
1671
  return Array.isArray(data) ? data : data.locations || [];
1672
1672
  }
1673
1673
  /**
1674
- * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1675
- * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
1676
- * token, so PFS callers use book() directly.
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(offerId) {
1679
- this.requireApiKey();
1680
- return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
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): charges ticket price + service fee via Stripe,
1692
- * creates a real PNR. Requires unlock() first. Always provide
1693
- * idempotencyKey to prevent double-bookings on retry.
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
- booking_type: "flight",
1717
- passengers,
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/bookings/book", body);
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 only] Attach a card to a PAID prepaid Developer API account.
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 call this. It is unrelated to authenticating for
1883
- * search and booking — for that, run `letsfg auth`, which puts a card on file
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 setupPayment(token = "tok_visa") {
1983
+ async connectPayment() {
1887
1984
  this.requireApiKey();
1888
- return this.post("/developers/api/v1/agents/setup-payment", { token });
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.
package/dist/cli.js CHANGED
@@ -427,13 +427,23 @@ var LetsFG = class {
427
427
  return Array.isArray(data) ? data : data.locations || [];
428
428
  }
429
429
  /**
430
- * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
431
- * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
432
- * token, so PFS callers use book() directly.
430
+ * RETIRED 2026-09-08. Throws instead of calling the server.
431
+ *
432
+ * There is no unlock step on any lane. Unlock existed to confirm a live price
433
+ * before charging; booking now HOLDS the fare on the connected payment method
434
+ * and captures only once a real airline PNR exists, so a fare that moved
435
+ * cannot become a charge for a ticket you did not get. If it moves at
436
+ * checkout you get a `price_change` question to accept or decline instead.
437
+ *
438
+ * Kept as a method, and throwing locally rather than making the request, so an
439
+ * older caller gets one clear sentence at the line that is actually wrong —
440
+ * not a 410 body to decode, and not a TypeError somewhere else.
433
441
  */
434
- async unlock(offerId) {
435
- this.requireApiKey();
436
- return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
442
+ async unlock(_offerId) {
443
+ throw new LetsFGError(
444
+ "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",
445
+ 410
446
+ );
437
447
  }
438
448
  /**
439
449
  * Book a flight.
@@ -444,9 +454,14 @@ var LetsFG = class {
444
454
  * complete, { ok, booked: false, booking_url } — hand the link to the user,
445
455
  * nothing was charged.
446
456
  *
447
- * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
448
- * creates a real PNR. Requires unlock() first. Always provide
449
- * idempotencyKey to prevent double-bookings on retry.
457
+ * Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
458
+ * REQUIRED — an offer is bookable only inside the search that produced it.
459
+ * The connected Revolut method is HELD, not charged; a LetsFG booking agent
460
+ * buys the ticket and the hold is captured only against a real airline PNR.
461
+ * Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
462
+ * poll getBooking(bookingId) until `terminal`, or use bookAndWait().
463
+ * Always provide idempotencyKey: a retry with the same key returns the
464
+ * existing booking instead of opening a second hold on the card.
450
465
  */
451
466
  async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
452
467
  this.requireAuth();
@@ -467,15 +482,92 @@ var LetsFG = class {
467
482
  });
468
483
  }
469
484
  this.requireApiKey();
485
+ if (!searchId) {
486
+ throw new LetsFGError(
487
+ "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.)",
488
+ 400
489
+ );
490
+ }
491
+ const pax = passengers.map((p) => ({ ...p }));
492
+ if (contactPhone && pax.length && !pax[0].phone_number) pax[0].phone_number = contactPhone;
470
493
  const body = {
494
+ search_id: searchId,
471
495
  offer_id: offerId,
472
- booking_type: "flight",
473
- passengers,
474
- contact_email: contactEmail,
475
- contact_phone: contactPhone
496
+ passengers: pax,
497
+ contact_email: contactEmail
476
498
  };
477
499
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
478
- return this.post("/developers/api/v1/bookings/book", body);
500
+ return this.post("/developers/api/v1/flights/book", body);
501
+ }
502
+ /**
503
+ * Poll a Developer API flight booking.
504
+ *
505
+ * Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
506
+ * knows you are still there, which is what keeps a booking paused on a
507
+ * question alive — so do not back off to minutes.
508
+ *
509
+ * States: authorised, card_issued, booking_in_progress, awaiting_settlement,
510
+ * then completed (with `pnr` and `charged_amount`), failed (hold released,
511
+ * nothing charged) or needs_attention (a human at LetsFG is on it — do not
512
+ * book again).
513
+ */
514
+ async getBooking(bookingId) {
515
+ this.requireApiKey();
516
+ return this.getWithAuth(
517
+ `/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}`
518
+ );
519
+ }
520
+ /**
521
+ * Answer the open `question` on a booking.
522
+ *
523
+ * Echo the question's `round`. A stale round is refused with 409 rather than
524
+ * guessed at, so an answer to an old question can never be applied to a new
525
+ * one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
526
+ * { confirm: true } or { skip: true } — declining an extra still completes
527
+ * the booking, without it.
528
+ */
529
+ async answerBooking(bookingId, round, answer = {}) {
530
+ this.requireApiKey();
531
+ return this.post(
532
+ `/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}/answer`,
533
+ { round, ...answer }
534
+ );
535
+ }
536
+ /**
537
+ * Book and poll to a terminal state. Mirrors bookHotelAndWait().
538
+ *
539
+ * Blocks for as long as the booking takes (4–11 minutes typically), so use
540
+ * book() + getBooking() instead if your caller has a request timeout.
541
+ *
542
+ * `onQuestion` returns the answer for answerBooking(). Without it, a fare
543
+ * increase is ACCEPTED and a paid extra is DECLINED — the conservative
544
+ * reading of "the traveller asked for this flight".
545
+ */
546
+ async bookAndWait(offerId, passengers, contactEmail, searchId, opts = {}) {
547
+ const { contactPhone = "", idempotencyKey = "", pollMs = 5e3, timeoutMs = 9e5, onQuestion } = opts;
548
+ const started = await this.book(
549
+ offerId,
550
+ passengers,
551
+ contactEmail,
552
+ contactPhone,
553
+ idempotencyKey,
554
+ searchId
555
+ );
556
+ if (!started || started.ok !== true) return started;
557
+ const bookingId = String(started.booking_id);
558
+ const deadline = Date.now() + timeoutMs;
559
+ while (Date.now() < deadline) {
560
+ await new Promise((r) => setTimeout(r, pollMs));
561
+ const state = await this.getBooking(bookingId);
562
+ const question = state.question;
563
+ if (question) {
564
+ const answer = onQuestion ? onQuestion(question) : question.kind === "extra" ? { skip: true } : { confirm: true };
565
+ await this.answerBooking(bookingId, Number(question.round), answer);
566
+ continue;
567
+ }
568
+ if (state.terminal) return state;
569
+ }
570
+ return this.getBooking(bookingId);
479
571
  }
480
572
  // ── Hotels ──────────────────────────────────────────────────────────
481
573
  //
@@ -633,15 +725,37 @@ var LetsFG = class {
633
725
  );
634
726
  }
635
727
  /**
636
- * [Developer API only] Attach a card to a PAID prepaid Developer API account.
728
+ * [Developer API] Mint a one-time link for connecting a Revolut payment method.
729
+ *
730
+ * This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
731
+ * card details never touch LetsFG: the returned `connect_url` opens a hosted page
732
+ * where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
733
+ * it in a browser — there is no endpoint that takes card details, so do not ask a
734
+ * user for a card number and do not try to automate this step.
637
735
  *
638
- * Most agents should NOT call this. It is unrelated to authenticating for
639
- * search and booking — for that, run `letsfg auth`, which puts a card on file
640
- * through a zero-amount setup and creates no billing account.
736
+ * Most agents should NOT need a Developer API account at all. To authenticate for
737
+ * search and booking, run `letsfg auth`, which creates no billing account.
641
738
  */
642
- async setupPayment(token = "tok_visa") {
739
+ async connectPayment() {
643
740
  this.requireApiKey();
644
- return this.post("/developers/api/v1/agents/setup-payment", { token });
741
+ return this.post("/developers/api/v1/agents/connect-payment", {});
742
+ }
743
+ /**
744
+ * RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
745
+ *
746
+ * `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
747
+ * Revolut rail as the rest of the product: call connectPayment() and open the
748
+ * `connect_url` it returns.
749
+ *
750
+ * Kept as a method, and throwing locally rather than making the request, for the
751
+ * same reason as unlock() — an older caller gets one clear sentence at the line that
752
+ * is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
753
+ */
754
+ async setupPayment(_token) {
755
+ throw new LetsFGError(
756
+ "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",
757
+ 410
758
+ );
645
759
  }
646
760
  /**
647
761
  * Get current agent profile and usage stats.
@@ -1051,38 +1165,15 @@ async function cmdSearch(args) {
1051
1165
  `);
1052
1166
  } else {
1053
1167
  console.log(`
1054
- To unlock: letsfg unlock <offer_id>`);
1055
- console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
1168
+ To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
1056
1169
  `);
1057
1170
  }
1058
1171
  }
1059
- async function cmdUnlock(args) {
1060
- const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
1061
- const apiKey = getFlag(args, "--api-key", "-k");
1062
- const baseUrl = getFlag(args, "--base-url");
1063
- const offerId = args[0];
1064
- if (!offerId) {
1065
- console.error("Usage: letsfg unlock <offer_id>");
1066
- process.exit(1);
1067
- }
1068
- const bt = new LetsFG({ apiKey, baseUrl });
1069
- const result = await bt.unlock(offerId);
1070
- if (jsonOut) {
1071
- console.log(JSON.stringify(result, null, 2));
1072
- return;
1073
- }
1074
- if (result.unlock_status === "unlocked") {
1075
- console.log(`
1076
- \u2713 Offer unlocked!`);
1077
- console.log(` Confirmed price: ${result.confirmed_currency} ${result.confirmed_price?.toFixed(2)}`);
1078
- console.log(` Expires at: ${result.offer_expires_at}`);
1079
- console.log(`
1080
- Next: letsfg book ${offerId} --passenger '{...}' --email you@example.com
1081
- `);
1082
- } else {
1083
- console.error(` \u2717 Unlock failed: ${result.message}`);
1084
- process.exit(1);
1085
- }
1172
+ async function cmdUnlock(_args) {
1173
+ console.error(
1174
+ "\n letsfg unlock was retired on 2026-09-08 and the endpoint answers 410 Gone.\n\n There is no unlock step any more. Book directly:\n letsfg book <offer_id> --search-id <search_id> --passenger '{...}' --email you@example.com\n\n The fare is HELD on your connected payment method and captured only once a real\n airline PNR exists, which is what unlock existed to protect against. If the fare moves\n at checkout you are asked to accept or decline it.\n"
1175
+ );
1176
+ process.exit(1);
1086
1177
  }
1087
1178
  async function cmdBook(args) {
1088
1179
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -1214,19 +1305,26 @@ async function cmdSetupPayment(args) {
1214
1305
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
1215
1306
  const apiKey = getFlag(args, "--api-key", "-k");
1216
1307
  const baseUrl = getFlag(args, "--base-url");
1217
- const token = getFlag(args, "--token", "-t") || "tok_visa";
1308
+ if (getFlag(args, "--token", "-t")) {
1309
+ console.log("\n Note: --token was part of the Stripe enrolment, retired 2026-09-08. Ignoring it.");
1310
+ }
1218
1311
  const bt = new LetsFG({ apiKey, baseUrl });
1219
- const result = await bt.setupPayment(token);
1312
+ const result = await bt.connectPayment();
1220
1313
  if (jsonOut) {
1221
1314
  console.log(JSON.stringify(result, null, 2));
1222
1315
  return;
1223
1316
  }
1224
- if (result.status === "ready") {
1317
+ const url = result.connect_url;
1318
+ if (url) {
1225
1319
  console.log(`
1226
- \u2713 Payment ready! You can now unlock offers and book flights.
1320
+ Open this in a browser to connect a payment method:
1321
+ `);
1322
+ console.log(` ${url}
1323
+ `);
1324
+ console.log(` Nothing is charged. Run \`letsfg me\` afterwards to confirm it landed.
1227
1325
  `);
1228
1326
  } else {
1229
- console.error(` \u2717 Payment setup failed: ${result.message || result.status}`);
1327
+ console.error(` \u2717 Could not mint a connect link: ${result.message || result.status}`);
1230
1328
  process.exit(1);
1231
1329
  }
1232
1330
  }
@@ -1247,10 +1345,9 @@ async function cmdMe(args) {
1247
1345
  console.log(` Email: ${p.email}`);
1248
1346
  console.log(` Tier: ${p.tier}`);
1249
1347
  const access = p.access_granted || false;
1250
- console.log(` Access: ${access ? "\u2713 Granted (search, unlock, book)" : "\u2717 Not granted"}`);
1348
+ console.log(` Access: ${access ? "\u2713 Granted (search, book)" : "\u2717 Not granted"}`);
1251
1349
  console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2014"}`);
1252
1350
  console.log(` Searches: ${u.total_searches || 0}`);
1253
- console.log(` Unlocks: ${u.total_unlocks || 0}`);
1254
1351
  console.log(` Bookings: ${u.total_bookings || 0}`);
1255
1352
  console.log(` Total spent: $${((u.total_spent_cents || 0) / 100).toFixed(2)}
1256
1353
  `);
@@ -1267,14 +1364,15 @@ Commands:
1267
1364
  auth Connect a card at letsfg.co/connect. Nothing charged
1268
1365
  search <origin> <dest> <date> Search for flights (free), prints search_id
1269
1366
  locations <query> Resolve city name to IATA codes
1270
- book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
1367
+ book <offer_id> --search-id ... Book a flight. No booking or transaction fee, no unlock step
1271
1368
  me Show agent profile
1272
1369
 
1273
1370
  Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
1274
1371
  they create a billing account. Use auth above instead):
1275
1372
  register --name ... --email ... Create a paid Developer API account
1276
- setup-payment Attach a card to that paid account
1277
- unlock <offer_id> [Developer API only] Unlock offer (legacy)
1373
+ connect-payment Print a link to connect a card to that paid account
1374
+ setup-payment Alias of connect-payment (the Stripe lane retired 2026-09-08)
1375
+ unlock <offer_id> RETIRED 2026-09-08 \u2014 no unlock step, book directly
1278
1376
 
1279
1377
  Options:
1280
1378
  --json, -j Output raw JSON
@@ -1310,6 +1408,7 @@ async function main() {
1310
1408
  case "register":
1311
1409
  await cmdRegister(args);
1312
1410
  break;
1411
+ case "connect-payment":
1313
1412
  case "setup-payment":
1314
1413
  await cmdSetupPayment(args);
1315
1414
  break;
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-GO3FXQXC.mjs";
6
+ } from "./chunk-F5BBI6XX.mjs";
7
7
 
8
8
  // src/auth.ts
9
9
  import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
@@ -329,38 +329,15 @@ async function cmdSearch(args) {
329
329
  `);
330
330
  } else {
331
331
  console.log(`
332
- To unlock: letsfg unlock <offer_id>`);
333
- console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
332
+ To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
334
333
  `);
335
334
  }
336
335
  }
337
- async function cmdUnlock(args) {
338
- const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
339
- const apiKey = getFlag(args, "--api-key", "-k");
340
- const baseUrl = getFlag(args, "--base-url");
341
- const offerId = args[0];
342
- if (!offerId) {
343
- console.error("Usage: letsfg unlock <offer_id>");
344
- process.exit(1);
345
- }
346
- const bt = new LetsFG({ apiKey, baseUrl });
347
- const result = await bt.unlock(offerId);
348
- if (jsonOut) {
349
- console.log(JSON.stringify(result, null, 2));
350
- return;
351
- }
352
- if (result.unlock_status === "unlocked") {
353
- console.log(`
354
- \u2713 Offer unlocked!`);
355
- console.log(` Confirmed price: ${result.confirmed_currency} ${result.confirmed_price?.toFixed(2)}`);
356
- console.log(` Expires at: ${result.offer_expires_at}`);
357
- console.log(`
358
- Next: letsfg book ${offerId} --passenger '{...}' --email you@example.com
359
- `);
360
- } else {
361
- console.error(` \u2717 Unlock failed: ${result.message}`);
362
- process.exit(1);
363
- }
336
+ async function cmdUnlock(_args) {
337
+ console.error(
338
+ "\n letsfg unlock was retired on 2026-09-08 and the endpoint answers 410 Gone.\n\n There is no unlock step any more. Book directly:\n letsfg book <offer_id> --search-id <search_id> --passenger '{...}' --email you@example.com\n\n The fare is HELD on your connected payment method and captured only once a real\n airline PNR exists, which is what unlock existed to protect against. If the fare moves\n at checkout you are asked to accept or decline it.\n"
339
+ );
340
+ process.exit(1);
364
341
  }
365
342
  async function cmdBook(args) {
366
343
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -492,19 +469,26 @@ async function cmdSetupPayment(args) {
492
469
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
493
470
  const apiKey = getFlag(args, "--api-key", "-k");
494
471
  const baseUrl = getFlag(args, "--base-url");
495
- const token = getFlag(args, "--token", "-t") || "tok_visa";
472
+ if (getFlag(args, "--token", "-t")) {
473
+ console.log("\n Note: --token was part of the Stripe enrolment, retired 2026-09-08. Ignoring it.");
474
+ }
496
475
  const bt = new LetsFG({ apiKey, baseUrl });
497
- const result = await bt.setupPayment(token);
476
+ const result = await bt.connectPayment();
498
477
  if (jsonOut) {
499
478
  console.log(JSON.stringify(result, null, 2));
500
479
  return;
501
480
  }
502
- if (result.status === "ready") {
481
+ const url = result.connect_url;
482
+ if (url) {
503
483
  console.log(`
504
- \u2713 Payment ready! You can now unlock offers and book flights.
484
+ Open this in a browser to connect a payment method:
485
+ `);
486
+ console.log(` ${url}
487
+ `);
488
+ console.log(` Nothing is charged. Run \`letsfg me\` afterwards to confirm it landed.
505
489
  `);
506
490
  } else {
507
- console.error(` \u2717 Payment setup failed: ${result.message || result.status}`);
491
+ console.error(` \u2717 Could not mint a connect link: ${result.message || result.status}`);
508
492
  process.exit(1);
509
493
  }
510
494
  }
@@ -525,10 +509,9 @@ async function cmdMe(args) {
525
509
  console.log(` Email: ${p.email}`);
526
510
  console.log(` Tier: ${p.tier}`);
527
511
  const access = p.access_granted || false;
528
- console.log(` Access: ${access ? "\u2713 Granted (search, unlock, book)" : "\u2717 Not granted"}`);
512
+ console.log(` Access: ${access ? "\u2713 Granted (search, book)" : "\u2717 Not granted"}`);
529
513
  console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2014"}`);
530
514
  console.log(` Searches: ${u.total_searches || 0}`);
531
- console.log(` Unlocks: ${u.total_unlocks || 0}`);
532
515
  console.log(` Bookings: ${u.total_bookings || 0}`);
533
516
  console.log(` Total spent: $${((u.total_spent_cents || 0) / 100).toFixed(2)}
534
517
  `);
@@ -545,14 +528,15 @@ Commands:
545
528
  auth Connect a card at letsfg.co/connect. Nothing charged
546
529
  search <origin> <dest> <date> Search for flights (free), prints search_id
547
530
  locations <query> Resolve city name to IATA codes
548
- book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
531
+ book <offer_id> --search-id ... Book a flight. No booking or transaction fee, no unlock step
549
532
  me Show agent profile
550
533
 
551
534
  Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
552
535
  they create a billing account. Use auth above instead):
553
536
  register --name ... --email ... Create a paid Developer API account
554
- setup-payment Attach a card to that paid account
555
- unlock <offer_id> [Developer API only] Unlock offer (legacy)
537
+ connect-payment Print a link to connect a card to that paid account
538
+ setup-payment Alias of connect-payment (the Stripe lane retired 2026-09-08)
539
+ unlock <offer_id> RETIRED 2026-09-08 \u2014 no unlock step, book directly
556
540
 
557
541
  Options:
558
542
  --json, -j Output raw JSON
@@ -588,6 +572,7 @@ async function main() {
588
572
  case "register":
589
573
  await cmdRegister(args);
590
574
  break;
575
+ case "connect-payment":
591
576
  case "setup-payment":
592
577
  await cmdSetupPayment(args);
593
578
  break;