Implementation Guide

1. Webhook Endpoint Setup for Business Verification

// 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/business/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: Identify event type
    const eventType = payload.event_type || payload.business_profile_event_type;
    
    // Step 4: Process based on event type
    await processBusinessEvent(payload, eventType);
    
    // Step 5: Respond quickly (200 OK)
    res.status(200).send('OK');
    
  } catch (error) {
    console.error('Business webhook processing error:', error);
    res.status(500).send('Internal Server Error');
  }
});

async function processBusinessEvent(payload, eventType) {
  switch(eventType) {
    case 'VERIFICATION_SESSION_STARTED':
      await handleBusinessSessionStarted(payload);
      break;
      
    case 'VERIFICATION_SESSION_COMPLETION':
      await handleBusinessSessionCompletion(payload);
      break;
      
    case 'RUN_BUSINESS_VERIFICATION':
      await handleBusinessProfileVerification(payload);
      break;
      
    case 'BUSINESS_OFFICER_RUN_KYC':
      await handleOfficerKYC(payload);
      break;
      
    case 'ADD_BUSINESS_OFFICER':
      await handleOfficerAdded(payload);
      break;
      
    case 'AML_MONITORING_UPDATE_BUSINESS_VERIFICATION':
    case 'AML_MONITORING_UPDATE_RELATED_BUSINESS_OFFICER':
      await handleBusinessAMLUpdate(payload);
      break;
      
    default:
      console.log(`Unhandled business event type: ${eventType}`);
  }
}

async function handleBusinessSessionStarted(payload) {
  await db.businessVerificationSessions.update(
    { id: payload.verification_session_id },
    { 
      status: 'IN_PROGRESS',
      started_at: new Date()
    }
  );
  
  // Notify compliance team
  await notifyComplianceTeam(
    payload.client_reference_id,
    'New business verification started'
  );
}

async function handleBusinessProfileVerification(payload) {
  const decision = payload.aiprise_summary.decision;
  const businessProfileId = payload.business_profile_id;
  
  // Update business profile
  await db.businessProfiles.update(
    { id: businessProfileId },
    {
      verification_status: decision,
      verified_at: decision === 'APPROVED' ? new Date() : null,
      kyb_completed: true
    }
  );
  
  // Process officers
  if (payload.related_persons) {
    for (const officer of payload.related_persons) {
      await db.businessOfficers.upsert({
        business_profile_id: businessProfileId,
        person_reference_id: officer.person_reference_id,
        roles: officer.roles,
        ownership_percent: officer.ownership_percent,
        verification_status: officer.verification_status
      });
    }
  }
  
  // Process related companies
  if (payload.related_companies) {
    for (const company of payload.related_companies) {
      await db.relatedCompanies.upsert({
        business_profile_id: businessProfileId,
        company_reference_id: company.company_reference_id,
        legal_name: company.legal_name,
        relationship_type: company.relationship_type,
        ownership_percent: company.ownership_percent
      });
    }
  }
  
  // Business logic based on decision
  if (decision === 'APPROVED') {
    await enableBusinessAccount(businessProfileId);
    await setTransactionLimits(businessProfileId, payload.risk_info);
    await generateServiceAgreement(businessProfileId);
  } else if (decision === 'DECLINED') {
    await notifyBusinessDecline(businessProfileId, payload.aiprise_summary.reasons);
  }
}

async function handleOfficerKYC(payload) {
  const businessProfileId = payload.business_profile_id;
  const personReferenceId = payload.person_reference_id;
  const decision = payload.aiprise_summary.decision;
  
  // Update officer record
  await db.businessOfficers.update(
    {
      business_profile_id: businessProfileId,
      person_reference_id: personReferenceId
    },
    {
      kyc_status: decision,
      kyc_completed_at: new Date(),
      aml_status: payload.aml_info?.result
    }
  );
  
  // Check if all officers are verified
  const allOfficers = await db.businessOfficers.findAll({
    where: { business_profile_id: businessProfileId }
  });
  
  const allVerified = allOfficers.every(
    officer => officer.kyc_status === 'APPROVED'
  );
  
  if (allVerified) {
    await db.businessProfiles.update(
      { id: businessProfileId },
      { all_officers_verified: true }
    );
    
    // Trigger next step in onboarding
    await triggerBusinessOnboardingStep(businessProfileId, 'OFFICERS_VERIFIED');
  }
}

app.listen(3000);

2. Decision Callback Handler for Business

// Separate endpoint for business decision callbacks
app.post('/webhooks/aiprise/business/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 KYB decision
    await processBusinessDecision(payload);
    
    res.status(200).send('OK');
    
  } catch (error) {
    console.error('Business decision callback error:', error);
    res.status(500).send('Internal Server Error');
  }
});

async function processBusinessDecision(payload) {
  const decision = payload.aiprise_summary.decision;
  const businessId = payload.client_reference_id;
  
  // Make idempotent using verification_session_id
  const existing = await db.businessVerificationResults.findOne({
    verification_session_id: payload.verification_session_id
  });
  
  if (existing) {
    console.log('Business decision already processed');
    return;
  }
  
  // Store KYB result
  await db.businessVerificationResults.create({
    verification_session_id: payload.verification_session_id,
    business_id: businessId,
    decision: decision,
    payload: payload,
    processed_at: new Date()
  });
  
  // Update business record
  await db.businesses.update(
    { id: businessId },
    {
      kyb_status: decision,
      kyb_completed_at: new Date(),
      registry_verified: payload.registration_info?.good_standing,
      aml_clear: payload.aml_info?.result === 'CLEAR',
      risk_tier: payload.risk_info?.risk_tier,
      credit_score: payload.lookup_info?.find(l => l.provider === 'credit_bureau')?.credit_score
    }
  );
  
  // Store officers
  if (payload.related_persons) {
    for (const officer of payload.related_persons) {
      await db.businessOfficers.create({
        business_id: businessId,
        person_reference_id: officer.person_reference_id,
        first_name: officer.user_profile.first_name,
        last_name: officer.user_profile.last_name,
        roles: officer.roles,
        ownership_percent: officer.ownership_percent,
        kyc_status: officer.verification_status
      });
    }
  }
  
  // Business decision logic
  switch(decision) {
    case 'APPROVED':
      await approveBusinessOnboarding(businessId, payload);
      break;
      
    case 'DECLINED':
      await declineBusinessOnboarding(businessId, payload.aiprise_summary.reasons);
      break;
      
    case 'MANUAL_REVIEW':
      await queueForComplianceReview(businessId, payload);
      break;
  }
}

async function approveBusinessOnboarding(businessId, payload) {
  // Enable business account
  await db.businesses.update(
    { id: businessId },
    { 
      account_status: 'ACTIVE',
      activated_at: new Date()
    }
  );
  
  // Set transaction limits based on risk
  const riskTier = payload.risk_info?.risk_tier;
  const limits = calculateTransactionLimits(riskTier);
  
  await db.businessLimits.create({
    business_id: businessId,
    daily_limit: limits.daily,
    monthly_limit: limits.monthly,
    transaction_limit: limits.transaction
  });
  
  // Generate contract
  await generateBusinessContract(businessId);
  
  // Notify business
  await sendBusinessApprovalEmail(businessId);
  
  // Notify account manager
  const accountManager = payload.client_reference_data?.account_manager;
  if (accountManager) {
    await notifyAccountManager(accountManager, businessId, 'approved');
  }
}

3. Officer Management Handler

async function handleOfficerAdded(payload) {
  const businessProfileId = payload.business_profile_id;
  const officer = payload.user_profile;
  const personReferenceId = payload.person_reference_id;
  
  // Create officer record
  await db.businessOfficers.create({
    business_profile_id: businessProfileId,
    person_reference_id: personReferenceId,
    first_name: officer.first_name,
    last_name: officer.last_name,
    email: officer.email,
    phone: officer.phone,
    roles: payload.roles,
    ownership_percent: payload.ownership_percent,
    shares_allocated: payload.shares_allocated,
    kyc_status: 'PENDING',
    created_at: new Date()
  });
  
  // Check if KYC is required for this officer
  const requiresKYC = payload.roles.some(role => 
    ['CEO', 'CFO', 'DIRECTOR', 'UBO'].includes(role)
  ) || payload.ownership_percent >= 25;
  
  if (requiresKYC) {
    // Generate KYC link for officer
    const kycUrl = await aipriseAPI.createOfficerKYCSession({
      business_profile_id: businessProfileId,
      person_reference_id: personReferenceId
    });
    
    // Send KYC invitation
    await sendOfficerKYCInvitation(officer.email, kycUrl);
  }
}

async function handleOfficerKYCCompletion(payload) {
  const businessProfileId = payload.business_profile_id;
  const personReferenceId = payload.person_reference_id;
  const kycDecision = payload.aiprise_summary.decision;
  
  // Update officer KYC status
  await db.businessOfficers.update(
    {
      business_profile_id: businessProfileId,
      person_reference_id: personReferenceId
    },
    {
      kyc_status: kycDecision,
      kyc_completed_at: new Date(),
      id_verified: payload.id_info?.authenticity?.result === 'PASS',
      face_matched: payload.face_match_info?.result === 'PASS',
      aml_status: payload.aml_info?.result
    }
  );
  
  // If officer is declined, flag for review
  if (kycDecision === 'DECLINED') {
    await createComplianceTicket({
      business_profile_id: businessProfileId,
      issue_type: 'OFFICER_KYC_DECLINED',
      person_reference_id: personReferenceId,
      details: payload.aiprise_summary.reasons
    });
  }
  
  // Check overall business verification progress
  await checkBusinessVerificationProgress(businessProfileId);
}

Examples

Example 1: Simple KYB Session Flow (Standalone)

Scenario: Business completes KYB, not linked to profile

1. Business initiates KYB
   → Your app creates verification session
   
2. Event: VERIFICATION_SESSION_STARTED
   {
     "event_type": "VERIFICATION_SESSION_STARTED",
     "verification_session_id": "bvs_123",
     "client_reference_id": "business_456"
   }
   → Log session start
   → Notify compliance team
   
3. Business submits company documents
   
4. Event: VERIFICATION_REQUEST_SUBMITTED
   {
     "event_type": "VERIFICATION_REQUEST_SUBMITTED",
     "verification_session_id": "bvs_123"
   }
   → Update status: "Processing your business verification"
   
5. KYB verification completes
   
6. Event: VERIFICATION_SESSION_COMPLETION
   {
     "event_type": "VERIFICATION_SESSION_COMPLETION",
     "aiprise_summary": { "decision": "APPROVED" },
     "business_info": { /* ... */ },
     "registration_info": { /* ... */ },
     "related_persons": [ /* officers */ ]
   }
   
7. Decision Callback: callback_url
   {
     "verification_session_id": "bvs_123",
     "aiprise_summary": { "decision": "APPROVED" },
     // Full KYB payload
   }
   → Enable business account
   → Generate service agreement
   → Notify business: "Verification successful"

Example 2: Business Profile with Officers KYC

Scenario: Complete business onboarding with officer verification

1. Create business profile

2. Event: BUSINESS_PROFILE_CREATE
   {
     "business_profile_id": "bp_789",
     "business_profile_event_type": "BUSINESS_PROFILE_CREATE"
   }
   → Profile created
   
3. Run business verification

4. Event: RUN_BUSINESS_VERIFICATION
   {
     "business_profile_id": "bp_789",
     "business_profile_event_type": "RUN_BUSINESS_VERIFICATION",
     "aiprise_summary": { "decision": "APPROVED" },
     // Embedded KYB payload
   }
   → Business entity verified
   → Now need to verify officers
   
5. Add CEO officer

6. Event: ADD_BUSINESS_OFFICER
   {
     "business_profile_id": "bp_789",
     "person_reference_id": "person_ceo_001",
     "roles": ["CEO", "DIRECTOR"],
     "ownership_percent": 45.0
   }
   → Officer added
   → Generate KYC link
   
7. Event: BUSINESS_OFFICER_CREATE_SESSION_URL
   {
     "business_profile_id": "bp_789",
     "person_reference_id": "person_ceo_001",
     "session_url": "https://verify.aiprise.com/officer/xyz"
   }
   → Send KYC link to CEO email
   
8. CEO completes KYC

9. Event: BUSINESS_OFFICER_RUN_KYC
   {
     "business_profile_id": "bp_789",
     "person_reference_id": "person_ceo_001",
     "aiprise_summary": { "decision": "APPROVED" },
     // Embedded User KYC payload
   }
   → CEO verified
   → Check if all required officers verified
   → If yes: Complete business onboarding

Example 3: AML Monitoring Alert on Business and Officer

Scenario: Sanctions match found during ongoing monitoring

1. Daily AML monitoring runs on business profile

2. Event: AML_MONITORING_UPDATE_BUSINESS_VERIFICATION
   {
     "business_profile_id": "bp_789",
     "aml_monitoring_update": {
       "status": "MATCH_FOUND",
       "matches": [{
         "list_name": "OFAC SDN",
         "entity_name": "Acme Corporation Ltd",
         "confidence": 0.96
       }]
     }
   }
   → URGENT: Freeze business account immediately
   → Notify compliance team
   → Create high-priority investigation ticket
   → Suspend all transactions
   
3. Simultaneously, officer match found

4. Event: AML_MONITORING_UPDATE_RELATED_BUSINESS_OFFICER
   {
     "business_profile_id": "bp_789",
     "person_reference_id": "person_ceo_001",
     "aml_monitoring_update": {
       "status": "MATCH_FOUND",
       "matches": [{
         "list_name": "PEP Database",
         "entity_name": "John Smith",
         "confidence": 0.91
       }]
     }
   }
   → Additional concern: Officer is PEP
   → Escalate to senior compliance
   → Request enhanced due diligence
   → Document all findings for regulators

Example 4: Related Company Verification

Scenario: Verifying parent company as part of UBO identification

1. Business profile created and verified

2. Add parent company

3. Event: ADD_RELATED_COMPANY
   {
     "business_profile_id": "bp_789",
     "company_reference_id": "company_parent_001",
     "legal_name": "Acme Holdings PLC",
     "relationship_type": "PARENT",
     "ownership_percent": 100.0
   }
   → Parent company added
   → Trigger KYB for parent
   
4. Parent company KYB completes

5. Event: RELATED_COMPANY_RUN_KYB
   {
     "business_profile_id": "bp_789",
     "company_reference_id": "company_parent_001",
     "aiprise_summary": { "decision": "APPROVED" },
     // Embedded parent company KYB payload
     "related_persons": [ /* parent company officers */ ]
   }
   → Parent company verified
   → Ultimate beneficial owners identified
   → Complete ownership chain
   → Update risk assessment with group structure

Example 5: Manual Review and Override

Scenario: Complex business structure requires manual review and override

1. Initial business verification completes

2. Event: VERIFICATION_SESSION_COMPLETION
   {
     "aiprise_summary": { "decision": "MANUAL_REVIEW" },
     "verification_session_id": "bvs_123"
   }
   → Complex ownership structure detected
   → Queued for manual review
   
3. Compliance analyst reviews case

4. Analyst requests additional documents

5. Event: COMMENT
   {
     "business_profile_id": "bp_789",
     "comment": "Requesting: 1) Updated ownership chart, 2) Board resolution, 3) Proof of business address"
   }
   → Notification sent to business
   → Documents requested via portal
   
6. Business uploads documents

7. Event: ADD_ADDITIONAL_MEDIA
   {
     "business_profile_id": "bp_789",
     "file_uuid": "file_doc_123",
     "media_type": "OWNERSHIP_CHART"
   }
   → Document received
   → Notify analyst for review
   
8. Analyst approves after document review

9. Event: RELATED_CASE_STATUS_UPDATE
   {
     "business_profile_id": "bp_789",
     "verification_result": "APPROVED",
     "author_id": "[email protected]",
     "reason": "Additional documentation verified. Complex but legitimate ownership structure confirmed."
   }
   → Override applied
   → Business account activated
   → Generate approval notification
   → Log override for audit