---
title: "Webhooks API"
description: "Webhook configuration and event delivery."
collection: "api-reference"
slug: "webhooks"
url: "https://naiza.ai/docs/api-reference/webhooks"
markdown: "https://naiza.ai/docs/api-reference/webhooks.md"
full_docs: "https://naiza.ai/docs.md"
product: "Naiza"
base_url: "https://api.naiza.ai/api/v1"
---

# Webhooks API

> Webhook configuration and event delivery.

## Table of contents

- [Auth](#auth)
- [Endpoints](#endpoints)
- [Create Subscription](#create-subscription)
  - [Endpoint](#endpoint)
  - [Request](#request)
  - [Response](#response)
  - [Event Types](#event-types)
  - [Requirements](#requirements)
- [List Subscriptions](#list-subscriptions)
  - [Endpoint](#endpoint)
  - [Response](#response)
- [Remove Subscription](#remove-subscription)
  - [Endpoint](#endpoint)
  - [Response](#response)
- [Send Test Event](#send-test-event)
- [Webhook Payload Signing](#webhook-payload-signing)
  - [Signature Header](#signature-header)
  - [Verifying Signatures](#verifying-signatures)
- [Webhook Payloads](#webhook-payloads)
  - [Event Risk Evaluated](#event-risk-evaluated)
  - [Event Feedback Submitted](#event-feedback-submitted)
  - [Blocklist Entry Added](#blocklist-entry-added)
  - [Blocklist Entry Removed](#blocklist-entry-removed)
- [Retries and Backoff](#retries-and-backoff)
  - [Handling Failures](#handling-failures)
  - [Example Handler](#example-handler)
- [Examples](#examples)
  - [cURL](#curl)
  - [Node.js](#nodejs)

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.

### Endpoint

```http
POST /api/v1/webhooks/subscriptions
```

### Request

```json
{
  "url": "https://api.example.com/webhooks",
  "eventTypes": [
    "EVENT_RISK_EVALUATED",
    "EVENT_FEEDBACK_SUBMITTED"
  ]
}
```

### Response

```json
{
  "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.

### Endpoint

```http
GET /api/v1/webhooks/subscriptions
```

### Response

```json
{
  "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.

### Endpoint

```http
DELETE /api/v1/webhooks/subscriptions/:id
```

### Response

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

## Send Test Event

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

Example response:

```json
{
  "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

```http
X-Webhook-Signature: sha256=<signature>
X-Webhook-Event-Type: EVENT_RISK_EVALUATED
```

### Verifying Signatures

#### Node.js

```javascript
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...
});
```
#### Python

```python
import hmac
import hashlib

def verify_webhook_signature(raw_body, signature, secret):
    """Verify HMAC-SHA256 signature against the raw request body bytes."""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    provided_signature = signature.replace('sha256=', '')

    return hmac.compare_digest(expected_signature, provided_signature)

# Flask example
@app.route('/webhooks/decisions', methods=['POST'])
def webhook_handler():
    signature = request.headers.get('X-Webhook-Signature')
    secret = os.getenv('WEBHOOK_SIGNING_SECRET')

    # IMPORTANT: Use raw body bytes for signature verification,
    # then parse JSON only after verification succeeds.
    raw_body = request.get_data()

    if not verify_webhook_signature(raw_body, signature, secret):
        return jsonify({'error': 'Invalid signature'}), 401

    payload = request.get_json()
    # Process webhook...
    return jsonify({'status': 'ok'}), 200
```

## Webhook Payloads

### Event Risk Evaluated

```json
{
  "eventType": "EVENT_RISK_EVALUATED",
  "timestamp": "2026-04-16T10:30:00.000Z",
  "data": {
    "id": "evt_ckm9876543210",
    "decision": "BLOCK",
    "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```javascript
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

### cURL

```bash
# 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_..."
```

### Node.js

```javascript
// Create subscription
const response = await fetch('https://api.naiza.ai/api/v1/webhooks/subscriptions', {
  method: 'POST',
  headers: {
    'x-api-key': 'naiza_api_sk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://api.example.com/webhooks',
    eventTypes: ['EVENT_RISK_EVALUATED', 'EVENT_FEEDBACK_SUBMITTED'],
  }),
});

const subscription = await response.json();
console.log(`Created subscription: ${subscription.id}`);
```

## Related documentation

- [API Overview](https://naiza.ai/docs/api-reference/overview.md) — Base URL, versioning, and high-level API surface.
- [Authentication](https://naiza.ai/docs/api-reference/authentication.md) — API keys, Web SDK tokens, and secure key handling.
- [Web SDK API](https://naiza.ai/docs/api-reference/websdk.md) — Browser SDK endpoints and device signal collection.
- [Events API](https://naiza.ai/docs/api-reference/events.md) — Submit and query product events for risk evaluation.
- [Sessions API](https://naiza.ai/docs/api-reference/sessions.md) — Session grouping and timeline endpoints.
- [Error Handling](https://naiza.ai/docs/api-reference/errors.md) — Error shapes, status codes, and retry guidance.
- [Rate Limiting](https://naiza.ai/docs/api-reference/rate-limiting.md) — Quota headers and rate-limit behavior.
- [Decisions API](https://naiza.ai/docs/api-reference/decisions.md) — approve / deny / review evaluation and decision payloads (Events API uses ALLOW/REVIEW/BLOCK).

---

*Source: [https://naiza.ai/docs/api-reference/webhooks](https://naiza.ai/docs/api-reference/webhooks) · Full docs: [https://naiza.ai/docs.md](https://naiza.ai/docs.md)*
