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/README.md +89 -49
- package/dist/{chunk-GO3FXQXC.mjs → chunk-GVKYPTY3.mjs} +214 -62
- package/dist/cli.js +237 -101
- package/dist/cli.mjs +25 -40
- package/dist/index.d.mts +172 -46
- package/dist/index.d.ts +172 -46
- package/dist/index.js +215 -62
- package/dist/index.mjs +3 -1
- package/package.json +56 -56
package/dist/index.d.mts
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 (
|
|
286
|
-
* const bt2 = new LetsFG({ apiKey:
|
|
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
|
|
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
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
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(
|
|
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):
|
|
513
|
-
*
|
|
514
|
-
*
|
|
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
|
-
*
|
|
530
|
-
*
|
|
531
|
-
* `
|
|
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
|
-
*
|
|
534
|
-
*
|
|
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
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
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
|
-
*
|
|
559
|
-
*
|
|
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`
|
|
562
|
-
* them
|
|
563
|
-
*
|
|
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
|
|
566
|
-
* the
|
|
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
|
-
|
|
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
|
|
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'
|
|
596
|
-
*
|
|
597
|
-
*
|
|
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
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
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
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
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
|
|
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
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
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(
|
|
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 };
|