blitz-api-js
v3.0.0
Published
Typed TypeScript SDK for the Blitz API — B2B data, search, and enrichment.
Maintainers
Readme
blitz-api-js
The typed TypeScript SDK for the Blitz API — B2B data, search, and enrichment.
- Fully typed — Zod-validated response models with inferred TypeScript types
and editor autocomplete; ships
.d.ts(ESM) and.d.cts(CJS). - Async,
fetch-based — a singleBlitzAPIclass; every method returns aPromise. Works on Node 20+ and any runtime with a globalfetch. - Resilient — built-in client-side rate limiting, retries with backoff on
429/5xx/network errors, and a typed exception hierarchy. - Forward-compatible — fields the API adds later are preserved, never dropped or rejected.
- 1:1 with the API — request and response field names are snake_case, exactly matching docs.blitz-api.ai.
Create and manage API keys at app.blitz-api.ai.
Billing. Blitz bills per result. A bare
for awaitover a search streams every match up to the server-side limit (people: 50k results), which can be a lot of records. Bound spend withmax_items(a client-side total cap, never sent on the wire) — details in Pagination.
Contents
- Installation
- Quickstart
- Example: find, enrich, collect
- Authentication
- Endpoints
- Pagination
- Configuration
- Usage & rate limit (
fair_usage) - Error handling
- Forward compatibility
- Development
Installation
npm install blitz-api-js
# or: pnpm add blitz-api-js / yarn add blitz-api-js / bun add blitz-api-jsRequires Node.js 20+. Ships both ESM and CommonJS builds.
Quickstart
import { BlitzAPI } from "blitz-api-js";
// api_key defaults to the BLITZ_API_KEY environment variable.
const client = new BlitzAPI();
// Health-check the key before a batch job.
const info = await client.account.key_info();
console.log(info.valid, info.records_remaining, info.max_requests_per_seconds);
// LinkedIn profile URL -> verified work email.
const email = await client.enrichment.email({
person_linkedin_url: "https://www.linkedin.com/in/example-person",
});
if (email.found) console.log(email.email);
// Search people with typed, autocompleted filters (snake_case, 1:1 with the API).
// List methods are paginated; iterate one page's items via `.data` (or stream
// every page with `for await` — see Pagination below).
const page = await client.search.people({
company: { industry: { include: ["Software Development"] } },
people: { job_level: ["VP"] },
max_results: 10,
});
for (const person of page.data) {
console.log(person.full_name, person.headline);
}CommonJS works too:
const { BlitzAPI } = require("blitz-api-js");Example: find, enrich, collect
A complete flow — find people, enrich each one's verified work email, collect the
contacts. max_items caps the total fetched so the run can't surprise you with records.
import { BlitzAPI } from "blitz-api-js";
const client = new BlitzAPI(); // reads BLITZ_API_KEY
// 1. Find up to 25 VPs at software companies (snake_case filters, 1:1 with the API).
const leads = await client.search
.people({
company: { industry: { include: ["Software Development"] } },
people: { job_level: ["VP"] },
max_results: 25,
max_items: 25, // client-side total cap — bounds record spend
})
.collect();
// 2. Enrich each lead's verified work email from their LinkedIn profile URL.
const contacts: { name?: string | null; email?: string | null }[] = [];
for (const person of leads) {
if (!person.linkedin_url) continue;
const result = await client.enrichment.email({
person_linkedin_url: person.linkedin_url,
});
if (result.found) {
contacts.push({ name: person.full_name, email: result.email });
}
}
console.log(`Collected ${contacts.length} contacts`);What comes back is typed and snake_case. A person from the search above (fields are a
superset — only what the profile has is populated, and unknown fields the API adds
later are preserved, typed as unknown):
{
full_name: "Jordan Lee",
headline: "VP of Engineering | @Acme", // derived from the first position
linkedin_url: "https://www.linkedin.com/in/example-person",
location: { city: "San Francisco", state_code: "CA", country_code: "US", continent: "North America" },
experiences: [
{ job_title: "VP of Engineering", company_name: "Acme", job_is_current: true },
],
// first_name, last_name, skills, education, certifications, … also present
}experiences[] on a search result carries the position that matched your query —
in practice a single entry — not the person's career. (The API reference page still
describes the old full-history behaviour; it is out of date, per the 2026-09-21 changelog
entry.) Write your code for one position and don't index past it. If you need the whole
career in profile order, client.enrichment.person({ person_linkedin_url }) returns the
same person shape with every position held (1 record on success, free on a miss):
const { found, person } = await client.enrichment.person({
person_linkedin_url: "https://www.linkedin.com/in/example-person",
});And enrichment.email(...) returns:
{
found: true,
email: "[email protected]",
all_emails: [{ email: "[email protected]", email_domain: "acme.com" }],
}Authentication
Pass the key explicitly or via the BLITZ_API_KEY environment variable:
const explicit = new BlitzAPI({ api_key: "sk_..." }); // explicit
const fromEnv = new BlitzAPI(); // reads BLITZ_API_KEYThe key is sent in the x-api-key header. Never expose it in client-side code —
always call the API from your backend.
Endpoints
All methods are grouped into seven namespaces:
| Namespace | Methods |
| --- | --- |
| client.account | key_info() |
| client.search | people(), companies(), employee_finder(), waterfall_icp() |
| client.jobs | search(), company() |
| client.company | tam_by_jobs(), tam_by_people() |
| client.enrichment | person(), email(), phone(), email_to_person(), phone_to_person(), company(), domain_to_linkedin(), linkedin_to_domain(), company_distribution_by_country(), company_distribution_by_department() |
| client.utils | current_date() |
| client.changelog | list() |
Each method takes a single options object (snake_case keys) and returns a typed,
Zod-validated response (also snake_case). Enum-backed filter fields (e.g.
industry, job_level, continent) accept either a known value — autocompleted
from a union like Industry — or any raw string, so a value missing from the
vendored taxonomy never blocks you.
import { INDUSTRY } from "blitz-api-js"; // the full value array (535 industries)
import type { CompanyFilter, Industry } from "blitz-api-js";The seven list methods — search.people, search.companies, search.employee_finder,
jobs.search, jobs.company, company.tam_by_jobs, company.tam_by_people — return a
paginated PagePromise instead of a plain response (see Pagination).
waterfall_icp, changelog.list, and the enrichment/utils/account methods return
their response directly.
The two client.company methods build a Total Addressable Market of companies, each
result a company plus a match count (1 record per result): tam_by_jobs sizes accounts
on who they are hiring, tam_by_people on who already works there.
client.changelog.list returns the public API changelog (no records, no key
required, not paginated):
// TAM: companies hiring for a role, with ≥3 matching postings each.
for await (const match of client.company.tam_by_jobs({
job: { title: { include: ["Account Executive"] }, min_per_company: 3 },
company: { industry: { include: ["Software Development"] } },
max_results: 50,
max_items: 200,
})) {
console.log(match.company?.name, match.matched_jobs);
}
// TAM: companies that already employ ≥3 of your buyer persona.
for await (const match of client.company.tam_by_people({
people: { job_title: { include: ["Head of Sales"] }, min_per_company: 3 },
company: { industry: { include: ["Software Development"] } },
max_results: 50,
max_items: 200,
})) {
console.log(match.company?.name, match.matched_people);
}
// Public changelog, last 30 days.
const entries = await client.changelog.list({ days: 30 });
for (const e of entries) console.log(e.date, e.type, e.title);Pagination
search.people, search.companies, jobs.search, jobs.company,
company.tam_by_jobs and company.tam_by_people are cursor-paginated;
search.employee_finder is
page-paginated. Each returns a PagePromise you can either await for the first
page or for await to stream every item across all pages — each page is fetched on
demand, through the client's rate limiter.
max_resultsis the page size, not a total. It's "results per page" (1–50), and the API bills 1 record per result returned. A barefor awaitstreams every match up to the server-side limit (people: 50k results / 1k pages; employee finder: 10k; jobs: 5k), which can be a lot of records. To bound it, passmax_items(a client-side total cap — never sent on the wire),breakout of the loop, or drive pages manually.
// Stream up to max_items results across pages, then stop fetching:
for await (const person of client.search.people({ company: {/* … */}, max_results: 50, max_items: 200 })) {
console.log(person.full_name);
}
// Or collect into an array (also honors max_items):
const people = await client.search.people({ max_results: 50, max_items: 200 }).collect();
// Or take the first page and drive pagination manually (uncapped — your loop, your call):
const page = await client.search.companies({ max_results: 25 });
page.data; // Company[] — items on this page
page.response.total_results; // the full parsed response (snake_case, 1:1 with the API)
if (page.has_next_page()) {
const next = await page.get_next_page();
}
// Or iterate page-by-page:
const first = await client.search.employee_finder({ company_linkedin_url: "…", max_results: 50 });
for await (const p of first.iter_pages()) {
console.log(`page ${p.response.page}/${p.response.total_pages} — ${p.data.length} items`);
}
// Jobs are cursor-paginated too — surface hiring signals matching your ICP:
for await (const job of client.jobs.search({
job: { title: { include: ["Head of Sales"] }, date_posted: { last_days: 30 } },
company: { industry: { include: ["Software Development"] }, size: { include: ["51-200"] } },
max_results: 50,
max_items: 200,
})) {
console.log(job.company_name, job.title, job.location?.city);
}max_items caps how many items for await / collect() yield and stops fetching once
reached; it bounds the total fetched to within one page (`≈ ceil(max_items / max_results)
- max_results
), so tunemax_resultstoo for tight spend control. Auto-pagination otherwise stops when the API signals the end (cursor: null, orpagebeyondtotal_pages).search.waterfall_icpreturns a single ranked result set, andchangelog.list` returns a plain array — neither is paginated.
Configuration
const client = new BlitzAPI({
api_key: undefined, // falls back to BLITZ_API_KEY
base_url: "https://api.blitz-api.ai",
timeout: 30, // default per-request timeout, seconds (via AbortSignal.timeout)
max_retries: 3, // retries on 429 / 5xx / pre-response network errors
rate_limit_rps: 5, // per-endpoint client-side token bucket; null to disable
fetch: undefined, // custom fetch implementation (tests / runtimes)
});
// Override the timeout for a single call — pass an options object as the last
// argument to any method (it never appears on the wire):
await client.enrichment.email({ person_linkedin_url: "…" }, { timeout: 5 });The client-side rate limiter is a token bucket applied per endpoint, mirroring the API,
whose limit is itself per endpoint (10 req/s on each endpoint independently — e.g.
enrichment.email and enrichment.phone get separate budgets; check yours via
(await client.account.key_info()).max_requests_per_seconds). Each endpoint gets its own
bucket that admits at most rate_limit_rps requests per second, so a single client instance
stays under the limit on every endpoint, and a burst on one never throttles another. The
default 5 deliberately sits at half the cap — raise it to your key's
max_requests_per_seconds to use the full budget. Across multiple processes you may still
hit 429 — the retry path handles that.
Usage & rate limit (fair_usage)
Every /v2 response carries a fair_usage block reporting what the request cost
and what is left, so you can meter a batch job without a second key_info() call:
const email = await client.enrichment.email({ person_linkedin_url: url });
const usage = email.fair_usage;
console.log(usage?.records_used); // records this request consumed
console.log(usage?.records_remaining); // number, or "unlimited"
console.log(usage?.next_reset_at); // null on an unlimited plan
console.log(usage?.rate_limit?.remaining_this_second);
console.log(usage?.request_id); // quote this id to supportOn a paginated method the block belongs to each page's raw body, so read it off
.response:
const page = await client.search.people({ company: { /* … */ }, max_results: 50 });
console.log(page.response.fair_usage?.records_used);rate_limit is absent on account.key_info(), the one endpoint that is not rate
limited. Every field is optional, so a response from a deployment that predates
the block still parses.
Error handling
import {
APIConnectionError,
APIResponseValidationError,
APIStatusError,
APITimeoutError,
AuthenticationError,
BlitzError,
InsufficientRecordsError,
NotFoundError,
RateLimitError,
ServerError,
} from "blitz-api-js";
try {
await client.enrichment.email({ person_linkedin_url: "..." });
} catch (err) {
if (err instanceof InsufficientRecordsError) {
// 402 — Fair Use record limit reached
} else if (err instanceof AuthenticationError) {
// 401 — bad key
} else if (err instanceof APIResponseValidationError) {
// 2xx, but the body wasn't valid JSON or didn't match the schema (err.cause has details)
} else if (err instanceof APIStatusError) {
console.log(err.status_code, err.message, err.body, err.request_id);
} else if (err instanceof BlitzError) {
// base class for everything this SDK raises
}
}The 402 class is InsufficientRecordsError as of 3.0.0, matching blitz-api-py;
FairUsageLimitError stays exported as a deprecated alias for the same class (so
instanceof keeps working) and goes in 4.0.0. One catch: err.name is
"InsufficientRecordsError" from 3.0.0 on, so if you compare that string rather than
using instanceof, update it now. The older InsufficientCreditsError alias, deprecated
in 2.0.0, is removed in this release.
429 and 5xx are retried automatically (with backoff + jitter) up to
max_retries; 401/402/404 throw immediately. A pre-response network
error (DNS failure, connection refused) is retried, then surfaces as
APIConnectionError. Timeouts are not retried — with fetch we can't tell
whether the request already reached the (per-result-billed) server, so a timeout
surfaces immediately as APITimeoutError rather than risk a double charge. Raise
the per-call timeout for genuinely slow endpoints instead.
Forward compatibility
Response objects keep their snake_case wire keys and preserve unknown fields —
if the API adds a property before this SDK models it, the value is still present on
the parsed object (typed as unknown). Known fields stay precisely typed.
Development
See CONTRIBUTING.md for local setup, the test/type/lint commands, the enum code generator, and the automated release process.
