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;
  }
}