Node.js SDK
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.
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?t=xit_...'
// 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)
});
// 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). |
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 |
|---|---|---|
id | string | Session UUID. |
status | SessionStatus | Current session status. |
isVerified() | boolean | true if status === 'completed'. |
isFailed() | boolean | true if status === 'failed'. |
isPending() | boolean | true if pending or in_progress. |
isTerminal() | boolean | true if completed, failed, canceled, or claimed. |
ageBracket() | number | null | Verified age bracket (12, 15, 18, 21, 25) or null. |
method() | string | null | Verification method ('ml_fast', 'ocr', etc.) or null. |
minAge | number | null | Requested age threshold. |
countryCode | string | null | ISO 3166-1 alpha-2 country code. |
externalUserId | string | null | Your user_id from init(). |
requiredMethods | string[] | null | List of required verification methods. |
remainingAttempts | number | null | Number of retry attempts left. |
livenessResult | object | null | Liveness check details. |
ageResult | object | null | Age verification details. |
ocrResult | object | null | Document OCR details. |
faceMatchResult | object | null | Face matching details. |
createdAt | string | ISO 8601 creation timestamp. |
startedAt | string | null | ISO 8601 start timestamp. |
completedAt | string | null | ISO 8601 completion timestamp. |
expiresAt | string | null | ISO 8601 expiry timestamp. |
WebhookEvent
Returned by constructEvent() and parseEvent().
| Field | Type | Description |
|---|---|---|
type | string | Event type: 'verification.completed', 'verification.failed', 'verification.expired'. |
data | Record<string, unknown> | Event payload data. |
id | string | null | Event ID. |
created | number | null | Event 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
- Python SDK — Server-side Python SDK with sync and async clients
- All SDKs — Overview of all Xident SDKs
- API Reference — Full REST API documentation