Webhooks
Receive real-time notifications when document statuses change.
Bizzlink can send HTTP POST requests to your server whenever a document status changes. This allows you to react in real time — e.g. update your ERP, send a notification, or trigger a workflow.
Configuration
All webhook endpoints follow the JSON:API specification. Requests and responses use Content-Type: application/vnd.api+json.
Create a Webhook
Webhooks are tenant-scoped — a webhook receives events for all documents belonging to your tenant.
curl -X POST https://gateway.vigasoft.lu/bizzlink/webhooks \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "webhooks",
"attributes": {
"url": "https://your-server.com/webhooks/bizzlink",
"events": ["bizzlink.document.status_changed", "bizzlink.document.received"]
}
}
}'
The response contains a signing secret generated by Bizzlink (128 hex characters) — store it securely. You cannot set your own secret; Bizzlink always generates it for you.
{
"data": {
"type": "webhooks",
"id": "550e8400-e29b-41d4-a716-446655440000",
"attributes": {
"enabled": true,
"url": "https://your-server.com/webhooks/bizzlink",
"secret": "whsec_9f3a7b2e1c4d8f0a6b5e3d7c...",
"events": ["bizzlink.document.status_changed", "bizzlink.document.received"],
"createdAt": "2026-03-22T10:00:00Z",
"updatedAt": "2026-03-22T10:00:00Z"
}
}
}
Available Events
| Event | Description |
|---|---|
bizzlink.document.status_changed | Fired when an outbound document changes status (SUBMITTED, DELIVERED, FAILED) |
bizzlink.document.received | Fired when an inbound document is received via Peppol |
List Webhooks
curl https://gateway.vigasoft.lu/bizzlink/webhooks
Returns all webhooks as a JSON:API collection.
Update a Webhook
Use PATCH with the webhook ID. Only include the attributes you want to change:
curl -X PATCH https://gateway.vigasoft.lu/bizzlink/webhooks/550e8400-... \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "webhooks",
"attributes": {
"enabled": false
}
}
}'
Delete a Webhook
curl -X DELETE https://gateway.vigasoft.lu/bizzlink/webhooks/550e8400-...
Secret Rotation
Rotate your signing secret at any time:
curl -X POST https://gateway.vigasoft.lu/bizzlink/webhooks/550e8400-.../actions/rotate-secret
The old secret is immediately invalidated. Update your verification code before rotating.
Testing
Send a test webhook to verify your endpoint and HMAC signature verification:
curl -X POST https://gateway.vigasoft.lu/bizzlink/webhooks/550e8400-.../actions/test
{
"success": true,
"httpStatus": 200,
"message": "Test webhook delivered successfully"
}
The test webhook is a real HTTP POST with a valid HMAC signature. The event type is bizzlink.webhook.test — your endpoint should accept it and return a 2xx status.
Payload Structure
The webhook POST body follows the JSON:API format. data carries the document resource; top-level meta carries the event envelope.
{
"data": {
"type": "documents",
"id": "770e8400-e29b-41d4-a716-446655440000",
"attributes": {
"documentType": "INVOICE",
"invoiceNumber": "INV-2026-001",
"senderId": "9930:lu12345678",
"receiverId": "0088:7300010000001",
"senderName": "ACME Luxembourg S.A.",
"receiverName": "Example GmbH",
"status": "DELIVERED",
"totalAmount": "1500.00",
"currency": "EUR",
"direction": "SEND"
}
},
"meta": {
"webhookId": "wh_550e8400-e29b-41d4-a716-446655440000",
"eventId": "evt_660e8400-e29b-41d4-a716-446655440000",
"eventType": "bizzlink.document.status_changed",
"apiVersion": "2026-03-01",
"createdAt": "2026-03-21T14:30:00Z",
"service": "bizzlink-api",
"targetUrl": "https://your-server.com/webhooks/bizzlink"
}
}
Inbound Documents
For inbound documents (direction: "RECV"), the attributes include download URLs for the UBL XML and a rendered PDF. Use the Accept header to select the format:
{
"data": {
"type": "documents",
"id": "770e8400-...",
"attributes": {
"direction": "RECV",
"status": "RECEIVED",
"xmlUrl": "/api/v1/documents/770e8400-...",
"pdfUrl": "/api/v1/documents/770e8400-..."
}
}
}
Download with Accept: application/xml for the UBL XML or Accept: application/pdf for the PDF.
Status Values
data.attributes.status uses symbolic names:
| Status | Direction | Meaning |
|---|---|---|
SUBMITTED | SEND | Document submitted to the Peppol network |
DELIVERED | SEND | Successfully delivered to recipient AP |
FAILED | SEND | Delivery failed |
INVALID | SEND | Schematron validation failed (validationErrors populated) |
RECEIVED | RECV | Inbound document received via Peppol |
HTTP Headers
Every webhook request includes these headers:
| Header | Description |
|---|---|
X-Bizzlink-Signature | HMAC-SHA512 signature: hmac-sha512=<hex> |
X-Bizzlink-Timestamp | Unix timestamp (seconds) when the signature was created |
X-Bizzlink-Delivery-Id | Unique delivery ID (use for idempotency). Also exposed as meta.webhookId in the body. |
X-Bizzlink-Event | Event type (e.g. bizzlink.document.status_changed) |
Content-Type | application/json |
Signature Verification
Every webhook is signed with HMAC-SHA512. Always verify the signature to ensure the request is authentic and untampered.
How it works
- Bizzlink concatenates
timestamp + "." + payload - Computes HMAC-SHA512 using your webhook secret as the key
- Sends the result as hex in the
X-Bizzlink-Signatureheader
Verification Examples
Node.js
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-bizzlink-signature'];
const timestamp = req.headers['x-bizzlink-timestamp'];
const payload = req.body; // raw string, not parsed JSON
// 1. Reject old timestamps (> 5 minutes)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) throw new Error('Timestamp too old');
// 2. Compute expected signature
const signatureInput = timestamp + '.' + payload;
const expected = 'hmac-sha512=' + crypto
.createHmac('sha512', secret)
.update(signatureInput)
.digest('hex');
// 3. Compare (constant-time)
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid signature');
}
return JSON.parse(payload);
}
Python
import hmac, hashlib, time
def verify_webhook(headers, body, secret):
signature = headers['X-Bizzlink-Signature']
timestamp = headers['X-Bizzlink-Timestamp']
# 1. Reject old timestamps (> 5 minutes)
if abs(time.time() - int(timestamp)) > 300:
raise ValueError('Timestamp too old')
# 2. Compute expected signature
signature_input = f"{timestamp}.{body}"
expected = 'hmac-sha512=' + hmac.new(
secret.encode(), signature_input.encode(), hashlib.sha512
).hexdigest()
# 3. Compare (constant-time)
if not hmac.compare_digest(signature, expected):
raise ValueError('Invalid signature')
return json.loads(body)
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.HexFormat;
public boolean verifyWebhook(String signature, String timestamp, String payload, String secret) {
// 1. Reject old timestamps (> 5 minutes)
long age = System.currentTimeMillis() / 1000 - Long.parseLong(timestamp);
if (age > 300) return false;
// 2. Compute expected signature
String signatureInput = timestamp + "." + payload;
Mac mac = Mac.getInstance("HmacSHA512");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA512"));
String expected = "hmac-sha512=" + HexFormat.of().formatHex(
mac.doFinal(signatureInput.getBytes()));
// 3. Compare
return signature.equals(expected);
}
Response Requirements
Your webhook endpoint must:
- Return a 2xx status code (e.g.
200 OK,202 Accepted) - Respond within 10 seconds
Any non-2xx response or a timeout beyond 10 seconds is treated as a failure and triggers the retry policy.
200 immediately after receiving the request, then handle the business logic in the background. This prevents timeouts on long-running operations.Retry Policy
If your endpoint fails (non-2xx or timeout), Bizzlink retries:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 60 seconds |
| 3rd retry | 5 minutes |
| 4th retry | 15 minutes |
| 5th+ retries | Every 30 minutes |
Retries continue for up to 48 hours. After that, the delivery is marked as permanently failed.
enabled = false). To re-enable it, use PATCH /webhooks/{id} with "enabled": true.X-Bizzlink-Delivery-Id header (or meta.webhookId in the body) for idempotency — you may receive the same webhook more than once during retries.Security Requirements
- Your webhook endpoint must use HTTPS with TLS 1.3
- TLS 1.2 and below are rejected
- Always verify the HMAC signature before processing
- Check the timestamp to prevent replay attacks (reject if older than 5 minutes)