Webhook Management
Webhooks enable real-time notifications when events occur in your Nafsi AI account. All webhooks use mTLS (mutual TLS) authentication for secure communication.
Overview
Webhooks allow you to:
- Receive real-time notifications when verifications complete
- Integrate verification checks via custom webhooks
- Subscribe to system events like low balance alerts
- Build custom workflows with external verification services
Webhook Types
1. Event Notification Webhooks
Receive notifications when system events occur:
Example Events:
verification.completed- Verification finished successfullyverification.failed- Verification failedverification.review_required- Manual review neededworkflow.created- New workflow createdunits.low_balance- Credits running low
2. Verification Check Webhooks
Create custom verification checks that call your external service:
Use Cases:
- Custom liveness detection
- Third-party AML screening
- Proprietary fraud detection
- Document validation against internal databases
Creating a Webhook
Navigate to Webhooks → Create Webhook
Step 1: Basic Information
| Field | Description | Required |
|---|---|---|
| Webhook Type | Event Notification or Verification Check | Yes |
| Name | Descriptive name (e.g., "Production Webhook") | Yes |
| Description | Purpose and use case | Optional |
| Webhook URL | HTTPS endpoint to receive webhooks | Yes |
| HTTP Method | POST, PUT, or PATCH | Yes (default: POST) |
Example:
Type: Event Notification
Name: Production Verification Webhook
Description: Receive notifications for all verification events
URL: https://api.myapp.com/webhooks/nafsi
Method: POST
Requirements:
- URL must use HTTPS (no HTTP allowed)
- URL must be publicly accessible
- Endpoint must respond within 30 seconds
- Endpoint must return 2xx status code for success
Step 2: Authentication (mTLS)
All webhooks require mTLS authentication for security.
What is mTLS?
mTLS (mutual Transport Layer Security) is a two-way authentication method where:
- Your server verifies Nafsi's identity
- Nafsi verifies your server's identity
- All communication is encrypted
Configure mTLS
- Upload CA Certificate (PEM format)
Click Choose File and select your CA certificate file.
Example CA Certificate:
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKL0UG+mRKP7MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
... (your certificate content) ...
-----END CERTIFICATE-----
- Click "Validate Certificate"
The system will:
- Verify the certificate format (PEM)
- Test TLS handshake with your webhook URL
- Check certificate expiry
- Extract certificate details
Validation Response:
✅ Certificate valid
Expires in 365 days
No expiry warning
If Validation Fails:
- Ensure certificate is in PEM format (not DER or other formats)
- Verify webhook URL is accessible
- Check that your server is configured for mTLS
- Ensure certificate is not expired
Generate a Self-Signed Certificate (for testing)
# Generate private key
openssl genrsa -out server.key 2048
# Generate certificate signing request
openssl req -new -key server.key -out server.csr
# Generate self-signed certificate
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# Convert to PEM format (if needed)
cat server.crt > server.pem
For production, use a certificate from a trusted CA.
- Webhook Secret (Optional but Recommended)
Add a secret key for HMAC signature verification:
Secret: sk_webhook_abc123xyz789
This secret is used to sign webhook payloads so you can verify authenticity.
Learn how to verify signatures →
Step 3: Custom Headers (Optional)
Add custom HTTP headers to include in webhook requests:
| Header Name | Header Value | Enabled |
|---|---|---|
X-Custom-Header | MyValue | ✅ |
X-API-Version | v1 | ✅ |
Authorization | Bearer token123 | ☐ |
Use Cases:
- Add API keys for your endpoint
- Include version headers
- Add correlation IDs
Step 4: Event Subscription
(Event Notification Webhooks Only)
Select which events you want to receive:
Verification Lifecycle Events
- ☑️
verification.started- Verification request received - ☑️
verification.completed- Verification succeeded - ☑️
verification.failed- Verification failed - ☑️
verification.timeout- Verification timed out - ☐
verification.review_required- Manual review needed
Workflow Events
- ☐
workflow.created- New workflow created - ☐
workflow.updated- Workflow modified - ☐
workflow.activated- Workflow activated - ☐
workflow.deactivated- Workflow deactivated
Billing Events
- ☑️
units.low_balance- Credits below threshold - ☑️
units.depleted- Credits exhausted
System Events
- ☐
webhook.delivery.failed- Webhook delivery failed - ☐
client.credits.low- Client credits low
Note: You can subscribe to as many events as needed. Each event will trigger a separate webhook delivery.
Step 5: Verification Criteria
(Verification Check Webhooks Only)
Define success criteria for custom verification checks:
| Setting | Description | Default |
|---|---|---|
| Require Success Field | Response must include success: true | ON |
| Require Score Field | Response must include confidence score | ON |
| Minimum Threshold | Minimum score for pass (0.0 - 1.0) | 0.85 |
Example Response from Your Webhook:
{
"success": true,
"score": 0.92,
"data": {
"result": "approved",
"reason": "Face match successful"
}
}
Request Schema (Optional): Define the JSON structure sent to your webhook.
Response Schema (Optional): Define the expected JSON structure from your webhook.
Step 6: Review & Create
Review all settings and click Create Webhook.
The webhook is created with status: Inactive
To activate: Click Activate on the webhook row.
Managing Webhooks
Webhook List
View all webhooks with:
| Column | Description |
|---|---|
| Name | Webhook name |
| Type | Event Notification or Verification Check |
| URL | Webhook endpoint (truncated) |
| Events | Number of subscribed events |
| Status | Active / Inactive / Paused |
| Success Rate | Delivery success percentage |
| Last Triggered | Timestamp of last delivery |
| Actions | Edit / Test / Delete / View Logs |
Testing a Webhook
Before going live, test your webhook:
- Click Test on the webhook row
- Select an event type (e.g.,
verification.completed) - Click Send Test Event
Test Payload Example:
{
"event": "verification.completed",
"timestamp": "2026-01-28T10:30:00Z",
"test": true,
"data": {
"verification_id": "test_ver_123",
"status": "success",
"confidence": 0.87
}
}
Test Results:
- ✅ Success (200ms) - Webhook endpoint responded correctly
- ❌ Failed (HTTP 500) - Server error
- ❌ TLS verification failed - Certificate issue
- ⏱️ Timeout (30s) - Endpoint too slow
Viewing Delivery Logs
Track webhook delivery history:
- Click View Logs on the webhook
- See all delivery attempts with:
- Timestamp
- Event type
- Status (Success/Failed/Retrying)
- Response code
- Response time
- Error message (if failed)
Filter logs by:
- Date range
- Status
- Event type
Export logs as CSV for analysis.
Webhook Statistics
View webhook performance:
- Total Deliveries: All attempts
- Successful Deliveries: 2xx responses
- Failed Deliveries: 4xx/5xx errors
- Average Response Time: Median latency
- Success Rate: Percentage of successful deliveries
- Last Success: Timestamp
- Last Failure: Timestamp
Editing a Webhook
- Click Edit on the webhook
- Modify settings (except webhook type cannot be changed)
- Click Update Webhook
Note: Editing a webhook does not affect pending deliveries.
Pausing/Resuming a Webhook
Temporarily stop webhook deliveries:
- Click Pause on the webhook
- Status changes to Paused
- Events will not be delivered while paused
To resume: Click Resume
Deleting a Webhook
Permanently delete a webhook:
- Click Delete on the webhook
- Confirm deletion
- Webhook and all logs are deleted
Warning: This action cannot be undone!
Webhook Delivery
Delivery Flow
Event Occurs → Queue Event → Validate Webhook → Prepare Payload
→ Add Signature → Deliver to Endpoint → Log Result → Retry if Failed
Payload Structure
All webhook payloads follow this structure:
{
"event": "verification.completed",
"event_id": "evt_abc123",
"timestamp": "2026-01-28T10:30:00Z",
"organisation_id": 18,
"data": {
-- Event-specific data
}
}
HTTP Headers
Each webhook request includes:
POST /webhooks/nafsi HTTP/1.1
Host: api.myapp.com
Content-Type: application/json
X-Nafsi-Signature: sha256=abc123...
X-Nafsi-Event: verification.completed
X-Webhook-ID: webhook_xyz789
X-Delivery-ID: delivery_uvw456
User-Agent: Nafsi-Webhooks/1.0
Verifying Webhook Signatures
Why Verify?
- Ensure webhook came from Nafsi
- Prevent replay attacks
- Detect tampering
How to Verify:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Compare signatures (constant-time comparison)
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from('sha256=' + expectedSignature)
);
}
// In your webhook endpoint
app.post('/webhooks/nafsi', (req, res) => {
const signature = req.headers['x-nafsi-signature'];
const payload = req.body;
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhookSignature(payload, signature, secret)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
console.log('Valid webhook received:', payload);
res.status(200).send('OK');
});
Python Example:
import hmac
import hashlib
import json
def verify_webhook_signature(payload, signature, secret):
expected = hmac.new(
secret.encode(),
json.dumps(payload).encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(
signature,
'sha256=' + expected
)
# In your webhook handler
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/nafsi', methods=['POST'])
def webhook():
signature = request.headers.get('X-Nafsi-Signature')
payload = request.json
secret = os.getenv('WEBHOOK_SECRET')
if not verify_webhook_signature(payload, signature, secret):
return 'Invalid signature', 401
# Process webhook
print('Valid webhook received:', payload)
return 'OK', 200
Retry Logic
If webhook delivery fails, Nafsi automatically retries:
| Attempt | Delay | Condition |
|---|---|---|
| 1st | Immediate | Initial delivery |
| 2nd | 2 seconds | If 1st fails |
| 3rd | 4 seconds | If 2nd fails |
Exponential Backoff: Delay = 2^attempt seconds
Retryable Errors:
- 5xx server errors
- Timeouts
- Connection errors
Non-Retryable Errors:
- 4xx client errors (bad request)
- TLS verification failures
After 3 Failed Attempts:
- Webhook delivery marked as failed_permanent
- Event
webhook.delivery.failedis triggered - Webhook stats updated (failure_count++)
Auto-Deactivation: After 10 consecutive failures, the webhook is automatically paused and you receive an email notification.
Response Requirements
Your webhook endpoint should:
- Respond quickly (< 30 seconds)
- Return 2xx status code for success
- Return 4xx for validation errors (won't retry)
- Return 5xx for server errors (will retry)
Good Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"received": true,
"message": "Webhook processed successfully"
}
Error Response:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "Database connection failed",
"retry": true
}
Event Payloads
verification.completed
{
"event": "verification.completed",
"event_id": "evt_abc123",
"timestamp": "2026-01-28T10:30:00Z",
"organisation_id": 18,
"data": {
"verification_id": "ver_xyz789",
"workflow_id": "wf_abc123",
"workflow_name": "Basic KYC",
"status": "success",
"confidence": 0.87,
"units_consumed": 11,
"processing_time_ms": 2340,
"result": {
"decision": "approved",
"extracted_data": {
"full_name": "JOHN KAMAU DOE",
"date_of_birth": "1990-01-15",
"national_id_number": "12345678"
},
"checks": {
"face_match": {
"result": "pass",
"confidence": 0.87
}
}
},
"metadata": {
"customer_id": "cust_123"
}
}
}
verification.failed
{
"event": "verification.failed",
"event_id": "evt_def456",
"timestamp": "2026-01-28T10:35:00Z",
"organisation_id": 18,
"data": {
"verification_id": "ver_uvw012",
"workflow_id": "wf_abc123",
"status": "failed",
"reason": "Face match confidence below threshold",
"failed_check": "face_match",
"confidence": 0.62,
"threshold": 0.75,
"units_consumed": 5
}
}
units.low_balance
{
"event": "units.low_balance",
"event_id": "evt_ghi789",
"timestamp": "2026-01-28T14:00:00Z",
"organisation_id": 18,
"data": {
"current_balance": 850,
"threshold": 1000,
"estimated_verifications_remaining": 70,
"projected_depletion_date": "2026-01-29"
}
}
Best Practices
1. Idempotency
Handle duplicate deliveries gracefully:
const processedEvents = new Set();
app.post('/webhooks/nafsi', (req, res) => {
const eventId = req.body.event_id;
if (processedEvents.has(eventId)) {
return res.status(200).send('Already processed');
}
// Process event
processWebhook(req.body);
// Mark as processed
processedEvents.add(eventId);
res.status(200).send('OK');
});
2. Async Processing
Don't block the webhook response:
app.post('/webhooks/nafsi', async (req, res) => {
// Immediately acknowledge receipt
res.status(200).send('OK');
// Process async
processWebhookAsync(req.body).catch(err => {
console.error('Error processing webhook:', err);
});
});
3. Error Handling
app.post('/webhooks/nafsi', async (req, res) => {
try {
await processWebhook(req.body);
res.status(200).send('OK');
} catch (err) {
console.error('Webhook processing error:', err);
// Return 5xx so Nafsi retries
res.status(500).send('Processing error');
}
});
4. Logging
Log all webhook receipts for debugging:
app.post('/webhooks/nafsi', (req, res) => {
console.log('Webhook received:', {
event: req.body.event,
eventId: req.body.event_id,
timestamp: req.body.timestamp,
data: req.body.data
});
// Process and respond
res.status(200).send('OK');
});
Troubleshooting
Issue: Certificate Validation Fails
Possible Causes:
- Certificate not in PEM format
- Certificate expired
- Webhook URL not accessible
- Server not configured for mTLS
Solutions:
- Convert certificate to PEM format
- Renew expired certificate
- Ensure URL is publicly accessible
- Configure your server for mTLS:
# Nginx example
ssl_client_certificate /path/to/ca.crt;
ssl_verify_client on;
Issue: Webhooks Not Being Delivered
Possible Causes:
- Webhook is paused/inactive
- Endpoint returning errors
- Firewall blocking requests
Solutions:
- Check webhook status (should be Active)
- Check delivery logs for errors
- Whitelist Nafsi IP addresses
- Test webhook endpoint manually
Issue: Duplicate Webhook Deliveries
Possible Causes:
- Endpoint slow to respond
- Network retries
- Multiple subscriptions to same event
Solutions:
- Implement idempotency (check event_id)
- Respond faster (< 1 second)
- Check event subscriptions for duplicates
Security Considerations
- Always verify signatures - Don't trust unverified webhooks
- Use HTTPS only - Never use HTTP
- Keep secrets secure - Store webhook secrets in environment variables
- Validate payload structure - Don't assume payload format
- Rate limit your endpoint - Protect against abuse
- Monitor for anomalies - Alert on unusual webhook activity
- Rotate secrets periodically - Best practice for production
Next Steps
- Event Catalog - View all available events
- Webhook Integration Guide - Step-by-step integration
- API Reference - Webhook API endpoints
- Custom Checks Tutorial - Build custom verification checks
Support
- 📧 Email: support@nafsi.ai
- 💬 Live Chat: Available in console
- 📚 Documentation: https://docs.nafsi.ai
Last Updated: 2026-01-28