Node.js Integration Examples
Comprehensive examples for integrating LoomAPI with Node.js applications, including Express, Fastify, and serverless environments.
Installation
npm install @loomapi/node-sdk
# or
yarn add @loomapi/node-sdk
# or
pnpm add @loomapi/node-sdk
Basic Usage
Simple Verification
const { LoomAPI } = require('@loomapi/node-sdk')
// Initialize client
const loom = new LoomAPI({
apiKey: process.env.LOOM_API_KEY,
// Optional configuration
baseURL: 'https://api.loomapi.com',
timeout: 30000,
maxRetries: 3
})
// Basic verification
async function verifyDocument(documentData) {
try {
const result = await loom.verify({
documentType: 'passport',
documentData: documentData, // Base64 encoded
verificationType: 'age_only'
})
if (result.status === 'verified') {
console.log(`✅ Verified age: ${result.verified_age}`)
console.log(`📊 Confidence: ${(result.confidence_score * 100).toFixed(1)}%`)
return { success: true, age: result.verified_age }
} else {
console.log('❌ Verification failed')
return { success: false, error: result.error?.message }
}
} catch (error) {
console.error('🚨 Error:', error.message)
return { success: false, error: error.message }
}
}
Express.js Integration
Age Verification Middleware
const express = require('express')
const { LoomAPI } = require('@loomapi/node-sdk')
const multer = require('multer')
const fs = require('fs').promises
const app = express()
const loom = new LoomAPI({ apiKey: process.env.LOOM_API_KEY })
// Configure multer for file uploads
const upload = multer({
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/') || file.mimetype === 'application/pdf') {
cb(null, true)
} else {
cb(new Error('Invalid file type'))
}
}
})
// Age verification middleware
const requireAgeVerification = (minAge = 18) => {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' })
}
if (req.user.age_verified && req.user.verified_age >= minAge) {
return next() // Already verified
}
res.status(403).json({
error: 'Age verification required',
redirectTo: '/verify-age'
})
}
}
// File upload and verification endpoint
app.post('/api/verify-age', upload.single('document'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No document uploaded' })
}
// Convert file to base64
const fileBuffer = await fs.readFile(req.file.path)
const base64Data = fileBuffer.toString('base64')
// Determine document type
const documentType = req.body.document_type || 'passport'
// Verify with LoomAPI
const result = await loom.verify({
documentType,
documentData: base64Data,
metadata: {
user_id: req.user?.id,
ip_address: req.ip,
user_agent: req.get('User-Agent')
}
})
// Clean up uploaded file
await fs.unlink(req.file.path)
if (result.status === 'verified') {
// Update user record
if (req.user) {
req.user.verified_age = result.verified_age
req.user.age_verified = true
req.user.verification_date = new Date()
await req.user.save()
}
res.json({
success: true,
age: result.verified_age,
confidence: result.confidence_score,
request_id: result.request_id
})
} else {
res.status(400).json({
success: false,
error: 'Verification failed',
details: result.error?.message
})
}
} catch (error) {
console.error('Verification error:', error)
// Clean up file if it exists
if (req.file?.path) {
try {
await fs.unlink(req.file.path)
} catch (cleanupError) {
console.error('Cleanup error:', cleanupError)
}
}
res.status(500).json({
success: false,
error: 'Verification service temporarily unavailable'
})
}
})
// Protected route example
app.get('/adult-content', requireAgeVerification(18), (req, res) => {
res.json({ message: 'Welcome to adult content!' })
})
app.listen(3000, () => {
console.log('Server running on port 3000')
})
User Registration Flow
const express = require('express')
const bcrypt = require('bcrypt')
const { LoomAPI } = require('@loomapi/node-sdk')
const app = express()
const loom = new LoomAPI({ apiKey: process.env.LOOM_API_KEY })
app.use(express.json())
class UserRegistrationService {
constructor() {
this.loom = loom
}
async registerUser(userData, documentData) {
const { email, password, firstName, lastName } = userData
// 1. Create user account (without age verification)
const hashedPassword = await bcrypt.hash(password, 12)
const user = await User.create({
email,
password: hashedPassword,
firstName,
lastName,
age_verified: false
})
// 2. Verify age in background
try {
const verificationResult = await this.loom.verify({
documentType: 'passport',
documentData: documentData,
metadata: { user_id: user.id, action: 'registration' }
})
if (verificationResult.status === 'verified') {
// 3. Update user with verified age
user.verified_age = verificationResult.verified_age
user.age_verified = true
user.verification_completed_at = new Date()
await user.save()
return {
success: true,
user: {
id: user.id,
email: user.email,
age: user.verified_age,
age_verified: true
},
message: 'Registration successful with age verification'
}
} else {
// Verification failed - mark for manual review
user.verification_status = 'failed'
user.verification_error = verificationResult.error?.message
await user.save()
return {
success: false,
error: 'Age verification failed. Please contact support.',
user_id: user.id
}
}
} catch (error) {
// Verification service unavailable - allow registration but flag for later verification
console.error('Age verification service error:', error)
user.verification_status = 'pending'
await user.save()
return {
success: true,
user: {
id: user.id,
email: user.email,
age_verified: false
},
message: 'Registration successful. Age verification will be completed shortly.',
requires_manual_verification: true
}
}
}
}
const registrationService = new UserRegistrationService()
app.post('/api/register', async (req, res) => {
try {
const { email, password, firstName, lastName, documentData } = req.body
// Basic validation
if (!email || !password || !documentData) {
return res.status(400).json({ error: 'Missing required fields' })
}
const result = await registrationService.registerUser(
{ email, password, firstName, lastName },
documentData
)
if (result.success) {
res.status(201).json(result)
} else {
res.status(400).json({ error: result.error })
}
} catch (error) {
console.error('Registration error:', error)
res.status(500).json({ error: 'Registration failed' })
}
})
Webhook Handling
Express Webhook Endpoint
const express = require('express')
const crypto = require('crypto')
const { verifyWebhookSignature } = require('@loomapi/node-sdk')
const app = express()
app.use(express.json())
const WEBHOOK_SECRET = process.env.LOOM_WEBHOOK_SECRET
// Webhook verification middleware
function verifyLoomWebhook(req, res, next) {
const signature = req.headers['x-loom-signature']
if (!signature) {
return res.status(401).json({ error: 'Missing signature' })
}
try {
const isValid = verifyWebhookSignature(
req.body,
signature,
WEBHOOK_SECRET
)
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' })
}
next()
} catch (error) {
console.error('Webhook verification error:', error)
res.status(401).json({ error: 'Signature verification failed' })
}
}
// Webhook endpoint
app.post('/webhooks/loom', verifyLoomWebhook, async (req, res) => {
const { event, request_id, verification, metadata } = req.body
console.log(`📥 Received webhook: ${event} for request ${request_id}`)
try {
switch (event) {
case 'verification.completed':
await handleVerificationCompleted(request_id, verification, metadata)
break
case 'verification.failed':
await handleVerificationFailed(request_id, verification, metadata)
break
case 'verification.expired':
await handleVerificationExpired(request_id, metadata)
break
default:
console.log(`⚠️ Unhandled event: ${event}`)
}
res.status(200).json({ received: true })
} catch (error) {
console.error('❌ Webhook processing error:', error)
res.status(500).json({ error: 'Processing failed' })
}
})
async function handleVerificationCompleted(requestId, verification, metadata) {
console.log(`✅ Verification completed: ${requestId}`)
// Find user by request ID or metadata
const user = await User.findOne({
$or: [
{ verification_request_id: requestId },
{ id: metadata?.user_id }
]
})
if (!user) {
console.error(`User not found for request ${requestId}`)
return
}
// Update user record
user.verified_age = verification.verified_age
user.age_verified = true
user.verification_completed_at = new Date()
user.verification_confidence = verification.confidence_score
await user.save()
// Send confirmation email
await sendVerificationSuccessEmail(user.email, {
age: verification.verified_age,
confidence: verification.confidence_score
})
// Log for analytics
await logVerificationEvent('completed', {
user_id: user.id,
request_id: requestId,
age: verification.verified_age,
confidence: verification.confidence_score
})
}
async function handleVerificationFailed(requestId, verification, metadata) {
console.log(`❌ Verification failed: ${requestId}`)
const user = await User.findOne({
$or: [
{ verification_request_id: requestId },
{ id: metadata?.user_id }
]
})
if (user) {
user.verification_failed_at = new Date()
user.verification_error = verification.error?.message
user.verification_attempts = (user.verification_attempts || 0) + 1
await user.save()
// Send failure notification
await sendVerificationFailedEmail(user.email, verification.error?.message)
}
// Log failure for monitoring
await logVerificationEvent('failed', {
request_id: requestId,
user_id: metadata?.user_id,
error: verification.error?.message
})
}
async function handleVerificationExpired(requestId, metadata) {
console.log(`⏰ Verification expired: ${requestId}`)
const user = await User.findOne({
$or: [
{ verification_request_id: requestId },
{ id: metadata?.user_id }
]
})
if (user && !user.age_verified) {
// Mark as expired, user can try again
user.verification_status = 'expired'
await user.save()
await sendVerificationExpiredEmail(user.email)
}
}
Error Handling & Retries
Comprehensive Error Handling
class LoomService {
constructor(apiKey) {
this.loom = new LoomAPI({
apiKey,
maxRetries: 3,
retryDelay: 1000
})
}
async verifyWithRetry(documentData, options = {}) {
const {
maxRetries = 3,
documentType = 'passport',
metadata = {}
} = options
let lastError
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`🔄 Verification attempt ${attempt}/${maxRetries}`)
const result = await this.loom.verify({
documentType,
documentData,
metadata: {
...metadata,
attempt,
max_retries: maxRetries
}
})
if (result.status === 'verified') {
console.log(`✅ Verification successful on attempt ${attempt}`)
return result
}
// Handle specific verification failures
if (result.error?.code === 'DOCUMENT_QUALITY_LOW') {
throw new Error('Document quality too low for verification')
}
if (result.error?.code === 'VERIFICATION_FAILED') {
throw new Error('Document verification failed')
}
// For other errors, continue to retry
lastError = new Error(result.error?.message || 'Verification failed')
} catch (error) {
lastError = error
// Check if error is retryable
if (this.isRetryableError(error)) {
if (attempt < maxRetries) {
const delay = this.calculateDelay(attempt, error)
console.log(`⏳ Retrying in ${delay}ms...`)
await this.delay(delay)
continue
}
} else {
// Non-retryable error (like invalid document)
break
}
}
}
console.error(`❌ All ${maxRetries} attempts failed:`, lastError.message)
throw lastError
}
isRetryableError(error) {
// Network errors
if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
return true
}
// Rate limiting
if (error.error?.code === 'RATE_LIMIT_EXCEEDED') {
return true
}
// Server errors
if (error.status >= 500) {
return true
}
return false
}
calculateDelay(attempt, error) {
// Use retry-after header if available
if (error.error?.details?.retry_after_seconds) {
return error.error.details.retry_after_seconds * 1000
}
// Exponential backoff
return Math.min(1000 * Math.pow(2, attempt - 1), 30000)
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
Testing
Unit Tests with Jest
const { LoomAPI } = require('@loomapi/node-sdk')
describe('LoomService', () => {
let loomService
let mockLoom
beforeEach(() => {
mockLoom = {
verify: jest.fn()
}
loomService = new LoomService('test-key')
loomService.loom = mockLoom
})
test('successful verification', async () => {
mockLoom.verify.mockResolvedValue({
request_id: 'req_123',
status: 'verified',
verified_age: 25,
confidence_score: 0.95
})
const result = await loomService.verifyWithRetry('base64data')
expect(result.status).toBe('verified')
expect(result.verified_age).toBe(25)
})
test('handles rate limiting with retry', async () => {
mockLoom.verify
.mockRejectedValueOnce({
error: {
code: 'RATE_LIMIT_EXCEEDED',
details: { retry_after_seconds: 1 }
}
})
.mockResolvedValueOnce({
request_id: 'req_123',
status: 'verified',
verified_age: 25
})
const result = await loomService.verifyWithRetry('base64data')
expect(mockLoom.verify).toHaveBeenCalledTimes(2)
expect(result.status).toBe('verified')
})
test('fails after max retries', async () => {
mockLoom.verify.mockRejectedValue(
new Error('Network timeout')
)
await expect(loomService.verifyWithRetry('base64data', { maxRetries: 2 }))
.rejects.toThrow('Network timeout')
expect(mockLoom.verify).toHaveBeenCalledTimes(2)
})
})
Integration Tests
describe('User Registration Integration', () => {
let testUser
let testDocument
beforeEach(async () => {
// Create test user
testUser = await User.create({
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
})
// Load test document
testDocument = await loadTestDocument('valid_passport.jpg')
})
test('complete registration flow', async () => {
const registrationService = new UserRegistrationService()
const result = await registrationService.registerUser({
email: 'test@example.com',
password: 'password123',
firstName: 'John',
lastName: 'Doe'
}, testDocument)
expect(result.success).toBe(true)
expect(result.user.age).toBeGreaterThanOrEqual(18)
expect(result.user.age_verified).toBe(true)
// Verify database state
const updatedUser = await User.findById(result.user.id)
expect(updatedUser.age_verified).toBe(true)
expect(updatedUser.verified_age).toBe(result.user.age)
})
test('handles verification service downtime', async () => {
// Mock service failure
jest.spyOn(LoomAPI.prototype, 'verify').mockRejectedValue(
new Error('Service unavailable')
)
const registrationService = new UserRegistrationService()
const result = await registrationService.registerUser({
email: 'test@example.com',
password: 'password123',
firstName: 'John',
lastName: 'Doe'
}, testDocument)
expect(result.success).toBe(true)
expect(result.requires_manual_verification).toBe(true)
// User should be created but not verified
const user = await User.findOne({ email: 'test@example.com' })
expect(user.age_verified).toBe(false)
expect(user.verification_status).toBe('pending')
})
})
Serverless (AWS Lambda)
Lambda Function
const { LoomAPI } = require('@loomapi/node-sdk')
const loom = new LoomAPI({
apiKey: process.env.LOOM_API_KEY
})
exports.handler = async (event) => {
console.log('Processing verification request')
try {
const { documentData, documentType = 'passport', userId } = JSON.parse(event.body)
const result = await loom.verify({
documentType,
documentData,
metadata: {
user_id: userId,
lambda_request_id: event.requestContext?.requestId,
source: 'lambda'
}
})
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
success: result.status === 'verified',
age: result.verified_age,
confidence: result.confidence_score,
requestId: result.request_id
})
}
} catch (error) {
console.error('Lambda error:', error)
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
success: false,
error: error.message
})
}
}
}
API Gateway Integration
# serverless.yml or CloudFormation
functions:
verifyAge:
handler: handler.verifyAge
events:
- http:
path: verify
method: post
cors: true
environment:
LOOM_API_KEY: ${env:LOOM_API_KEY}
Performance Optimization
Connection Pooling
const https = require('https')
// Create agent for connection reuse
const agent = new https.Agent({
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 10
})
const loom = new LoomAPI({
apiKey: process.env.LOOM_API_KEY,
// Custom axios configuration
axiosConfig: {
httpsAgent: agent,
timeout: 30000
}
})
Caching
const NodeCache = require('node-cache')
class CachedLoomService {
constructor(apiKey) {
this.loom = new LoomAPI({ apiKey })
this.cache = new NodeCache({ stdTTL: 3600 }) // 1 hour TTL
}
async verifyWithCache(documentData, cacheKey) {
// Check cache first
const cached = this.cache.get(cacheKey)
if (cached) {
console.log('🔄 Using cached result')
return cached
}
// Perform verification
const result = await this.loom.verify({
documentType: 'passport',
documentData
})
// Cache successful results
if (result.status === 'verified') {
this.cache.set(cacheKey, result)
}
return result
}
}
Monitoring & Logging
Structured Logging
const winston = require('winston')
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'loom-verifications.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
})
class MonitoredLoomService {
constructor(apiKey) {
this.loom = new LoomAPI({ apiKey })
}
async verify(documentData, metadata = {}) {
const startTime = Date.now()
const requestId = metadata.request_id || `req_${Date.now()}`
logger.info('Starting verification', {
requestId,
documentType: metadata.document_type || 'unknown',
userId: metadata.user_id
})
try {
const result = await this.loom.verify({
documentType: metadata.document_type || 'passport',
documentData,
metadata: { ...metadata, request_id: requestId }
})
const duration = Date.now() - startTime
logger.info('Verification completed', {
requestId,
status: result.status,
age: result.verified_age,
confidence: result.confidence_score,
duration,
userId: metadata.user_id
})
return result
} catch (error) {
const duration = Date.now() - startTime
logger.error('Verification failed', {
requestId,
error: error.message,
duration,
userId: metadata.user_id,
stack: error.stack
})
throw error
}
}
}
This comprehensive Node.js integration guide covers everything from basic usage to advanced patterns, error handling, testing, and production deployment scenarios.