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 |
|---|---|---|
Keine | – | Nur |
Basic Auth |
|
|
Bearer Token |
|
|
Custom Header (API Key) |
|
|
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
Extrahiere
timestampundsignatureaus dem HeaderBaue den signierten String:
{timestamp}.{request_body}Berechne HMAC-SHA256 mit dem Endpunkt-Secret
Vergleiche die berechnete Signatur mit
v1aus 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 |
|---|---|
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) |
|---|---|---|
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
Erstelle einen neuen Workflow in n8n
Füge einen Webhook-Node hinzu
Setze die Methode auf POST
Kopiere die generierte URL und trage sie im Shop-Backend als Endpunkt-URL ein
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 anlegenorder.line_item.created-> Position wurde freigegebendraft_order.created-> Bestellung vormerken (wartet auf Zahlung/Freigabe)approval.line_item.requested-> Position als „wartet auf Freigabe" vormerken, noch nicht produzieren (data.approval.typeunterscheidet 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