Webhooks

Der volle Auftrags-Lebenszyklus als signierte Push-Benachrichtigungen — statt Polling.

Webhooks benachrichtigen Ihr System über jeden relevanten Statuswechsel eines Auftrags. Die Verwaltung erfordert den Scope webhooks:manage; die genauen Endpoint-Pfade stehen in der API-Referenz.

Endpoint anlegen

Sie registrieren eine HTTPS-URL (HTTP wird abgelehnt), optional mit einem Event-Filter (ohne Filter: alle Events). Die Antwort auf das Anlegen enthält das Signatur-Secret — nur dieses eine Mal. Speichern Sie es direkt in Ihrem Secret-Management; es kann später nicht erneut abgerufen, nur der Endpoint neu angelegt werden.

Handshake (Verifizierung)

Beim Anlegen und bei jeder URL-Änderung verifiziert Hemostat den Endpoint, bevor Events zugestellt werden:

  1. Hemostat ruft GET <ihre-url>?verification=<nonce> auf.
  2. Ihr Endpoint antwortet mit 2xx und gibt den Wert von verification im Response-Body zurück (Echo).
  3. Erst danach wechselt der Endpoint von PENDING_VERIFICATION auf ENABLED.

Schlägt der Handshake fehl, können Sie ihn über die Endpoint-Verwaltung erneut anstoßen; außerdem lässt sich ein Beispiel-Event zum Testen der Verarbeitung auslösen (siehe API-Referenz).

Zustellung: Header

Jede Zustellung ist ein POST mit JSON-Body und diesen Headern:

Header Inhalt
X-Hemostat-Signature t=<unix_ts>,v1=<hex_hmac> — Signatur, siehe unten
X-Hemostat-Event Event-Name, z. B. order.delivered
X-Hemostat-Delivery eindeutige Zustellungs-ID — nutzen Sie sie zur Dedupe, dieselbe Zustellung kann im Fehlerfall mehrfach ankommen

Signatur verifizieren

Die Signatur ist HMAC-SHA256 über die Zeichenkette "<t>.<raw_body>" mit Ihrem Endpoint-Secret als Schlüssel (Stripe-Stil):

X-Hemostat-Signature: t=1758100000,v1=5f6a1c9e…

Regeln:

Python

import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300  # 5 Minuten


def verify_signature(secret: str, signature_header: str, raw_body: bytes) -> bool:
    """Verifiziert X-Hemostat-Signature. raw_body ist der unveränderte Request-Body."""
    try:
        parts = dict(p.split("=", 1) for p in signature_header.split(","))
        timestamp = int(parts["t"])
        received = parts["v1"]
    except (ValueError, KeyError):
        return False

    if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False

    signed_payload = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)


# Beispiel (z. B. in einem Flask-Handler):
# ok = verify_signature(WEBHOOK_SECRET, request.headers["X-Hemostat-Signature"], request.get_data())

if __name__ == "__main__":
    secret = "whsec_beispiel"
    body = b'{"event":"order.delivered"}'
    t = int(time.time())
    sig = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    assert verify_signature(secret, f"t={t},v1={sig}", body)
    print("Signatur gültig")

Node.js

const crypto = require("node:crypto");

const TOLERANCE_SECONDS = 300; // 5 Minuten

/**
 * Verifiziert X-Hemostat-Signature.
 * @param {string} secret   Endpoint-Secret
 * @param {string} header   Wert von X-Hemostat-Signature
 * @param {Buffer} rawBody  unveränderter Request-Body (kein re-serialisiertes JSON)
 */
function verifySignature(secret, header, rawBody) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split(/=(.*)/s).slice(0, 2))
  );
  const timestamp = Number.parseInt(parts.t, 10);
  const received = parts.v1;
  if (!Number.isFinite(timestamp) || !received) return false;

  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(received, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Beispiel Express: Raw-Body sichern, damit die Signatur stimmt.
// app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
// const ok = verifySignature(SECRET, req.get("X-Hemostat-Signature"), req.rawBody);

// Selbsttest
const secret = "whsec_beispiel";
const body = Buffer.from('{"event":"order.delivered"}');
const t = Math.floor(Date.now() / 1000);
const sig = crypto.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex");
console.log(verifySignature(secret, `t=${t},v1=${sig}`, body) ? "Signatur gültig" : "FEHLER");

Event-Typen

Event Bedeutung
order.submitted Der Auftrag wurde eingereicht und ist im System angelegt.
order.assigned Der Auftrag wurde einer Transport-Organisation zugewiesen.
order.accepted Die Transport-Organisation hat den Auftrag angenommen.
order.driver_assigned Dem Auftrag wurde ein Fahrer zugeteilt.
order.pickup_started Die Abholung am Abholort hat begonnen.
order.sealed Das Transportgut wurde versiegelt und das Siegel dokumentiert.
order.in_transit Der Transport ist unterwegs zum Zustellort.
order.delivered Der Auftrag wurde zugestellt.
order.closed Der Auftrag ist abgeschlossen.
order.cancelled Der Auftrag wurde storniert.
order.updated Auftragsdaten wurden geändert.
order.incident_reported Zu dem Auftrag wurde ein Vorkommnis gemeldet.
order.escalated Der Auftrag wurde eskaliert.

Payload

Payloads sind bewusst schlank: eine Zusammenfassung plus Link. data.order enthält die Kernfelder des Auftrags (inklusive Ihrer external_id), links.order den API-Link zum vollständigen Auftrag. Positions- und Nachweisdaten sind nicht enthalten — holen Sie Details bei Bedarf über die API (Datensparsamkeit, falls eine Endpoint-URL kompromittiert wird).

Beispiel (Feldbestand kann additiv wachsen, siehe Versionierung):

{
  "event": "order.delivered",
  "occurred_at": "2026-09-17T10:41:23Z",
  "data": {
    "order": {
      "id": "6f1c9e2a-…",
      "external_id": "BESTELLUNG-4711",
      "status": "delivered"
    }
  },
  "links": {
    "order": "https://my.hemostat.de/api/partner/v1/orders/6f1c9e2a-…"
  }
}

Antworten: die Immer-200-Regel

Antworten Sie auf jede Zustellung schnell mit 2xx — auch wenn Sie das Event fachlich nicht interessiert oder Sie es als Duplikat erkennen. Nehmen Sie das Event entgegen, bestätigen Sie es, und verarbeiten Sie es asynchron. Jede Nicht-2xx-Antwort (und jeder Timeout) zählt als Fehlschlag und löst Wiederholungen aus.

Retry-Verhalten und Status BROKEN