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

EventDescription
bizzlink.document.status_changedFired when an outbound document changes status (SUBMITTED, DELIVERED, FAILED)
bizzlink.document.receivedFired 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:

StatusDirectionMeaning
SUBMITTEDSENDDocument submitted to the Peppol network
DELIVEREDSENDSuccessfully delivered to recipient AP
FAILEDSENDDelivery failed
INVALIDSENDSchematron validation failed (validationErrors populated)
RECEIVEDRECVInbound document received via Peppol

HTTP Headers

Every webhook request includes these headers:

HeaderDescription
X-Bizzlink-SignatureHMAC-SHA512 signature: hmac-sha512=<hex>
X-Bizzlink-TimestampUnix timestamp (seconds) when the signature was created
X-Bizzlink-Delivery-IdUnique delivery ID (use for idempotency). Also exposed as meta.webhookId in the body.
X-Bizzlink-EventEvent type (e.g. bizzlink.document.status_changed)
Content-Typeapplication/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

  1. Bizzlink concatenates timestamp + "." + payload
  2. Computes HMAC-SHA512 using your webhook secret as the key
  3. Sends the result as hex in the X-Bizzlink-Signature header

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.

Process webhook payloads asynchronously. Return 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:

AttemptDelay
1st retry30 seconds
2nd retry60 seconds
3rd retry5 minutes
4th retry15 minutes
5th+ retriesEvery 30 minutes

Retries continue for up to 48 hours. After that, the delivery is marked as permanently failed.

After 48 hours of continuous delivery failure, Bizzlink automatically disables the webhook (enabled = false). To re-enable it, use PATCH /webhooks/{id} with "enabled": true.
Use the 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)