letsfg 2026.5.64 → 2026.5.66

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 CHANGED
@@ -9,8 +9,8 @@
9
9
 
10
10
  | | **CLI / SDK** (this package) | **Developer API** |
11
11
  |---|---|---|
12
- | **Search cost** | Free (Twitter/X Bearer token via `letsfg auth`) | Prepaid credits |
13
- | **Booking** | `POST /api/agent-book` | Direct airline URL |
12
+ | **Search cost** | Free (Bearer token via `letsfg auth` — zero-amount card setup) | Prepaid credits |
13
+ | **Booking** | `POST /api/agent-book` — confirmed order or a booking link, no LetsFG fee | Direct airline URL (unlock required first) |
14
14
  | **Speed** | 60–90 s | 2–5 s (discover) · 60–90 s (full) |
15
15
  | **Setup** | `npm install letsfg` then `letsfg auth` | [letsfg.co/developers](https://letsfg.co/developers) |
16
16
 
@@ -27,60 +27,60 @@ npm install letsfg
27
27
  ```typescript
28
28
  import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
29
29
 
30
- // Register (one-time)
31
- const creds = await LetsFG.register('my-agent', 'agent@example.com');
32
- console.log(creds.api_key); // Save this
33
-
34
- // Use
35
- const bt = new LetsFG({ apiKey: 'trav_...' });
30
+ // PFS — free. Get a Bearer token once with `letsfg auth` (zero-amount card
31
+ // setup, nothing charged), then pass it here.
32
+ const bt = new LetsFG({ bearerToken: 'eyJ...' });
36
33
 
37
34
  // Search — FREE
38
35
  const flights = await bt.search('GDN', 'BER', '2026-03-03');
39
36
  const best = cheapestOffer(flights);
40
37
  console.log(offerSummary(best));
41
38
 
42
- // Unlock
43
- const unlock = await bt.unlock(best.id);
44
-
45
- // Book
46
- const booking = await bt.book(
39
+ // Book — free, ticket price only, no LetsFG fee. No unlock step.
40
+ const result = await bt.book(
47
41
  best.id,
48
- [{
49
- id: flights.passenger_ids[0],
50
- given_name: 'John',
51
- family_name: 'Doe',
52
- born_on: '1990-01-15',
53
- gender: 'm',
54
- title: 'mr',
55
- email: 'john@example.com',
56
- }],
57
- 'john@example.com'
42
+ [{ given_name: 'John', family_name: 'Doe', born_on: '1990-01-15', gender: 'm' }],
43
+ 'john@example.com',
44
+ '',
45
+ '',
46
+ flights.search_id,
58
47
  );
59
- console.log(`PNR: ${booking.booking_reference}`);
48
+ if (result.booked) {
49
+ console.log(`Order: ${result.order_id}`);
50
+ } else {
51
+ console.log(`Booking link (nothing charged): ${result.booking_url}`);
52
+ }
60
53
  ```
61
54
 
55
+ Prefer the paid Developer API instead? Pass `apiKey` instead of `bearerToken` —
56
+ `search()`/`book()` dispatch automatically. That path requires `unlock()`
57
+ (1% fee, min $3) before `book()`.
58
+
62
59
  ## Quick Start (CLI)
63
60
 
64
61
  ```bash
65
- export LETSFG_BEARER_TOKEN=<your-bearer-token>
62
+ export LETSFG_BEARER_TOKEN=<your-bearer-token> # from `letsfg auth`
66
63
 
67
64
  letsfg search GDN BER 2026-03-03 --sort price
68
65
  letsfg search LON BCN 2026-04-01 --json # Machine-readable
69
- letsfg unlock off_xxx
70
- letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
66
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m"}' -e john@example.com
71
67
  ```
72
68
 
73
69
  ## API
74
70
 
75
- ### `new LetsFG({ apiKey, baseUrl?, timeout? })`
71
+ ### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
76
72
 
77
73
  ### `bt.search(origin, destination, dateFrom, options?)`
78
74
  ### `bt.resolveLocation(query)`
79
- ### `bt.unlock(offerId)`
80
- ### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
81
- ### `bt.setupPayment(token?)`
75
+ ### `bt.unlock(offerId)` — Developer API only
76
+ ### `bt.book(offerId, passengers, contactEmail, contactPhone?, idempotencyKey?, searchId?)`
77
+ Dispatches on which credential is set: `bearerToken` → free PFS booking via
78
+ `POST /api/agent-book` (pass `searchId`, one passenger). `apiKey` → paid
79
+ Developer API `book` (requires `unlock()` first, supports multiple passengers
80
+ and `idempotencyKey`).
81
+ ### `bt.setupPayment(token?)` — Developer API only
82
82
  ### `bt.me()`
83
- ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)`
83
+ ### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
84
84
 
85
85
  ### Helpers
86
86
  - `offerSummary(offer)` — One-line string summary
@@ -0,0 +1,16 @@
1
+ import {
2
+ BearerTokenError,
3
+ getBearerToken,
4
+ paymentAuth,
5
+ requestEnrolment,
6
+ saveToken,
7
+ verifyPaymentMethod
8
+ } from "./chunk-XNO2W77S.mjs";
9
+ export {
10
+ BearerTokenError,
11
+ getBearerToken,
12
+ paymentAuth,
13
+ requestEnrolment,
14
+ saveToken,
15
+ verifyPaymentMethod
16
+ };
@@ -1629,18 +1629,44 @@ var LetsFG = class {
1629
1629
  }
1630
1630
  /**
1631
1631
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1632
- * Cost: 1% of ticket price, min $3. Free with Developer API.
1632
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
1633
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
1633
1634
  */
1634
1635
  async unlock(offerId) {
1635
- this.requireAuth();
1636
- const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
1637
- return this.postWithAuth(path, { offer_id: offerId });
1636
+ this.requireApiKey();
1637
+ return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
1638
1638
  }
1639
1639
  /**
1640
- * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
1641
- * Always provide idempotencyKey to prevent double-bookings on retry.
1640
+ * Book a flight.
1641
+ *
1642
+ * PFS (Bearer token): POST /api/agent-book. Free — nothing charged beyond
1643
+ * the ticket price. Pass searchId (from search()'s result). Returns either
1644
+ * { ok, booked: true, order_id } or, when the booking genuinely could not
1645
+ * complete, { ok, booked: false, booking_url } — hand the link to the user,
1646
+ * nothing was charged.
1647
+ *
1648
+ * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
1649
+ * creates a real PNR. Requires unlock() first. Always provide
1650
+ * idempotencyKey to prevent double-bookings on retry.
1642
1651
  */
1643
- async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
1652
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
1653
+ this.requireAuth();
1654
+ if (this.usingPFS) {
1655
+ if (!searchId) {
1656
+ throw new LetsFGError(
1657
+ "searchId is required to book via PFS \u2014 pass the search_id from search()'s result.",
1658
+ 400
1659
+ );
1660
+ }
1661
+ const passenger = { ...passengers[0] };
1662
+ if (contactPhone && !passenger.phone_number) passenger.phone_number = contactPhone;
1663
+ return this.postWithBearer("/api/agent-book", {
1664
+ search_id: searchId,
1665
+ offer_id: offerId,
1666
+ passenger,
1667
+ contact_email: contactEmail
1668
+ });
1669
+ }
1644
1670
  this.requireApiKey();
1645
1671
  const body = {
1646
1672
  offer_id: offerId,
@@ -0,0 +1,126 @@
1
+ // src/auth.ts
2
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
3
+ import { homedir, platform } from "os";
4
+ import { join, dirname } from "path";
5
+ var BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
6
+ var TOKEN_TTL_MS = 90 * 24 * 3600 * 1e3;
7
+ var BearerTokenError = class extends Error {
8
+ };
9
+ function configPath() {
10
+ const base = platform() === "win32" ? process.env.APPDATA || homedir() : homedir();
11
+ return join(base, ".letsfg", "config.json");
12
+ }
13
+ function loadConfig() {
14
+ const p = configPath();
15
+ if (!existsSync(p)) return {};
16
+ try {
17
+ return JSON.parse(readFileSync(p, "utf-8"));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+ function saveConfig(cfg) {
23
+ const p = configPath();
24
+ mkdirSync(dirname(p), { recursive: true });
25
+ writeFileSync(p, JSON.stringify(cfg, null, 2));
26
+ try {
27
+ chmodSync(p, 384);
28
+ } catch {
29
+ }
30
+ }
31
+ function getBearerToken() {
32
+ const env = process.env.LETSFG_BEARER_TOKEN;
33
+ if (env) return env;
34
+ const auth = loadConfig().pfs_auth;
35
+ if (auth?.token && Date.now() < auth.expires_at - 36e5) {
36
+ return auth.token;
37
+ }
38
+ throw new BearerTokenError(
39
+ "No valid LetsFG Bearer token.\n Run: letsfg auth (adds a payment method \u2014 nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
40
+ );
41
+ }
42
+ function saveToken(token, expiresAt) {
43
+ const cfg = loadConfig();
44
+ cfg.pfs_auth = { token, expires_at: expiresAt ?? Date.now() + TOKEN_TTL_MS };
45
+ saveConfig(cfg);
46
+ }
47
+ async function postJson(path, payload) {
48
+ const resp = await fetch(`${BASE_URL}${path}`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0", "X-Client-Type": "js-sdk" },
51
+ body: JSON.stringify(payload)
52
+ });
53
+ const data = await resp.json().catch(() => ({}));
54
+ return { status: resp.status, data };
55
+ }
56
+ function parseExpiry(raw) {
57
+ if (typeof raw === "number") return raw * 1e3;
58
+ if (typeof raw === "string") {
59
+ const ms = Date.parse(raw);
60
+ if (!Number.isNaN(ms)) return ms;
61
+ }
62
+ return Date.now() + TOKEN_TTL_MS;
63
+ }
64
+ async function requestEnrolment() {
65
+ const { status, data } = await postJson("/api/agent-access/request", {});
66
+ if (status !== 200 && status !== 402) {
67
+ throw new BearerTokenError(`Could not start authentication (HTTP ${status}): ${data.error || ""}`);
68
+ }
69
+ return data;
70
+ }
71
+ async function verifyPaymentMethod(opts) {
72
+ const payload = {};
73
+ if (opts.setupSessionId) payload.setup_session_id = opts.setupSessionId;
74
+ else if (opts.paymentMethodId) payload.payment_method_id = opts.paymentMethodId;
75
+ else if (opts.cardToken) payload.card_token = opts.cardToken;
76
+ else throw new BearerTokenError("Provide one of setupSessionId, paymentMethodId, or cardToken.");
77
+ const { status, data } = await postJson("/api/agent-access/verify", payload);
78
+ if (status !== 200 || !data.token) {
79
+ throw new BearerTokenError(`Verification failed (HTTP ${status}). ${data.hint || data.error || ""}`.trim());
80
+ }
81
+ const token = data.token;
82
+ saveToken(token, parseExpiry(data.expires_at));
83
+ return token;
84
+ }
85
+ function openBrowser(url) {
86
+ import("child_process").then(({ exec }) => {
87
+ const cmd = platform() === "win32" ? `start "" "${url}"` : platform() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
88
+ exec(cmd, () => {
89
+ });
90
+ }).catch(() => {
91
+ });
92
+ }
93
+ async function paymentAuth(openBrowserFlag = true) {
94
+ console.log("\n Connecting to LetsFG...");
95
+ const data = await requestEnrolment();
96
+ const setupUrl = data.setup_url;
97
+ const sessionId = data.setup_session_id;
98
+ if (!setupUrl || !sessionId) {
99
+ throw new BearerTokenError("Server did not return a setup URL. Check https://letsfg.co/for-agents");
100
+ }
101
+ console.log("\n LetsFG needs a payment method on file before it can search or book.");
102
+ console.log(" Nothing is charged now \u2014 this is a zero-amount card setup.\n");
103
+ console.log(" Step 1 \u2014 add a card here:\n");
104
+ console.log(` ${setupUrl}
105
+ `);
106
+ if (openBrowserFlag) openBrowser(setupUrl);
107
+ console.log(" Step 2 \u2014 press Enter once you've finished.");
108
+ const readline = await import("readline/promises");
109
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
110
+ await rl.question("");
111
+ rl.close();
112
+ process.stdout.write(" Verifying... ");
113
+ const token = await verifyPaymentMethod({ setupSessionId: sessionId });
114
+ const expiresAt = loadConfig().pfs_auth?.expires_at ?? Date.now() + TOKEN_TTL_MS;
115
+ console.log(`done. Token valid until ${new Date(expiresAt).toISOString().slice(0, 10)}.`);
116
+ return token;
117
+ }
118
+
119
+ export {
120
+ BearerTokenError,
121
+ getBearerToken,
122
+ saveToken,
123
+ requestEnrolment,
124
+ verifyPaymentMethod,
125
+ paymentAuth
126
+ };
package/dist/cli.js CHANGED
@@ -1,5 +1,167 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // src/auth.ts
34
+ var auth_exports = {};
35
+ __export(auth_exports, {
36
+ BearerTokenError: () => BearerTokenError,
37
+ getBearerToken: () => getBearerToken,
38
+ paymentAuth: () => paymentAuth,
39
+ requestEnrolment: () => requestEnrolment,
40
+ saveToken: () => saveToken,
41
+ verifyPaymentMethod: () => verifyPaymentMethod
42
+ });
43
+ function configPath() {
44
+ const base = (0, import_node_os.platform)() === "win32" ? process.env.APPDATA || (0, import_node_os.homedir)() : (0, import_node_os.homedir)();
45
+ return (0, import_node_path.join)(base, ".letsfg", "config.json");
46
+ }
47
+ function loadConfig() {
48
+ const p = configPath();
49
+ if (!(0, import_node_fs.existsSync)(p)) return {};
50
+ try {
51
+ return JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+ function saveConfig(cfg) {
57
+ const p = configPath();
58
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(p), { recursive: true });
59
+ (0, import_node_fs.writeFileSync)(p, JSON.stringify(cfg, null, 2));
60
+ try {
61
+ (0, import_node_fs.chmodSync)(p, 384);
62
+ } catch {
63
+ }
64
+ }
65
+ function getBearerToken() {
66
+ const env = process.env.LETSFG_BEARER_TOKEN;
67
+ if (env) return env;
68
+ const auth = loadConfig().pfs_auth;
69
+ if (auth?.token && Date.now() < auth.expires_at - 36e5) {
70
+ return auth.token;
71
+ }
72
+ throw new BearerTokenError(
73
+ "No valid LetsFG Bearer token.\n Run: letsfg auth (adds a payment method \u2014 nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
74
+ );
75
+ }
76
+ function saveToken(token, expiresAt) {
77
+ const cfg = loadConfig();
78
+ cfg.pfs_auth = { token, expires_at: expiresAt ?? Date.now() + TOKEN_TTL_MS };
79
+ saveConfig(cfg);
80
+ }
81
+ async function postJson(path, payload) {
82
+ const resp = await fetch(`${BASE_URL}${path}`, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0", "X-Client-Type": "js-sdk" },
85
+ body: JSON.stringify(payload)
86
+ });
87
+ const data = await resp.json().catch(() => ({}));
88
+ return { status: resp.status, data };
89
+ }
90
+ function parseExpiry(raw) {
91
+ if (typeof raw === "number") return raw * 1e3;
92
+ if (typeof raw === "string") {
93
+ const ms = Date.parse(raw);
94
+ if (!Number.isNaN(ms)) return ms;
95
+ }
96
+ return Date.now() + TOKEN_TTL_MS;
97
+ }
98
+ async function requestEnrolment() {
99
+ const { status, data } = await postJson("/api/agent-access/request", {});
100
+ if (status !== 200 && status !== 402) {
101
+ throw new BearerTokenError(`Could not start authentication (HTTP ${status}): ${data.error || ""}`);
102
+ }
103
+ return data;
104
+ }
105
+ async function verifyPaymentMethod(opts) {
106
+ const payload = {};
107
+ if (opts.setupSessionId) payload.setup_session_id = opts.setupSessionId;
108
+ else if (opts.paymentMethodId) payload.payment_method_id = opts.paymentMethodId;
109
+ else if (opts.cardToken) payload.card_token = opts.cardToken;
110
+ else throw new BearerTokenError("Provide one of setupSessionId, paymentMethodId, or cardToken.");
111
+ const { status, data } = await postJson("/api/agent-access/verify", payload);
112
+ if (status !== 200 || !data.token) {
113
+ throw new BearerTokenError(`Verification failed (HTTP ${status}). ${data.hint || data.error || ""}`.trim());
114
+ }
115
+ const token = data.token;
116
+ saveToken(token, parseExpiry(data.expires_at));
117
+ return token;
118
+ }
119
+ function openBrowser(url) {
120
+ import("child_process").then(({ exec }) => {
121
+ const cmd = (0, import_node_os.platform)() === "win32" ? `start "" "${url}"` : (0, import_node_os.platform)() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
122
+ exec(cmd, () => {
123
+ });
124
+ }).catch(() => {
125
+ });
126
+ }
127
+ async function paymentAuth(openBrowserFlag = true) {
128
+ console.log("\n Connecting to LetsFG...");
129
+ const data = await requestEnrolment();
130
+ const setupUrl = data.setup_url;
131
+ const sessionId = data.setup_session_id;
132
+ if (!setupUrl || !sessionId) {
133
+ throw new BearerTokenError("Server did not return a setup URL. Check https://letsfg.co/for-agents");
134
+ }
135
+ console.log("\n LetsFG needs a payment method on file before it can search or book.");
136
+ console.log(" Nothing is charged now \u2014 this is a zero-amount card setup.\n");
137
+ console.log(" Step 1 \u2014 add a card here:\n");
138
+ console.log(` ${setupUrl}
139
+ `);
140
+ if (openBrowserFlag) openBrowser(setupUrl);
141
+ console.log(" Step 2 \u2014 press Enter once you've finished.");
142
+ const readline = await import("readline/promises");
143
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
144
+ await rl.question("");
145
+ rl.close();
146
+ process.stdout.write(" Verifying... ");
147
+ const token = await verifyPaymentMethod({ setupSessionId: sessionId });
148
+ const expiresAt = loadConfig().pfs_auth?.expires_at ?? Date.now() + TOKEN_TTL_MS;
149
+ console.log(`done. Token valid until ${new Date(expiresAt).toISOString().slice(0, 10)}.`);
150
+ return token;
151
+ }
152
+ var import_node_fs, import_node_os, import_node_path, BASE_URL, TOKEN_TTL_MS, BearerTokenError;
153
+ var init_auth = __esm({
154
+ "src/auth.ts"() {
155
+ "use strict";
156
+ import_node_fs = require("fs");
157
+ import_node_os = require("os");
158
+ import_node_path = require("path");
159
+ BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
160
+ TOKEN_TTL_MS = 90 * 24 * 3600 * 1e3;
161
+ BearerTokenError = class extends Error {
162
+ };
163
+ }
164
+ });
3
165
 
4
166
  // src/ranking.ts
5
167
  var W = {
@@ -384,18 +546,44 @@ var LetsFG = class {
384
546
  }
385
547
  /**
386
548
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
387
- * Cost: 1% of ticket price, min $3. Free with Developer API.
549
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
550
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
388
551
  */
389
552
  async unlock(offerId) {
390
- this.requireAuth();
391
- const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
392
- return this.postWithAuth(path, { offer_id: offerId });
553
+ this.requireApiKey();
554
+ return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
393
555
  }
394
556
  /**
395
- * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
396
- * Always provide idempotencyKey to prevent double-bookings on retry.
557
+ * Book a flight.
558
+ *
559
+ * PFS (Bearer token): POST /api/agent-book. Free — nothing charged beyond
560
+ * the ticket price. Pass searchId (from search()'s result). Returns either
561
+ * { ok, booked: true, order_id } or, when the booking genuinely could not
562
+ * complete, { ok, booked: false, booking_url } — hand the link to the user,
563
+ * nothing was charged.
564
+ *
565
+ * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
566
+ * creates a real PNR. Requires unlock() first. Always provide
567
+ * idempotencyKey to prevent double-bookings on retry.
397
568
  */
398
- async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
569
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
570
+ this.requireAuth();
571
+ if (this.usingPFS) {
572
+ if (!searchId) {
573
+ throw new LetsFGError(
574
+ "searchId is required to book via PFS \u2014 pass the search_id from search()'s result.",
575
+ 400
576
+ );
577
+ }
578
+ const passenger = { ...passengers[0] };
579
+ if (contactPhone && !passenger.phone_number) passenger.phone_number = contactPhone;
580
+ return this.postWithBearer("/api/agent-book", {
581
+ search_id: searchId,
582
+ offer_id: offerId,
583
+ passenger,
584
+ contact_email: contactEmail
585
+ });
586
+ }
399
587
  this.requireApiKey();
400
588
  const body = {
401
589
  offer_id: offerId,
@@ -658,6 +846,16 @@ var LetsFG = class {
658
846
  };
659
847
 
660
848
  // src/cli.ts
849
+ init_auth();
850
+ function resolveCredentials(apiKeyFlag) {
851
+ const apiKey = apiKeyFlag || process.env.LETSFG_API_KEY;
852
+ if (apiKey) return { apiKey };
853
+ try {
854
+ return { bearerToken: getBearerToken() };
855
+ } catch {
856
+ return {};
857
+ }
858
+ }
661
859
  function getFlag(args, flag, alias) {
662
860
  for (let i = 0; i < args.length; i++) {
663
861
  if (args[i] === flag || alias && args[i] === alias) {
@@ -712,7 +910,8 @@ async function cmdSearch(args) {
712
910
  console.error("Usage: letsfg search <origin> <destination> <date> [options]");
713
911
  process.exit(1);
714
912
  }
715
- const bt = new LetsFG({ apiKey, baseUrl });
913
+ const creds = resolveCredentials(apiKey);
914
+ const bt = new LetsFG({ ...creds, baseUrl });
716
915
  const result = await bt.search(origin, destination, date, {
717
916
  returnDate,
718
917
  adults,
@@ -726,6 +925,7 @@ async function cmdSearch(args) {
726
925
  });
727
926
  if (jsonOut) {
728
927
  console.log(JSON.stringify({
928
+ search_id: result.search_id,
729
929
  passenger_ids: result.passenger_ids,
730
930
  total_results: result.total_results,
731
931
  offers: result.offers.map((o) => ({
@@ -749,16 +949,25 @@ async function cmdSearch(args) {
749
949
  }
750
950
  console.log(`
751
951
  ${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
952
+ if (result.search_id) {
953
+ console.log(` search_id: ${result.search_id} (needed for \`letsfg book\`, offers expire ~15 min after search)`);
954
+ }
752
955
  console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
753
956
  `);
754
957
  result.offers.forEach((o, i) => {
755
958
  console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
756
959
  console.log(` ID: ${o.id}`);
757
960
  });
758
- console.log(`
961
+ if (creds.bearerToken) {
962
+ console.log(`
963
+ To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
964
+ `);
965
+ } else {
966
+ console.log(`
759
967
  To unlock: letsfg unlock <offer_id>`);
760
- console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
968
+ console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
761
969
  `);
970
+ }
762
971
  }
763
972
  async function cmdUnlock(args) {
764
973
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -792,33 +1001,55 @@ async function cmdBook(args) {
792
1001
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
793
1002
  const apiKey = getFlag(args, "--api-key", "-k");
794
1003
  const baseUrl = getFlag(args, "--base-url");
1004
+ const searchId = getFlag(args, "--search-id");
795
1005
  const email = getFlag(args, "--email", "-e") || "";
796
1006
  const phone = getFlag(args, "--phone") || "";
797
1007
  const passengerStrs = getAllFlags(args, "--passenger", "-p");
798
1008
  const offerId = args[0];
799
1009
  if (!offerId || !passengerStrs.length || !email) {
800
- console.error(`Usage: letsfg book <offer_id> --passenger '{"id":"pas_xxx",...}' --email you@example.com`);
1010
+ console.error(`Usage: letsfg book <offer_id> --search-id <id> --passenger '{"given_name":"John",...}' --email you@example.com`);
801
1011
  process.exit(1);
802
1012
  }
803
1013
  const passengers = passengerStrs.map((s) => JSON.parse(s));
804
- const bt = new LetsFG({ apiKey, baseUrl });
805
- const result = await bt.book(offerId, passengers, email, phone);
1014
+ const creds = resolveCredentials(apiKey);
1015
+ if (creds.bearerToken && !searchId) {
1016
+ console.error("Error: --search-id is required (from your `letsfg search` results) to book via the free PFS path.");
1017
+ process.exit(1);
1018
+ }
1019
+ const bt = new LetsFG({ ...creds, baseUrl });
1020
+ const result = await bt.book(offerId, passengers, email, phone, "", searchId);
806
1021
  if (jsonOut) {
807
1022
  console.log(JSON.stringify(result, null, 2));
808
1023
  return;
809
1024
  }
810
- if (result.status === "confirmed") {
1025
+ if ("booked" in result) {
1026
+ if (result.booked) {
1027
+ console.log(`
1028
+ \u2713 Booking confirmed!`);
1029
+ console.log(` Order ID: ${result.order_id}`);
1030
+ console.log(` Charged: ${result.charged ?? 0} ${result.currency ?? ""}
1031
+ `);
1032
+ } else {
1033
+ console.log(`
1034
+ Could not complete a confirmed booking. Nothing was charged.`);
1035
+ console.log(` Booking link: ${result.booking_url ?? "(none)"}
1036
+ `);
1037
+ }
1038
+ return;
1039
+ }
1040
+ const br = result;
1041
+ if (br.status === "confirmed") {
811
1042
  console.log(`
812
1043
  \u2713 Booking confirmed!`);
813
- console.log(` PNR: ${result.booking_reference}`);
814
- console.log(` Flight: ${result.currency} ${result.flight_price.toFixed(2)}`);
815
- console.log(` Fee: ${result.currency} ${result.service_fee.toFixed(2)} (${result.service_fee_percentage}%)`);
816
- console.log(` Total: ${result.currency} ${result.total_charged.toFixed(2)}`);
817
- console.log(` Order: ${result.order_id}
1044
+ console.log(` PNR: ${br.booking_reference}`);
1045
+ console.log(` Flight: ${br.currency} ${br.flight_price.toFixed(2)}`);
1046
+ console.log(` Fee: ${br.currency} ${br.service_fee.toFixed(2)} (${br.service_fee_percentage}%)`);
1047
+ console.log(` Total: ${br.currency} ${br.total_charged.toFixed(2)}`);
1048
+ console.log(` Order: ${br.order_id}
818
1049
  `);
819
1050
  } else {
820
1051
  console.error(` \u2717 Booking failed`);
821
- console.error(JSON.stringify(result.details, null, 2));
1052
+ console.error(JSON.stringify(br.details, null, 2));
822
1053
  process.exit(1);
823
1054
  }
824
1055
  }
@@ -850,6 +1081,19 @@ async function cmdLocations(args) {
850
1081
  console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
851
1082
  }
852
1083
  }
1084
+ async function cmdAuth(args) {
1085
+ const cardToken = getFlag(args, "--card-token");
1086
+ const paymentMethodId = getFlag(args, "--payment-method");
1087
+ const noBrowser = hasFlag(args, "--no-browser");
1088
+ if (cardToken || paymentMethodId) {
1089
+ const { verifyPaymentMethod: verifyPaymentMethod2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
1090
+ await verifyPaymentMethod2({ cardToken: cardToken || void 0, paymentMethodId: paymentMethodId || void 0 });
1091
+ console.log("\n \u2713 Authenticated. Nothing was charged.");
1092
+ } else {
1093
+ await paymentAuth(!noBrowser);
1094
+ }
1095
+ console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
1096
+ }
853
1097
  async function cmdRegister(args) {
854
1098
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
855
1099
  const baseUrl = getFlag(args, "--base-url");
@@ -930,11 +1174,11 @@ Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
930
1174
  charged \u2014 then search and book.
931
1175
 
932
1176
  Commands:
933
- auth Put a card on file -> 90-day token. Nothing charged
934
- search <origin> <dest> <date> Search for flights (free)
935
- locations <query> Resolve city name to IATA codes
936
- book <offer_id> --passenger ... Book a flight. No LetsFG fee
937
- me Show agent profile
1177
+ auth Put a card on file -> 90-day token. Nothing charged
1178
+ search <origin> <dest> <date> Search for flights (free), prints search_id
1179
+ locations <query> Resolve city name to IATA codes
1180
+ book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
1181
+ me Show agent profile
938
1182
 
939
1183
  Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
940
1184
  they create a billing account. Use auth above instead):
@@ -943,20 +1187,26 @@ they create a billing account. Use auth above instead):
943
1187
  unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
944
1188
 
945
1189
  Options:
946
- --json, -j Output raw JSON
947
- --api-key, -k API key (or set LETSFG_API_KEY)
948
- --base-url API URL (default: https://letsfg.co/developers)
1190
+ --json, -j Output raw JSON
1191
+ --api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
1192
+ --base-url API URL (default: https://letsfg.co)
1193
+ --card-token (auth only) Stripe tok_... you already hold, for a headless auth
1194
+ --payment-method (auth only) Stripe pm_... you already hold, for a headless auth
1195
+ --no-browser (auth only) Don't try to auto-open the card setup page
949
1196
 
950
1197
  Examples:
951
1198
  letsfg auth
952
1199
  letsfg search GDN BER 2026-03-03 --sort price
953
- letsfg book off_xxx -p '{"given_name":"Ada",...}' -e ada@example.com
1200
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"Ada","family_name":"Lovelace","born_on":"1990-04-01","gender":"f"}' -e ada@example.com
954
1201
  `;
955
1202
  async function main() {
956
1203
  const args = process.argv.slice(2);
957
1204
  const command = args.shift();
958
1205
  try {
959
1206
  switch (command) {
1207
+ case "auth":
1208
+ await cmdAuth(args);
1209
+ break;
960
1210
  case "search":
961
1211
  await cmdSearch(args);
962
1212
  break;
@@ -990,7 +1240,7 @@ async function main() {
990
1240
  process.exit(1);
991
1241
  }
992
1242
  } catch (e) {
993
- if (e instanceof LetsFGError) {
1243
+ if (e instanceof LetsFGError || e instanceof BearerTokenError) {
994
1244
  console.error(`Error: ${e.message}`);
995
1245
  process.exit(1);
996
1246
  }
package/dist/cli.mjs CHANGED
@@ -3,9 +3,23 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-ZX2EIEN7.mjs";
6
+ } from "./chunk-2SNO3AGS.mjs";
7
+ import {
8
+ BearerTokenError,
9
+ getBearerToken,
10
+ paymentAuth
11
+ } from "./chunk-XNO2W77S.mjs";
7
12
 
8
13
  // src/cli.ts
14
+ function resolveCredentials(apiKeyFlag) {
15
+ const apiKey = apiKeyFlag || process.env.LETSFG_API_KEY;
16
+ if (apiKey) return { apiKey };
17
+ try {
18
+ return { bearerToken: getBearerToken() };
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
9
23
  function getFlag(args, flag, alias) {
10
24
  for (let i = 0; i < args.length; i++) {
11
25
  if (args[i] === flag || alias && args[i] === alias) {
@@ -60,7 +74,8 @@ async function cmdSearch(args) {
60
74
  console.error("Usage: letsfg search <origin> <destination> <date> [options]");
61
75
  process.exit(1);
62
76
  }
63
- const bt = new LetsFG({ apiKey, baseUrl });
77
+ const creds = resolveCredentials(apiKey);
78
+ const bt = new LetsFG({ ...creds, baseUrl });
64
79
  const result = await bt.search(origin, destination, date, {
65
80
  returnDate,
66
81
  adults,
@@ -74,6 +89,7 @@ async function cmdSearch(args) {
74
89
  });
75
90
  if (jsonOut) {
76
91
  console.log(JSON.stringify({
92
+ search_id: result.search_id,
77
93
  passenger_ids: result.passenger_ids,
78
94
  total_results: result.total_results,
79
95
  offers: result.offers.map((o) => ({
@@ -97,16 +113,25 @@ async function cmdSearch(args) {
97
113
  }
98
114
  console.log(`
99
115
  ${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
116
+ if (result.search_id) {
117
+ console.log(` search_id: ${result.search_id} (needed for \`letsfg book\`, offers expire ~15 min after search)`);
118
+ }
100
119
  console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
101
120
  `);
102
121
  result.offers.forEach((o, i) => {
103
122
  console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
104
123
  console.log(` ID: ${o.id}`);
105
124
  });
106
- console.log(`
125
+ if (creds.bearerToken) {
126
+ console.log(`
127
+ To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
128
+ `);
129
+ } else {
130
+ console.log(`
107
131
  To unlock: letsfg unlock <offer_id>`);
108
- console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
132
+ console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
109
133
  `);
134
+ }
110
135
  }
111
136
  async function cmdUnlock(args) {
112
137
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
@@ -140,33 +165,55 @@ async function cmdBook(args) {
140
165
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
141
166
  const apiKey = getFlag(args, "--api-key", "-k");
142
167
  const baseUrl = getFlag(args, "--base-url");
168
+ const searchId = getFlag(args, "--search-id");
143
169
  const email = getFlag(args, "--email", "-e") || "";
144
170
  const phone = getFlag(args, "--phone") || "";
145
171
  const passengerStrs = getAllFlags(args, "--passenger", "-p");
146
172
  const offerId = args[0];
147
173
  if (!offerId || !passengerStrs.length || !email) {
148
- console.error(`Usage: letsfg book <offer_id> --passenger '{"id":"pas_xxx",...}' --email you@example.com`);
174
+ console.error(`Usage: letsfg book <offer_id> --search-id <id> --passenger '{"given_name":"John",...}' --email you@example.com`);
149
175
  process.exit(1);
150
176
  }
151
177
  const passengers = passengerStrs.map((s) => JSON.parse(s));
152
- const bt = new LetsFG({ apiKey, baseUrl });
153
- const result = await bt.book(offerId, passengers, email, phone);
178
+ const creds = resolveCredentials(apiKey);
179
+ if (creds.bearerToken && !searchId) {
180
+ console.error("Error: --search-id is required (from your `letsfg search` results) to book via the free PFS path.");
181
+ process.exit(1);
182
+ }
183
+ const bt = new LetsFG({ ...creds, baseUrl });
184
+ const result = await bt.book(offerId, passengers, email, phone, "", searchId);
154
185
  if (jsonOut) {
155
186
  console.log(JSON.stringify(result, null, 2));
156
187
  return;
157
188
  }
158
- if (result.status === "confirmed") {
189
+ if ("booked" in result) {
190
+ if (result.booked) {
191
+ console.log(`
192
+ \u2713 Booking confirmed!`);
193
+ console.log(` Order ID: ${result.order_id}`);
194
+ console.log(` Charged: ${result.charged ?? 0} ${result.currency ?? ""}
195
+ `);
196
+ } else {
197
+ console.log(`
198
+ Could not complete a confirmed booking. Nothing was charged.`);
199
+ console.log(` Booking link: ${result.booking_url ?? "(none)"}
200
+ `);
201
+ }
202
+ return;
203
+ }
204
+ const br = result;
205
+ if (br.status === "confirmed") {
159
206
  console.log(`
160
207
  \u2713 Booking confirmed!`);
161
- console.log(` PNR: ${result.booking_reference}`);
162
- console.log(` Flight: ${result.currency} ${result.flight_price.toFixed(2)}`);
163
- console.log(` Fee: ${result.currency} ${result.service_fee.toFixed(2)} (${result.service_fee_percentage}%)`);
164
- console.log(` Total: ${result.currency} ${result.total_charged.toFixed(2)}`);
165
- console.log(` Order: ${result.order_id}
208
+ console.log(` PNR: ${br.booking_reference}`);
209
+ console.log(` Flight: ${br.currency} ${br.flight_price.toFixed(2)}`);
210
+ console.log(` Fee: ${br.currency} ${br.service_fee.toFixed(2)} (${br.service_fee_percentage}%)`);
211
+ console.log(` Total: ${br.currency} ${br.total_charged.toFixed(2)}`);
212
+ console.log(` Order: ${br.order_id}
166
213
  `);
167
214
  } else {
168
215
  console.error(` \u2717 Booking failed`);
169
- console.error(JSON.stringify(result.details, null, 2));
216
+ console.error(JSON.stringify(br.details, null, 2));
170
217
  process.exit(1);
171
218
  }
172
219
  }
@@ -198,6 +245,19 @@ async function cmdLocations(args) {
198
245
  console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
199
246
  }
200
247
  }
248
+ async function cmdAuth(args) {
249
+ const cardToken = getFlag(args, "--card-token");
250
+ const paymentMethodId = getFlag(args, "--payment-method");
251
+ const noBrowser = hasFlag(args, "--no-browser");
252
+ if (cardToken || paymentMethodId) {
253
+ const { verifyPaymentMethod } = await import("./auth-SYZ5AYZ6.mjs");
254
+ await verifyPaymentMethod({ cardToken: cardToken || void 0, paymentMethodId: paymentMethodId || void 0 });
255
+ console.log("\n \u2713 Authenticated. Nothing was charged.");
256
+ } else {
257
+ await paymentAuth(!noBrowser);
258
+ }
259
+ console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
260
+ }
201
261
  async function cmdRegister(args) {
202
262
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
203
263
  const baseUrl = getFlag(args, "--base-url");
@@ -278,11 +338,11 @@ Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
278
338
  charged \u2014 then search and book.
279
339
 
280
340
  Commands:
281
- auth Put a card on file -> 90-day token. Nothing charged
282
- search <origin> <dest> <date> Search for flights (free)
283
- locations <query> Resolve city name to IATA codes
284
- book <offer_id> --passenger ... Book a flight. No LetsFG fee
285
- me Show agent profile
341
+ auth Put a card on file -> 90-day token. Nothing charged
342
+ search <origin> <dest> <date> Search for flights (free), prints search_id
343
+ locations <query> Resolve city name to IATA codes
344
+ book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
345
+ me Show agent profile
286
346
 
287
347
  Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
288
348
  they create a billing account. Use auth above instead):
@@ -291,20 +351,26 @@ they create a billing account. Use auth above instead):
291
351
  unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
292
352
 
293
353
  Options:
294
- --json, -j Output raw JSON
295
- --api-key, -k API key (or set LETSFG_API_KEY)
296
- --base-url API URL (default: https://letsfg.co/developers)
354
+ --json, -j Output raw JSON
355
+ --api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
356
+ --base-url API URL (default: https://letsfg.co)
357
+ --card-token (auth only) Stripe tok_... you already hold, for a headless auth
358
+ --payment-method (auth only) Stripe pm_... you already hold, for a headless auth
359
+ --no-browser (auth only) Don't try to auto-open the card setup page
297
360
 
298
361
  Examples:
299
362
  letsfg auth
300
363
  letsfg search GDN BER 2026-03-03 --sort price
301
- letsfg book off_xxx -p '{"given_name":"Ada",...}' -e ada@example.com
364
+ letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"Ada","family_name":"Lovelace","born_on":"1990-04-01","gender":"f"}' -e ada@example.com
302
365
  `;
303
366
  async function main() {
304
367
  const args = process.argv.slice(2);
305
368
  const command = args.shift();
306
369
  try {
307
370
  switch (command) {
371
+ case "auth":
372
+ await cmdAuth(args);
373
+ break;
308
374
  case "search":
309
375
  await cmdSearch(args);
310
376
  break;
@@ -338,7 +404,7 @@ async function main() {
338
404
  process.exit(1);
339
405
  }
340
406
  } catch (e) {
341
- if (e instanceof LetsFGError) {
407
+ if (e instanceof LetsFGError || e instanceof BearerTokenError) {
342
408
  console.error(`Error: ${e.message}`);
343
409
  process.exit(1);
344
410
  }
package/dist/index.d.mts CHANGED
@@ -472,14 +472,24 @@ declare class LetsFG {
472
472
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
473
473
  /**
474
474
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
475
- * Cost: 1% of ticket price, min $3. Free with Developer API.
475
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
476
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
476
477
  */
477
478
  unlock(offerId: string): Promise<UnlockResult>;
478
479
  /**
479
- * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
480
- * Always provide idempotencyKey to prevent double-bookings on retry.
480
+ * Book a flight.
481
+ *
482
+ * PFS (Bearer token): POST /api/agent-book. Free — nothing charged beyond
483
+ * the ticket price. Pass searchId (from search()'s result). Returns either
484
+ * { ok, booked: true, order_id } or, when the booking genuinely could not
485
+ * complete, { ok, booked: false, booking_url } — hand the link to the user,
486
+ * nothing was charged.
487
+ *
488
+ * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
489
+ * creates a real PNR. Requires unlock() first. Always provide
490
+ * idempotencyKey to prevent double-bookings on retry.
481
491
  */
482
- book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
492
+ book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string, searchId?: string): Promise<BookingResult | Record<string, unknown>>;
483
493
  /**
484
494
  * Resolve a place name to the city id that searchHotels() needs.
485
495
  *
package/dist/index.d.ts CHANGED
@@ -472,14 +472,24 @@ declare class LetsFG {
472
472
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
473
473
  /**
474
474
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
475
- * Cost: 1% of ticket price, min $3. Free with Developer API.
475
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
476
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
476
477
  */
477
478
  unlock(offerId: string): Promise<UnlockResult>;
478
479
  /**
479
- * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
480
- * Always provide idempotencyKey to prevent double-bookings on retry.
480
+ * Book a flight.
481
+ *
482
+ * PFS (Bearer token): POST /api/agent-book. Free — nothing charged beyond
483
+ * the ticket price. Pass searchId (from search()'s result). Returns either
484
+ * { ok, booked: true, order_id } or, when the booking genuinely could not
485
+ * complete, { ok, booked: false, booking_url } — hand the link to the user,
486
+ * nothing was charged.
487
+ *
488
+ * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
489
+ * creates a real PNR. Requires unlock() first. Always provide
490
+ * idempotencyKey to prevent double-bookings on retry.
481
491
  */
482
- book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
492
+ book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string, searchId?: string): Promise<BookingResult | Record<string, unknown>>;
483
493
  /**
484
494
  * Resolve a place name to the city id that searchHotels() needs.
485
495
  *
package/dist/index.js CHANGED
@@ -1677,18 +1677,44 @@ var LetsFG = class {
1677
1677
  }
1678
1678
  /**
1679
1679
  * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1680
- * Cost: 1% of ticket price, min $3. Free with Developer API.
1680
+ * Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
1681
+ * endpoint on a PFS Bearer token, so PFS callers use book() directly.
1681
1682
  */
1682
1683
  async unlock(offerId) {
1683
- this.requireAuth();
1684
- const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
1685
- return this.postWithAuth(path, { offer_id: offerId });
1684
+ this.requireApiKey();
1685
+ return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
1686
1686
  }
1687
1687
  /**
1688
- * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
1689
- * Always provide idempotencyKey to prevent double-bookings on retry.
1688
+ * Book a flight.
1689
+ *
1690
+ * PFS (Bearer token): POST /api/agent-book. Free — nothing charged beyond
1691
+ * the ticket price. Pass searchId (from search()'s result). Returns either
1692
+ * { ok, booked: true, order_id } or, when the booking genuinely could not
1693
+ * complete, { ok, booked: false, booking_url } — hand the link to the user,
1694
+ * nothing was charged.
1695
+ *
1696
+ * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
1697
+ * creates a real PNR. Requires unlock() first. Always provide
1698
+ * idempotencyKey to prevent double-bookings on retry.
1690
1699
  */
1691
- async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
1700
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
1701
+ this.requireAuth();
1702
+ if (this.usingPFS) {
1703
+ if (!searchId) {
1704
+ throw new LetsFGError(
1705
+ "searchId is required to book via PFS \u2014 pass the search_id from search()'s result.",
1706
+ 400
1707
+ );
1708
+ }
1709
+ const passenger = { ...passengers[0] };
1710
+ if (contactPhone && !passenger.phone_number) passenger.phone_number = contactPhone;
1711
+ return this.postWithBearer("/api/agent-book", {
1712
+ search_id: searchId,
1713
+ offer_id: offerId,
1714
+ passenger,
1715
+ contact_email: contactEmail
1716
+ });
1717
+ }
1692
1718
  this.requireApiKey();
1693
1719
  const body = {
1694
1720
  offer_id: offerId,
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  offerSummary,
23
23
  rankOffers,
24
24
  selectDiverseTop
25
- } from "./chunk-ZX2EIEN7.mjs";
25
+ } from "./chunk-2SNO3AGS.mjs";
26
26
  export {
27
27
  AuthenticationError,
28
28
  BoostedTravel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letsfg",
3
- "version": "2026.5.64",
3
+ "version": "2026.5.66",
4
4
  "description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with free cancellation and pay-later terms. Includes open-source ranking engine.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
18
- "test": "tsx --test src/index.test.ts",
18
+ "test": "tsx --test src/index.test.ts src/auth.test.ts",
19
19
  "prepublishOnly": "npm run build"
20
20
  },
21
21
  "keywords": [