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

@xident/node

v3.2.0

Published

Official Xident Node.js SDK for age and identity verification

Readme

@xident/node

Official Node.js SDK for Xident age and identity verification. Try it live at demo.xident.io.

Zero runtime dependencies. TypeScript strict. Native fetch (Node 18+). Dual ESM+CJS.

Installation

npm install @xident/node

v2.0.0 — Breaking Changes

SessionResult (the return value of verification.getResult(), and the shape of every webhook's data field) moved to the API's frozen v1 result contract. The old blob-style fields are gone, replaced by a typed checks object:

| Removed | Replaced by | |---|---| | result.livenessResult (raw object) | result.checks.liveness.performed / .passed | | result.ageResult (raw object) | result.checks.age.performed / .passed / .gate | | result.ocrResult (raw object) | result.checks.document.performed / .passed / .documentType / .country | | result.faceMatchResult (raw object) | result.checks.faceMatch.performed / .passed | | result.ocrTaskId | (removed — was never part of the frozen contract) | | result.countryCode | result.checks.document.country | | result.regime | (removed — was never part of the frozen contract) | | result.requiredMethods | (removed — was never part of the frozen contract) | | result.remainingAttempts | (removed — was never part of the frozen contract) |

New fields: result.verified (server-derived mirror of status === "success" — prefer isVerified()), result.verificationType, result.externalUserId, result.completedAt.

ageBracket() and method() keep their signatures but now read from checksageBracket() returns checks.age.gate only when checks.age.passed is true, null otherwise; method() returns verificationType. isVerified() is unchanged: still status === "success".

Webhook event names were already session.success / session.failed / session.canceled server-side; this release just fixes the SDK's own docs/examples, which still showed the retired verification.completed / verification.failed names from before that rename.

This SDK still tolerates payloads from a deployment predating this change — fromObject() never throws on a missing checks object, it falls back to an all-performed: false zero value.

Quick Start

import { Xident } from '@xident/node';

const xident = new Xident('sk_live_your_key_here');

// 1. Create a verification session, then redirect the browser to init.verifyUrl
const init = await xident.verification.init({
  callback_url: 'https://your-site.com/verify/callback', // a GET route you own
  min_age: 18,
  user_id: 'user_12345',
});

console.log('Redirect user to:', init.verifyUrl);
// init.token is the xit_ init token — used only to build verifyUrl. Do NOT
// pass it to getResult().

// 2. The widget redirects the browser BACK to your callback_url with query
//    params: ?status=success|failed|canceled&token=xtk_...&user_id=...
//    In that GET route, read the xtk_ RESULT token and verify it server-side:
const result = await xident.verification.getResult(resultTokenFromQuery); // xtk_...

if (result.isVerified()) {
  console.log('User verified! Age bracket:', result.ageBracket());
}

Two different tokens. init.token (xit_…) is a one-time, 10-minute init token that only builds the redirect URL. The result token (xtk_…) arrives as the token query parameter on your callback_url — that is the one you pass to getResult(). Never reuse the init token for the result lookup.

Configuration

const xident = new Xident('sk_live_xxx', {
  baseUrl: 'https://api.xident.io', // default
  timeout: 30_000,                   // 30 seconds (default)
  maxRetries: 3,                     // retries on 5xx errors (default)
  headers: { 'X-Custom': 'value' },  // extra headers
});

Verification

verification.init(params)

Create an init token for starting a verification session.

const init = await xident.verification.init({
  callback_url: 'https://your-site.com/verify/callback', // required, HTTPS (or http://localhost in dev)
  min_age: 18,             // 1–99 (0–99 when purpose is "id_verification")
  user_id: 'user_123',    // your internal user ID (echoed back on the callback)
  theme: 'system',        // "light" | "dark" | "system" (default "system")
  locale: 'de',           // widget language
  purpose: 'age_verification', // or "id_verification" (allows min_age: 0)
  liveness_difficulty: 'medium', // optional: "easy" | "medium" | "hard"
});

// init.token     — short-lived xit_ init token (one-time, 10 min); only used to build verifyUrl
// init.verifyUrl — redirect the browser here (verify.xident.io?t=<xit_...>)

Handling the callback

The widget redirects the browser back to your callback_url with query parameters (a plain GET redirect — this is NOT the webhook):

| Param | Value | |---|---| | status | success | failed | canceled | | token | the xtk_… result token (different from the xit_ init token) | | user_id | echoed back if you supplied one |

Read the token and verify it server-side. Treat the status param as a hint only — getResult() is the source of truth.

// Express: GET route registered at your callback_url
app.get('/verify/callback', async (req, res) => {
  const { status, token, user_id } = req.query;

  if (status === 'canceled') return res.redirect('/verify/canceled');

  const result = await xident.verification.getResult(String(token)); // xtk_...
  if (result.isVerified()) {
    // mark user_id as verified in your database
    return res.redirect('/verified');
  }
  return res.redirect('/failed');
});

verification.getResult(token)

Pass the xtk_ result token from the callback query string (never the xit_ init token). Always verify server-side; never trust the status query parameter alone.

const result = await xident.verification.getResult(resultToken); // xtk_...

result.token             // the xtk_ result token
result.isVerified()      // true if verification succeeded
result.isFailed()        // true if verification failed
result.isPending()       // true if still in progress
result.isTerminal()      // true if no more changes possible
result.ageBracket()      // 12, 15, 18, 21, or 25 — only when the age check passed, else null
result.method()          // "full" | "age_check" | "xident_id" | "eu_wallet" (or null)
result.reason            // e.g. "age_below_threshold" (empty string on success)
result.externalUserId    // your user_id, if you supplied one (or null)
result.checks.liveness   // { performed, passed }
result.checks.age        // { performed, passed, gate }  — gate is the age threshold tested against
result.checks.document   // { performed, passed, documentType, country }
result.checks.faceMatch  // { performed, passed }
result.createdAt         // RFC 3339
result.completedAt       // RFC 3339, or null while not yet terminal
result.expiresAt         // RFC 3339, or null
result.status            // "pending" | "in_progress" | "success" | "failed" | "canceled" | "claimed"

Webhooks (optional)

Webhooks are a separate, server-to-server channel from the browser callback above. Use them for out-of-band notifications; the browser callback + getResult() remains the primary flow.

Verify webhook signatures using HMAC-SHA256 with constant-time comparison.

// Express.js example — use raw body!
app.post('/webhooks/xident', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = xident.webhooks.constructEvent(
      req.body.toString(),
      req.headers['x-xident-signature'] as string,
      process.env.XIDENT_WEBHOOK_SECRET!,
    );

    switch (event.type) {
      case 'session.success':
        // Handle success
        break;
      case 'session.failed':
        // Handle failure
        break;
      case 'session.canceled':
        // Handle cancellation
        break;
    }

    res.json({ received: true });
  } catch (err) {
    res.status(400).json({ error: 'Invalid webhook' });
  }
});

webhooks.constructEvent(payload, signature, secret, tolerance?)

Verify signature and parse the event in one call.

webhooks.verifySignature(payload, signature, secret, tolerance?)

Verify only the signature (returns true or throws ValidationError).

Face 2FA

Standalone face-based second factor for your users. Both mutating calls are async: they return a challenge in processing status and you poll getStatus() for the pass/fail verdict. The API returns pass/fail only — never confidence scores or biometric data.

// 1. Enroll a face for one of your users (free of charge)
const enroll = await xident.face2fa.register({
  user_id: 'user_123',          // YOUR identifier, opaque to Xident
  image: base64Selfie,          // base64-encoded face image
});

// 2. Later, at login: verify a fresh selfie against the enrolled face
const challenge = await xident.face2fa.verify({
  user_id: 'user_123',
  image: base64LoginSelfie,
});

// 3. Poll for the verdict
const status = await xident.face2fa.getStatus(challenge.challengeId);

status.isProcessing() // still working — keep polling
status.isTerminal()   // completed | failed | expired
status.hasPassed()    // the check to gate on
status.kind           // "enroll" | "verify"
status.failureReason  // "invalid_image" | "no_face_detected" | "not_enrolled"
                      // | "face_mismatch" | "blacklist_match" | "expired"
                      // | "internal_error" | null

face2fa.getUser(userId) / face2fa.deleteUser(userId)

const info = await xident.face2fa.getUser('user_123');
// info.enrolled    — whether the user has a face enrolled
// info.enrolledAt  — when, or null

await xident.face2fa.deleteUser('user_123'); // GDPR hard delete, idempotent

Blacklist

Your tenant's face deny list — a blacklisted face fails any future verification with reason blacklist_match. Entries are added by session or image only; embeddings are derived server-side and never leave the server. Additions are async — the entry appears in list() once processed.

// Blacklist the person from one of your completed verification sessions
// (must be terminal and within the 24h document-retention window):
await xident.blacklist.addBySession({
  session_token: 'xvs_...',
  reason: 'chargeback fraud',
}); // → { status: "processing" }

// Or blacklist the face in an image you supply:
await xident.blacklist.addByImage({
  image: base64Image,
  reason: 'known fraudster',
}); // → { status: "processing" }

// List entries (paginated, newest first):
const page = await xident.blacklist.list({ page: 1, per_page: 20 });
for (const entry of page.entries) {
  console.log(entry.id, entry.reason, entry.source, entry.createdAt);
}
console.log(page.pagination.total, 'entries in', page.pagination.totalPages, 'pages');

// Un-ban (the audit record survives server-side):
await xident.blacklist.remove(entryId); // → { message: "blacklist entry removed" }

Error Handling

All errors extend XidentError with errorCode, requestId, and httpStatus.

import { Xident, XidentError, AuthenticationError, RateLimitError } from '@xident/node';

try {
  await xident.verification.init({ callback_url: '...' });
} catch (err) {
  if (err instanceof RateLimitError) {
    console.log('Retry after', err.retryAfter, 'seconds');
  } else if (err instanceof AuthenticationError) {
    console.log('Check your API key');
  } else if (err instanceof XidentError) {
    console.log(err.message, err.errorCode, err.requestId);
  }
}

| Error Class | HTTP Status | When | |---|---|---| | AuthenticationError | 401, 403 | Invalid or missing API key | | ValidationError | 400, 422 | Invalid request parameters | | NotFoundError | 404 | Token or resource not found | | RateLimitError | 429 | Rate limit exceeded | | ServerError | 5xx | Server-side error (retried automatically) | | NetworkError | n/a | DNS, timeout, connection failure |

Requirements

  • Node.js 18+ (uses native fetch)
  • TypeScript 5.5+ (optional but recommended)

License

MIT