npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

renidly

v0.3.0

Published

The official Node.js & TypeScript SDK for the Renidly B2B professional data APIs (Data, Live, Email, Account).

Readme

Renidly Node SDK

npm version CI License: MIT Types Node

The official Node/TypeScript SDK for the Renidly B2B professional data APIs — resolve, search, enrich, and verify professional identities (people, organizations, institutions, skills, professional activity, job opportunities, and business email) through one clean, typed client.

import { Renidly } from "renidly";

const renidly = new Renidly("rnd-...");

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);
  • One client, four products — data, live, emails, account, all off the same key.
  • Fully typed — method names, parameters, and returns autocomplete in your editor (ships .d.ts).
  • Batteries included — automatic retries, transparent pagination, batch jobs, a typed error hierarchy, and an optional self-tuning rate limiter.
  • Runs everywhere — native fetch, zero dependencies, ESM and CommonJS, Node 18+ and modern browsers.

Table of contents


Install

npm install renidly

Works with pnpm, yarn, and bun too. Requires Node 18+ (for the global fetch).

Both module systems are supported out of the box:

import { Renidly } from "renidly";        // ESM / TypeScript
const { Renidly } = require("renidly");   // CommonJS

Quickstart

import { Renidly } from "renidly";

const renidly = new Renidly("rnd-...");           // or new Renidly() and set RENIDLY_API_KEY

// Retrieve a single record (resolves to null if nothing matched)
const person = await renidly.data.people.retrieve({ id: "prsn_..." });
if (person) console.log(person.first_name, person.headline);

// Search with any filters — they autocomplete in your editor
for await (const p of renidly.data.people.search({ title: "cto", current_only: true })) {
  console.log(p.headline);
}

// Verify an email
const v = await renidly.emails.verify("[email protected]");
console.log(v.deliverable, v.reason);

Every call is async. The whole surface is renidly.<product>.<resource>.<action>(...).


Authentication

Pass your key to the constructor or through the environment — whichever you prefer.

new Renidly("rnd-...");                            // positional
new Renidly();                                     // reads RENIDLY_API_KEY
new Renidly(undefined, { apiKey: "rnd-..." });     // inside the config object

Per-request override (e.g. multi-tenant apps) — the second argument on any method:

await renidly.data.people.search({ title: "cto" }, { apiKey: "rnd-tenant-key" });

Grab your key from Workspace → API Keys.


The four products

data — clean, queryable records

Deduplicated professional records addressable by stable opaque IDs (prsn_, org_, inst_, skl_) or rich filters.

// People
await renidly.data.people.retrieve({ id: "prsn_..." });                 // or { handle: "..." }
await renidly.data.people.search({ title: "cto", skills: "python", geo_country_code: "US" });
await renidly.data.people.enrichBatch({ handles: [...], ids: [...], live: true });  // bulk (see Batch jobs)

// Companies
await renidly.data.companies.retrieve({ slug: "google" });              // or { id: "org_..." }
await renidly.data.companies.search({ name: "stripe", staff_count_min: 100 });
await renidly.data.companies.employees("google", { title: "engineer", current_only: true });
await renidly.data.companies.enrichBatch({ ids: [...] });               // bulk (see Batch jobs)

// Institutions
await renidly.data.institutions.retrieve("stanford");                   // by normalized name
await renidly.data.institutions.search("stanford");
await renidly.data.institutions.alumni("stanford", { degree: "MBA" });

// Skills
await renidly.data.skills.retrieve("skl_...");
await renidly.data.skills.search("python");

// Job changes — trigger-based prospecting
await renidly.data.jobChanges.search({ event_type: "joined", days_ago: 30 });

live — freshest snapshot on demand

Resolve a single subject or run a discovery search.

// People — resolve a public handle to a stable id once, then reuse it
const eid = (await renidly.live.people.resolveHandle("williamhgates")).entityId;
await renidly.live.people.enrich({ entityId: eid });    // or { handle: "..." }
await renidly.live.people.employmentHistory(eid);
await renidly.live.people.endorsements(eid);
await renidly.live.people.lookalikes(eid);
await renidly.live.people.interests(eid);

// Organizations
const oid = (await renidly.live.organizations.resolveSlug("google")).id;
await renidly.live.organizations.enrich(oid);
await renidly.live.organizations.headcount(oid);
await renidly.live.organizations.similar(oid);
await renidly.live.organizations.affiliated(oid);
await renidly.live.organizations.activities(oid);
await renidly.live.organizations.opportunities("1441,1035");   // comma-separated org ids

// Opportunities (job postings)
await renidly.live.opportunities.retrieve("4019200001");
await renidly.live.opportunities.similar("4019200001");
await renidly.live.opportunities.relatedViews("4019200001");
await renidly.live.opportunities.hiringTeam("4019200001");
await renidly.live.opportunities.byPerson(eid);

// Activity
await renidly.live.activities.feed(eid);
await renidly.live.activities.retrieve(activityId);
await renidly.live.activities.reactions(activityId);
await renidly.live.activities.replies(activityId, { sortBy: "date_posted" });
await renidly.live.activities.repliesByAuthor(eid);

// Discover (search)
await renidly.live.discover.people({ keyword: "cto", count: 25 });
await renidly.live.discover.organizations({ keyword: "fintech", headcountRange: "51-200" });
await renidly.live.discover.opportunities({ keyword: "python", workplaceTypes: "remote" });

emails — verify, find, and resolve

await renidly.emails.verify("[email protected]");
await renidly.emails.find({ firstName: "Patrick", lastName: "Collison", domain: "stripe.com" });
await renidly.emails.findByUrl("https://example.com/in/someone");   // from a professional profile URL
await renidly.emails.reverse("[email protected]");                      // who is behind this business email
await renidly.emails.prospects("acme.com", "verified_only");        // known contacts for a domain

// Bulk (see Batch jobs)
await renidly.emails.verifyBatch(["[email protected]", "[email protected]"]);
await renidly.emails.findBatch([{ firstName: "A", lastName: "B", domain: "acme.com" }]);

account — balance, tier, and pricing

(await renidly.account.balance()).balance;
(await renidly.account.tier()).current_tier.limit_per_minute;
await renidly.account.enterpriseBalance();   // for an Enterprise workspace
await renidly.account.tiers();               // public tier ladder (no key needed)
await renidly.account.routeCosts();          // per-endpoint credit costs (no key needed)

Pagination

Every search/list method returns a value that is both a Promise of the first page and async-iterable — so you can await it for one page, or for await it to walk every page.

// await it → one page (a RenidlyList)
const page = await renidly.data.people.search({ title: "cto", limit: 25 });
page.length;         // items on this page
page.data[0];        // index into this page
page.hasMore;        // is there more?
for (const p of page) { /* synchronous: iterates just this page */ }

// ...or for-await it → walk EVERY page lazily (fetches as it goes, one page in memory)
for await (const person of renidly.data.people.search({ title: "cto" })) {
  console.log(person.headline);
  // break whenever you like — nothing beyond what you consume is fetched
}

// each page is a separate billed request — see its cost/balance on .meta
console.log(page.meta.creditConsumed, page.meta.remainingBalance);

Prefer an explicit async generator? Await the page, then call .autoPagingIter():

const iter = (await renidly.data.people.search({ title: "cto" })).autoPagingIter();
for await (const person of iter) console.log(person.headline);

Batch jobs

Process up to 1000 items in one async job. Submit resolves to a handle instantly.

const job = await renidly.data.people.enrichBatch({
  handles: ["ryanroslansky", "williamhgates"],
  live: true,
});

// block until done and collect everything
const result = await job.wait({ onProgress: (n) => console.log("resolved", n) });
console.log(result.status, result.resolved, "/", result.total);
for (const row of result.results) {
  console.log(row.matched_input, "->", row.headline);
}
console.log("not found:", result.notFound);

// ...or stream results as they resolve
for await (const row of (await renidly.emails.verifyBatch(["[email protected]", "[email protected]"])).stream()) {
  console.log(row.email, row.deliverable);
}

Available on data.people.enrichBatch, data.companies.enrichBatch, emails.verifyBatch, emails.findBatch.


Errors

Every failure rejects with a specific subclass of RenidlyError, and the message tells you exactly what went wrong.

import {
  RenidlyError, AuthenticationError, InvalidRequestError,
  InsufficientCreditsError, NotFoundError, RateLimitError,
  PermissionDeniedError, ServiceUnavailableError,
} from "renidly";

try {
  await renidly.emails.find({ firstName: "A", lastName: "B", domain: "bad" });
} catch (e) {
  if (e instanceof InvalidRequestError) {
    console.log(e.serverMessage);   // "Validation failed"
    console.log(e.fieldErrors);     // { domain: "must be a bare hostname" }
  } else if (e instanceof RateLimitError) {
    console.log(e.tier, e.limit, e.retryAfter);
  } else if (e instanceof RenidlyError) {
    console.log(e.status, e.errorCode, e.serverMessage, e.errors);
  }
}

The .message includes the detail, so an uncaught error is self-explanatory:

InvalidRequestError: Validation failed — domain: must be a bare hostname (VALIDATION_ERROR, HTTP 400)

| Error | When | |---|---| | AuthenticationError | missing / invalid key | | PermissionDeniedError | key valid but not allowed here | | InvalidRequestError | bad input (see .fieldErrors) | | InsufficientCreditsError | not enough credits | | NotFoundError | job not found / expired | | RateLimitError | per-minute limit hit (.tier, .limit, .retryAfter) | | ServiceUnavailableError | temporary — retry shortly | | APIConnectionError | network / timeout |

Not-found lookups: a single retrieve(...) that resolves nothing returns null by default (not a rejection). Set throwOnNotFound: true to throw instead.


Configuration

All options live on the second constructor argument:

const renidly = new Renidly("rnd-...", {
  timeout: 30_000,          // ms
  maxRetries: 3,            // auto-retry on 429 / 503 / connection errors (backoff + jitter)
  unwrapData: true,         // return the data model (false -> the full envelope)
  throwOnNotFound: false,   // single lookups return null when empty (true -> throw)
  throwOnApiError: true,    // map failures to typed errors
  autoRateLimit: false,     // see below
});

| Option | Default | Meaning | |---|---|---| | apiKey | RENIDLY_API_KEY env | Your key (the positional arg overrides this). | | timeout | 30000 | Per-request timeout (ms). | | maxRetries | 2 | Retries on transient failures. | | backoffFactor | 500 | Base ms for exponential backoff. | | baseUrl | https://renidly.com | Override the API host. | | defaultHeaders | {} | Extra headers on every request. | | fetch | global fetch | Bring your own fetch implementation. | | unwrapData | true | Return data vs the full envelope. | | throwOnNotFound | false | null vs NotFoundError on empty lookups. | | throwOnApiError | true | Throw vs return null on API errors. | | autoRateLimit | false | Self-throttle to your tier's limit. | | rateLimitPerMinute | — | Override the limit read from your account. | | rateLimitSafety | 1.0 | Fraction of the limit to target (e.g. 0.9). |


Automatic rate limiting

Turn it on and the SDK keeps you under your per-minute limit automatically — no limiter to build.

// Any key (tiered or enterprise): the limit is read from your account.
new Renidly("rnd-...", { autoRateLimit: true });

// Optional: override it with a fixed limit.
new Renidly("rnd-...", { autoRateLimit: true, rateLimitPerMinute: 550 });

It uses a sliding 60-second window so you never exceed the limit, and re-reads your tier after a 429. If the limit can't be determined, the SDK emits a RenidlyRateLimitWarning and does not throttle client-side (server 429s are still retried) until a later refresh succeeds.


Response objects

Responses are plain, dynamic, drill-able objects — access any field (nested included) directly, no schema classes required.

const t = await renidly.account.tier();
t.current_tier.name;             // nested access, arbitrarily deep

// HTTP metadata is attached to every object under .meta (non-enumerable, so it won't show in JSON.stringify)
t.meta.statusCode;
t.meta.requestId;

Using TypeScript and want a shape for a field? Cast it — responses are intentionally loosely typed so new API fields are always reachable:

const name = (t.current_tier as { name: string }).name;

Prefer the raw envelope? Set unwrapData: false and every call resolves to { success, statusCode, message, data, ... } — which also carries .meta.


Credits & response metadata

Every result carries a non-enumerable meta object describing the HTTP call that produced it — including how many credits it cost and your balance afterward. It's kept off the response data, so person.headline is your data and person.meta.creditConsumed is billing info. Being non-enumerable, meta never shows up in JSON.stringify(person) or Object.keys(person).

const person = await renidly.data.people.retrieve({ id: "prsn_06d0d44d…" });

person.meta.creditConsumed;      // -> 1     credits charged for THIS request
person.meta.remainingBalance;    // -> 19813 balance after the charge
person.meta.statusCode;          // -> 200
person.meta.requestId;           // server request id (if provided)
person.meta.headers;             // raw response headers (record)
person.meta.body;                // parsed JSON envelope
person.meta.rawBody;             // raw response text
person.meta.rawHttp;             // the underlying fetch Response (everything else)

meta is on every result — single objects, list pages, and each item in a page:

const page = await renidly.data.people.search({ title: "cto" });
page.meta.creditConsumed;        // cost of fetching this page
page.data[0].meta.remainingBalance;

Notes:

  • creditConsumed / remainingBalance are undefined for endpoints that aren't credit-billed (e.g. account.*) or when a request wasn't charged (errors, cached hits, zero-result billing).
  • Result-billed endpoints report the real dynamic amount — e.g. emails.prospects("acme.com", { kind: "full" }) returning 18 emails shows meta.creditConsumed === 18.
  • Cached responses are served free: meta.creditConsumed === 0 with the balance unchanged.
  • When walking pages with for await, each page is a separate billed request, so each item reflects its own page's meta.
  • .lastResponse remains as a deprecated alias for .meta.

Advanced

Per-request options override config for a single call:

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" },
});
console.log(env.success, env.data);

Requirements & support

  • Node 18+ (or any runtime with a global fetch).
  • Zero runtime dependencies. ESM + CommonJS, with TypeScript declarations bundled.

Questions or issues? Open one on GitHub.

Contributing

Contributions are welcome and appreciated — bug reports, docs, tests, and features alike. See CONTRIBUTING.md to get set up in a couple of minutes, and please review our Code of Conduct. Found a security issue? See SECURITY.md.

License

MIT © Renidly