Bouncer

Webhooks

Receive real-time notifications for verification events

Webhooks

Webhooks allow you to receive real-time HTTP notifications when verification events occur. This is the recommended way to handle verification results in production.

Overview

Instead of polling the API for session status, configure a webhook URL to receive instant notifications when:

  • A session is completed (success or failure)
  • A user visits the verification page
  • Biometric verification starts
  • KYC process begins

Setting Up Webhooks

Per-Session Webhooks

Include a webhook_url when creating a session:

curl -X POST https://your-bouncer-instance.com/api/v2/session \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "age_target": 18,
    "redirect_url": "https://your-app.com/callback",
    "webhook_url": "https://your-app.com/webhooks/bouncer"
  }'

Device Webhooks

Configure a webhook URL in device settings for all sessions created by that device.

Webhook Payload

When a verification completes, Bouncer sends a POST request to your webhook URL:

{
  "event": "validation.completed",
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "success": true,
  "status": "completed",
  "age_target": 18,
  "metadata": ["order:12345", "user:67890"],
  "completed_at": "2024-01-15T10:34:25.000000Z",
  "organization_id": "org-uuid-here"
}

Payload Fields

FieldTypeDescription
eventstringEvent type
session_idUUIDThe verification session ID
successbooleanWhether verification passed
statusstringFinal session status
age_targetintegerThe target age that was verified
metadataarray/nullCustom metadata from session creation
completed_atstringISO 8601 completion timestamp
organization_idUUIDYour organization ID

Event Types

EventDescription
validation.completedVerification finished (success or failure)
validation.page_visitedUser opened verification page
validation.biometry_startedLiveness check began
validation.kyc_startedID scanning began

Handling Webhooks

Basic Handler (Node.js/Express)

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/bouncer', async (req, res) => {
  const { event, session_id, success, metadata } = req.body;

  console.log(`Received ${event} for session ${session_id}`);

  switch (event) {
    case 'validation.completed':
      if (success) {
        await handleVerificationSuccess(session_id, metadata);
      } else {
        await handleVerificationFailure(session_id, metadata);
      }
      break;

    case 'validation.page_visited':
      await updateSessionStatus(session_id, 'user_viewing');
      break;

    default:
      console.log(`Unhandled event: ${event}`);
  }

  // Always respond 200 to acknowledge receipt
  res.status(200).send('OK');
});

async function handleVerificationSuccess(sessionId, metadata) {
  // Extract your custom data from metadata
  const orderId = metadata?.find(m => m.startsWith('order:'))?.split(':')[1];

  if (orderId) {
    await db.orders.update(orderId, { verified: true });
    await notifyCustomer(orderId, 'Age verification successful!');
  }
}

async function handleVerificationFailure(sessionId, metadata) {
  const orderId = metadata?.find(m => m.startsWith('order:'))?.split(':')[1];

  if (orderId) {
    await db.orders.update(orderId, { verified: false });
    await notifyCustomer(orderId, 'Age verification failed. Please try again.');
  }
}

PHP Handler (Laravel)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Order;

class BouncerWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $event = $request->input('event');
        $sessionId = $request->input('session_id');
        $success = $request->input('success');
        $metadata = $request->input('metadata', []);

        switch ($event) {
            case 'validation.completed':
                $this->handleCompletion($sessionId, $success, $metadata);
                break;

            case 'validation.page_visited':
                $this->logPageVisit($sessionId);
                break;
        }

        return response('OK', 200);
    }

    private function handleCompletion($sessionId, $success, $metadata)
    {
        // Find order from metadata
        $orderId = collect($metadata)
            ->first(fn($m) => str_starts_with($m, 'order:'));

        if ($orderId) {
            $orderId = str_replace('order:', '', $orderId);

            Order::where('id', $orderId)->update([
                'age_verified' => $success,
                'bouncer_session_id' => $sessionId,
            ]);
        }
    }
}

Python Handler (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/bouncer', methods=['POST'])
def bouncer_webhook():
    data = request.json

    event = data.get('event')
    session_id = data.get('session_id')
    success = data.get('success')
    metadata = data.get('metadata', [])

    if event == 'validation.completed':
        handle_completion(session_id, success, metadata)
    elif event == 'validation.page_visited':
        log_page_visit(session_id)

    return 'OK', 200

def handle_completion(session_id, success, metadata):
    # Extract order ID from metadata
    order_id = None
    for item in metadata:
        if item.startswith('order:'):
            order_id = item.split(':')[1]
            break

    if order_id:
        # Update your database
        db.orders.update(order_id, {'verified': success})

Security

Verifying Webhook Origin

To ensure webhooks are genuinely from Bouncer, implement these security measures:

1. HTTPS Only

Always use HTTPS for your webhook endpoints:

✓ https://your-app.com/webhooks/bouncer
✗ http://your-app.com/webhooks/bouncer

2. IP Allowlisting

If possible, restrict webhook endpoints to Bouncer's IP addresses. Contact support for the current IP list.

3. Session Verification

Verify the session exists and belongs to your organization:

app.post('/webhooks/bouncer', async (req, res) => {
  const { session_id } = req.body;

  // Verify session with Bouncer API
  const response = await fetch(
    `https://your-bouncer-instance.com/api/v2/session/${session_id}`,
    {
      headers: { 'Authorization': `Bearer ${process.env.BOUNCER_TOKEN}` }
    }
  );

  if (response.status !== 200) {
    console.error('Invalid session ID in webhook');
    return res.status(400).send('Invalid session');
  }

  // Process webhook...
  res.status(200).send('OK');
});

Retry Policy

Bouncer automatically retries failed webhook deliveries:

AttemptDelay
1Immediate
230 seconds
32 minutes
410 minutes
51 hour

After 5 failed attempts, the webhook is marked as failed.

Handling Retries

Ensure your webhook handler is idempotent:

app.post('/webhooks/bouncer', async (req, res) => {
  const { session_id, event } = req.body;

  // Check if already processed
  const existing = await db.webhookLogs.findOne({
    sessionId: session_id,
    event: event
  });

  if (existing) {
    // Already processed, just acknowledge
    return res.status(200).send('OK');
  }

  // Process and log
  await processWebhook(req.body);
  await db.webhookLogs.insert({
    sessionId: session_id,
    event: event,
    processedAt: new Date()
  });

  res.status(200).send('OK');
});

Testing Webhooks

Local Development

Use tools like ngrok to expose your local server:

ngrok http 3000

Then use the ngrok URL for your webhook:

https://abc123.ngrok.io/webhooks/bouncer

Manual Testing

Create a test session and complete verification to trigger the webhook:

# Create session with webhook
curl -X POST https://your-bouncer-instance.com/api/v2/session \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "age_target": 18,
    "redirect_url": "https://your-app.com/callback",
    "webhook_url": "https://abc123.ngrok.io/webhooks/bouncer"
  }'

Best Practices

  1. Respond quickly: Return 200 status immediately, process async
  2. Be idempotent: Handle duplicate webhook deliveries gracefully
  3. Log everything: Keep records of webhook payloads for debugging
  4. Monitor failures: Alert on webhook processing errors
  5. Validate payloads: Verify session IDs against your records
  6. Use queues: For high volume, queue webhook processing
// Good: Async processing with immediate response
app.post('/webhooks/bouncer', async (req, res) => {
  // Immediately acknowledge
  res.status(200).send('OK');

  // Process in background
  await queue.add('process-bouncer-webhook', req.body);
});

On this page