All documentation

Webhooks API

Webhook configuration and event delivery.

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