@fiberai/sdk 0.0.13 → 0.0.15

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
@@ -5,6 +5,21 @@ Official TypeScript/JavaScript SDK for the [Fiber AI](https://fiber.ai) API - Re
5
5
  [![npm version](https://img.shields.io/npm/v/@fiberai/sdk.svg)](https://www.npmjs.com/package/@fiberai/sdk)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
+ ## For AI agents (Cursor / Claude / Codex / ChatGPT / Copilot)
9
+
10
+ Do NOT guess operation names — read the canonical agent-facing docs first:
11
+
12
+ - **Routing index + critical rules:** https://api.fiber.ai/llms.txt
13
+ - **Per-operation markdown:** `https://api.fiber.ai/ai-docs/<operationId>.md`
14
+ (e.g. [`companySearch`](https://api.fiber.ai/ai-docs/companySearch.md),
15
+ [`syncQuickContactReveal`](https://api.fiber.ai/ai-docs/syncQuickContactReveal.md),
16
+ [`triggerExhaustiveContactEnrichment`](https://api.fiber.ai/ai-docs/triggerExhaustiveContactEnrichment.md))
17
+ - **Operation index:** https://api.fiber.ai/ai-docs/index.md
18
+ - **Full corpus (single file):** https://api.fiber.ai/llms-full.txt
19
+ - **OpenAPI spec:** `https://api.fiber.ai/openapi.json`
20
+ (send `Accept: text/markdown` for the agent-friendly variant)
21
+ - **MCP server:** `https://mcp.fiber.ai/mcp/v2` (also see [fiber-ai/mcp](https://github.com/fiber-ai/mcp))
22
+
8
23
  ## Table of Contents
9
24
 
10
25
  - [Installation](#installation)
@@ -14,13 +29,13 @@ Official TypeScript/JavaScript SDK for the [Fiber AI](https://fiber.ai) API - Re
14
29
  - [Company & People Search](#company--people-search)
15
30
  - [Contact Enrichment](#contact-enrichment)
16
31
  - [LinkedIn Live Enrichment](#linkedin-live-enrichment)
17
- - [Saved Searches (Audiences)](#saved-searches-audiences)
18
32
  - [Exclusion Lists](#exclusion-lists)
19
33
  - [Google Maps Search](#google-maps-search)
20
34
  - [AI-Powered Research](#ai-powered-research)
21
35
  - [Advanced Usage](#advanced-usage)
22
36
  - [Error Handling](#error-handling)
23
37
  - [TypeScript Support](#typescript-support)
38
+ - [Runtime Validation with Zod](#runtime-validation-with-zod)
24
39
  - [Rate Limits & Credits](#rate-limits--credits)
25
40
  - [Support](#support)
26
41
 
@@ -39,48 +54,58 @@ pnpm add @fiberai/sdk
39
54
  ## Quick Start
40
55
 
41
56
  ```typescript
42
- import { peopleSearch, companySearch, getOrgCredits } from '@fiberai/sdk';
57
+ import { peopleSearch, companySearch, getOrgCredits } from "@fiberai/sdk";
43
58
 
44
59
  // Check your organization's credit balance
45
60
  const credits = await getOrgCredits({
46
- query: { apiKey: 'your-api-key' }
61
+ query: { apiKey: "your-api-key" },
47
62
  });
48
63
 
49
- console.log(`Available credits: ${credits.data.output.available}`);
64
+ console.log(`Available credits: ${credits.data?.output.available}`);
50
65
 
51
66
  // Search for companies
52
67
  const companies = await companySearch({
53
68
  body: {
54
- apiKey: 'your-api-key',
69
+ apiKey: "your-api-key",
55
70
  searchParams: {
56
71
  industriesV2: {
57
- anyOf: ['Software', 'Information Technology']
72
+ anyOf: ["Software", "Information Technology"],
58
73
  },
74
+ // Bucket-valued range. Allowed bounds: 0 | 1 | 10 | 50 | 200 | 500 | 1000 | 5000 | 10000 | null.
59
75
  employeeCountV2: {
60
- lowerBoundExclusive: 100,
61
- upperBoundInclusive: 1000
76
+ lowerBoundExclusive: 50,
77
+ upperBoundInclusive: 1000,
62
78
  },
63
79
  headquartersCountryCode: {
64
- anyOf: ['USA']
65
- }
80
+ anyOf: ["USA"],
81
+ },
66
82
  },
67
- pageSize: 25
68
- }
83
+ pageSize: 25,
84
+ },
69
85
  });
70
86
 
71
87
  // Search for people
72
88
  const people = await peopleSearch({
73
89
  body: {
74
- apiKey: 'your-api-key',
90
+ apiKey: "your-api-key",
75
91
  searchParams: {
76
- title: ['CEO', 'CTO', 'VP Engineering'],
77
- location: {
78
- cities: ['San Francisco, CA']
79
- }
92
+ jobTitleV2: {
93
+ anyOf: [
94
+ { type: "term", term: "CEO" },
95
+ { type: "term", term: "CTO" },
96
+ { type: "static-groups", groups: ["c-suite"] },
97
+ ],
98
+ },
99
+ country3LetterCode: { anyOf: ["USA"] },
80
100
  },
81
- pageSize: 25
82
- }
101
+ pageSize: 25,
102
+ },
83
103
  });
104
+
105
+ // Every successful response also carries a `chargeInfo` envelope alongside
106
+ // `output`. Source of truth for cost — prefer this over the static table below.
107
+ console.log(people.data?.chargeInfo);
108
+ // e.g. { method: 'charged-now', creditsCharged: 25, lowCreditAlert: null }
84
109
  ```
85
110
 
86
111
  ## Authentication
@@ -97,13 +122,15 @@ All API requests require an API key. Get yours at [fiber.ai/app/api](https://fib
97
122
  await companySearch({
98
123
  body: {
99
124
  apiKey: process.env.FIBERAI_API_KEY,
100
- searchParams: { /* ... */ }
101
- }
125
+ searchParams: {
126
+ /* ... */
127
+ },
128
+ },
102
129
  });
103
130
 
104
131
  // GET request example
105
132
  await getOrgCredits({
106
- query: { apiKey: process.env.FIBERAI_API_KEY }
133
+ query: { apiKey: process.env.FIBERAI_API_KEY },
107
134
  });
108
135
  ```
109
136
 
@@ -123,7 +150,7 @@ FIBERAI_API_KEY=your_api_key_here
123
150
  Search for companies using 40+ filters including industry, location, revenue, funding, and more.
124
151
 
125
152
  ```typescript
126
- import { companySearch, companyCount } from '@fiberai/sdk';
153
+ import { companySearch, companyCount } from "@fiberai/sdk";
127
154
 
128
155
  // Advanced company search
129
156
  const result = await companySearch({
@@ -132,46 +159,51 @@ const result = await companySearch({
132
159
  searchParams: {
133
160
  // Industry filters
134
161
  industriesV2: {
135
- anyOf: ['Software', 'Cloud']
162
+ anyOf: ["Software", "Cloud"],
136
163
  },
137
-
164
+
138
165
  // Size filters
139
166
  employeeCountV2: {
140
167
  lowerBoundExclusive: 50,
141
- upperBoundInclusive: 500
168
+ upperBoundInclusive: 500,
142
169
  },
143
-
170
+
144
171
  // Location filters
145
172
  headquartersCountryCode: {
146
- anyOf: ['USA']
173
+ anyOf: ["USA"],
147
174
  },
148
-
175
+
149
176
  // Funding filters
150
177
  totalFundingUSD: {
151
- lowerBound: 1000000
178
+ lowerBound: 1000000,
152
179
  },
153
-
180
+
154
181
  // Keywords filter
155
182
  keywords: {
156
- containsAny: ['venture-backed-startup']
157
- }
183
+ containsAny: ["venture-backed-startup"],
184
+ },
158
185
  },
159
186
  pageSize: 50,
160
- cursor: null // For pagination
161
- }
187
+ cursor: null, // For pagination
188
+ },
162
189
  });
163
190
 
164
- console.log(`Found ${result.data.output.data.items.length} companies`);
191
+ console.log(`Found ${result.data?.output.data.length} companies`);
192
+ console.log(
193
+ `Cursor for next page: ${result.data?.output.nextCursor ?? "none"}`,
194
+ );
165
195
 
166
196
  // Get total count before searching
167
197
  const count = await companyCount({
168
198
  body: {
169
199
  apiKey: process.env.FIBERAI_API_KEY,
170
- searchParams: { /* same filters */ }
171
- }
200
+ searchParams: {
201
+ /* same filters */
202
+ },
203
+ },
172
204
  });
173
205
 
174
- console.log(`Total companies matching: ${count.data.output.count}`);
206
+ console.log(`Total companies matching: ${count.data?.output.count}`);
175
207
  ```
176
208
 
177
209
  #### People Search
@@ -179,244 +211,283 @@ console.log(`Total companies matching: ${count.data.output.count}`);
179
211
  Find decision-makers and key contacts with precise targeting.
180
212
 
181
213
  ```typescript
182
- import { peopleSearch, peopleSearchCount } from '@fiberai/sdk';
214
+ import { peopleSearch, peopleSearchCount } from "@fiberai/sdk";
183
215
 
184
216
  const people = await peopleSearch({
185
217
  body: {
186
- apiKey: process.env.FIBERAI_API_KEY,
218
+ apiKey: process.env.FIBERAI_API_KEY!,
187
219
  searchParams: {
188
- // Job title filters
189
- title: ['CEO', 'Chief Executive Officer', 'Founder'],
190
-
191
- // Seniority and function
192
- seniority: ['Executive'],
193
- jobFunction: ['Entrepreneurship'],
194
-
195
- // Location filters
220
+ // Job title filter. Mix free-form `term` matches with curated
221
+ // `static-groups` ('founder' | 'c-suite' | 'board-member') and
222
+ // `dynamic-groups` ('vp' | 'director' | 'management' | 'entry-level' | ...).
223
+ jobTitleV2: {
224
+ anyOf: [
225
+ { type: "term", term: "CEO" },
226
+ { type: "term", term: "Chief Executive Officer" },
227
+ { type: "static-groups", groups: ["founder", "c-suite"] },
228
+ ],
229
+ },
230
+
231
+ // Country filter — ISO 3166-1 alpha-3 codes, not city names.
232
+ country3LetterCode: { anyOf: ["USA"] },
233
+
234
+ // Geographic radius filter (use this, not "cities"/"countries").
196
235
  location: {
197
- cities: ['San Francisco, CA', 'New York, NY'],
198
- countries: ['USA']
236
+ unionAll: [
237
+ {
238
+ strategy: "radial-distance",
239
+ center: { latitude: 37.7749, longitude: -122.4194 }, // San Francisco
240
+ radius: { unit: "miles", quantity: 50 },
241
+ },
242
+ ],
199
243
  },
200
-
201
- // Company filters (search within specific companies)
202
- currentCompany: {
203
- domains: ['example.com'],
204
- linkedinUrls: ['https://linkedin.com/company/example']
244
+
245
+ // Curated tags. Allowed values include 'decision-maker', 'c-suite',
246
+ // 'experienced-executive', 'second-time-founder', 'phd', etc.
247
+ tags: { anyOf: ["decision-maker", "c-suite"] },
248
+
249
+ // Education filter — match by school identifier (LinkedIn id, slug,
250
+ // or domain), not by free-form school name.
251
+ education: {
252
+ anyOf: [
253
+ {
254
+ school: {
255
+ anyOf: [{ domain: "stanford.edu" }, { domain: "harvard.edu" }],
256
+ },
257
+ },
258
+ ],
205
259
  },
206
-
207
- // Tags and special filters
208
- tags: ['decision-maker', 'c-suite'],
209
-
210
- // Education filters
211
- schools: ['Stanford University', 'Harvard University']
260
+
261
+ getDetailedWorkExperience: true,
262
+ getDetailedEducation: true,
212
263
  },
264
+
265
+ // `currentCompanies` is a TOP-LEVEL body field, NOT inside searchParams.
266
+ // It expects concrete identifiers, not industry filters. To filter by
267
+ // industry/size/etc, use `syncCombinedSearch` and put company filters
268
+ // under `companyParams`.
269
+ currentCompanies: [
270
+ { domain: "example.com" },
271
+ { linkedinSlugOrURL: "https://linkedin.com/company/example" },
272
+ ],
273
+
213
274
  pageSize: 100,
214
- getDetailedWorkExperience: true,
215
- getDetailedEducation: true
216
- }
275
+ },
217
276
  });
218
277
 
219
- // Access profile data
220
- people.data.output.data.items.forEach(profile => {
221
- console.log(`${profile.name} - ${profile.headline}`);
278
+ // Access profile data — `output.data` is a direct array, no `.items` wrapper.
279
+ people.data?.output.data.forEach((profile) => {
280
+ console.log(`${profile.name} ${profile.headline}`);
222
281
  console.log(`LinkedIn: ${profile.url}`);
223
- if (profile.currentJob) {
224
- console.log(`Current: ${profile.currentJob.title} at ${profile.currentJob.company_name}`);
282
+
283
+ // There is no `profile.currentJob` field. Current role lives in
284
+ // `experiences[]` filtered by `is_current`.
285
+ const currentRole = profile.experiences?.find((e) => e.is_current);
286
+ if (currentRole) {
287
+ console.log(`Current: ${currentRole.title} at ${currentRole.company_name}`);
225
288
  }
226
289
  });
290
+
291
+ console.log(`Charged: ${people.data?.chargeInfo.method}`);
227
292
  ```
228
293
 
229
294
  #### Combined Search (Companies + People)
230
295
 
231
- Search for companies and their employees in one workflow.
296
+ Return matching companies and their employees in a single synchronous call. Filter on the company side (`companyParams`) and the person side (`profileParams`) at the same time. For larger result sets you can also run `companySearch` and `peopleSearch` independently and paginate each.
232
297
 
233
298
  ```typescript
234
- import { combinedSearch, pollCombinedSearch } from '@fiberai/sdk';
299
+ import { syncCombinedSearch } from "@fiberai/sdk";
235
300
 
236
- // Start async search
237
- const searchTask = await combinedSearch({
301
+ const result = await syncCombinedSearch({
238
302
  body: {
239
- apiKey: process.env.FIBERAI_API_KEY,
240
- companySearchParams: {
303
+ apiKey: process.env.FIBERAI_API_KEY!,
304
+ companyParams: {
241
305
  industriesV2: {
242
- anyOf: ['Software']
306
+ anyOf: ["Software"],
243
307
  },
244
308
  employeeCountV2: {
245
- lowerBoundExclusive: 100
246
- }
309
+ lowerBoundExclusive: 200,
310
+ },
247
311
  },
248
- personSearchParams: {
249
- title: ['VP of Sales', 'Sales Director'],
250
- seniority: ['Director', 'Executive']
312
+ profileParams: {
313
+ jobTitleV2: {
314
+ anyOf: [
315
+ { type: "term", term: "VP of Sales" },
316
+ { type: "term", term: "Sales Director" },
317
+ { type: "static-groups", groups: ["c-suite"] },
318
+ { type: "dynamic-groups", groups: ["vp", "director"] },
319
+ ],
320
+ },
251
321
  },
252
- maxCompanies: 100,
253
- maxProspects: 500
254
- }
322
+ companyItemLimit: 25,
323
+ profileItemLimit: 100,
324
+ },
255
325
  });
256
326
 
257
- const searchId = searchTask.data.output.searchId;
327
+ result.data?.output.companies?.forEach((company) => {
328
+ console.log(company.preferred_name);
329
+ });
258
330
 
259
- // Poll for companies
260
- let companyCursor = null;
261
- do {
262
- const companies = await pollCombinedSearch({
263
- body: {
264
- apiKey: process.env.FIBERAI_API_KEY,
265
- searchId,
266
- entityType: 'company',
267
- cursor: companyCursor,
268
- pageSize: 25
269
- }
270
- });
271
-
272
- // Process companies
273
- companies.data.output.data.items.forEach(company => {
274
- console.log(company.preferred_name);
275
- });
276
-
277
- companyCursor = companies.data.output.nextCursor;
278
- } while (companyCursor);
279
-
280
- // Poll for prospects
281
- let prospectCursor = null;
282
- do {
283
- const prospects = await pollCombinedSearch({
284
- body: {
285
- apiKey: process.env.FIBERAI_API_KEY,
286
- searchId,
287
- entityType: 'profile',
288
- cursor: prospectCursor,
289
- pageSize: 100
290
- }
291
- });
292
-
293
- // Process profiles
294
- prospects.data.output.data.items.forEach(profile => {
295
- console.log(`${profile.name} - ${profile.headline}`);
296
- });
297
-
298
- prospectCursor = prospects.data.output.nextCursor;
299
- } while (prospectCursor);
331
+ result.data?.output.profiles?.forEach((profile) => {
332
+ console.log(`${profile.name} - ${profile.headline}`);
333
+ });
300
334
  ```
301
335
 
336
+ > See [`syncCombinedSearch`](https://api.fiber.ai/ai-docs/syncCombinedSearch.md) for the full parameter surface, or drive the two-step flow via `companySearch` + `peopleSearch` when you need cursor-based pagination.
337
+
302
338
  ### Contact Enrichment
303
339
 
304
- Get emails and phone numbers for LinkedIn profiles.
340
+ Reveal emails and phone numbers for a known LinkedIn profile. Fiber exposes a **three-tier public contract** — pick the tier that matches your latency, coverage, and cost tradeoffs:
341
+
342
+ | Tier | Operation | Shape | When to use |
343
+ | ---------- | ------------------------------------------------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------- |
344
+ | Default | `syncQuickContactReveal` | Sync, one HTTP call | Default single-profile reveal. Fast, good coverage. |
345
+ | Premium | `syncTurboContactEnrichment` | Sync, one HTTP call | You need the absolute fastest response and want the widest first-pass waterfall. Premium cost. |
346
+ | Exhaustive | `triggerExhaustiveContactEnrichment` + `pollExhaustiveContactEnrichmentResult` | Async, trigger + poll | Highest coverage. Kick off a background waterfall that tries every available provider, then poll for results. |
305
347
 
306
- #### Single Contact Enrichment (Async)
348
+ For lists of 10–2000 identifiers, use the batch endpoints instead (see [Batch Contact Reveal](#batch-contact-reveal)).
349
+
350
+ #### Default — `syncQuickContactReveal`
307
351
 
308
352
  ```typescript
309
- import { triggerContactEnrichment, pollContactEnrichmentResult } from '@fiberai/sdk';
353
+ import { syncQuickContactReveal } from "@fiberai/sdk";
310
354
 
311
- // Start enrichment
312
- const task = await triggerContactEnrichment({
355
+ const result = await syncQuickContactReveal({
313
356
  body: {
314
- apiKey: process.env.FIBERAI_API_KEY,
315
- linkedinUrl: 'https://www.linkedin.com/in/example',
316
- dataToFetch: {
317
- workEmail: true,
318
- personalEmail: true,
319
- phoneNumber: true
357
+ apiKey: process.env.FIBERAI_API_KEY!,
358
+ linkedinUrl: "https://www.linkedin.com/in/example",
359
+ enrichmentType: {
360
+ getWorkEmails: true,
361
+ getPersonalEmails: true,
362
+ getPhoneNumbers: true,
320
363
  },
321
- liveFetch: false // Set to true for real-time LinkedIn scraping (+1 credit)
322
- }
364
+ // Emails are bounce-validated by default. Set validateEmails: false to skip.
365
+ },
323
366
  });
324
367
 
325
- // Poll for results
326
- let done = false;
327
- while (!done) {
328
- await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
329
-
330
- const result = await pollContactEnrichmentResult({
331
- body: {
332
- apiKey: process.env.FIBERAI_API_KEY,
333
- taskId: task.data.output.taskId
334
- }
335
- });
336
-
337
- done = result.data.output.done;
338
-
339
- if (done) {
340
- const profile = result.data.output.profile;
341
- console.log('Emails:', profile.emails);
342
- console.log('Phone numbers:', profile.phoneNumbers);
343
- }
344
- }
368
+ console.log("Emails:", result.data?.output.profile?.emails);
369
+ console.log("Phones:", result.data?.output.profile?.phoneNumbers);
345
370
  ```
346
371
 
347
- #### Single Contact Enrichment (Sync)
372
+ #### Premium `syncTurboContactEnrichment`
348
373
 
349
374
  ```typescript
350
- import { syncContactEnrichment } from '@fiberai/sdk';
375
+ import { syncTurboContactEnrichment } from "@fiberai/sdk";
351
376
 
352
- // Synchronous enrichment (waits for completion)
353
- const result = await syncContactEnrichment({
377
+ const result = await syncTurboContactEnrichment({
354
378
  body: {
355
- apiKey: process.env.FIBERAI_API_KEY,
356
- linkedinUrl: 'https://www.linkedin.com/in/example',
357
- dataToFetch: {
358
- workEmail: true,
359
- personalEmail: false,
360
- phoneNumber: false
361
- }
362
- }
379
+ apiKey: process.env.FIBERAI_API_KEY!,
380
+ linkedinUrl: "https://www.linkedin.com/in/example",
381
+ enrichmentType: {
382
+ getWorkEmails: true,
383
+ getPersonalEmails: true,
384
+ getPhoneNumbers: true,
385
+ },
386
+ },
363
387
  });
364
388
 
365
- console.log('Work emails:', result.data.output.profile.emails);
389
+ console.log("Emails:", result.data?.output.profile?.emails);
366
390
  ```
367
391
 
368
- #### Batch Contact Enrichment
392
+ #### Exhaustive (async waterfall) — `triggerExhaustiveContactEnrichment` + `pollExhaustiveContactEnrichmentResult`
369
393
 
370
- Enrich up to 10,000 contacts in one request.
394
+ `triggerExhaustiveContactEnrichment` starts a background waterfall that returns the highest coverage at the cost of latency. It returns a `taskId` immediately; poll `pollExhaustiveContactEnrichmentResult` every 5–15s until `output.done === true`.
371
395
 
372
396
  ```typescript
373
- import { startBatchContactEnrichment, pollBatchContactEnrichment } from '@fiberai/sdk';
397
+ import {
398
+ triggerExhaustiveContactEnrichment,
399
+ pollExhaustiveContactEnrichmentResult,
400
+ } from "@fiberai/sdk";
374
401
 
375
- // Start batch enrichment
376
- const batch = await startBatchContactEnrichment({
377
- body: {
378
- apiKey: process.env.FIBERAI_API_KEY,
379
- people: [
380
- { linkedinUrl: { value: 'https://linkedin.com/in/person1' } },
381
- { linkedinUrl: { value: 'https://linkedin.com/in/person2' } },
382
- // ... up to 10,000 people
383
- ],
384
- dataToFetch: {
385
- workEmail: true,
386
- personalEmail: true,
387
- phoneNumber: true
388
- }
402
+ const trigger: Awaited<ReturnType<typeof triggerExhaustiveContactEnrichment>> =
403
+ await triggerExhaustiveContactEnrichment({
404
+ body: {
405
+ apiKey: process.env.FIBERAI_API_KEY!,
406
+ linkedinUrl: "https://www.linkedin.com/in/example",
407
+ enrichmentType: {
408
+ getWorkEmails: true,
409
+ getPersonalEmails: true,
410
+ getPhoneNumbers: true,
411
+ },
412
+ },
413
+ });
414
+
415
+ const taskId: string = trigger.data!.output.taskId;
416
+
417
+ let done: boolean = false;
418
+ while (!done) {
419
+ await new Promise<void>((resolve) => setTimeout(resolve, 5000));
420
+
421
+ const poll: Awaited<ReturnType<typeof pollExhaustiveContactEnrichmentResult>> =
422
+ await pollExhaustiveContactEnrichmentResult({
423
+ body: { apiKey: process.env.FIBERAI_API_KEY!, taskId },
424
+ });
425
+
426
+ done = poll.data?.output.done ?? false;
427
+ if (done) {
428
+ console.log("Emails:", poll.data?.output.profile.emails);
429
+ console.log("Phones:", poll.data?.output.profile.phoneNumbers);
430
+ console.log("Status:", poll.data?.output.profile.status);
389
431
  }
390
- });
432
+ }
433
+ ```
434
+
435
+ #### Batch Contact Reveal — `startBatchContactEnrichment` + `pollBatchContactEnrichment`
436
+
437
+ For 10–2000 LinkedIn identifiers in one task. `startBatchContactEnrichment` charges credits up front and returns a `taskId`; `pollBatchContactEnrichment` returns paginated results with both per-task progress (`output.overallStats`) and `output.done` to gate the loop. For larger lists or repeatable workflows, upload a CSV-backed audience and enrich it through the audience workflow instead.
391
438
 
392
- // Poll for results with pagination
393
- let cursor = null;
394
- let allDone = false;
439
+ ```typescript
440
+ import {
441
+ startBatchContactEnrichment,
442
+ pollBatchContactEnrichment,
443
+ } from "@fiberai/sdk";
395
444
 
396
- while (!allDone) {
397
- await new Promise(resolve => setTimeout(resolve, 5000));
398
-
399
- const results = await pollBatchContactEnrichment({
445
+ const start: Awaited<ReturnType<typeof startBatchContactEnrichment>> =
446
+ await startBatchContactEnrichment({
400
447
  body: {
401
- apiKey: process.env.FIBERAI_API_KEY,
402
- taskId: batch.data.output.taskId,
403
- cursor,
404
- take: 100
405
- }
406
- });
407
-
408
- allDone = results.data.output.done;
409
- cursor = results.data.output.nextCursor;
410
-
411
- // Process page results
412
- results.data.output.pageResults.forEach(person => {
413
- if (person.outputs) {
414
- console.log('LinkedIn:', person.inputs.linkedinUrl.value);
415
- console.log('Emails:', person.outputs.emails);
416
- console.log('Phones:', person.outputs.phoneNumbers);
417
- console.log('---');
418
- }
448
+ apiKey: process.env.FIBERAI_API_KEY!,
449
+ personDetails: [
450
+ { linkedinUrl: { value: "https://www.linkedin.com/in/example1" } },
451
+ { linkedinUrl: { value: "https://www.linkedin.com/in/example2" } },
452
+ ],
453
+ enrichmentTypes: {
454
+ getWorkEmails: true,
455
+ getPersonalEmails: true,
456
+ getPhoneNumbers: true,
457
+ },
458
+ },
419
459
  });
460
+
461
+ const taskId: string = start.data!.output.taskId;
462
+ console.log(`Queued ${start.data!.output.numPeopleEnqueued} profiles`);
463
+
464
+ let cursor: string | null | undefined = undefined;
465
+ let done: boolean = false;
466
+
467
+ while (!done) {
468
+ await new Promise<void>((resolve) => setTimeout(resolve, 10000));
469
+
470
+ const poll: Awaited<ReturnType<typeof pollBatchContactEnrichment>> =
471
+ await pollBatchContactEnrichment({
472
+ body: {
473
+ apiKey: process.env.FIBERAI_API_KEY!,
474
+ taskId,
475
+ cursor,
476
+ take: 100,
477
+ },
478
+ });
479
+
480
+ if (!poll.data) break;
481
+
482
+ for (const row of poll.data.output.pageResults) {
483
+ console.log(row.inputs.linkedinUrl.value, row.outputs?.emails);
484
+ }
485
+
486
+ done = poll.data.output.done;
487
+ cursor = poll.data.output.nextCursor ?? null;
488
+
489
+ // `nextCursor` may go null before `done` flips — pause and re-poll if so.
490
+ if (!done && !cursor) await new Promise<void>((r) => setTimeout(r, 5000));
420
491
  }
421
492
  ```
422
493
 
@@ -427,134 +498,76 @@ Get real-time data from LinkedIn with live scraping.
427
498
  #### Live Profile Enrichment
428
499
 
429
500
  ```typescript
430
- import { profileLiveEnrich } from '@fiberai/sdk';
501
+ import { profileLiveEnrich } from "@fiberai/sdk";
431
502
 
432
503
  const profile = await profileLiveEnrich({
433
504
  body: {
434
- apiKey: process.env.FIBERAI_API_KEY,
435
- linkedinUrl: 'https://www.linkedin.com/in/example'
436
- }
505
+ apiKey: process.env.FIBERAI_API_KEY!,
506
+ // `identifier` accepts a slug ('williamhgates'), a full LinkedIn URL,
507
+ // a Sales Navigator URN ('ACwAAA...'), or a numeric LinkedIn user ID.
508
+ identifier: "https://www.linkedin.com/in/example",
509
+ },
437
510
  });
438
511
 
439
- console.log(profile.data.output.name);
440
- console.log(profile.data.output.summary);
441
- console.log(profile.data.output.experiences);
442
- console.log(profile.data.output.education);
512
+ // `output` is a discriminated union: { found: true; profile: ... } |
513
+ // { found: false; message: string }. Narrow on `output.found` before
514
+ // touching `output.profile`.
515
+ const out = profile.data?.output;
516
+ if (out?.found) {
517
+ console.log(out.profile.name);
518
+ console.log(out.profile.summary);
519
+ console.log(out.profile.experiences);
520
+ console.log(out.profile.detailed_education);
521
+ } else if (out) {
522
+ console.warn("not found:", out.message);
523
+ }
443
524
  ```
444
525
 
445
526
  #### Live Company Enrichment
446
527
 
447
528
  ```typescript
448
- import { companyLiveEnrich } from '@fiberai/sdk';
529
+ import { companyLiveEnrich } from "@fiberai/sdk";
449
530
 
450
531
  const company = await companyLiveEnrich({
451
532
  body: {
452
- apiKey: process.env.FIBERAI_API_KEY,
453
- linkedinUrl: 'https://www.linkedin.com/company/example'
454
- }
533
+ apiKey: process.env.FIBERAI_API_KEY!,
534
+ // The `type` discriminator picks how `value` is interpreted:
535
+ // 'slug' — LinkedIn slug, e.g. 'microsoft'
536
+ // 'orgId' — LinkedIn organization id, e.g. '1441'
537
+ // 'liUrl' — full LinkedIn URL, e.g. 'https://www.linkedin.com/company/microsoft'
538
+ type: "liUrl",
539
+ value: "https://www.linkedin.com/company/example",
540
+ },
455
541
  });
456
542
 
457
- console.log(company.data.output.preferred_name);
458
- console.log(company.data.output.li_description);
459
- console.log(company.data.output.li_follower_count);
543
+ // Company fields live under `output.company`, not directly on `output`.
544
+ console.log(company.data?.output.company.headline);
545
+ console.log(company.data?.output.company.description);
546
+ console.log(company.data?.output.company.follower_count);
460
547
  ```
461
548
 
462
549
  #### Fetch LinkedIn Posts
463
550
 
464
551
  ```typescript
465
- import { profilePostsLiveFetch, companyPostsLiveFetch } from '@fiberai/sdk';
552
+ import { profilePostsLiveFetch, companyPostsLiveFetch } from "@fiberai/sdk";
466
553
 
467
- // Get profile posts
554
+ // Get profile posts. `identifier` accepts a slug, full LinkedIn URL,
555
+ // or Sales Navigator URN — same shape as `profileLiveEnrich`.
468
556
  const profilePosts = await profilePostsLiveFetch({
469
557
  body: {
470
- apiKey: process.env.FIBERAI_API_KEY,
471
- linkedinUrl: 'https://www.linkedin.com/in/example',
472
- cursor: null // For pagination
473
- }
558
+ apiKey: process.env.FIBERAI_API_KEY!,
559
+ identifier: "https://www.linkedin.com/in/example",
560
+ cursor: null, // For pagination
561
+ },
474
562
  });
475
563
 
476
- // Get company posts
564
+ // Get company posts. `identifier` accepts a LinkedIn slug, URL, or org ID.
477
565
  const companyPosts = await companyPostsLiveFetch({
478
566
  body: {
479
- apiKey: process.env.FIBERAI_API_KEY,
480
- linkedinUrl: 'https://www.linkedin.com/company/example',
481
- cursor: null
482
- }
483
- });
484
- ```
485
-
486
- ### Saved Searches (Audiences)
487
-
488
- Create, manage, and run saved searches with automatic updates.
489
-
490
- ```typescript
491
- import {
492
- createSavedSearch,
493
- listSavedSearch,
494
- manuallySpawnSavedSearchRun,
495
- getSavedSearchRunStatus,
496
- getSavedSearchRunCompanies,
497
- getSavedSearchRunProfiles
498
- } from '@fiberai/sdk';
499
-
500
- // Create a saved search
501
- const savedSearch = await createSavedSearch({
502
- body: {
503
- apiKey: process.env.FIBERAI_API_KEY,
504
- name: 'Tech Executives in SF',
505
- searchParams: {
506
- companySearchParams: {
507
- industriesV2: {
508
- anyOf: ['Software']
509
- }
510
- },
511
- personSearchParams: {
512
- title: ['CEO', 'CTO'],
513
- seniority: ['Executive']
514
- }
515
- }
516
- }
517
- });
518
-
519
- // List all saved searches
520
- const searches = await listSavedSearch({
521
- body: {
522
- apiKey: process.env.FIBERAI_API_KEY
523
- }
524
- });
525
-
526
- // Manually run a saved search
527
- const run = await manuallySpawnSavedSearchRun({
528
- body: {
529
- apiKey: process.env.FIBERAI_API_KEY,
530
- savedSearchId: savedSearch.data.output.savedSearchId
531
- }
532
- });
533
-
534
- // Check run status
535
- const status = await getSavedSearchRunStatus({
536
- body: {
537
- apiKey: process.env.FIBERAI_API_KEY,
538
- runId: run.data.output.runId
539
- }
540
- });
541
-
542
- // Get companies from run
543
- const companies = await getSavedSearchRunCompanies({
544
- body: {
545
- apiKey: process.env.FIBERAI_API_KEY,
546
- runId: run.data.output.runId,
547
- pageSize: 100
548
- }
549
- });
550
-
551
- // Get profiles from run
552
- const profiles = await getSavedSearchRunProfiles({
553
- body: {
554
- apiKey: process.env.FIBERAI_API_KEY,
555
- runId: run.data.output.runId,
556
- pageSize: 100
557
- }
567
+ apiKey: process.env.FIBERAI_API_KEY!,
568
+ identifier: "https://www.linkedin.com/company/example",
569
+ cursor: null,
570
+ },
558
571
  });
559
572
  ```
560
573
 
@@ -567,16 +580,16 @@ import {
567
580
  createCompanyExclusionList,
568
581
  addCompaniesToExclusionList,
569
582
  getExcludedCompaniesForExclusionList,
570
- createCompanyExclusionListFromAudience
571
- } from '@fiberai/sdk';
583
+ createCompanyExclusionListFromAudience,
584
+ } from "@fiberai/sdk";
572
585
 
573
586
  // Create an exclusion list
574
587
  const list = await createCompanyExclusionList({
575
588
  body: {
576
589
  apiKey: process.env.FIBERAI_API_KEY,
577
- name: 'Competitors',
578
- isOrganizationWide: true
579
- }
590
+ name: "Competitors",
591
+ isOrganizationWide: true,
592
+ },
580
593
  });
581
594
 
582
595
  // Add companies to the list
@@ -585,20 +598,20 @@ await addCompaniesToExclusionList({
585
598
  apiKey: process.env.FIBERAI_API_KEY,
586
599
  listId: list.data.output.listId,
587
600
  companies: [
588
- { domain: 'competitor1.com', linkedinUrl: null },
589
- { domain: 'competitor2.com', linkedinUrl: null }
590
- ]
591
- }
601
+ { domain: "competitor1.com", linkedinUrl: null },
602
+ { domain: "competitor2.com", linkedinUrl: null },
603
+ ],
604
+ },
592
605
  });
593
606
 
594
607
  // Create exclusion list from an existing audience
595
608
  const audienceList = await createCompanyExclusionListFromAudience({
596
609
  body: {
597
610
  apiKey: process.env.FIBERAI_API_KEY,
598
- audienceId: 'audience-123',
599
- name: 'Existing Customers',
600
- isOrganizationWide: true
601
- }
611
+ audienceId: "audience-123",
612
+ name: "Existing Customers",
613
+ isOrganizationWide: true,
614
+ },
602
615
  });
603
616
 
604
617
  // View excluded companies
@@ -606,8 +619,8 @@ const excluded = await getExcludedCompaniesForExclusionList({
606
619
  body: {
607
620
  apiKey: process.env.FIBERAI_API_KEY,
608
621
  exclusionListId: list.data.output.listId,
609
- pageSize: 100
610
- }
622
+ pageSize: 100,
623
+ },
611
624
  });
612
625
  ```
613
626
 
@@ -619,41 +632,54 @@ Search for local businesses on Google Maps.
619
632
  import {
620
633
  googleMapsSearch,
621
634
  checkGoogleMapsResults,
622
- pollGoogleMapsResults
623
- } from '@fiberai/sdk';
635
+ pollGoogleMapsResults,
636
+ } from "@fiberai/sdk";
624
637
 
625
- // Start Google Maps search
638
+ // Start Google Maps search. `query` is the keywords only (no location info)
639
+ // and `strategy` is required — see below for the available strategies.
626
640
  const search = await googleMapsSearch({
627
641
  body: {
628
- apiKey: process.env.FIBERAI_API_KEY,
629
- query: 'coffee shops',
630
- location: 'San Francisco, CA',
631
- maxResults: 100
632
- }
642
+ apiKey: process.env.FIBERAI_API_KEY!,
643
+ query: "coffee shops",
644
+ maxResults: 100,
645
+ strategy: {
646
+ // Options:
647
+ // { strategy: 'whole-usa' }
648
+ // { strategy: 'specific-areas', unionAll: [{ regionType: 'circle', center: {latitude, longitude}, radiusMiles }] }
649
+ // { strategy: 'world-cities', countriesAndRegions: { unionAll: ['USA', 'GBR', ...] } }
650
+ strategy: "specific-areas",
651
+ unionAll: [
652
+ {
653
+ regionType: "circle",
654
+ center: { latitude: 37.7749, longitude: -122.4194 },
655
+ radiusMiles: 10,
656
+ },
657
+ ],
658
+ },
659
+ },
633
660
  });
634
661
 
635
- // Check search progress
662
+ const searchID = search.data!.output.searchID;
663
+
664
+ // Check search progress.
636
665
  const progress = await checkGoogleMapsResults({
637
- body: {
638
- apiKey: process.env.FIBERAI_API_KEY,
639
- searchID: search.data.output.projectID
640
- }
666
+ body: { apiKey: process.env.FIBERAI_API_KEY!, searchID },
641
667
  });
642
668
 
643
- console.log(`Progress: ${progress.data.output.percentageCompleted}%`);
669
+ console.log(`Progress: ${progress.data?.output.percentageCompleted}%`);
644
670
 
645
- // Poll for results when complete
646
- if (progress.data.output.status === 'COMPLETED') {
671
+ // Poll for results once complete.
672
+ if (progress.data?.output.status === "COMPLETED") {
647
673
  const results = await pollGoogleMapsResults({
648
674
  body: {
649
- apiKey: process.env.FIBERAI_API_KEY,
650
- projectID: search.data.output.projectID,
651
- pageSize: 50
652
- }
675
+ apiKey: process.env.FIBERAI_API_KEY!,
676
+ searchID,
677
+ pageSize: 50,
678
+ },
653
679
  });
654
-
655
- results.data.output.results.forEach(place => {
656
- console.log(`${place.name} - ${place.address}`);
680
+
681
+ results.data?.output.results.forEach((place) => {
682
+ console.log(`${place.name} ${place.address}`);
657
683
  console.log(`Rating: ${place.rating}, Reviews: ${place.numReviews}`);
658
684
  console.log(`Website: ${place.website}`);
659
685
  });
@@ -665,37 +691,42 @@ if (progress.data.output.status === 'COMPLETED') {
665
691
  Use AI agents for intelligent company research and domain lookup.
666
692
 
667
693
  ```typescript
668
- import { domainLookupTrigger, domainLookupPolling } from '@fiberai/sdk';
694
+ import { domainLookupTrigger, domainLookupPolling } from "@fiberai/sdk";
669
695
 
670
- // Trigger domain lookup for company names
696
+ // Trigger domain lookup. `overAllContext` is required and helps the agent
697
+ // disambiguate similar names; `companyInfo` is an array of objects (not
698
+ // a list of bare strings).
671
699
  const lookup = await domainLookupTrigger({
672
700
  body: {
673
- apiKey: process.env.FIBERAI_API_KEY,
674
- companyNames: [
675
- 'OpenAI',
676
- 'Anthropic',
677
- 'Stripe'
678
- ]
679
- }
701
+ apiKey: process.env.FIBERAI_API_KEY!,
702
+ overAllContext: "YC startups in the US",
703
+ companyInfo: [
704
+ { name: "Acme Corp" },
705
+ { name: "Globex", country: "USA" },
706
+ { name: "Initech" },
707
+ ],
708
+ },
680
709
  });
681
710
 
682
- // Poll for results
711
+ const domainAgentRunId = lookup.data!.output.domainAgentRunId;
712
+
713
+ // Poll for results.
683
714
  let lookupDone = false;
684
715
  while (!lookupDone) {
685
- await new Promise(resolve => setTimeout(resolve, 3000));
686
-
716
+ await new Promise((resolve) => setTimeout(resolve, 3000));
717
+
687
718
  const results = await domainLookupPolling({
688
719
  body: {
689
- apiKey: process.env.FIBERAI_API_KEY,
690
- domainAgentRunId: lookup.data.output.domainAgentRunId,
691
- pageSize: 10
692
- }
720
+ apiKey: process.env.FIBERAI_API_KEY!,
721
+ domainAgentRunId,
722
+ pageSize: 10,
723
+ },
693
724
  });
694
-
695
- lookupDone = results.data.output.status === 'DONE';
696
-
725
+
726
+ lookupDone = results.data?.output.status === "DONE";
727
+
697
728
  if (lookupDone) {
698
- results.data.output.data.forEach(company => {
729
+ results.data?.output.data.forEach((company) => {
699
730
  console.log(`${company.companyName}: ${company.bestDomain}`);
700
731
  console.log(`Confidence: ${company.confidence}/10`);
701
732
  console.log(`Rationale: ${company.rationale}`);
@@ -706,53 +737,62 @@ while (!lookupDone) {
706
737
 
707
738
  ## Advanced Usage
708
739
 
709
- ### Custom Client Configuration
740
+ ### Configuring the built-in client
741
+
742
+ The SDK exports a pre-configured `client` that already points at `https://api.fiber.ai`. Every operation uses it by default — you do **not** need to call `createClient` to make requests.
743
+
744
+ If you need to customize headers, swap in a custom `fetch`, or add request/response interceptors, mutate the shared instance:
710
745
 
711
746
  ```typescript
712
- import { createClient } from '@fiberai/sdk';
747
+ import { client } from "@fiberai/sdk";
713
748
 
714
- // Create a custom client
715
- const customClient = createClient({
716
- baseUrl: 'https://api.fiber.ai', // Production URL
717
- // Add custom headers, interceptors, etc.
749
+ // One-time configuration: extra headers, a custom fetch, etc.
750
+ client.setConfig({
751
+ headers: { "X-Trace-Id": "my-app/1.0.0" },
718
752
  });
719
753
 
720
- // Use with any SDK function
721
- import { companySearch } from '@fiberai/sdk';
722
-
723
- const result = await companySearch({
724
- client: customClient,
725
- body: { /* ... */ }
754
+ // Log every outgoing request — handy as a poor man's debug mode.
755
+ client.interceptors.request.use((request) => {
756
+ console.log(`[fiberai] ${request.method} ${request.url}`);
757
+ return request;
726
758
  });
727
759
  ```
728
760
 
761
+ `createClient` from the package is for the rare case where you want a second, independent client (e.g. multi-tenant apps). Pass it via the `client:` option on any operation.
762
+
729
763
  ### Pagination Helper
730
764
 
731
765
  ```typescript
732
- async function* paginateSearch(searchFn, params) {
733
- let cursor = null;
734
-
766
+ import type { CompanySearchData, CompanySearchResponse } from "@fiberai/sdk";
767
+
768
+ async function* paginateCompanies(
769
+ initial: CompanySearchData,
770
+ ): AsyncGenerator<
771
+ NonNullable<CompanySearchResponse["data"]>["output"]["data"]
772
+ > {
773
+ let cursor: string | null | undefined = initial.body.cursor ?? null;
774
+
735
775
  do {
736
- const result = await searchFn({
737
- ...params,
738
- body: {
739
- ...params.body,
740
- cursor
741
- }
776
+ const result = await companySearch({
777
+ ...initial,
778
+ body: { ...initial.body, cursor },
742
779
  });
743
-
744
- yield result.data.output.data.items;
780
+
781
+ if (!result.data) break;
782
+ yield result.data.output.data;
745
783
  cursor = result.data.output.nextCursor;
746
784
  } while (cursor);
747
785
  }
748
786
 
749
787
  // Usage
750
- for await (const companies of paginateSearch(companySearch, {
788
+ for await (const companies of paginateCompanies({
751
789
  body: {
752
- apiKey: process.env.FIBERAI_API_KEY,
753
- searchParams: { /* ... */ },
754
- pageSize: 100
755
- }
790
+ apiKey: process.env.FIBERAI_API_KEY!,
791
+ searchParams: {
792
+ /* ... */
793
+ },
794
+ pageSize: 100,
795
+ },
756
796
  })) {
757
797
  console.log(`Processing batch of ${companies.length} companies`);
758
798
  // Process batch
@@ -769,22 +809,22 @@ import {
769
809
  getIndustries,
770
810
  getTags,
771
811
  getNaicsCodes,
772
- getAccelerators
773
- } from '@fiberai/sdk';
812
+ getAccelerators,
813
+ } from "@fiberai/sdk";
774
814
 
775
815
  // Get available regions for filtering
776
816
  const regions = await getRegions({
777
- query: { apiKey: process.env.FIBERAI_API_KEY }
817
+ query: { apiKey: process.env.FIBERAI_API_KEY },
778
818
  });
779
819
 
780
820
  // Get available industries
781
821
  const industries = await getIndustries({
782
- query: { apiKey: process.env.FIBERAI_API_KEY }
822
+ query: { apiKey: process.env.FIBERAI_API_KEY },
783
823
  });
784
824
 
785
825
  // Get profile and company tags
786
826
  const tags = await getTags({
787
- query: { apiKey: process.env.FIBERAI_API_KEY }
827
+ query: { apiKey: process.env.FIBERAI_API_KEY },
788
828
  });
789
829
  ```
790
830
 
@@ -792,28 +832,44 @@ const tags = await getTags({
792
832
 
793
833
  ### Standard Error Handling
794
834
 
835
+ By default, every operation returns `{ data, error, response }`. Branch on the HTTP status from `response.status` rather than string-matching the error body — error payloads are not guaranteed to contain a parseable `message` for every failure mode.
836
+
795
837
  ```typescript
796
- import { companySearch } from '@fiberai/sdk';
838
+ import { companySearch } from "@fiberai/sdk";
797
839
 
798
840
  const result = await companySearch({
799
841
  body: {
800
- apiKey: process.env.FIBERAI_API_KEY,
801
- searchParams: { /* ... */ }
802
- }
842
+ apiKey: process.env.FIBERAI_API_KEY!,
843
+ searchParams: {
844
+ /* ... */
845
+ },
846
+ },
803
847
  });
804
848
 
805
849
  if (result.error) {
806
- console.error('API Error:', result.error);
807
- // Handle specific error codes
808
- if (result.error.message.includes('401')) {
809
- console.error('Invalid API key');
810
- } else if (result.error.message.includes('402')) {
811
- console.error('Insufficient credits');
812
- } else if (result.error.message.includes('429')) {
813
- console.error('Rate limit exceeded');
850
+ switch (result.response.status) {
851
+ case 400:
852
+ console.error("Bad request:", result.error);
853
+ break;
854
+ case 401:
855
+ console.error("Invalid API key");
856
+ break;
857
+ case 402:
858
+ // 402 errors carry an `outOfCreditsAlert` with a top-up link.
859
+ console.error("Insufficient credits:", result.error);
860
+ break;
861
+ case 429:
862
+ console.error("Rate limit exceeded — back off and retry");
863
+ break;
864
+ default:
865
+ console.error(
866
+ `Request failed (${result.response.status}):`,
867
+ result.error,
868
+ );
814
869
  }
815
870
  } else {
816
- console.log('Success:', result.data);
871
+ console.log("Success:", result.data?.output);
872
+ console.log("Charged:", result.data?.chargeInfo);
817
873
  }
818
874
  ```
819
875
 
@@ -821,86 +877,109 @@ if (result.error) {
821
877
 
822
878
  ```typescript
823
879
  try {
824
- const result = await companySearch<true>({
880
+ const result = await companySearch({
825
881
  body: {
826
- apiKey: process.env.FIBERAI_API_KEY,
827
- searchParams: { /* ... */ }
882
+ apiKey: process.env.FIBERAI_API_KEY!,
883
+ searchParams: {
884
+ /* ... */
885
+ },
828
886
  },
829
- throwOnError: true
887
+ throwOnError: true,
830
888
  });
831
-
832
- // result.data is guaranteed to exist
889
+
890
+ // result.data is guaranteed to exist when throwOnError is true.
833
891
  console.log(result.data.output);
834
892
  } catch (error) {
835
- console.error('Request failed:', error);
893
+ console.error("Request failed:", error);
836
894
  }
837
895
  ```
838
896
 
839
897
  ### Common HTTP Error Codes
840
898
 
841
- | Code | Meaning | Solution |
842
- |------|---------|----------|
843
- | 400 | Bad Request | Check your request parameters |
844
- | 401 | Unauthorized | Verify your API key is valid |
845
- | 402 | Payment Required | Insufficient credits - top up your account |
846
- | 403 | Forbidden | You don't have access to this resource |
847
- | 404 | Not Found | Resource doesn't exist |
848
- | 429 | Too Many Requests | Rate limit exceeded - slow down requests |
849
- | 500 | Internal Server Error | Contact support |
899
+ | Code | Meaning | Solution |
900
+ | ---- | --------------------- | ------------------------------------------ |
901
+ | 400 | Bad Request | Check your request parameters |
902
+ | 401 | Unauthorized | Verify your API key is valid |
903
+ | 402 | Payment Required | Insufficient credits - top up your account |
904
+ | 403 | Forbidden | You don't have access to this resource |
905
+ | 404 | Not Found | Resource doesn't exist |
906
+ | 429 | Too Many Requests | Rate limit exceeded - slow down requests |
907
+ | 500 | Internal Server Error | Contact support |
850
908
 
851
909
  ## TypeScript Support
852
910
 
853
911
  All SDK methods are fully typed with TypeScript.
854
912
 
855
913
  ```typescript
856
- import type {
857
- CompanySearchData,
858
- CompanySearchResponse,
859
- PeopleSearchData,
860
- PeopleSearchResponse,
861
- TriggerContactEnrichmentData,
862
- PollContactEnrichmentResultResponse
863
- } from '@fiberai/sdk';
864
-
865
- // Type-safe request
866
- const searchParams: CompanySearchData = {
914
+ import { companySearch } from "@fiberai/sdk";
915
+
916
+ // SDK functions return `{ data, error, response }`. Annotate the awaited call
917
+ // with `Awaited<ReturnType<typeof fn>>` — the exported `<Op>Response` /
918
+ // `<Op>Errors` types describe the inner body union, NOT the wrapper.
919
+ const result: Awaited<ReturnType<typeof companySearch>> = await companySearch({
867
920
  body: {
868
921
  apiKey: process.env.FIBERAI_API_KEY!,
869
922
  searchParams: {
870
- industriesV2: {
871
- anyOf: ['Software']
872
- },
923
+ industriesV2: { anyOf: ["Software"] },
873
924
  employeeCountV2: {
874
- lowerBoundExclusive: 100,
875
- upperBoundInclusive: 1000
876
- }
925
+ lowerBoundExclusive: 50,
926
+ upperBoundInclusive: 1000,
927
+ },
877
928
  },
878
- pageSize: 25
879
- }
880
- };
929
+ pageSize: 25,
930
+ },
931
+ });
932
+
933
+ if (result.data) {
934
+ console.log(`${result.data.output.data.length} companies`);
935
+ console.log(result.data.chargeInfo);
936
+ }
881
937
 
882
- const result: CompanySearchResponse = await companySearch(searchParams);
938
+ // Need a name for the request shape (e.g. building a request in one place,
939
+ // passing it elsewhere)? Derive it from the function — the bare `<Op>Data`
940
+ // export carries an internal `url` literal and is not meant to be
941
+ // hand-constructed.
942
+ type CompanySearchArgs = Parameters<typeof companySearch>[0];
883
943
  ```
884
944
 
885
- ### Runtime Validation
945
+ > **Naming convention:** every operation `foo` ships companion types:
946
+ > `FooResponse` (200 body union) and `FooErrors` (4xx/5xx body union). Use `Parameters<typeof foo>[0]` for input args and `Awaited<ReturnType<typeof foo>>` for the result envelope. The same applies to `peopleSearch`, `syncQuickContactReveal`, etc.
947
+
948
+ ### Runtime Validation with Zod
886
949
 
887
- The SDK includes Zod schemas for runtime validation. Zod is included automatically with the SDK installation.
950
+ Every request and response shape is also published as a [Zod](https://zod.dev) schema, generated from the same OpenAPI spec as the TypeScript types. They live behind the dedicated `@fiberai/sdk/zod` subpath so apps that don't need runtime validation pay zero bundle cost — the main entry stays under 50 KB while the Zod bundle (~3–6 MB depending on the API surface) is only loaded if you import it.
888
951
 
889
952
  ```typescript
890
- import { z } from 'zod';
891
- import { zCompanySearchData } from '@fiberai/sdk/dist/generated/zod.gen';
953
+ import { zPeopleSearchData, zCompanySearchData } from "@fiberai/sdk/zod";
954
+ import { peopleSearch } from "@fiberai/sdk";
892
955
 
893
- const requestData = {
894
- body: {
895
- apiKey: process.env.FIBERAI_API_KEY,
896
- searchParams: { /* ... */ }
897
- }
898
- };
956
+ const rawInput: unknown = JSON.parse(req.body);
957
+
958
+ const parsed: ReturnType<typeof zPeopleSearchData.parse> =
959
+ zPeopleSearchData.parse(rawInput);
960
+
961
+ const result: Awaited<ReturnType<typeof peopleSearch>> =
962
+ await peopleSearch(parsed);
963
+ ```
964
+
965
+ Use `safeParse` if you want to handle validation failures without throwing. Zod 4 ships top-level error helpers — use `z.flattenError` for form-friendly output or `z.treeifyError` for nested shapes:
966
+
967
+ ```typescript
968
+ import { z } from "zod";
969
+ import { zCompanySearchData } from "@fiberai/sdk/zod";
899
970
 
900
- // Validate at runtime
901
- const validatedData = zCompanySearchData.parse(requestData);
971
+ const parsed: ReturnType<typeof zCompanySearchData.safeParse> =
972
+ zCompanySearchData.safeParse(req.body);
973
+
974
+ if (!parsed.success) {
975
+ return res.status(400).json({ errors: z.flattenError(parsed.error) });
976
+ }
902
977
  ```
903
978
 
979
+ > **Naming convention:** every TypeScript type `Foo` has a matching `zFoo` schema. So `PeopleSearchData` ↔ `zPeopleSearchData`, `CompanySearchResponse` ↔ `zCompanySearchResponse`, etc.
980
+
981
+ `zod` ships as a runtime dependency of `@fiberai/sdk` and is externalized from the bundle (so package managers dedupe with whatever Zod copy your app already has). If you don't import `@fiberai/sdk/zod`, your bundler tree-shakes the schemas out and you never pay for them.
982
+
904
983
  ## Rate Limits & Credits
905
984
 
906
985
  ### Rate Limits
@@ -914,43 +993,44 @@ Each endpoint has its own rate limit. Common limits:
914
993
 
915
994
  ### Credit Costs
916
995
 
917
- **Default pricing** (may vary - check your organization settings):
918
-
919
- | Operation | Cost (credits) |
920
- |-----------|----------------|
921
- | Company search | 1 per company found |
922
- | People search | 1 per profile found |
923
- | Combined search | 1 per company + 1 per profile |
924
- | Work email reveal | 2 |
925
- | Personal email reveal | 2 |
926
- | Phone number reveal | 3 |
927
- | All contact data | 5 |
928
- | Live profile enrichment | 2 |
929
- | Live company enrichment | 2 |
930
- | LinkedIn posts (per page) | 2 |
931
- | Google Maps search | 3 per result |
996
+ Pricing varies by plan and is configured per-organization. **Don't hard-code costs into your app** — every successful response carries a `chargeInfo` envelope alongside `output` that tells you exactly what was charged for that call:
997
+
998
+ ```typescript
999
+ const result = await peopleSearch({
1000
+ /* ... */
1001
+ });
1002
+
1003
+ console.log(result.data?.chargeInfo);
1004
+ // {
1005
+ // method: 'charged-now', // 'charged-now' | 'charging-later' | 'charged-for-async-process' | 'free'
1006
+ // creditsCharged: 25, // present when method !== 'free'
1007
+ // lowCreditAlert: null // populated with a top-up URL when running low
1008
+ // }
1009
+ ```
1010
+
1011
+ For a non-charging dry-run estimate of contact enrichment costs, use [`estimateEnrichmentCost`](https://api.fiber.ai/ai-docs/estimateEnrichmentCost.md). For the per-org pricing breakdown across operations, inspect `getOrgCredits().data.output.creditsPerOperation`.
932
1012
 
933
1013
  ### Free Endpoints
934
1014
 
935
- The following endpoints are **completely free** (no credits charged):
1015
+ The following endpoints never charge credits:
936
1016
 
937
- - `getOrgCredits` - Check credit balance
938
- - `getRegions`, `getLanguages`, `getTimeZones`, `getIndustries`, `getTags`, `getNaicsCodes`, `getAccelerators` - Utility endpoints
939
- - All exclusion list management endpoints
1017
+ - `getOrgCredits` Check credit balance
1018
+ - `getRegions`, `getLanguages`, `getTimeZones`, `getIndustries`, `getTags`, `getNaicsCodes`, `getAccelerators` Utility endpoints
1019
+ - All exclusion-list management endpoints
940
1020
 
941
1021
  ### Check Your Credits
942
1022
 
943
1023
  ```typescript
944
- import { getOrgCredits } from '@fiberai/sdk';
1024
+ import { getOrgCredits } from "@fiberai/sdk";
945
1025
 
946
1026
  const credits = await getOrgCredits({
947
- query: { apiKey: process.env.FIBERAI_API_KEY }
1027
+ query: { apiKey: process.env.FIBERAI_API_KEY! },
948
1028
  });
949
1029
 
950
- console.log(`Available: ${credits.data.output.available}`);
951
- console.log(`Used: ${credits.data.output.used}`);
952
- console.log(`Max: ${credits.data.output.max}`);
953
- console.log(`Resets on: ${credits.data.output.usagePeriodResetsOn}`);
1030
+ console.log(`Available: ${credits.data?.output.available}`);
1031
+ console.log(`Used: ${credits.data?.output.used}`);
1032
+ console.log(`Max: ${credits.data?.output.max}`);
1033
+ console.log(`Resets on: ${credits.data?.output.usagePeriodResetsOn}`);
954
1034
  ```
955
1035
 
956
1036
  ## Support