> ## Documentation Index
> Fetch the complete documentation index at: https://help.kuverbooks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time push notifications from the POS system to your server

Webhooks allow you to register a destination URL and receive HTTP `POST` requests when key events (like invoice creations or stock adjustments) happen in real-time.

***

## Webhook Architecture & Event Delivery Flow

The diagram below shows how events generated in the POS backend are queued in background workers, delivered to your server, and verified via HMAC signatures.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant POS as POS Backend Event System
    participant Queue as BullMQ Message Queue
    participant Worker as Webhook Dispatcher Worker
    participant Client as Client Webhook Endpoint
    
    POS->>Queue: Push Event (e.g., billing.invoice.created)
    Queue->>Worker: Consume Event Job
    Worker->>Worker: Compute x-pos-signature = HMAC-SHA256(webhookSecret, payload)
    Worker->>Client: HTTP POST https://your-server.com/webhook
    
    alt Client Responds 2xx (Success)
        Client-->>Worker: 200 OK
        Worker->>Queue: Mark Job Complete
    else Client Responds Non-2xx or Times Out (> 10s)
        Client-->>Worker: 500 Error / Timeout
        Worker->>Queue: Schedule Retry (Exponential Backoff)
    end
```

***

## Registering a Webhook

When creating an API integration (see [API Key Management](/key-management)), you can optionally provide webhook configuration fields:

| Field           | Type   | Description                                                        |
| :-------------- | :----- | :----------------------------------------------------------------- |
| `webhookUrl`    | string | The HTTPS endpoint on your server that will receive webhook events |
| `webhookEvents` | string | Comma-separated list of event names to subscribe to                |

> \[!IMPORTANT] You do **not** set the webhook secret yourself. When you provide a `webhookUrl`, the server automatically generates a strong `webhookSecret` (prefixed `veda_whs_`) and returns it **only once** in the creation response. Copy it immediately and store it securely on your server — it cannot be retrieved again.

### Example Request

```json theme={null}
{
  "clientName": "ERP Connector",
  "allowedIps": "203.0.113.12",
  "webhookUrl": "https://my-erp.com/webhook",
  "webhookEvents": "billing.invoice.created,billing.invoice.voided"
}
```

### Example Response

```json theme={null}
{
  "data": {
    "clientId": "veda_pub_a935d5f172268cdf...",
    "clientSecret": "veda_sec_cceac1bd9d25bef8...",
    "webhookUrl": "https://my-erp.com/webhook",
    "webhookSecret": "veda_whs_9f3a21bc7e4d8f0c...",
    "webhookEvents": "billing.invoice.created,billing.invoice.voided"
  }
}
```

> Copy `webhookSecret` and store it securely on your server as an environment variable (e.g. `WEBHOOK_SECRET`). You will use it to verify every incoming webhook request.

***

## Supported Events

| Event Name                      | Trigger                                       |
| :------------------------------ | :-------------------------------------------- |
| `billing.invoice.created`       | A sales order invoice is approved and created |
| `billing.invoice.voided`        | A sales order invoice is voided               |
| `inventory.adjustment.approved` | A product stock adjustment is approved        |

### Choosing Which Events to Receive

The `webhookEvents` field is a **subscription filter** — it controls which of the above events are delivered to your endpoint.

| `webhookEvents` value                              | Events you receive         |
| :------------------------------------------------- | :------------------------- |
| `"billing.invoice.created"`                        | Invoice creation only      |
| `"billing.invoice.created,billing.invoice.voided"` | Invoice created and voided |
| `null` / not provided                              | **All supported events**   |

You can subscribe to any combination of the supported events by passing them as a comma-separated string. To receive everything, simply omit the `webhookEvents` field when creating the integration.

***

## Webhook Request

All webhooks are delivered as an HTTP `POST` request to your `webhookUrl` with the following headers and a JSON body.

### Request Headers

| Header            | Value                          | Description                                                                     |
| :---------------- | :----------------------------- | :------------------------------------------------------------------------------ |
| `Content-Type`    | `application/json`             | Always JSON                                                                     |
| `x-pos-event`     | e.g. `billing.invoice.created` | The name of the event that was triggered                                        |
| `x-pos-signature` | `<hex string>`                 | HMAC-SHA256 signature — use this to verify the request came from the POS system |

### Body Structure

```json theme={null}
{
  "event": "billing.invoice.created",
  "payload": {
    "id": "e3b0c442-98fc-1c14-9af4-000000000001",
    "code": "INV-2082-0001",
    "branchId": "44444444-4444-4444-4444-444444444444",
    "resource": "INVOICE",
    "action": "CREATE",
    "timestamp": "2026-07-01T09:00:00.000Z"
  }
}
```

***

## How the Webhook Secret Works

The `webhookSecret` is the shared key between the POS system and your server. Here is the full lifecycle:

**Step 1 — Secret is generated** When you register a `webhookUrl`, the POS system generates a unique `webhookSecret` and returns it in the API response. You store this value on your server (e.g. as an environment variable).

**Step 2 — POS signs the outgoing webhook** Every time the POS system dispatches a webhook to your `webhookUrl`, it computes a signature:

```text theme={null}
x-pos-signature = HMAC-SHA256(webhookSecret, JSON.stringify(payload))
```

This signature is attached to the request as the `x-pos-signature` header.

> \[!NOTE] The signature is computed over the **`payload` object only** — not the entire request body.

**Step 3 — Your server verifies the signature** When your server receives the webhook, it recomputes the same signature using the stored `webhookSecret` and compares it against the `x-pos-signature` header. If they match, the request is authentic. If they don't match, reject it.

***

## Signature Verification Examples

### Node.js (Express)

```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();

// The webhookSecret returned during integration creation — store in an env variable
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // e.g. "veda_whs_9f3a21bc7e4d8f0c..."

app.post('/webhook', express.json(), (req, res) => {
  const receivedSignature = req.headers['x-pos-signature'];
  const event = req.headers['x-pos-event'];

  // Recompute the signature using the payload object (not the full body)
  const computed = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(JSON.stringify(req.body.payload))
    .digest('hex');

  if (receivedSignature !== computed) {
    console.error('Webhook rejected: invalid signature');
    return res.status(401).send('Invalid signature');
  }

  // Signature verified — safe to process the event
  console.log('Verified Webhook Event:', event);
  console.log('Payload:', req.body.payload);

  res.status(200).send('OK');
});
```

### Python (Flask)

```python theme={null}
import hmac
import hashlib
import json
import os
from flask import Flask, request, abort

app = Flask(__name__)

# The webhookSecret returned during integration creation — store in an env variable
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')  # e.g. "veda_whs_9f3a21bc7e4d8f0c..."

@app.route('/webhook', methods=['POST'])
def webhook():
    received_signature = request.headers.get('x-pos-signature')
    payload = request.json.get('payload')

    # Recompute the signature using the payload object
    computed = hmac.new(
        WEBHOOK_SECRET.encode(),
        json.dumps(payload, separators=(',', ':')).encode(),
        hashlib.sha256
    ).hexdigest()

    if received_signature != computed:
        abort(401, 'Webhook rejected: invalid signature')

    print('Verified event:', request.headers.get('x-pos-event'))
    return 'OK', 200
```

***

## Retry Policy & Failure Handling

When an outgoing webhook attempt fails, the system executes an automated retry flow illustrated below:

```mermaid theme={null}
flowchart TD
    Dispatch[Dispatch Webhook HTTP POST] --> CheckResp{HTTP Response Status?}
    CheckResp -->|2xx Status (200, 201, 204)| Success[Delivered Successfully]
    CheckResp -->|Non-2xx Status or Timeout > 10s| CheckAttempts{Retry Attempt < 5?}
    CheckAttempts -->|Yes| Backoff[Wait Exponential Backoff Delay]
    Backoff --> Dispatch
    CheckAttempts -->|No| DLQ[Move Job to Dead-Letter Queue - DLQ]
    DLQ --> Alert[Log Admin Alert & Pause Endpoint]
```

### Delivery Expectations & Errors

| Metric / Error              | Details                                                                                        |
| :-------------------------- | :--------------------------------------------------------------------------------------------- |
| **Timeout Limit**           | Your server must respond with HTTP `2xx` within **10 seconds**.                                |
| **Retries**                 | Retried up to **5 times** using exponential backoff (e.g., 1 min, 5 min, 15 min, 1 hr, 6 hrs). |
| **Dead-Letter Queue (DLQ)** | If all 5 retries fail, the event is moved to the DLQ for manual inspection.                    |
| **Concurrency**             | Up to **5 webhook jobs** per tenant are processed in parallel.                                 |

### Webhook Error Handling Examples

#### 1. Signature Verification Failure on Client Server (`401 Unauthorized`)

If your webhook listener rejects a request due to signature mismatch, return HTTP `401`:

```json theme={null}
{
  "success": false,
  "status": 401,
  "message": "Invalid HMAC signature"
}
```

#### 2. Temporary Server Busy / Down (`503 Service Unavailable`)

If your server is undergoing maintenance, return `503 Service Unavailable`. The POS system will register the delivery failure and retry automatically via exponential backoff.
