letsfg 2026.5.73 → 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.mts CHANGED
@@ -283,7 +283,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
283
283
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
284
284
  *
285
285
  * // Developer API (look-to-book search: 200 free after every booking)
286
- * const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
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
  */
@@ -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;
@@ -587,12 +593,17 @@ declare class LetsFG {
587
593
  * Slow by nature — the supplier streams a whole city and every rate is priced
588
594
  * — so this gets its own generous timeout rather than the client default.
589
595
  *
590
- * Each offer carries `price` (what the guest pays), `reservation_fee_now`
591
- * (the 5% taken at booking), `balance_to_supplier`, `balance_due_by` and
592
- * `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`.
593
602
  *
594
- * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
595
- * 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`.
596
607
  */
597
608
  searchHotels(params: {
598
609
  cityId: number;
@@ -606,76 +617,113 @@ declare class LetsFG {
606
617
  nationality?: string;
607
618
  limit?: number;
608
619
  withImages?: boolean;
620
+ /** ISO code every `price` is quoted in. Default USD; PLN is the supplier's own. */
621
+ currency?: string;
609
622
  }): Promise<Record<string, unknown>>;
610
623
  /**
611
624
  * Start a booking. Returns a job immediately — it does NOT book inline.
612
625
  *
613
- * A booking takes minutes: the rate is re-blocked at the supplier, every
614
- * price and date rail is checked, the 5% reservation fee is charged to your
615
- * card, and only then is the room committed. No proxy holds a connection that
616
- * long, so this returns at once and you poll hotelBooking() for the outcome.
617
- * 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.
618
632
  *
619
- * Because the fee is taken BEFORE the commit, a declined card costs nothing
620
- * 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.
621
636
  *
622
- * Send `expectedPrice` and `expectedBalance` back exactly as search returned
623
- * them — the booking is refused if the supplier has moved beyond tolerance,
624
- * 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.
625
643
  *
626
- * Do NOT call this again for the same rate while a job is running: that books
627
- * 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.
628
647
  */
629
648
  bookHotel(params: {
630
649
  sessionId: string;
631
650
  hotelCode: number;
632
651
  combinationIdV2: string;
652
+ /** The offer's `price`, verbatim. */
633
653
  expectedPrice: number;
634
- 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;
635
660
  cityId: number;
636
661
  cityName: string;
637
662
  checkIn: string;
638
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
+ */
639
670
  guests: Array<{
640
671
  title: string;
641
672
  first_name: string;
642
673
  last_name: string;
643
674
  }>;
644
- /** 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. */
645
676
  email: string;
646
677
  phone: string;
678
+ /** Adults in the room, as searched. */
647
679
  adults?: number;
648
680
  combinationId?: number;
649
681
  hotelName?: string;
650
682
  phoneCountryCode?: string;
651
683
  specialRequests?: string[];
684
+ /** Optional. A retry with the same key returns the booking already under way. */
685
+ idempotencyKey?: string;
652
686
  }): Promise<Record<string, unknown>>;
653
687
  /**
654
688
  * Collect the result of a booking started with bookHotel().
655
689
  *
656
- * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
657
- * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
658
- * `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.
659
704
  */
660
705
  hotelBooking(bookingJobId: string): Promise<Record<string, unknown>>;
661
706
  /**
662
707
  * bookHotel(), then poll until the booking settles. Convenience only.
663
708
  *
664
- * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
665
- * still complete. The returned object carries `booking_job_id` so you can
666
- * 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.
667
714
  */
668
715
  bookHotelAndWait(params: Parameters<LetsFG['bookHotel']>[0] & {
669
716
  pollIntervalMs?: number;
670
717
  maxWaitMs?: number;
671
718
  }): Promise<Record<string, unknown>>;
672
719
  /**
673
- * Release a reservation at the supplier.
720
+ * Release a reservation at the supplier and refund the guest.
674
721
  *
675
- * Free until `balance_due_by`; after that the hotel's own cancellation ladder
676
- * applies and can reach 100%. The ladder ships in the booking's `terms`, so
677
- * you can see the cost before calling this. The 5% reservation fee is NOT
678
- * 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`.
679
727
  *
680
728
  * This drives a browser at the supplier and takes over a minute. If it times
681
729
  * out, do not assume it failed — re-check before retrying.
@@ -730,4 +778,4 @@ declare const BoostedTravel: typeof LetsFG;
730
778
  declare const BoostedTravelError: typeof LetsFGError;
731
779
  type BoostedTravelConfig = LetsFGConfig;
732
780
 
733
- 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 };
package/dist/index.d.ts CHANGED
@@ -283,7 +283,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
283
283
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
284
284
  *
285
285
  * // Developer API (look-to-book search: 200 free after every booking)
286
- * const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
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
  */
@@ -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;
@@ -587,12 +593,17 @@ declare class LetsFG {
587
593
  * Slow by nature — the supplier streams a whole city and every rate is priced
588
594
  * — so this gets its own generous timeout rather than the client default.
589
595
  *
590
- * Each offer carries `price` (what the guest pays), `reservation_fee_now`
591
- * (the 5% taken at booking), `balance_to_supplier`, `balance_due_by` and
592
- * `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`.
593
602
  *
594
- * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
595
- * 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`.
596
607
  */
597
608
  searchHotels(params: {
598
609
  cityId: number;
@@ -606,76 +617,113 @@ declare class LetsFG {
606
617
  nationality?: string;
607
618
  limit?: number;
608
619
  withImages?: boolean;
620
+ /** ISO code every `price` is quoted in. Default USD; PLN is the supplier's own. */
621
+ currency?: string;
609
622
  }): Promise<Record<string, unknown>>;
610
623
  /**
611
624
  * Start a booking. Returns a job immediately — it does NOT book inline.
612
625
  *
613
- * A booking takes minutes: the rate is re-blocked at the supplier, every
614
- * price and date rail is checked, the 5% reservation fee is charged to your
615
- * card, and only then is the room committed. No proxy holds a connection that
616
- * long, so this returns at once and you poll hotelBooking() for the outcome.
617
- * 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.
618
632
  *
619
- * Because the fee is taken BEFORE the commit, a declined card costs nothing
620
- * 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.
621
636
  *
622
- * Send `expectedPrice` and `expectedBalance` back exactly as search returned
623
- * them — the booking is refused if the supplier has moved beyond tolerance,
624
- * 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.
625
643
  *
626
- * Do NOT call this again for the same rate while a job is running: that books
627
- * 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.
628
647
  */
629
648
  bookHotel(params: {
630
649
  sessionId: string;
631
650
  hotelCode: number;
632
651
  combinationIdV2: string;
652
+ /** The offer's `price`, verbatim. */
633
653
  expectedPrice: number;
634
- 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;
635
660
  cityId: number;
636
661
  cityName: string;
637
662
  checkIn: string;
638
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
+ */
639
670
  guests: Array<{
640
671
  title: string;
641
672
  first_name: string;
642
673
  last_name: string;
643
674
  }>;
644
- /** 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. */
645
676
  email: string;
646
677
  phone: string;
678
+ /** Adults in the room, as searched. */
647
679
  adults?: number;
648
680
  combinationId?: number;
649
681
  hotelName?: string;
650
682
  phoneCountryCode?: string;
651
683
  specialRequests?: string[];
684
+ /** Optional. A retry with the same key returns the booking already under way. */
685
+ idempotencyKey?: string;
652
686
  }): Promise<Record<string, unknown>>;
653
687
  /**
654
688
  * Collect the result of a booking started with bookHotel().
655
689
  *
656
- * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
657
- * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
658
- * `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.
659
704
  */
660
705
  hotelBooking(bookingJobId: string): Promise<Record<string, unknown>>;
661
706
  /**
662
707
  * bookHotel(), then poll until the booking settles. Convenience only.
663
708
  *
664
- * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
665
- * still complete. The returned object carries `booking_job_id` so you can
666
- * 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.
667
714
  */
668
715
  bookHotelAndWait(params: Parameters<LetsFG['bookHotel']>[0] & {
669
716
  pollIntervalMs?: number;
670
717
  maxWaitMs?: number;
671
718
  }): Promise<Record<string, unknown>>;
672
719
  /**
673
- * Release a reservation at the supplier.
720
+ * Release a reservation at the supplier and refund the guest.
674
721
  *
675
- * Free until `balance_due_by`; after that the hotel's own cancellation ladder
676
- * applies and can reach 100%. The ladder ships in the booking's `terms`, so
677
- * you can see the cost before calling this. The 5% reservation fee is NOT
678
- * 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`.
679
727
  *
680
728
  * This drives a browser at the supplier and takes over a minute. If it times
681
729
  * out, do not assume it failed — re-check before retrying.
@@ -730,4 +778,4 @@ declare const BoostedTravel: typeof LetsFG;
730
778
  declare const BoostedTravelError: typeof LetsFGError;
731
779
  type BoostedTravelConfig = LetsFGConfig;
732
780
 
733
- 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 };
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;
@@ -1863,16 +1865,18 @@ var LetsFG = class {
1863
1865
  }
1864
1866
  // ── Hotels ──────────────────────────────────────────────────────────
1865
1867
  //
1866
- // A card on file is required for EVERY hotel call, search included. That is
1867
- // deliberate: a hotel search opens a real session at the supplier and booking
1868
- // blocks a real rate, so a caller is never allowed to reach the point of
1869
- // commitment only to discover it cannot pay. The same card that authorises
1870
- // flight booking authorises hotels — there is no separate hotel enrolment.
1868
+ // A connected payment method is required for EVERY hotel call, search
1869
+ // included. That is deliberate: a hotel search opens a real session at the
1870
+ // supplier and booking blocks a real rate, so a caller is never allowed to
1871
+ // reach the point of commitment only to discover it cannot pay. The same
1872
+ // method that authorises flight booking authorises hotels.
1871
1873
  //
1872
- // Only free-cancellation, pay-later rates are sold. Those are the rates where
1873
- // the guest's balance can safely be settled with the supplier after booking,
1874
- // which is what makes 5%-now/rest-later work. The result set is smaller than
1875
- // a metasearch's, and every row in it can actually be booked.
1874
+ // How a hotel is paid (since 2026-09-11): the full `price` is HELD on the
1875
+ // connected Revolut method, LetsFG books and pays the supplier itself, and the
1876
+ // hold is captured only once the supplier has confirmed. A booking that fails
1877
+ // releases the hold. There is no reservation fee, no deposit and no pay link —
1878
+ // those belonged to the process retired on 2026-09-11. Every rate type is
1879
+ // sold, refundable and non-refundable.
1876
1880
  /**
1877
1881
  * Resolve a place name to the city id that searchHotels() needs.
1878
1882
  *
@@ -1893,12 +1897,17 @@ var LetsFG = class {
1893
1897
  * Slow by nature — the supplier streams a whole city and every rate is priced
1894
1898
  * — so this gets its own generous timeout rather than the client default.
1895
1899
  *
1896
- * Each offer carries `price` (what the guest pays), `reservation_fee_now`
1897
- * (the 5% taken at booking), `balance_to_supplier`, `balance_due_by` and
1898
- * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
1900
+ * The response carries `session_id`, `currency`, `supplier_currency`,
1901
+ * `markup_rate`, `fx_rate`, `fx_as_of`, `count`, `hotels`, `terms` and
1902
+ * `caveats`. Each offer carries `price` (what the guest pays, in `currency`),
1903
+ * `currency`, `fx_rate`, `expected_cost` (the supplier's cost, in PLN),
1904
+ * `refundable`, `free_cancellation_until` (refundable rates only),
1905
+ * `cancellation_policy` and its own `session_id`.
1899
1906
  *
1900
- * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
1901
- * identify the exact rate, and booking needs both.
1907
+ * `price` is the supplier's cost plus `markup_rate` (6.4% for Revolut Pay or an
1908
+ * EEA-issued card, 8.3% for a card issued outside the EEA); nothing is added at
1909
+ * booking. Keep the chosen offer whole: bookHotel() needs its `session_id`,
1910
+ * `combination_id_v2`, `price`, `expected_cost`, `currency` and `fx_rate`.
1902
1911
  */
1903
1912
  async searchHotels(params) {
1904
1913
  this.requireApiKey();
@@ -1911,7 +1920,8 @@ var LetsFG = class {
1911
1920
  children: params.children ?? 0,
1912
1921
  nationality: params.nationality ?? "PL",
1913
1922
  limit: params.limit ?? 40,
1914
- with_images: params.withImages ?? true
1923
+ with_images: params.withImages ?? true,
1924
+ currency: params.currency ?? "USD"
1915
1925
  };
1916
1926
  if (params.childAges?.length) body.child_ages = params.childAges;
1917
1927
  return this.post("/developers/api/v1/hotels/search", body, 24e4);
@@ -1919,30 +1929,42 @@ var LetsFG = class {
1919
1929
  /**
1920
1930
  * Start a booking. Returns a job immediately — it does NOT book inline.
1921
1931
  *
1922
- * A booking takes minutes: the rate is re-blocked at the supplier, every
1923
- * price and date rail is checked, the 5% reservation fee is charged to your
1924
- * card, and only then is the room committed. No proxy holds a connection that
1925
- * long, so this returns at once and you poll hotelBooking() for the outcome.
1926
- * Use bookHotelAndWait() if you would rather block.
1932
+ * What happens, in order: the offer's full `price` is HELD on the Revolut
1933
+ * payment method connected to this account (authorised, not taken); LetsFG
1934
+ * books the room with the supplier and pays the supplier itself; the hold is
1935
+ * captured only once the supplier has confirmed. If the booking fails for any
1936
+ * reason, the hold is released and nothing is charged. There is no
1937
+ * reservation fee, no deposit and no pay link.
1927
1938
  *
1928
- * Because the fee is taken BEFORE the commit, a declined card costs nothing
1929
- * to unwind: no reservation exists and nothing is charged.
1939
+ * A booking takes minutes and no proxy holds a connection that long, so this
1940
+ * returns at once and you poll hotelBooking() for the outcome. Use
1941
+ * bookHotelAndWait() if you would rather block.
1930
1942
  *
1931
- * Send `expectedPrice` and `expectedBalance` back exactly as search returned
1932
- * them — the booking is refused if the supplier has moved beyond tolerance,
1933
- * so a guest is never charged a price they did not agree to.
1943
+ * Send `expectedPrice` (the offer's `price`), `expectedCost`, `currency` and
1944
+ * `fxRate` exactly as search returned them. The booking is refused if the
1945
+ * supplier's live cost is above `expectedCost`, and a USD offer sent without
1946
+ * its `currency` is refused with 400 price_mismatch (the API assumes PLN).
1947
+ * Guest names, phone and e-mail are checked before anything is held; a problem
1948
+ * returns 400 invalid_details naming the fields.
1934
1949
  *
1935
- * Do NOT call this again for the same rate while a job is running: that books
1936
- * the room twice and charges two reservation fees.
1950
+ * Do NOT call this again for a booking whose job is still running: poll it.
1951
+ * A retry of the same booking returns the job already under way
1952
+ * (`duplicate: true`) rather than holding the money twice.
1937
1953
  */
1938
1954
  async bookHotel(params) {
1939
1955
  this.requireApiKey();
1956
+ if (typeof params.expectedCost !== "number") {
1957
+ throw new LetsFGError(
1958
+ "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",
1959
+ 400
1960
+ );
1961
+ }
1940
1962
  const body = {
1941
1963
  session_id: params.sessionId,
1942
1964
  hotel_code: params.hotelCode,
1943
1965
  combination_id_v2: params.combinationIdV2,
1944
1966
  expected_price: params.expectedPrice,
1945
- expected_balance: params.expectedBalance,
1967
+ expected_cost: params.expectedCost,
1946
1968
  city_id: params.cityId,
1947
1969
  city_name: params.cityName,
1948
1970
  check_in: params.checkIn,
@@ -1954,16 +1976,30 @@ var LetsFG = class {
1954
1976
  phone_country_code: params.phoneCountryCode ?? "48",
1955
1977
  special_requests: params.specialRequests ?? []
1956
1978
  };
1979
+ if (params.currency) body.currency = params.currency;
1980
+ if (params.fxRate != null) body.fx_rate = params.fxRate;
1957
1981
  if (params.combinationId != null) body.combination_id = params.combinationId;
1958
1982
  if (params.hotelName) body.hotel_name = params.hotelName;
1983
+ if (params.idempotencyKey) body.idempotency_key = params.idempotencyKey;
1959
1984
  return this.post("/developers/api/v1/hotels/book", body, 9e4);
1960
1985
  }
1961
1986
  /**
1962
1987
  * Collect the result of a booking started with bookHotel().
1963
1988
  *
1964
- * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
1965
- * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
1966
- * `balance_due_by` and `terms` (including the full cancellation ladder).
1989
+ * `status` is 'in_progress', 'succeeded', 'failed' or 'attention'; the last
1990
+ * three are final (HOTEL_BOOKING_FINAL_STATUSES).
1991
+ *
1992
+ * - succeeded: `confirmation`, `booking_id`, `hotel`, `room`, `total_price` +
1993
+ * `currency` (what the guest is charged), `supplier_paid` +
1994
+ * `supplier_currency` (what the supplier was paid), `payment_status`,
1995
+ * `refundable`, `free_cancellation_until`, `cancellation_ladder`, `terms`.
1996
+ * - failed: `error`, written for the guest. The hold has been released and
1997
+ * nothing was charged.
1998
+ * - attention: `error` (and `confirmation` when known). The outcome could not
1999
+ * be settled automatically; the hold is kept — nothing is charged — while a
2000
+ * person checks with the supplier. Do not book again.
2001
+ *
2002
+ * The guest is e-mailed in every case.
1967
2003
  */
1968
2004
  async hotelBooking(bookingJobId) {
1969
2005
  this.requireApiKey();
@@ -1975,13 +2011,15 @@ var LetsFG = class {
1975
2011
  /**
1976
2012
  * bookHotel(), then poll until the booking settles. Convenience only.
1977
2013
  *
1978
- * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
1979
- * still complete. The returned object carries `booking_job_id` so you can
1980
- * keep polling, and the confirmation is emailed to the guest regardless.
2014
+ * Stops at 'succeeded', 'failed' or 'attention' and never re-books. Giving up
2015
+ * after `maxWaitMs` (default 30 minutes: a booking usually takes 5-10, and
2016
+ * hotel bookings run one at a time) does NOT cancel anything — the booking may
2017
+ * still complete. The returned object carries `booking_job_id` so you can keep
2018
+ * polling, and the guest is e-mailed the outcome regardless.
1981
2019
  */
1982
2020
  async bookHotelAndWait(params) {
1983
2021
  const pollIntervalMs = params.pollIntervalMs ?? 2e4;
1984
- const maxWaitMs = params.maxWaitMs ?? 6e5;
2022
+ const maxWaitMs = params.maxWaitMs ?? 18e5;
1985
2023
  const job = await this.bookHotel(params);
1986
2024
  const jobId = job.booking_job_id;
1987
2025
  if (!jobId) return job;
@@ -1991,19 +2029,19 @@ var LetsFG = class {
1991
2029
  await new Promise((r) => setTimeout(r, pollIntervalMs));
1992
2030
  waited += pollIntervalMs;
1993
2031
  result = await this.hotelBooking(jobId);
1994
- const st = result.status;
1995
- if (st === "succeeded" || st === "failed") return result;
2032
+ if (HOTEL_BOOKING_FINAL_STATUSES.includes(result.status)) return result;
1996
2033
  }
1997
2034
  if (result.booking_job_id == null) result.booking_job_id = jobId;
1998
2035
  return result;
1999
2036
  }
2000
2037
  /**
2001
- * Release a reservation at the supplier.
2038
+ * Release a reservation at the supplier and refund the guest.
2002
2039
  *
2003
- * Free until `balance_due_by`; after that the hotel's own cancellation ladder
2004
- * applies and can reach 100%. The ladder ships in the booking's `terms`, so
2005
- * you can see the cost before calling this. The 5% reservation fee is NOT
2006
- * refunded.
2040
+ * Only this account's own bookings can be cancelled (anything else is 404). A
2041
+ * zero-charge cancellation — a refundable rate before its
2042
+ * `free_cancellation_until` — refunds the charge in full, or releases a hold
2043
+ * not yet captured. A cancellation that would cost money is refused with 409;
2044
+ * the hotel's own ladder is in the booking's `terms`.
2007
2045
  *
2008
2046
  * This drives a browser at the supplier and takes over a minute. If it times
2009
2047
  * out, do not assume it failed — re-check before retrying.
@@ -2142,6 +2180,7 @@ var BoostedTravelError = LetsFGError;
2142
2180
  BoostedTravelError,
2143
2181
  ErrorCategory,
2144
2182
  ErrorCode,
2183
+ HOTEL_BOOKING_FINAL_STATUSES,
2145
2184
  LetsFG,
2146
2185
  LetsFGError,
2147
2186
  OfferExpiredError,