Developer API
Webhooks
Register a webhook URL when the integration is created, choose the events, verify the HMAC signature on each delivery in Node.js or Python, and understand the retry policy.
Updated 20 September 2026
A webhook turns the API around: instead of your system asking, Kuver Books calls your server the moment something happens. You register an HTTPS URL, choose the events, and each event arrives as an HTTP POST with a signature you can verify.
How a delivery works
- An event happens in Kuver Books, for example an invoice is approved.
- The event is queued and picked up by a dispatcher.
- The dispatcher computes a signature over the payload with your webhook secret and posts the event to your URL.
- If your server answers with a 2xx status within ten seconds, the delivery is complete. Anything else, or a timeout, schedules a retry.
Register a webhook
Webhooks are configured when your integration is created. Alongside the client name and the allowed IP addresses, give webhookUrl, the HTTPS endpoint on your server, and optionally webhookEvents, a comma-separated list of the events you want.
{
"clientName": "ERP Connector",
"allowedIps": "203.0.113.12",
"webhookUrl": "https://my-erp.com/webhook",
"webhookEvents": "billing.invoice.created,billing.invoice.voided"
}
The response carries your client ID and client secret and, because a URL was given, a generated webhookSecret beginning veda_whs_. It is shown once. Store it on your server, for example as an environment variable, straight away; it cannot be read back later.
Events
billing.invoice.created: a sales invoice is approved and created.billing.invoice.voided: a sales invoice is voided.inventory.adjustment.approved: a stock adjustment is approved.
webhookEvents is a filter. Name one event to receive only that; name several, comma separated, to receive those; leave the field out to receive every supported event.
What arrives
Each delivery is a POST with Content-Type: application/json, an x-pos-event header naming the event and an x-pos-signature header carrying the signature.
{
"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"
}
}
Verify the signature
The signature is HMAC-SHA256 of the JSON string of the payload object, not the whole body, keyed with your webhook secret. Recompute it and compare; reject the request if they differ.
Node.js with Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
app.post('/webhook', express.json(), (req, res) => {
const received = req.headers['x-pos-signature'];
const computed = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body.payload))
.digest('hex');
if (received !== computed) {
return res.status(401).send('Invalid signature');
}
console.log('Verified event:', req.headers['x-pos-event'], req.body.payload);
res.status(200).send('OK');
});
Python with Flask:
import hmac
import hashlib
import json
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
@app.route('/webhook', methods=['POST'])
def webhook():
received = request.headers.get('x-pos-signature')
payload = request.json.get('payload')
computed = hmac.new(
WEBHOOK_SECRET.encode(),
json.dumps(payload, separators=(',', ':')).encode(),
hashlib.sha256,
).hexdigest()
if received != computed:
abort(401, 'Invalid signature')
return 'OK', 200
Retries
- Your server must answer with a 2xx status within ten seconds.
- A failed delivery is retried up to five times with exponential backoff, roughly one minute, five minutes, fifteen minutes, one hour and six hours.
- After the fifth failure the event is moved to a dead-letter queue for inspection and the endpoint is flagged.
- Up to five deliveries per organisation run in parallel.
If your server is down for maintenance, answer 503 and the delivery will be retried. If a signature does not match, answer 401; the delivery is treated as failed, and you should look for a secret that has changed.