Node / TypeScript SDK
One typed client for everything Renidly does — resolve, search, enrich, verify, and check your balance — with the production plumbing already written: retries, pagination, batch jobs, typed errors, and rate limiting. If you're integrating from Node or the browser, this is the fastest correct path.
npm i renidlyWorks with pnpm, yarn, and bun too. Requires Node 18+ (for the global fetch) — or any runtime that has one, the browser included. Zero runtime dependencies, ships ESM and CommonJS with bundled TypeScript declarations. Source on GitHub, releases on npm, MIT licensed.
import { Renidly } from "renidly";
const renidly = new Renidly("rnd-..."); // or new Renidly() and set RENIDLY_API_KEY
const person = await renidly.data.people.retrieve({ handle: "ryanroslansky" });
const company = await renidly.data.companies.retrieve({ slug: "stripe" });
const email = await renidly.emails.verify("[email protected]");
console.log(person.headline, company.name, email.deliverable);Why not just call the API yourself?
You can — the REST surface is one header and one envelope, and it will always work with any HTTP client. But a correct production integration is never just the happy-path request. It's everything around it, and every team hand-rolling a client rebuilds the same stack of plumbing — then maintains it forever:
| With your own fetch calls, you write… | With the SDK… |
|---|---|
| Retry logic with backoff + jitter for rate limits, transient outages, and dropped connections | Built in and on by default — transient failures retry themselves. |
| Pagination loops: read the cursor, fetch, append, repeat, don’t double-fetch | await the search, then .autoPagingIter() walks every page lazily, one page in memory at a time. |
| Batch orchestration: submit, poll, advance cursors, collect, handle partial completion | await job.wait() blocks to completion; job.stream() yields results as they resolve. |
| Error handling: parse the envelope, branch on codes, map to your own error types | A typed error hierarchy — if (e instanceof RateLimitError) e.retryAfter. |
| A rate limiter tuned to your tier — and retuned when your tier changes | autoRateLimit: true — a sliding-window throttle that reads your tier and adapts. |
| Guesswork: which fields exist, what they’re called, what comes back | Fully typed (ships .d.ts) — methods, parameters, and returns autocomplete in your editor. |
| Runtime portability across ESM/CJS, Node, and the browser | Native fetch, zero dependencies, ESM + CommonJS, Node 18+ and modern browsers. |
One client, four products
The whole surface reads as renidly.<product>.<resource>.<action>(...) — if you can say what you want, you can usually type it. Every call is async:
| Product | What it is | A taste |
|---|---|---|
data | Clean, deduplicated records — people, companies, institutions, skills, job changes — by stable ID or rich filters. | data.people.search({ title: "cto" }) |
live | The freshest snapshot of a single subject on demand — profiles, headcount, activity, opportunities, discovery. | live.organizations.headcount(oid) |
emails | Verify deliverability, find work emails, reverse-lookup an address, list known contacts for a domain. | emails.find({ firstName, lastName, domain }) |
account | Your balance, tier, rate limit, and per-endpoint costs — readable at runtime, so nothing is hardcoded. | account.balance() |
All four authenticate with the same key and bill from the same credit balance — one client object covers your whole integration.
The plumbing, in practice
Pagination disappears
A search resolves to a RenidlyList. Call .autoPagingIter() on it to walk every page lazily, fetching one page at a time:
// Walks EVERY page lazily — fetches as it goes, one page in memory at a time.
const results = await renidly.data.people.search({ title: "cto" });
for await (const person of results.autoPagingIter()) {
console.log(person.headline);
}Batch jobs are two lines
Up to 1,000 items per job. Submitting resolves to a handle instantly; you choose whether to block or stream:
const job = await renidly.emails.verifyBatch(["[email protected]", "[email protected]"]);
const result = await job.wait(); // block until done, collect everything
// ...or stream verdicts the moment each one lands:
for await (const row of job.stream()) {
console.log(row.email, row.deliverable);
}Errors tell you what to do
Every failure rejects with a specific subclass of RenidlyError carrying the machine-usable detail — no envelope parsing, no string matching:
import { InvalidRequestError, RateLimitError } from "renidly";
try {
await renidly.emails.find({ firstName: "A", lastName: "B", domain: "bad" });
} catch (e) {
if (e instanceof InvalidRequestError) {
console.log(e.fieldErrors); // { domain: "must be a bare hostname" }
} else if (e instanceof RateLimitError) {
await sleep(e.retryAfter); // the wait is handed to you
}
}A lookup that simply finds nothing isn't an error — single retrieve(...) calls resolve to nullby default, so "not found" stays a branch, not a try/catch. Set throwOnNotFound: true to throw instead.
Rate limiting you don't have to build
const renidly = new Renidly("rnd-...", { autoRateLimit: true });The SDK reads your tier's per-minute limit, throttles itself under it with a sliding 60-second window, and re-reads the tier if a limit response ever slips through. Upgrade your tier and the throttle widens on its own — nothing to redeploy. See Rate Limits & Credits for how tiers work.
ESM and CommonJS, out of the box
import { Renidly } from "renidly"; // ESM / TypeScript
const { Renidly } = require("renidly"); // CommonJSIt never boxes you in
Convenience shouldn't cost you control. Every default is a switch on the second constructor argument: timeout, maxRetries, throwOnNotFound, and whether you get the unwrapped model or the full response envelope (unwrapData). A second argument on any method overrides those for a single call — handy for multi-tenant apps passing a different key per request:
await renidly.data.skills.search("python", { timeout: 5_000, apiKey: "rnd-other" });
// escape hatch — call any endpoint directly:
const env = await renidly.rawRequest("GET", "/people/search", {
service: "data",
params: { title: "cto" },
});Need full control of the transport? Pass your own fetch implementation via the fetch config option and the SDK uses it as-is.