Renidlyrenidly
SDKs

Node / TypeScript SDK

4 min read

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 renidly

Works 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 connectionsBuilt in and on by default — transient failures retry themselves.
Pagination loops: read the cursor, fetch, append, repeat, don’t double-fetchawait the search, then .autoPagingIter() walks every page lazily, one page in memory at a time.
Batch orchestration: submit, poll, advance cursors, collect, handle partial completionawait 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 typesA typed error hierarchy — if (e instanceof RateLimitError) e.retryAfter.
A rate limiter tuned to your tier — and retuned when your tier changesautoRateLimit: true — a sliding-window throttle that reads your tier and adapts.
Guesswork: which fields exist, what they’re called, what comes backFully typed (ships .d.ts) — methods, parameters, and returns autocomplete in your editor.
Runtime portability across ESM/CJS, Node, and the browserNative 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:

ProductWhat it isA taste
dataClean, deduplicated records — people, companies, institutions, skills, job changes — by stable ID or rich filters.data.people.search({ title: "cto" })
liveThe freshest snapshot of a single subject on demand — profiles, headcount, activity, opportunities, discovery.live.organizations.headcount(oid)
emailsVerify deliverability, find work emails, reverse-lookup an address, list known contacts for a domain.emails.find({ firstName, lastName, domain })
accountYour 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");   // CommonJS

It 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.