Implementation Guide
1. Webhook Endpoint Setup
// Express.js example
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/aiprise/events', async (req, res) => {
try {
// Step 1: Verify signature
const signature = req.headers['x-hmac-signature'];
const rawBody = req.body.toString('utf8');
const expectedSignature = crypto
.createHmac('sha256', process.env.AIPRISE_API_KEY)
.update(rawBody)
.digest('hex');
if (signature !== expectedSignature) {
console.error('Invalid signature');
return res.status(401).send('Unauthorized');
}
// Step 2: Parse the payload
const payload = JSON.parse(rawBody);
// Step 3: Process based on event type
await processEvent(payload);
// Step 4: Respond quickly (200 OK)
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Internal Server Error');
}
});
async function processEvent(payload) {
const eventType = payload.event_type || payload.user_profile_event_type;
switch(eventType) {
case 'VERIFICATION_SESSION_STARTED':
await handleSessionStarted(payload);
break;
case 'VERIFICATION_SESSION_COMPLETION':
await handleSessionCompletion(payload);
break;
case 'RUN_USER_VERIFICATION':
await handleProfileVerification(payload);
break;
case 'AML_MONITORING_UPDATE':
await handleAMLUpdate(payload);
break;
default:
console.log(`Unhandled event type: ${eventType}`);
}
}
async function handleSessionStarted(payload) {
// Update database
await db.verificationSessions.update(
{ id: payload.verification_session_id },
{ status: 'IN_PROGRESS', started_at: new Date() }
);
// Notify user
await notifyUser(payload.client_reference_id,
'Your verification has started');
}
async function handleSessionCompletion(payload) {
const decision = payload.aiprise_summary.decision;
// Update user status
await db.users.update(
{ id: payload.client_reference_id },
{
verification_status: decision,
verified_at: decision === 'APPROVED' ? new Date() : null
}
);
// Grant/revoke access
if (decision === 'APPROVED') {
await grantUserAccess(payload.client_reference_id);
} else if (decision === 'DECLINED') {
await notifyDecline(payload.client_reference_id,
payload.aiprise_summary.reasons);
}
}
app.listen(3000);2. Decision Callback Handler
// Separate endpoint for decision callbacks
app.post('/webhooks/aiprise/decision', async (req, res) => {
try {
// Verify signature (same as above)
const signature = req.headers['x-hmac-signature'];
const rawBody = req.body.toString('utf8');
const expectedSignature = crypto
.createHmac('sha256', process.env.AIPRISE_API_KEY)
.update(rawBody)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('Unauthorized');
}
const payload = JSON.parse(rawBody);
// Process final decision
await processFinalDecision(payload);
res.status(200).send('OK');
} catch (error) {
console.error('Decision callback error:', error);
res.status(500).send('Internal Server Error');
}
});
async function processFinalDecision(payload) {
const decision = payload.aiprise_summary.decision;
const userId = payload.client_reference_id;
// Make idempotent using verification_session_id
const existing = await db.verificationResults.findOne({
verification_session_id: payload.verification_session_id
});
if (existing) {
console.log('Decision already processed');
return;
}
// Store verification result
await db.verificationResults.create({
verification_session_id: payload.verification_session_id,
user_id: userId,
decision: decision,
payload: payload,
processed_at: new Date()
});
// Update user record
await db.users.update(
{ id: userId },
{
kyc_status: decision,
kyc_completed_at: new Date(),
id_verified: payload.id_info?.authenticity?.result === 'PASS',
face_matched: payload.face_match_info?.result === 'PASS',
aml_clear: payload.aml_info?.result === 'CLEAR'
}
);
// Business logic based on decision
switch(decision) {
case 'APPROVED':
await enableUserFeatures(userId);
await sendWelcomeEmail(userId);
break;
case 'DECLINED':
await notifyUserDecline(userId, payload.aiprise_summary.reasons);
break;
case 'MANUAL_REVIEW':
await notifyComplianceTeam(payload);
break;
}
}3. Idempotency Implementation
// Using callback_uuid for idempotency
async function processEventIdempotent(payload) {
const callbackUuid = payload.callback_uuid ||
generateUuid(payload.verification_session_id);
// Check if already processed
const processed = await redis.get(`callback:${callbackUuid}`);
if (processed) {
console.log(`Callback ${callbackUuid} already processed`);
return;
}
try {
// Process the event
await processEvent(payload);
// Mark as processed (with 7 day expiry)
await redis.setex(`callback:${callbackUuid}`, 604800, 'processed');
} catch (error) {
console.error(`Error processing callback ${callbackUuid}:`, error);
throw error;
}
}Updated about 6 hours ago
