Skip to main content

Overview

Webhooks provide real-time notifications when verifications complete, eliminating the need for polling and enabling event-driven workflows.

How Webhooks Work

1

Configure Webhook

Provide webhook URL when creating verification
2

Verification Processes

AnyCheck processes your verification asynchronously
3

Webhook Delivered

When complete, POST request sent to your webhook URL
4

Process Results

Your endpoint processes the webhook payload

Configuring Webhooks

During Verification Creation

curl -X POST https://api.anycheck.id/verifications \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "service_id": "service-uuid",
    "folder_id": "folder-uuid",
    "configuration": {...},
    "webhook_url": "https://your-app.com/webhooks/anycheck",
    "webhook_metadata": {
      "customer_id": "cust-123",
      "order_id": "order-456",
      "internal_ref": "ref-789"
    }
  }'

Metadata

Include custom data in webhook_metadata to identify the verification in your system:
{
  "webhook_metadata": {
    "customer_id": "cust-123",
    "order_id": "order-456",
    "application_id": "app-789",
    "user_email": "[email protected]",
    "custom_field": "any value"
  }
}
This metadata is included in the webhook payload.

Webhook Payload

When verification completes, your endpoint receives:
{
  "event": "verification.completed",
  "verification_id": "verification-uuid",
  "result_id": "result-uuid",
  "status": "COMPLETED",
  "service": {
    "id": "service-uuid",
    "name": "KTP Verification"
  },
  "output": {
    "verified": true,
    "match_score": 0.95,
    "data": {
      "nik": "1234567890123456",
      "name": "JOHN DOE",
      "birth_date": "1990-01-01"
    }
  },
  "metadata": {
    "customer_id": "cust-123",
    "order_id": "order-456",
    "internal_ref": "ref-789"
  },
  "execution_time": 5.2,
  "completed_at": "2024-01-15T10:05:30Z",
  "credit_used": 1
}

Payload Fields

event
string
Event type: verification.completed, verification.failed, verification.needs_review
verification_id
string
Unique verification identifier
status
string
Final status: COMPLETED, FAILED, NEED_REVIEW
output
object
Verification results (only if COMPLETED)
metadata
object
Your custom metadata from webhook_metadata

Implementing Webhook Handler

Node.js/Express Example

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

const app = express();
app.use(express.json());

app.post('/webhooks/anycheck', async (req, res) => {
  try {
    // 1. Verify webhook signature (if configured)
    const signature = req.headers['x-webhook-signature'];
    if (!verifySignature(req.body, signature)) {
      return res.status(401).send('Invalid signature');
    }

    // 2. Respond quickly
    res.status(200).send('OK');

    // 3. Process asynchronously
    await processWebhook(req.body);

  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).send('Error processing webhook');
  }
});

async function processWebhook(payload) {
  const { verification_id, status, output, metadata } = payload;

  // Update your database
  await db.verifications.update({
    id: verification_id,
    status: status,
    results: output,
    completed_at: new Date()
  });

  // Trigger business logic
  if (status === 'COMPLETED' && output.verified) {
    await approveCustomerApplication(metadata.customer_id);
    await sendNotificationEmail(metadata.customer_id);
  } else if (status === 'NEED_REVIEW') {
    await addToReviewQueue(verification_id);
  } else if (status === 'FAILED') {
    await handleFailedVerification(verification_id);
  }
}

Python/Flask Example

from flask import Flask, request
import hmac
import hashlib

app = Flask(__name__)

@app.route('/webhooks/anycheck', methods=['POST'])
def webhook_handler():
    try:
        # 1. Verify signature
        signature = request.headers.get('X-Webhook-Signature')
        if not verify_signature(request.data, signature):
            return 'Invalid signature', 401

        # 2. Respond quickly
        payload = request.json

        # 3. Process asynchronously
        process_webhook.delay(payload)  # Using Celery

        return 'OK', 200

    except Exception as e:
        print(f'Webhook error: {e}')
        return 'Error', 500

def process_webhook(payload):
    verification_id = payload['verification_id']
    status = payload['status']
    output = payload['output']
    metadata = payload['metadata']

    # Update database
    db.verifications.update(
        id=verification_id,
        status=status,
        results=output
    )

    # Business logic
    if status == 'COMPLETED' and output['verified']:
        approve_application(metadata['customer_id'])
    elif status == 'NEED_REVIEW':
        add_to_review_queue(verification_id)

Webhook Events

verification.completed

Verification successfully completed:
{
  "event": "verification.completed",
  "status": "COMPLETED",
  "output": {
    "verified": true,
    "data": {...}
  }
}

verification.needs_review

Verification requires manual review:
{
  "event": "verification.needs_review",
  "status": "NEED_REVIEW",
  "review_reason": "Low confidence score",
  "output": {
    "verified": null,
    "confidence": 0.65
  }
}

verification.failed

Verification failed:
{
  "event": "verification.failed",
  "status": "FAILED",
  "error": {
    "code": "PROVIDER_ERROR",
    "message": "External verification service unavailable"
  }
}

Security

Webhook Signature Verification

Verify webhook authenticity using HMAC signature (if webhook secret is configured):
function verifySignature(payload, signature) {
  const secret = process.env.WEBHOOK_SECRET;
  const hmac = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return hmac === signature;
}

Best Practices

Always use HTTPS endpoints for webhooks:
✅ https://your-app.com/webhooks/anycheck
❌ http://your-app.com/webhooks/anycheck
Return 200 OK within 5 seconds:
app.post('/webhook', async (req, res) => {
  res.status(200).send('OK');  // Respond first

  await processWebhook(req.body);  // Process async
});
Handle duplicate webhook deliveries:
const processedWebhooks = new Set();

async function processWebhook(payload) {
  const webhookId = payload.verification_id;

  if (processedWebhooks.has(webhookId)) {
    return; // Already processed
  }

  // Process...

  processedWebhooks.add(webhookId);
}
Validate webhook structure:
function validateWebhook(payload) {
  if (!payload.verification_id) {
    throw new Error('Missing verification_id');
  }
  if (!payload.status) {
    throw new Error('Missing status');
  }
  // More validations...
}
Log webhook failures for debugging:
app.post('/webhook', async (req, res) => {
  try {
    await processWebhook(req.body);
    res.status(200).send('OK');
  } catch (error) {
    await logWebhookError(req.body, error);
    res.status(500).send('Error');
  }
});

Retry Mechanism

AnyCheck automatically retries failed webhook deliveries:
  • Retry Count: Up to 3 attempts
  • Retry Delay: Exponential backoff (5s, 25s, 125s)
  • Failure Condition: Non-2xx response or timeout
If all retries fail, webhook is marked as failed and can be manually retriggered.

Testing Webhooks

Local Development

Use tools like ngrok to expose local server:
# Start ngrok
ngrok http 3000

# Use ngrok URL as webhook
https://abc123.ngrok.io/webhooks/anycheck

Webhook Testing Tools

  • RequestBin: Capture and inspect webhooks
  • Webhook.site: Test webhook URLs
  • Postman: Mock webhook requests

Manual Webhook Trigger

For testing, manually trigger webhook:
curl -X POST https://your-app.com/webhooks/anycheck \
  -H "Content-Type: application/json" \
  -d '{
    "event": "verification.completed",
    "verification_id": "test-uuid",
    "status": "COMPLETED",
    "output": {...}
  }'

Webhook vs Polling

Use Webhooks When:

✅ Real-time processing needed ✅ High volume of verifications ✅ Event-driven architecture ✅ Reducing server load

Use Polling When:

❌ Cannot expose public endpoint ❌ Behind strict firewall ❌ Low verification volume ❌ Simple integration needed

Troubleshooting

Possible Causes:
  • Webhook URL not accessible
  • Firewall blocking requests
  • SSL certificate issues
  • Endpoint returning non-2xx
Solution:
  • Verify URL is publicly accessible
  • Check firewall rules
  • Ensure valid SSL certificate
  • Return 200 OK from endpoint
Cause: Retry mechanism or network issuesSolution: Implement idempotency using verification_id
Cause: Synchronous processing in webhook handlerSolution: Process webhooks asynchronously:
res.status(200).send('OK');  // Respond first
await queueJob(payload);      // Process async

Next Steps