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_..."