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. 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
- JavaScript SDK — Client-side browser integration
- Python SDK — Server-side Python SDK with sync and async clients
- All SDKs — Overview of all Xident SDKs
- API Reference — Full REST API documentation