Node.js SDK

v1.0.0 Secret Key (sk_live_*) TypeScript-first Node.js 18+ Native fetch

The Node.js SDK (@xident/node) is a server-side TypeScript client for creating verification sessions, verifying tokens, and handling webhooks. It uses native fetch (Node.js 18+) with automatic retries and exponential backoff.

Server-side only. This SDK uses your secret API key (sk_live_* or sk_test_*). Never expose it in client-side code. For browser integration, use the JavaScript SDK with your public key.

Installation

npm install @xident/node

Quick Start

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

const xident = new Xident('sk_live_your_secret_key');

// 1. Create an init token (redirect user to result.verifyUrl)
const init = await xident.verification.init({
  callback_url: 'https://yoursite.com/verified',
  min_age: 18,
});
console.log(init.token);      // 'xit_...'
console.log(init.verifyUrl);  // 'https://verify.xident.io/...'

// 2. After callback, verify the token server-side
const result = await xident.verification.getResult(init.token);
if (result.isVerified()) {
  console.log('Age bracket:', result.ageBracket()); // 18
  console.log('Method:', result.method());           // 'ml_fast'
}

Configuration

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

const xident = new Xident('sk_live_your_secret_key', {
  baseUrl: 'https://api.xident.io',  // Default
  timeout: 30000,                      // Request timeout in ms (default: 30s)
  maxRetries: 3,                       // Retries on 5xx and network errors (default: 3)
  headers: {                           // Extra headers on every request
    'X-Custom-Header': 'value',
  },
});

// Access config
console.log(Xident.VERSION);       // '1.0.0'
console.log(xident.config.apiUrl); // 'https://api.xident.io/verify/v1'
Option Type Default Description
apiKey (1st arg) string Required. Secret API key (sk_live_* or sk_test_*).
baseUrl string https://api.xident.io API base URL. The SDK appends /verify/v1 automatically.
timeout number 30000 Request timeout in milliseconds.
maxRetries number 3 Max retries on 5xx server errors and network failures. Exponential backoff (1s, 2s, 4s + jitter).
headers Record<string, string> {} Extra headers sent with every request.

Verification

xident.verification.init(params)

Create an init token for starting a verification session. Returns a token and the full URL to redirect the user to. The token is valid for 10 minutes.

const init = await xident.verification.init({
  // Required
  callback_url: 'https://yoursite.com/verified',

  // Optional
  min_age: 18,                       // Age threshold (12, 15, 18, 21, 25)
  success_url: 'https://yoursite.com/success',   // Override redirect on pass
  failed_url: 'https://yoursite.com/failed',     // Override redirect on fail
  user_id: 'user-123',              // Your internal user ID
  theme: 'dark',                     // Widget theme: 'light', 'dark', 'auto'
  locale: 'de',                      // Widget locale: 'en', 'de', 'fr', etc.
  metadata: 'order-456',            // Opaque string stored with session (max 500 chars)
  liveness_difficulty: 'medium',     // Challenge difficulty: 'easy', 'medium', 'hard'
  purpose: 'age-gate',              // Verification purpose for display
});

// InitResult
console.log(init.token);      // 'xit_...' (short-lived, 10-minute TTL)
console.log(init.verifyUrl);  // Full URL to redirect the user to
Parameter Type Required Description
callback_url string Yes URL where user is redirected after verification.
min_age number No Minimum age threshold (12, 15, 18, 21, 25).
success_url string No Override redirect URL on success.
failed_url string No Override redirect URL on failure.
user_id string No Your internal user ID for correlation.
theme string No Widget theme: 'light', 'dark', 'auto'.
locale string No Widget locale: 'en', 'de', 'fr', etc.
metadata string No Opaque metadata string (max 500 chars).
liveness_difficulty string No Challenge difficulty: 'easy', 'medium', 'hard'.
purpose string No Verification purpose displayed to the user.

xident.verification.getResult(token)

Get the verification result for a token. Call this after the user returns from the verification widget. Never trust URL parameters alone.

// After the user returns from verification, verify the token server-side.
// NEVER trust URL parameters alone.
const result = await xident.verification.getResult('xit_abc123');

// Status helpers
result.isVerified();   // true if status === 'completed'
result.isFailed();     // true if status === 'failed'
result.isPending();    // true if status === 'pending' or 'in_progress'
result.isTerminal();   // true if completed, failed, canceled, or claimed

// Verification details
result.ageBracket();   // 12 | 15 | 18 | 21 | 25 | null
result.method();       // 'ml_fast' | 'ocr' | 'self_declaration' | null

// Session data
result.id;                  // Session UUID
result.status;              // 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled' | 'claimed'
result.minAge;              // Requested age threshold
result.countryCode;         // ISO 3166-1 alpha-2 (e.g. 'DE')
result.externalUserId;      // Your user_id from init()
result.requiredMethods;     // ['liveness', 'age'] etc.
result.remainingAttempts;   // Number of retries left
result.createdAt;           // ISO 8601 timestamp
result.startedAt;           // ISO 8601 or null
result.completedAt;         // ISO 8601 or null
result.expiresAt;           // ISO 8601 or null

// Sub-results (raw objects)
result.livenessResult;      // Liveness check details or null
result.ageResult;           // Age verification details or null
result.ocrResult;           // Document OCR details or null
result.faceMatchResult;     // Face matching details or null

Webhook Verification

Xident sends webhook events to your callback URL when verification sessions complete, fail, or expire. Events are signed with HMAC-SHA256 using a Stripe-style signature header.

// Xident sends webhook events via HTTP POST with an HMAC-SHA256 signature.
// Header: X-Xident-Signature: t=1710345600,v1=5257a869abcdef...

// constructEvent() verifies the signature AND parses the event in one call.
const event = xident.webhooks.constructEvent(
  payload,     // Raw JSON body string
  signature,   // Value of X-Xident-Signature header
  secret,      // Webhook secret from dashboard (whsec_xxx)
  300,         // Tolerance in seconds (default: 300 = 5 minutes)
);

// WebhookEvent
console.log(event.type);     // 'verification.completed', 'verification.failed', etc.
console.log(event.data);     // Event payload object
console.log(event.id);       // Event ID or null
console.log(event.created);  // Unix timestamp or null

// Or verify the signature separately:
xident.webhooks.verifySignature(payload, signature, secret, 300); // throws on failure
const event2 = xident.webhooks.parseEvent(payload); // parse without verifying
Method Description
constructEvent(payload, signature, secret, tolerance?) Verify signature + parse event in one call. Throws ValidationError on failure.
verifySignature(payload, signature, secret, tolerance?) Verify the HMAC-SHA256 signature only. Returns true or throws.
parseEvent(payload) Parse a webhook payload without verifying the signature.

Important: Use the raw request body (not parsed JSON) for signature verification. Most frameworks have a way to access the raw body (Express: express.raw(), Next.js: req.text()).

Error Handling

All SDK errors extend XidentError, which carries errorCode, requestId, and httpStatus. Include requestId in support tickets.

import {
  Xident,
  XidentError,          // Base class (has errorCode, requestId, httpStatus)
  AuthenticationError,  // 401/403 -- invalid or missing API key
  ValidationError,      // 400 -- bad request params
  NotFoundError,        // 404 -- token/resource not found
  RateLimitError,       // 429 -- rate limited (has retryAfter)
  ServerError,          // 5xx -- server error (auto-retried)
  NetworkError,         // DNS, timeout, connection refused (has cause)
} from '@xident/node';

try {
  const result = await xident.verification.getResult(token);
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error('Invalid API key');
    console.log(error.errorCode);   // API error code (e.g. 'UNAUTHORIZED')
    console.log(error.requestId);   // Include in support tickets
    console.log(error.httpStatus);  // 401 or 403
  } else if (error instanceof NotFoundError) {
    console.error('Token not found or expired');
  } else if (error instanceof RateLimitError) {
    console.log('Retry after:', error.retryAfter, 'seconds');
  } else if (error instanceof ValidationError) {
    console.error('Bad request:', error.message);
  } else if (error instanceof ServerError) {
    // Auto-retried up to maxRetries times with exponential backoff
    console.error('Server error after retries:', error.message);
  } else if (error instanceof NetworkError) {
    console.error('Network failed:', error.cause);
  } else if (error instanceof XidentError) {
    // Catch-all for any SDK error
    console.error(error.message);
  }
}
Error Class HTTP Status When Thrown Key Properties
XidentError any Base class for all SDK errors message, errorCode, requestId, httpStatus
AuthenticationError 401/403 Invalid, expired, or missing API key inherits XidentError
ValidationError 400 Invalid request parameters inherits XidentError
NotFoundError 404 Token or resource not found inherits XidentError
RateLimitError 429 Rate limit exceeded retryAfter (seconds or null)
ServerError 5xx Server error (auto-retried with backoff) inherits XidentError
NetworkError 0 DNS, timeout, connection refused cause (original error)

Framework Examples

Express.js

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

const app = express();
const xident = new Xident(process.env.XIDENT_SECRET_KEY!);

// Create verification session
app.post('/api/verify', express.json(), async (req, res) => {
  try {
    const init = await xident.verification.init({
      callback_url: 'https://your-site.com/webhooks/xident',
      min_age: req.body.min_age ?? 18,
      success_url: 'https://your-site.com/verified',
      failed_url: 'https://your-site.com/failed',
      user_id: req.body.user_id,
    });

    res.json({ token: init.token, verifyUrl: init.verifyUrl });
  } catch (err) {
    if (err instanceof RateLimitError) {
      res.status(429).json({ error: 'Rate limited', retryAfter: err.retryAfter });
    } else {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

// Check verification result
app.get('/api/verify/:token', async (req, res) => {
  try {
    const result = await xident.verification.getResult(req.params.token);
    res.json({
      verified: result.isVerified(),
      status: result.status,
      ageBracket: result.ageBracket(),
      method: result.method(),
    });
  } catch (err) {
    if (err instanceof NotFoundError) {
      res.status(404).json({ error: 'Token not found' });
    } else if (err instanceof XidentError) {
      res.status(err.httpStatus || 500).json({ error: err.message });
    } else {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

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

    switch (event.type) {
      case 'verification.completed':
        console.log('User verified! Session:', event.data['session_id']);
        break;
      case 'verification.failed':
        console.log('Verification failed:', event.data['session_id']);
        break;
    }

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

app.listen(3000);

Next.js App Router

// app/api/xident/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Xident, RateLimitError, ValidationError } from '@xident/node';

const xident = new Xident(process.env.XIDENT_SECRET_KEY!);

// POST /api/xident -- Create verification session
export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const init = await xident.verification.init({
      callback_url: `${process.env.NEXT_PUBLIC_APP_URL}/api/xident/webhook`,
      min_age: body.minAge ?? 18,
      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/verified`,
      failed_url: `${process.env.NEXT_PUBLIC_APP_URL}/failed`,
      user_id: body.userId,
    });

    return NextResponse.json({ token: init.token, verifyUrl: init.verifyUrl });
  } catch (err) {
    if (err instanceof RateLimitError) {
      return NextResponse.json(
        { error: 'Rate limited', retryAfter: err.retryAfter },
        { status: 429 },
      );
    }
    if (err instanceof ValidationError) {
      return NextResponse.json({ error: err.message }, { status: 400 });
    }
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}

// app/api/xident/webhook/route.ts -- Webhook handler
// export async function POST(req: NextRequest) {
//   const body = await req.text();
//   const signature = req.headers.get('x-xident-signature') ?? '';
//   try {
//     const event = xident.webhooks.constructEvent(
//       body, signature, process.env.XIDENT_WEBHOOK_SECRET!
//     );
//     switch (event.type) {
//       case 'verification.completed': /* update user */ break;
//       case 'verification.failed': /* handle failure */ break;
//     }
//     return NextResponse.json({ received: true });
//   } catch {
//     return NextResponse.json({ error: 'Invalid webhook' }, { status: 400 });
//   }
// }

Fastify

import Fastify from 'fastify';
import { Xident, NotFoundError, RateLimitError, XidentError } from '@xident/node';

const fastify = Fastify({ logger: true });
const xident = new Xident(process.env.XIDENT_SECRET_KEY!);

// Create verification session
fastify.post('/api/verify', async (request, reply) => {
  const body = request.body as { min_age?: number; user_id?: string };
  try {
    const init = await xident.verification.init({
      callback_url: 'https://your-site.com/webhooks/xident',
      min_age: body.min_age ?? 18,
      success_url: 'https://your-site.com/verified',
      failed_url: 'https://your-site.com/failed',
      user_id: body.user_id,
    });
    return { token: init.token, verifyUrl: init.verifyUrl };
  } catch (err) {
    if (err instanceof RateLimitError) {
      return reply.status(429).send({ error: 'Rate limited', retryAfter: err.retryAfter });
    }
    throw err;
  }
});

// Check verification result
fastify.get<{ Params: { token: string } }>('/api/verify/:token', async (request, reply) => {
  try {
    const result = await xident.verification.getResult(request.params.token);
    return {
      verified: result.isVerified(),
      status: result.status,
      ageBracket: result.ageBracket(),
      method: result.method(),
    };
  } catch (err) {
    if (err instanceof NotFoundError) {
      return reply.status(404).send({ error: 'Token not found' });
    }
    if (err instanceof XidentError) {
      return reply.status(err.httpStatus || 500).send({ error: err.message });
    }
    throw err;
  }
});

// Webhook endpoint
fastify.post('/webhooks/xident', async (request, reply) => {
  const signature = request.headers['x-xident-signature'] as string;
  const rawBody = typeof request.body === 'string'
    ? request.body
    : JSON.stringify(request.body);
  try {
    const event = xident.webhooks.constructEvent(
      rawBody, signature, process.env.XIDENT_WEBHOOK_SECRET!
    );
    fastify.log.info({ type: event.type }, 'Webhook received');
    return { received: true };
  } catch {
    return reply.status(400).send({ error: 'Invalid webhook' });
  }
});

fastify.listen({ port: 3000 });

Response Types

InitResult

Returned by xident.verification.init().

Field Type Description
token string Short-lived init token (xit_ prefixed, 10-minute TTL).
verifyUrl string Full verification URL. Redirect the user here.

SessionResult

Returned by xident.verification.getResult().

Field / Method Type Description
idstringSession UUID.
statusSessionStatusCurrent session status.
isVerified()booleantrue if status === 'completed'.
isFailed()booleantrue if status === 'failed'.
isPending()booleantrue if pending or in_progress.
isTerminal()booleantrue if completed, failed, canceled, or claimed.
ageBracket()number | nullVerified age bracket (12, 15, 18, 21, 25) or null.
method()string | nullVerification method ('ml_fast', 'ocr', etc.) or null.
minAgenumber | nullRequested age threshold.
countryCodestring | nullISO 3166-1 alpha-2 country code.
externalUserIdstring | nullYour user_id from init().
requiredMethodsstring[] | nullList of required verification methods.
remainingAttemptsnumber | nullNumber of retry attempts left.
livenessResultobject | nullLiveness check details.
ageResultobject | nullAge verification details.
ocrResultobject | nullDocument OCR details.
faceMatchResultobject | nullFace matching details.
createdAtstringISO 8601 creation timestamp.
startedAtstring | nullISO 8601 start timestamp.
completedAtstring | nullISO 8601 completion timestamp.
expiresAtstring | nullISO 8601 expiry timestamp.

WebhookEvent

Returned by constructEvent() and parseEvent().

Field Type Description
typestringEvent type: 'verification.completed', 'verification.failed', 'verification.expired'.
dataRecord<string, unknown>Event payload data.
idstring | nullEvent ID.
creatednumber | nullEvent creation timestamp (unix seconds).

Session Statuses

// SessionStatus enum values (from @xident/node)
import { SessionStatus } from '@xident/node';

SessionStatus.Pending;     // 'pending'     -- Session created, user hasn't started
SessionStatus.InProgress;  // 'in_progress' -- Verification in progress
SessionStatus.Completed;   // 'completed'   -- Passed verification
SessionStatus.Failed;      // 'failed'      -- Failed verification
SessionStatus.Canceled;    // 'canceled'    -- Canceled by user or system
SessionStatus.Claimed;     // 'claimed'     -- Token has been claimed/used

Environment Variables

Variable Description
XIDENT_SECRET_KEY Your secret API key (sk_live_* or sk_test_*). Never commit to version control.
XIDENT_WEBHOOK_SECRET Webhook signing secret (whsec_*). Found in your dashboard webhook settings.

Related