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
| Field | Type | Description |
|---|---|---|
event | string | Event type |
session_id | UUID | The verification session ID |
success | boolean | Whether verification passed |
status | string | Final session status |
age_target | integer | The target age that was verified |
metadata | array/null | Custom metadata from session creation |
completed_at | string | ISO 8601 completion timestamp |
organization_id | UUID | Your organization ID |
Event Types
| Event | Description |
|---|---|
validation.completed | Verification finished (success or failure) |
validation.page_visited | User opened verification page |
validation.biometry_started | Liveness check began |
validation.kyc_started | ID 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/bouncer2. 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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 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 3000Then use the ngrok URL for your webhook:
https://abc123.ngrok.io/webhooks/bouncerManual 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
- Respond quickly: Return 200 status immediately, process async
- Be idempotent: Handle duplicate webhook deliveries gracefully
- Log everything: Keep records of webhook payloads for debugging
- Monitor failures: Alert on webhook processing errors
- Validate payloads: Verify session IDs against your records
- 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);
});