Webhooks -- Integrationsleitfaden (Empfänger-Seite)

Webhooks -- Integrationsleitfaden (Empfänger-Seite)

Webhooks -- Integrationsleitfaden (Empfänger-Seite)

Dieses Dokument beschreibt, wie ein externes System (ERP, n8n, Eigenentwicklung) die Webhooks empfängt und verarbeitet.

HTTP-Request Format

Jeder Webhook wird als HTTP POST gesendet:

POST https://dein-system.example.com/webhook Content-Type: application/json X-Shop-Event: order.created X-Shop-Signature: t=1712345678,v1=a1b2c3d4e5... X-Idempotency-Key: sha256-hash Authorization: Basic dXNlcjpwYXNz (optional, je nach Konfiguration)

Authentifizierung

Neben der HMAC-Signatur (die immer mitgesendet wird) kann pro Endpunkt eine zusätzliche Authentifizierung konfiguriert werden. Das Zielsystem erhält dann einen entsprechenden Header:

Methode

Header

Beispiel

Methode

Header

Beispiel

Keine

Nur X-Shop-Signature

Basic Auth

Authorization: Basic {base64}

Authorization: Basic dXNlcjpwYXNz

Bearer Token

Authorization: Bearer {token}

Authorization: Bearer sk-abc123

Custom Header (API Key)

{Name}: {Wert}

X-Api-Key: mein-geheimer-schluessel

Empfangenen Auth-Header prüfen

// Beispiel: API Key via Custom Header prüfen $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? ''; if ($apiKey !== 'mein-geheimer-schluessel') { http_response_code(401); exit('Unauthorized'); }
// Beispiel: Bearer Token in n8n/Node.js prüfen const authHeader = req.headers['authorization'] || ''; const token = authHeader.replace('Bearer ', ''); if (token !== process.env.WEBHOOK_TOKEN) { res.status(401).send('Unauthorized'); return; }

Die Authentifizierung ersetzt nicht die Signatur-Verifikation — beide sollten geprüft werden. Die Signatur stellt sicher, dass der Payload nicht manipuliert wurde, die Authentifizierung schützt den Endpunkt vor unbefugtem Zugriff.

Signatur verifizieren

Jeder Request wird mit HMAC-SHA256 signiert. Die Signatur steht im Header X-Shop-Signature im Format:

X-Shop-Signature: t=<timestamp>,v1=<signature>

Verifikations-Algorithmus

  1. Extrahiere timestamp und signature aus dem Header

  2. Baue den signierten String: {timestamp}.{request_body}

  3. Berechne HMAC-SHA256 mit dem Endpunkt-Secret

  4. Vergleiche die berechnete Signatur mit v1 aus dem Header

Beispielcode (PHP)

function verifyWebhookSignature(string $payload, string $signatureHeader, string $secret): bool { // Header parsen: "t=123456,v1=abcdef" $parts = []; foreach (explode(',', $signatureHeader) as $part) { [$key, $value] = explode('=', $part, 2); $parts[$key] = $value; } $timestamp = $parts['t'] ?? ''; $signature = $parts['v1'] ?? ''; // Replay-Schutz: Timestamp darf nicht älter als 5 Minuten sein if (abs(time() - (int) $timestamp) > 300) { return false; } $expectedSignature = hash_hmac('sha256', $timestamp . '.' . $payload, $secret); return hash_equals($expectedSignature, $signature); } // Verwendung $payload = file_get_contents('php://input'); $signatureHeader = $_SERVER['HTTP_X_SHOP_SIGNATURE'] ?? ''; $secret = 'dein-endpunkt-secret'; // aus der Webhook-Konfiguration if (!verifyWebhookSignature($payload, $signatureHeader, $secret)) { http_response_code(401); exit('Invalid signature'); } $data = json_decode($payload, true); // Webhook verarbeiten...

Beispielcode (Node.js)

const crypto = require('crypto'); function verifyWebhookSignature(payload, signatureHeader, secret) { const parts = {}; signatureHeader.split(',').forEach(part => { const [key, value] = part.split('=', 2); parts[key] = value; }); const timestamp = parts.t || ''; const signature = parts.v1 || ''; // Replay-Schutz if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) { return false; } const expected = crypto .createHmac('sha256', secret) .update(timestamp + '.' + payload) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); }

Beispielcode (Python)

import hmac import hashlib import time def verify_webhook_signature(payload: str, signature_header: str, secret: str) -> bool: parts = dict(p.split('=', 1) for p in signature_header.split(',')) timestamp = parts.get('t', '') signature = parts.get('v1', '') # Replay-Schutz if abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new( secret.encode(), f"{timestamp}.{payload}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)

Idempotency

Jeder Webhook-Versand hat einen eindeutigen X-Idempotency-Key Header. Da Webhooks bei Fehlern wiederholt werden, kann derselbe Event mehrfach ankommen.

Empfehlung: Speichere verarbeitete Idempotency-Keys und überspringe Duplikate:

$idempotencyKey = $_SERVER['HTTP_X_IDEMPOTENCY_KEY'] ?? ''; if ($db->exists('processed_webhooks', ['key' => $idempotencyKey])) { http_response_code(200); // Erfolgreich, aber bereits verarbeitet exit; } // Webhook verarbeiten... $db->insert('processed_webhooks', ['key' => $idempotencyKey, 'processed_at' => now()]);

Test-Dispatches erkennen

Manuell ausgelöste Events (über den Test-Bereich im Backend) enthalten das Feld "_test": true im Payload:

{ "event": "order.created", "_test": true, "data": { ... } }

Das Zielsystem kann dieses Feld prüfen um Test-Aufrufe anders zu behandeln (z.B. nicht in Produktionsdaten übernehmen).

Antwort-Verhalten

HTTP-Statuscode

Interpretation

HTTP-Statuscode

Interpretation

2xx (200, 201, 204)

Erfolgreich zugestellt

3xx

Wird als Fehler gewertet (kein Redirect-Follow)

4xx

Fehler, wird wiederholt

5xx

Fehler, wird wiederholt

Timeout (> 30s)

Fehler, wird wiederholt

Wichtig: Antworte mit HTTP 200 so schnell wie möglich. Verarbeite die Daten asynchron, wenn die Verarbeitung länger dauert.

Retry-Verhalten

Bei einem Fehler wird der Webhook wiederholt:

Versuch

Wartezeit

Zeitpunkt (ab erstem Versuch)

Versuch

Wartezeit

Zeitpunkt (ab erstem Versuch)

2

1 Minute

nach 1 Min

3

5 Minuten

nach 6 Min

4

15 Minuten

nach 21 Min

5

1 Stunde

nach 1h 21min

Nach 5 fehlgeschlagenen Versuchen wird der Webhook als failed markiert.

Nach 10 aufeinanderfolgenden Fehlern (über mehrere Events hinweg) wird der Endpunkt automatisch deaktiviert (Circuit Breaker). Der Shop-Administrator wird per notification.webhook_disabled-Event an andere Endpunkte benachrichtigt.

Event-Routing

Der Event-Name steht im Header X-Shop-Event und im Payload-Feld event. Verwende diesen um zu entscheiden, wie die Daten verarbeitet werden:

$event = $data['event']; switch ($event) { case 'order.created': handleNewOrder($data['data']); break; case 'order.cancelled': handleCancellation($data['data']); break; case 'order.line_item.created': handleItemReleased($data['data']); break; case 'draft_order.created': handleDraftOrder($data['data']); break; case 'approval.line_item.requested': // Position wartet ab jetzt auf eine Freigabe; $data['data']['approval']['type'] // sagt, um welche Freigabeart es sich handelt handleApprovalRequested($data['data']); break; case 'approval.line_item.completed': case 'approval.line_item.step_rejected': // Endzustand einer Positionsfreigabe; $data['data']['approval']['log'] // enthält die komplette Freigabe-Historie der Position handleApprovalFinished($data['data']); break; case 'webhook.test': // Test-Ping, nichts tun break; default: // Unbekanntes Event -- loggen und ignorieren break; }

n8n Workflow erstellen

Webhook-Node einrichten

  1. Erstelle einen neuen Workflow in n8n

  2. Füge einen Webhook-Node hinzu

  3. Setze die Methode auf POST

  4. Kopiere die generierte URL und trage sie im Shop-Backend als Endpunkt-URL ein

  5. Setze den Pfad z.B. auf /shop-webhook

Signatur-Verifikation in n8n

Füge nach dem Webhook-Node einen Code-Node hinzu:

const crypto = require('crypto'); const signature = $input.first().headers['x-shop-signature']; const body = JSON.stringify($input.first().json); const secret = 'DEIN_SECRET_HIER'; const parts = {}; signature.split(',').forEach(p => { const [k, v] = p.split('=', 2); parts[k] = v; }); const expected = crypto .createHmac('sha256', secret) .update(parts.t + '.' + body) .digest('hex'); if (expected !== parts.v1) { throw new Error('Invalid webhook signature'); } return $input.all();

Event-basiertes Routing in n8n

Füge einen Switch-Node hinzu, der auf {{ $json.event }} prüft:

  • order.created -> Bestellung in ERP anlegen

  • order.line_item.created -> Position wurde freigegeben

  • draft_order.created -> Bestellung vormerken (wartet auf Zahlung/Freigabe)

  • approval.line_item.requested -> Position als „wartet auf Freigabe" vormerken, noch nicht produzieren (data.approval.type unterscheidet die Freigabeart)

  • approval.line_item.completed / approval.line_item.step_rejected -> Freigabe-Historie ins Reporting übernehmen (data.approval.log)

  • approval.line_item.escalated -> Zuständigkeitswechsel im Zielsystem nachziehen (data.approval.escalation.recipients)

  • approval.line_item.print_data_changed / article_changed / options_changed / workflow_changed -> Positionsdaten im Zielsystem aktualisieren; die Zusatzobjekte (printDataChange, articleChange, optionsChange, workflowChange) liefern den Vorher-Nachher-Vergleich

Checkliste für die Integration

  • [ ] HTTPS-Endpunkt eingerichtet

  • [ ] Signatur-Verifikation implementiert

  • [ ] Idempotency-Prüfung eingebaut

  • [ ] Schnelle Antwort (< 5 Sekunden, idealerweise < 1 Sekunde)

  • [ ] Asynchrone Verarbeitung für aufwendige Operationen

  • [ ] Unbekannte Events werden ignoriert (nicht als Fehler gewertet)

  • [ ] _test-Flag wird geprüft um Test-Dispatches zu erkennen

  • [ ] Logging für Debugging eingerichtet

  • [ ] Test-Ping im Backend erfolgreich gesendet