← LumenSign API reference English version

Webhooks empfangen und prüfen

Webhooks melden Ihrem System, was passiert ist – ein Vorgang wurde unterschrieben, ein Dokument ist geprüft, ein Zertifikat läuft ab. Dieser Guide zeigt, wie Sie ein Abo anlegen, jede Zustellung prüfen und Ereignisse genau einmal verarbeiten. Die vollständige Liste der Ereignisse mit Body und Beispiel steht in der API-Referenz im Abschnitt „Webhooks“ und unter GET /v1/event-types.

1. Abo anlegen

curl -X POST "$BASE_URL/v1/webhook-subscriptions" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: crm-abo-1' \
  -d '{ "url": "https://example.com/hooks/signatur", "events": ["envelope.completed", "recipient.*"], "description": "CRM" }'

Die Antwort (201) enthält secret (whsec_…) nur dieses eine Mal. Haben Sie es verloren, erzeugen Sie mit POST /v1/webhook-subscriptions/{id}/rotate-secret ein neues.

Zum Ausprobieren schickt POST /v1/webhook-subscriptions/{id}/test eine signierte Test-Zustellung ("test": true im Body).

2. Was ankommt

Jede Zustellung ist ein POST mit JSON-Body:

{
  "id": "0192f1c2-7a10-7c3e-9d2b-3f0a1b2c3d4e",
  "object": "event",
  "type": "envelope.completed",
  "event": "envelope.completed",
  "apiVersion": "1",
  "createdAt": "2026-11-03T09:15:02.113Z",
  "orgId": "0190a000-0000-7000-8000-000000000001",
  "mode": "TEST",
  "subscriptionId": "0192e0aa-0000-7000-8000-00000000000a",
  "context": { "envelopeId": "0192f1b0-0000-7000-8000-000000000101", "externalId": "crm-4711" },
  "data": { "object": "envelope", "id": "0192f1b0-0000-7000-8000-000000000101", "status": "COMPLETED" }
}

data ist das Objekt der passenden GET-Route zum Zeitpunkt des Ereignisses (hier gekürzt), context nennt die Bezüge und Ihre externalId, previous (falls vorhanden) die geänderten Felder mit ihrem alten Wert.

Dazu kommen zwei Sätze von Signatur-Headern über denselben Roh-Body. Es genügt, einen davon zu prüfen:

HeaderInhalt
webhook-idEreignis-ID (gleich id), bei jedem Versuch gleich
webhook-timestampUnix-Sekunden dieses Versuchs
webhook-signaturev1,<Base64 HMAC-SHA256> über <webhook-id>.<webhook-timestamp>.<Roh-Body>; Schlüssel: die Base64-dekodierten Bytes nach whsec_
X-QGW-Signaturet=<Unix-Sekunden>,v1=<Hex HMAC-SHA256> über <t>.<Roh-Body>; Schlüssel: der ganze Text whsec_…
X-QGW-Delivery-IdID dieser Zustellung (je Abo, bei jedem Versuch gleich)
X-QGW-EventName des Ereignisses

Während einer Secret-Rotation tragen beide Signatur-Header je einen Wert pro gültigem Secret; akzeptieren Sie die Anfrage, wenn einer passt.

3. Signatur prüfen

Prüfen Sie immer gegen den Roh-Body (die empfangenen Bytes, bevor JSON geparst wird), lehnen Sie Zeitstempel ab, die älter als fünf Minuten sind, und vergleichen Sie in konstanter Zeit.

Node.js (Standard Webhooks)

import express from 'express';
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(process.env.WEBHOOK_SECRET!); // whsec_…
const app = express();

app.post('/hooks/signatur', express.raw({ type: 'application/json' }), async (req, res) => {
  let event: { id: string; type: string };
  try {
    event = wh.verify(req.body, {
      'webhook-id': req.header('webhook-id') ?? '',
      'webhook-timestamp': req.header('webhook-timestamp') ?? '',
      'webhook-signature': req.header('webhook-signature') ?? '',
    }) as { id: string; type: string };
  } catch {
    return res.status(400).end();
  }
  if (await alreadyProcessed(event.id)) return res.status(204).end();
  await enqueue(event); // Arbeit im Hintergrund
  res.status(204).end();
});

Python (X-QGW-Signature, nur Standardbibliothek)

import hashlib, hmac, time

def verify_qgw_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    timestamp, signatures = None, []
    for part in (header or "").split(","):
        key, _, value = part.partition("=")
        if key == "t" and value.isdigit():
            timestamp = int(value)
        elif key == "v1" and value:
            signatures.append(value)
    if timestamp is None or not signatures or abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, sig) for sig in signatures)

In Flask liefert request.get_data() den Roh-Body, in FastAPI await request.body(). Für den Header-Satz webhook-* gibt es die offiziellen Standard-Webhooks-Bibliotheken auch für Python (pip install standardwebhooks), Go, Java und weitere Sprachen.

4. Genau einmal verarbeiten

5. Wiederholungen und Fehler

Jede andere Antwort, ein Timeout oder ein Netzwerkfehler führt zu weiteren Versuchen (bei Abos nach etwa 1, 5, 15 und 60 Minuten, 3 und 6 Stunden, danach alle 12 Stunden, bis 72 Stunden nach der Anlage; jeder Versuch zu einem zufälligen Zeitpunkt in der zweiten Hälfte seines Schritts; danach, oder wenn eine zurückgehaltene Zustellung die 72 Stunden erreicht, gilt sie als fehlgeschlagen). Weiterleitungen (3xx) werden nicht verfolgt und zählen als Fehler. 410 Gone beendet die Zustellung und schaltet das Abo ab.

Das Zustellprotokoll lesen Sie mit GET /v1/webhook-subscriptions/{id}/deliveries und GET /v1/webhook-deliveries/{id}; POST /v1/webhook-deliveries/{id}/retry stellt eine Zustellung erneut zu (gleiche webhook-id). Was Ihr Endpunkt verpasst hat, finden Sie für 30 Tage unter GET /v1/events.

Nach 10 endgültig gescheiterten Zustellungen in Folge über mindestens 24 Stunden wird das Abo pausiert; PATCH /v1/webhook-subscriptions/{id} mit { "state": "ACTIVE" } setzt es fort.