Webhooks API
Naiza fraud and AML webhooks: subscribe to decisions, verify signatures, retry deliveries, and connect each result to the right investigation case.
Webhooks API
The Webhooks API lets you subscribe to platform events and receive signed callbacks.
Auth
x-api-key is required.
Endpoints
POST /api/v1/webhooks/subscriptionsGET /api/v1/webhooks/subscriptionsPOST /api/v1/webhooks/subscriptions/:id/testDELETE /api/v1/webhooks/subscriptions/:id
Create Subscription
Create a new webhook subscription.
POST
/api/v1/webhooks/subscriptionsRequest
{
"url": "https://api.example.com/webhooks",
"eventTypes": [
"EVENT_RISK_EVALUATED",
"EVENT_FEEDBACK_SUBMITTED"
]
}Response
{
"id": "wh_ckm1234567890",
"url": "https://api.example.com/webhooks",
"eventTypes": ["EVENT_RISK_EVALUATED", "EVENT_FEEDBACK_SUBMITTED"],
"status": "ACTIVE",
"createdAt": "2026-04-16T10:30:00.000Z",
"updatedAt": "2026-04-16T10:30:00.000Z",
"lastTriggeredAt": null,
"signingSecret": "whsec_a1b2c3d4e5f6..."
}signingSecret is returned once on create. Store it securely.
Event Types
EVENT_RISK_EVALUATEDEVENT_FEEDBACK_SUBMITTEDBLOCKLIST_ENTRY_ADDEDBLOCKLIST_ENTRY_REMOVED
Requirements
- URL must use HTTPS
- At least one event type must be specified
List Subscriptions
List all webhook subscriptions for your tenant.
GET
/api/v1/webhooks/subscriptionsResponse
{
"subscriptions": [
{
"id": "wh_ckm1234567890",
"url": "https://api.example.com/webhooks",
"eventTypes": ["EVENT_RISK_EVALUATED", "EVENT_FEEDBACK_SUBMITTED"],
"status": "ACTIVE",
"createdAt": "2026-04-16T10:00:00.000Z",
"updatedAt": "2026-04-16T10:00:00.000Z",
"lastTriggeredAt": "2026-04-16T10:30:00.000Z"
}
]
}Remove Subscription
Delete a webhook subscription.
DELETE
/api/v1/webhooks/subscriptions/:idResponse
{
"success": true,
"message": "Subscription removed successfully"
}Send Test Event
POST
/api/v1/webhooks/subscriptions/:id/testExample response:
{
"success": true,
"statusCode": 200,
"message": "Test webhook delivered successfully"
}Webhook Payload Signing
All webhook payloads are signed with HMAC-SHA256 using your secret.
Signature Header
X-Webhook-Signature: sha256=<signature> X-Webhook-Event-Type: EVENT_RISK_EVALUATED
Verifying Signatures
const crypto = require('crypto');
function verifyWebhookSignature(payloadString, signature, secret) {
// payloadString should be the raw request body as a string or Buffer
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
const providedSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
}
// Express middleware - use raw body parser to get exact bytes
app.post('/webhooks/decisions', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;
// req.body is already a Buffer from express.raw()
// Convert to string for verification (must match exact format sent)
const payloadString = req.body.toString('utf8');
if (!verifyWebhookSignature(payloadString, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse JSON only after verification
const payload = JSON.parse(payloadString);
// Process webhook...
});Webhook Payloads
Event Risk Evaluated
{
"eventType": "EVENT_RISK_EVALUATED",
"timestamp": "2026-04-16T10:30:00.000Z",
"data": {
"id": "evt_ckm9876543210",
"decision": "deny",
"riskScore": 75,
"riskLevel": "high",
"reasonCodes": [
{
"code": "RULE_BLOCK",
"explanation": "Blocked by rule: High Risk IP",
"ruleId": "rule_123",
"ruleName": "Block High Risk IPs"
}
],
"correlationIds": {
"requestId": "req_abc123",
"eventId": "evt_ckm9876543210",
"customerId": "cus_123456"
},
"evaluatedAt": "2025-01-15T14:30:00.000Z"
}
}Event Feedback Submitted
{
"eventType": "EVENT_FEEDBACK_SUBMITTED",
"timestamp": "2026-04-16T10:31:00.000Z",
"data": {
"decisionId": "evt_ckm9876543210",
"feedback": {
"label": "FRAUD",
"notes": "Customer confirmed fraud",
"externalCaseId": "case_12345"
},
"updatedAt": "2025-01-15T15:00:00.000Z"
}
}Blocklist Entry Added
{
"eventType": "BLOCKLIST_ENTRY_ADDED",
"timestamp": "2026-04-16T10:32:00.000Z",
"data": {
"id": "list_abc123",
"type": "IP",
"value": "203.0.113.50",
"listType": "block",
"reason": "Known malicious IP",
"createdAt": "2025-01-15T14:30:00.000Z"
}
}Blocklist Entry Removed
{
"eventType": "BLOCKLIST_ENTRY_REMOVED",
"timestamp": "2026-04-16T10:33:00.000Z",
"data": {
"id": "list_abc123",
"type": "IP",
"value": "203.0.113.50",
"listType": "block",
"deletedAt": "2025-01-15T14:30:00.000Z"
}
}Retries and Backoff
Webhook delivery includes automatic retries with exponential backoff via BullMQ queue:
- Initial retry delay: 1 second
- Max retries: 5 attempts
- Backoff: Exponential (1s, 2s, 4s, 8s, 16s)
- Retry triggers: Network errors, timeouts, non-2xx HTTP responses
- Queue-based: All webhook deliveries are queued for reliability
Handling Failures
Your webhook endpoint should:
- Return 2xx quickly: Respond within 10 seconds
- Handle duplicates: Use idempotency keys
- Log all requests: For debugging and audit
- Validate signatures: Always verify HMAC signatures
Example Handler
app.post('/webhooks/decisions', async (req, res) => {
// Verify signature
const signature = req.headers['x-webhook-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
// Parse payload
const payload = JSON.parse(req.body);
// Process asynchronously
processWebhook(payload).catch(error => {
console.error('Webhook processing error:', error);
// Don't fail the request - webhook will be retried
});
// Respond quickly
res.status(200).json({ received: true });
});
async function processWebhook(payload) {
// Check for duplicates using idempotency
const idempotencyKey = `${payload.eventType}-${payload.data.id}`;
if (await isDuplicate(idempotencyKey)) {
return; // Already processed
}
// Process webhook
await handleDecisionCreated(payload.data);
// Mark as processed
await markProcessed(idempotencyKey);
}Examples
# Create subscription
curl -X POST https://api.naiza.ai/api/v1/webhooks/subscriptions \
-H "x-api-key: naiza_api_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com/webhooks",
"eventTypes": ["EVENT_RISK_EVALUATED", "EVENT_FEEDBACK_SUBMITTED"]
}'
# List subscriptions
curl -X GET https://api.naiza.ai/api/v1/webhooks/subscriptions \
-H "x-api-key: naiza_api_sk_live_..."
# Send test webhook
curl -X POST https://api.naiza.ai/api/v1/webhooks/subscriptions/wh_ckm1234567890/test \
-H "x-api-key: naiza_api_sk_live_..."
# Remove subscription
curl -X DELETE https://api.naiza.ai/api/v1/webhooks/subscriptions/wh_ckm1234567890 \
-H "x-api-key: naiza_api_sk_live_..."