letsfg 2026.5.72 → 2026.5.74

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/dist/index.d.ts CHANGED
@@ -282,8 +282,8 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
282
282
  * const bt = new LetsFG({ bearerToken: process.env.LETSFG_BEARER_TOKEN });
283
283
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
284
284
  *
285
- * // Developer API (prepaid credits)
286
- * const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
285
+ * // Developer API (look-to-book search: 200 free after every booking)
286
+ * const bt2 = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
287
287
  * const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
288
288
  * ```
289
289
  */
@@ -402,7 +402,7 @@ interface SearchOptions {
402
402
  interface LetsFGConfig {
403
403
  /** PFS Bearer token from `letsfg auth`. Enables free search via POST /api/search polling. */
404
404
  bearerToken?: string;
405
- /** Developer API key (prepaid credits, no per-booking fee). */
405
+ /** Developer API key. Look-to-book search; no booking fee, no transaction fee. */
406
406
  apiKey?: string;
407
407
  baseUrl?: string;
408
408
  timeout?: number;
@@ -458,6 +458,12 @@ declare class ValidationError extends LetsFGError {
458
458
  declare function offerSummary(offer: FlightOffer): string;
459
459
  /** Get cheapest offer from search results */
460
460
  declare function cheapestOffer(result: FlightSearchResult): FlightOffer | null;
461
+ /**
462
+ * Every hotel booking job status after which polling is pointless. `attention` is
463
+ * final for the caller too: a person settles it, and booking again would book twice.
464
+ */
465
+ declare const HOTEL_BOOKING_FINAL_STATUSES: readonly ["succeeded", "failed", "attention"];
466
+ type HotelBookingStatus = 'in_progress' | (typeof HOTEL_BOOKING_FINAL_STATUSES)[number];
461
467
  declare class LetsFG {
462
468
  private bearerToken;
463
469
  private apiKey;
@@ -495,11 +501,19 @@ declare class LetsFG {
495
501
  */
496
502
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
497
503
  /**
498
- * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
499
- * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
500
- * token, so PFS callers use book() directly.
504
+ * RETIRED 2026-09-08. Throws instead of calling the server.
505
+ *
506
+ * There is no unlock step on any lane. Unlock existed to confirm a live price
507
+ * before charging; booking now HOLDS the fare on the connected payment method
508
+ * and captures only once a real airline PNR exists, so a fare that moved
509
+ * cannot become a charge for a ticket you did not get. If it moves at
510
+ * checkout you get a `price_change` question to accept or decline instead.
511
+ *
512
+ * Kept as a method, and throwing locally rather than making the request, so an
513
+ * older caller gets one clear sentence at the line that is actually wrong —
514
+ * not a 410 body to decode, and not a TypeError somewhere else.
501
515
  */
502
- unlock(offerId: string): Promise<UnlockResult>;
516
+ unlock(_offerId: string): Promise<UnlockResult>;
503
517
  /**
504
518
  * Book a flight.
505
519
  *
@@ -509,11 +523,64 @@ declare class LetsFG {
509
523
  * complete, { ok, booked: false, booking_url } — hand the link to the user,
510
524
  * nothing was charged.
511
525
  *
512
- * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
513
- * creates a real PNR. Requires unlock() first. Always provide
514
- * idempotencyKey to prevent double-bookings on retry.
526
+ * Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
527
+ * REQUIRED — an offer is bookable only inside the search that produced it.
528
+ * The connected Revolut method is HELD, not charged; a LetsFG booking agent
529
+ * buys the ticket and the hold is captured only against a real airline PNR.
530
+ * Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
531
+ * poll getBooking(bookingId) until `terminal`, or use bookAndWait().
532
+ * Always provide idempotencyKey: a retry with the same key returns the
533
+ * existing booking instead of opening a second hold on the card.
515
534
  */
516
535
  book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string, searchId?: string): Promise<BookingResult | Record<string, unknown>>;
536
+ /**
537
+ * Poll a Developer API flight booking.
538
+ *
539
+ * Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
540
+ * knows you are still there, which is what keeps a booking paused on a
541
+ * question alive — so do not back off to minutes.
542
+ *
543
+ * States: authorised, card_issued, booking_in_progress, awaiting_settlement,
544
+ * then completed (with `pnr` and `charged_amount`), failed (hold released,
545
+ * nothing charged) or needs_attention (a human at LetsFG is on it — do not
546
+ * book again).
547
+ */
548
+ getBooking(bookingId: string): Promise<Record<string, unknown>>;
549
+ /**
550
+ * Answer the open `question` on a booking.
551
+ *
552
+ * Echo the question's `round`. A stale round is refused with 409 rather than
553
+ * guessed at, so an answer to an old question can never be applied to a new
554
+ * one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
555
+ * { confirm: true } or { skip: true } — declining an extra still completes
556
+ * the booking, without it.
557
+ */
558
+ answerBooking(bookingId: string, round: number, answer?: {
559
+ seats?: Array<Record<string, unknown>>;
560
+ confirm?: boolean;
561
+ skip?: boolean;
562
+ }): Promise<Record<string, unknown>>;
563
+ /**
564
+ * Book and poll to a terminal state. Mirrors bookHotelAndWait().
565
+ *
566
+ * Blocks for as long as the booking takes (4–11 minutes typically), so use
567
+ * book() + getBooking() instead if your caller has a request timeout.
568
+ *
569
+ * `onQuestion` returns the answer for answerBooking(). Without it, a fare
570
+ * increase is ACCEPTED and a paid extra is DECLINED — the conservative
571
+ * reading of "the traveller asked for this flight".
572
+ */
573
+ bookAndWait(offerId: string, passengers: Passenger[], contactEmail: string, searchId: string, opts?: {
574
+ contactPhone?: string;
575
+ idempotencyKey?: string;
576
+ pollMs?: number;
577
+ timeoutMs?: number;
578
+ onQuestion?: (q: Record<string, unknown>) => {
579
+ seats?: Array<Record<string, unknown>>;
580
+ confirm?: boolean;
581
+ skip?: boolean;
582
+ };
583
+ }): Promise<Record<string, unknown>>;
517
584
  /**
518
585
  * Resolve a place name to the city id that searchHotels() needs.
519
586
  *
@@ -526,12 +593,17 @@ declare class LetsFG {
526
593
  * Slow by nature — the supplier streams a whole city and every rate is priced
527
594
  * — so this gets its own generous timeout rather than the client default.
528
595
  *
529
- * Each offer carries `price` (what the guest pays), `reservation_fee_now`
530
- * (the 5% taken at booking), `balance_to_supplier`, `balance_due_by` and
531
- * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
596
+ * The response carries `session_id`, `currency`, `supplier_currency`,
597
+ * `markup_rate`, `fx_rate`, `fx_as_of`, `count`, `hotels`, `terms` and
598
+ * `caveats`. Each offer carries `price` (what the guest pays, in `currency`),
599
+ * `currency`, `fx_rate`, `expected_cost` (the supplier's cost, in PLN),
600
+ * `refundable`, `free_cancellation_until` (refundable rates only),
601
+ * `cancellation_policy` and its own `session_id`.
532
602
  *
533
- * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
534
- * identify the exact rate, and booking needs both.
603
+ * `price` is the supplier's cost plus `markup_rate` (6.4% for Revolut Pay or an
604
+ * EEA-issued card, 8.3% for a card issued outside the EEA); nothing is added at
605
+ * booking. Keep the chosen offer whole: bookHotel() needs its `session_id`,
606
+ * `combination_id_v2`, `price`, `expected_cost`, `currency` and `fx_rate`.
535
607
  */
536
608
  searchHotels(params: {
537
609
  cityId: number;
@@ -545,89 +617,143 @@ declare class LetsFG {
545
617
  nationality?: string;
546
618
  limit?: number;
547
619
  withImages?: boolean;
620
+ /** ISO code every `price` is quoted in. Default USD; PLN is the supplier's own. */
621
+ currency?: string;
548
622
  }): Promise<Record<string, unknown>>;
549
623
  /**
550
624
  * Start a booking. Returns a job immediately — it does NOT book inline.
551
625
  *
552
- * A booking takes minutes: the rate is re-blocked at the supplier, every
553
- * price and date rail is checked, the 5% reservation fee is charged to your
554
- * card, and only then is the room committed. No proxy holds a connection that
555
- * long, so this returns at once and you poll hotelBooking() for the outcome.
556
- * Use bookHotelAndWait() if you would rather block.
626
+ * What happens, in order: the offer's full `price` is HELD on the Revolut
627
+ * payment method connected to this account (authorised, not taken); LetsFG
628
+ * books the room with the supplier and pays the supplier itself; the hold is
629
+ * captured only once the supplier has confirmed. If the booking fails for any
630
+ * reason, the hold is released and nothing is charged. There is no
631
+ * reservation fee, no deposit and no pay link.
557
632
  *
558
- * Because the fee is taken BEFORE the commit, a declined card costs nothing
559
- * to unwind: no reservation exists and nothing is charged.
633
+ * A booking takes minutes and no proxy holds a connection that long, so this
634
+ * returns at once and you poll hotelBooking() for the outcome. Use
635
+ * bookHotelAndWait() if you would rather block.
560
636
  *
561
- * Send `expectedPrice` and `expectedBalance` back exactly as search returned
562
- * them — the booking is refused if the supplier has moved beyond tolerance,
563
- * so a guest is never charged a price they did not agree to.
637
+ * Send `expectedPrice` (the offer's `price`), `expectedCost`, `currency` and
638
+ * `fxRate` exactly as search returned them. The booking is refused if the
639
+ * supplier's live cost is above `expectedCost`, and a USD offer sent without
640
+ * its `currency` is refused with 400 price_mismatch (the API assumes PLN).
641
+ * Guest names, phone and e-mail are checked before anything is held; a problem
642
+ * returns 400 invalid_details naming the fields.
564
643
  *
565
- * Do NOT call this again for the same rate while a job is running: that books
566
- * the room twice and charges two reservation fees.
644
+ * Do NOT call this again for a booking whose job is still running: poll it.
645
+ * A retry of the same booking returns the job already under way
646
+ * (`duplicate: true`) rather than holding the money twice.
567
647
  */
568
648
  bookHotel(params: {
569
649
  sessionId: string;
570
650
  hotelCode: number;
571
651
  combinationIdV2: string;
652
+ /** The offer's `price`, verbatim. */
572
653
  expectedPrice: number;
573
- expectedBalance: number;
654
+ /** The offer's `expected_cost` (the supplier's cost, PLN), verbatim. */
655
+ expectedCost: number;
656
+ /** The offer's `currency`. Copy it — omitted means PLN. */
657
+ currency?: string;
658
+ /** The offer's `fx_rate` (null for a PLN offer). */
659
+ fxRate?: number | null;
574
660
  cityId: number;
575
661
  cityName: string;
576
662
  checkIn: string;
577
663
  checkOut: string;
664
+ /**
665
+ * ONE entry per guest in the room, children included: adults first, then children in the
666
+ * `childAges` order used to search (the party travels with the offer's session). The hotel
667
+ * requires a name for every guest; fewer names than guests is refused before anything is
668
+ * submitted, and the hold is released.
669
+ */
578
670
  guests: Array<{
579
671
  title: string;
580
672
  first_name: string;
581
673
  last_name: string;
582
674
  }>;
583
- /** The voucher and the pay link go here. A typo loses the booking. */
675
+ /** The guest's e-mail: the confirmation, or a note that it did not go through, goes here. */
584
676
  email: string;
585
677
  phone: string;
678
+ /** Adults in the room, as searched. */
586
679
  adults?: number;
587
680
  combinationId?: number;
588
681
  hotelName?: string;
589
682
  phoneCountryCode?: string;
590
683
  specialRequests?: string[];
684
+ /** Optional. A retry with the same key returns the booking already under way. */
685
+ idempotencyKey?: string;
591
686
  }): Promise<Record<string, unknown>>;
592
687
  /**
593
688
  * Collect the result of a booking started with bookHotel().
594
689
  *
595
- * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
596
- * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
597
- * `balance_due_by` and `terms` (including the full cancellation ladder).
690
+ * `status` is 'in_progress', 'succeeded', 'failed' or 'attention'; the last
691
+ * three are final (HOTEL_BOOKING_FINAL_STATUSES).
692
+ *
693
+ * - succeeded: `confirmation`, `booking_id`, `hotel`, `room`, `total_price` +
694
+ * `currency` (what the guest is charged), `supplier_paid` +
695
+ * `supplier_currency` (what the supplier was paid), `payment_status`,
696
+ * `refundable`, `free_cancellation_until`, `cancellation_ladder`, `terms`.
697
+ * - failed: `error`, written for the guest. The hold has been released and
698
+ * nothing was charged.
699
+ * - attention: `error` (and `confirmation` when known). The outcome could not
700
+ * be settled automatically; the hold is kept — nothing is charged — while a
701
+ * person checks with the supplier. Do not book again.
702
+ *
703
+ * The guest is e-mailed in every case.
598
704
  */
599
705
  hotelBooking(bookingJobId: string): Promise<Record<string, unknown>>;
600
706
  /**
601
707
  * bookHotel(), then poll until the booking settles. Convenience only.
602
708
  *
603
- * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
604
- * still complete. The returned object carries `booking_job_id` so you can
605
- * keep polling, and the confirmation is emailed to the guest regardless.
709
+ * Stops at 'succeeded', 'failed' or 'attention' and never re-books. Giving up
710
+ * after `maxWaitMs` (default 30 minutes: a booking usually takes 5-10, and
711
+ * hotel bookings run one at a time) does NOT cancel anything — the booking may
712
+ * still complete. The returned object carries `booking_job_id` so you can keep
713
+ * polling, and the guest is e-mailed the outcome regardless.
606
714
  */
607
715
  bookHotelAndWait(params: Parameters<LetsFG['bookHotel']>[0] & {
608
716
  pollIntervalMs?: number;
609
717
  maxWaitMs?: number;
610
718
  }): Promise<Record<string, unknown>>;
611
719
  /**
612
- * Release a reservation at the supplier.
720
+ * Release a reservation at the supplier and refund the guest.
613
721
  *
614
- * Free until `balance_due_by`; after that the hotel's own cancellation ladder
615
- * applies and can reach 100%. The ladder ships in the booking's `terms`, so
616
- * you can see the cost before calling this. The 5% reservation fee is NOT
617
- * refunded.
722
+ * Only this account's own bookings can be cancelled (anything else is 404). A
723
+ * zero-charge cancellation — a refundable rate before its
724
+ * `free_cancellation_until` — refunds the charge in full, or releases a hold
725
+ * not yet captured. A cancellation that would cost money is refused with 409;
726
+ * the hotel's own ladder is in the booking's `terms`.
618
727
  *
619
728
  * This drives a browser at the supplier and takes over a minute. If it times
620
729
  * out, do not assume it failed — re-check before retrying.
621
730
  */
622
731
  cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
623
732
  /**
624
- * [Developer API only] Attach a card to a PAID prepaid Developer API account.
733
+ * [Developer API] Mint a one-time link for connecting a Revolut payment method.
734
+ *
735
+ * This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
736
+ * card details never touch LetsFG: the returned `connect_url` opens a hosted page
737
+ * where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
738
+ * it in a browser — there is no endpoint that takes card details, so do not ask a
739
+ * user for a card number and do not try to automate this step.
740
+ *
741
+ * Most agents should NOT need a Developer API account at all. To authenticate for
742
+ * search and booking, run `letsfg auth`, which creates no billing account.
743
+ */
744
+ connectPayment(): Promise<Record<string, unknown>>;
745
+ /**
746
+ * RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
747
+ *
748
+ * `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
749
+ * Revolut rail as the rest of the product: call connectPayment() and open the
750
+ * `connect_url` it returns.
625
751
  *
626
- * Most agents should NOT call this. It is unrelated to authenticating for
627
- * search and booking — for that, run `letsfg auth`, which puts a card on file
628
- * through a zero-amount setup and creates no billing account.
752
+ * Kept as a method, and throwing locally rather than making the request, for the
753
+ * same reason as unlock() — an older caller gets one clear sentence at the line that
754
+ * is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
629
755
  */
630
- setupPayment(token?: string): Promise<Record<string, unknown>>;
756
+ setupPayment(_token?: string): Promise<Record<string, unknown>>;
631
757
  /**
632
758
  * Get current agent profile and usage stats.
633
759
  */
@@ -652,4 +778,4 @@ declare const BoostedTravel: typeof LetsFG;
652
778
  declare const BoostedTravelError: typeof LetsFGError;
653
779
  type BoostedTravelConfig = LetsFGConfig;
654
780
 
655
- export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, LetsFG, type LetsFGConfig, LetsFGError, type OfferDetailSignals, OfferExpiredError, type Passenger, PaymentRequiredError, type RankOffer, type RankedOffer, type RankingContext, type ScoreBreakdown, type SearchOptions, type StarlinkOfferVerdict, type StarlinkSegmentVerdict, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };
781
+ export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, HOTEL_BOOKING_FINAL_STATUSES, type HotelBookingStatus, LetsFG, type LetsFGConfig, LetsFGError, type OfferDetailSignals, OfferExpiredError, type Passenger, PaymentRequiredError, type RankOffer, type RankedOffer, type RankingContext, type ScoreBreakdown, type SearchOptions, type StarlinkOfferVerdict, type StarlinkSegmentVerdict, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };