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

@orisift/sdk

v1.3.0

Published

Official TypeScript SDK for Orisift contact-data validation. Server-side only.

Readme

@orisift/sdk

Official TypeScript SDK for Orisift. Server-side only.

No runtime dependencies.

Build on Node 22 LTS or later. That is the current supported line. The package installs on Node 20 and later so an existing integration is not locked out by an upgrade, but Node 20 reached end of life in March 2026 and receives no security fixes, so it is a compatibility floor rather than a recommendation. Continuous integration builds and exercises the package on Node 20 and Node 22, so the floor is tested rather than assumed.

Both module systems work, from the same file:

import { Orisift } from "@orisift/sdk";     // any supported Node
const { Orisift } = require("@orisift/sdk"); // Node 20.19+ or 22.12+

require needs a Node where require(esm) is unflagged, which is 20.19 and 22.12 onwards. import works on every Node this package supports. The floor in engines is the lower of the two, because refusing to install for someone whose import works perfectly would be the wrong trade.

Getting a key

  1. Create an account at orisift.com/signup. A new account starts with free credits, and no card is required to try it.
  2. Create a key in the dashboard. It is shown once and only a hash is stored, so a lost key is rotated rather than recovered.
  3. Keep it server-side. Anyone holding the key can spend the account's credits.

The full API reference, including the response shape, the reason-code catalogue and the coverage limits, is at orisift.com/docs. The section covering this package is at orisift.com/docs#sdk.

Quick start

npm install @orisift/sdk
import { Orisift } from "@orisift/sdk";

const orisift = new Orisift({ apiKey: process.env.ORISIFT_API_KEY! });

const result = await orisift.validate({
  input: "[email protected]",
  policy: "contact_standard",
  idempotencyKey: `${submissionId}:email`,
});

switch (result.action.type) {
  case "continue":       return accept();
  case "ask_to_correct": return askToFix(result.action.reasons);
  case "verify_contact": return acceptPendingVerification();
  case "review":         return acceptAndFlag();
  case "retry_later":    return acceptAndRecheckLater();
}

That switch is exhaustive against ActionType. If a new action is ever added, your code fails to compile rather than falling through silently.

What a continue means, and what it does not

continue means Orisift did not establish a blocking structural or infrastructural issue under the policy you chose.

It does not mean the person is real, the mailbox exists, the phone is active, the contact is reachable, the message will be delivered, or the transaction is safe. Orisift establishes structural and infrastructural evidence. It does not establish existence, ownership or activity of any identifier, and action.not_established lists what it did not check.

verify_contact is a prompt to run your own verification. It does not mean Orisift verified anything.

Branch on action.type, not on verdict

verdict is a human-facing summary and verified there means "no blocking evidence", not the industry sense of a confirmed mailbox. The action vocabulary has no word that can be misread that way, so use it for control flow.

The two are allowed to disagree. A listed disposable domain comes back with verdict: "rejected" and action.type: "review", because the disposable list has known gaps and that is not strong enough to turn a person away.

Errors are about the call, not the identifier

validate() resolves for every lookup outcome, including rejected and ask_to_correct. Those are answers. It rejects only when no usable answer was obtained.

| Class | When | |---|---| | OrisiftConfigError | no API key. Thrown before any request | | OrisiftBrowserError | constructed in a browser | | OrisiftAuthError | 401 | | OrisiftInvalidRequestError | 400, with .param | | OrisiftInsufficientCreditsError | 402, with .balance and .required | | OrisiftIdempotencyConflictError | 409, key reused for a different request | | OrisiftUnrecognisedInputError | 422 | | OrisiftRateLimitError | 429, with .retryAfterSeconds | | OrisiftMalformedActionError | 200, action absent or unusable | | OrisiftServerError | 5xx | | OrisiftTimeoutError | no response in the budget | | OrisiftConnectionError | Orisift was never reached | | OrisiftAbortedError | your AbortSignal fired |

Every error carries requestId. Quote it when asking for help.

A capability Orisift does not offer is never an error. It is action.not_established in a successful response. Evidence that could not be gathered is never an error either. It is action.type === "retry_later". You never have to read an exception to learn something about an identifier.

OrisiftMalformedActionError deserves a note

If Orisift cannot decide an action it returns the response without one rather than guessing. The SDK then throws rather than substituting continue, because a fabricated green light is the one failure you have no way to detect. An action type this SDK version does not recognise throws the same way.

Handle it as you would a 5xx: do not proceed as though the check passed.

Retries

Transport retries are automatic, bounded and invisible.

| Condition | Retried | |---|---| | 429 | yes, honouring Retry-After | | 500, 502, 503, 504 | yes, exponential backoff with full jitter | | network error, timeout | yes | | 400, 401, 402, 409, 422 | no | | 200 with any action | no |

Bounded by maxRetries (default 2) and maxTotalMs (default 30000), which caps total wall time including backoff. An AbortSignal cancels immediately, mid-backoff included.

Every transport retry reuses the same idempotency key, which is what makes automatic retry safe: the server replays rather than charging again.

retry_later is different. It is an application decision about evidence, not a transport failure, and the SDK never acts on it for you.

A later recheck is a new lookup. It needs a new idempotency key and costs another credit. Reusing the original key would replay the original incomplete result, and a changed request under the same key is a 409.

Queue the recheck with a backoff. Do not loop.

Idempotency

A key is generated per call when you do not supply one, so retries are safe by default.

Supply your own to make a user-visible operation idempotent, for example against a double-clicked form. Two rules:

  • A retry of the same operation keeps its key.
  • A different request gets a new key. A corrected field is a different request.

Validating an email and a phone in one form submission are two different requests and need two different keys:

const email = await orisift.validate({ input: form.email, policy: "contact_standard",
                                       idempotencyKey: `${submissionId}:email` });
const phone = await orisift.validate({ input: form.phone, policy: "contact_standard",
                                       idempotencyKey: `${submissionId}:phone` });

Reusing one key for a different request returns 409 idempotency_key_reused, not a replay. Orisift will not guess which request you meant.

Policy versions

policy is required by validate(). policyVersion is optional.

  • Omitted means the latest version at the time of the request. The integer it resolved to always comes back in action.policy_version, so a response never leaves the version ambiguous.
  • Supplied pins that version.
  • An unknown version is a 400 naming the supported versions. It never falls back.

Pin a version if you have written logic against a specific rule set.

Security

The API key is a bearer credential for your whole account. Keep it server-side. There is no publishable or restricted key.

The SDK refuses to construct in a browser, declares "browser": false, and exports no browser condition, so a bundler resolving for the browser fails at build time. Those are guards against a mistake, not a security boundary: the boundary is that the key never leaves your server.

The API also refuses cross-origin preflight, which stops a page calling it directly. This does not protect a leaked key. A leaked key works from curl, a script, or any server anywhere. If a key is exposed, rotate it in the dashboard immediately.

Logging

Nothing is logged by default. The optional hooks receive an explicit allowlist:

new Orisift({
  apiKey: process.env.ORISIFT_API_KEY!,
  onResponse: (e) => logger.info("orisift", e),
});
interface ResponseEvent {
  requestId: string | null; status: number; latencyMs: number; attempt: number;
  action?: ActionType; reasonCodes?: ReasonCode[]; policyVersion?: number;
}

The identifier is not a field, so logging the whole event cannot leak it. Reason codes are included because they describe Orisift's evidence in a closed vocabulary. Reason messages are excluded because they can quote part of what was submitted.

If you log elsewhere in your own code, do not log input, normalized or action.reasons[].message.

Compatibility

The SDK targets api_version: "1". Additive response fields appear without a version bump and the SDK ignores what it does not know. Removals or meaning changes would ship as /v2 and a new major version of this package.

X-Orisift-Client carries the SDK version on every request, so deprecations can be planned against real usage rather than guesswork.

Changes

1.3.0 — read this before upgrading

Some configurations that 1.2.0 accepted now throw at construction. If your handler is built with one of them, your process will fail to start where it previously started. That is deliberate, and a minor version number does not by itself make it safe to upgrade unattended — check the list below first.

The configurations now rejected:

| configuration | why it is rejected | |---|---| | dedupe: {mode: "attempt"} with no attemptId, or a non-function attemptId | attemptId is a required member of that mode. Without it there is no attempt identity to deduplicate on. | | dedupe: {mode: "shared"} with no store, or a store without claim() | store is a required member of that mode. | | dedupe: {mode: <anything else>} | not one of the four documented modes. |

None of these are a new requirement. Each member was already required by DedupePolicy in the type definitions shipped with 1.2.0, and a TypeScript build has always rejected them. What changed is that the requirement is now enforced at runtime too, so a JavaScript caller gets the same answer as a TypeScript one. If your code type-checks against 1.2.0, nothing here can affect you.

Why it was worth a breaking change. Previously these configurations constructed and then failed on every request into the onUnavailable path. With the common onUnavailable: "allow", that meant every signup was allowed with nothing screened and nothing charged, returning HTTP 200 in about 12ms. It is indistinguishable from a healthy screened allow unless you had registered onUnavailableEvent. Measured on 1.2.0 while writing an integration from scratch. Failing at startup is strictly better than running for months in a state where the thing you are paying for is not happening.

Also new: an allowed-but-unscreened signup is no longer silent. When onUnavailable is "allow" and no onUnavailableEvent is registered, a warning goes to console.warn naming the reason, at most once per reason per minute.

  • The response to Supabase is unchanged. Your explicit outage choice is preserved: allow still allows, deny still denies.
  • Nothing operational reaches the end user. The warning is server-side only, and the response body carries no reason, no vendor name and no detail.
  • The line contains no identifiers, credentials or payload: no API key, hook secret, email address, user id, webhook id or request body.
  • Register onUnavailableEvent and the warning stops, because then you are already being told.

Upgrading: if you use TypeScript, nothing to do. If you use plain JavaScript, check your dedupe option against the table above before deploying.

1.1.2

Fixed: a 429 carrying a Retry-After longer than the client's remaining deadline now raises straight away instead of sleeping until the deadline expires.

Orisift's daily unbilled-lookup ceiling clears at 00:00 UTC, so it can ask you to wait tens of thousands of seconds. Earlier versions slept for whatever time was left in maxTotalMs, 30 seconds by default, and then raised anyway, because a retry issued at the deadline has no time left to run. The wait could never succeed; it was pure latency in front of an error you were going to get.

The error is unchanged and still carries retryAfterSeconds, so you can schedule a retry for after the reset rather than guess.

Short delays are unaffected. An ordinary rate_limited asking for a couple of seconds still retries exactly as before, and cancellation still wins over any wait.

1.1.1

Retry-After is read into OrisiftRateLimitError.retryAfterSeconds, from the header or the JSON body. The idempotency contract in the docs was corrected to match the service: a key reused for a different request is refused with 409, never answered with the earlier result.