All documentation

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/subscriptions
  • GET /api/v1/webhooks/subscriptions
  • POST /api/v1/webhooks/subscriptions/:id/test
  • DELETE /api/v1/webhooks/subscriptions/:id

Create Subscription

Create a new webhook subscription.

POST/api/v1/webhooks/subscriptions

Request

{
  "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_EVALUATED
  • EVENT_FEEDBACK_SUBMITTED
  • BLOCKLIST_ENTRY_ADDED
  • BLOCKLIST_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/subscriptions

Response

{
  "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/:id

Response

{
  "success": true,
  "message": "Subscription removed successfully"
}

Send Test Event

POST/api/v1/webhooks/subscriptions/:id/test

Example 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:

  1. Return 2xx quickly: Respond within 10 seconds
  2. Handle duplicates: Use idempotency keys
  3. Log all requests: For debugging and audit
  4. 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_..."