Webhooks
Der Dienst informiert Ihren Server mit zwei signierten Webhooks über Bestellungen. Die Zustellung erfolgt mindestens einmal, daher prüft Ihr Empfänger die Signatur, dedupliziert anhand der Ereignis-ID und antwortet schnell.
Pfade, Feldnamen, Fehlercodes, Header-Namen und Codebeispiele bleiben englisch: Sie sind der Vertrag.
Ereignisse
Abonnieren Sie, indem Sie channels.webhooks[] in Ihrer Shop-Konfiguration veröffentlichen (eine id, eine url und die events). Die URL muss ein öffentliches https sein; private, Loopback- und interne Adressen werden beim Speichern der Konfiguration und erneut beim Senden abgewiesen.
| Ereignis | Gesendet, wenn |
|---|---|
| order.created | Eine Live-Bestellung wurde gespeichert (POST /cfg/v1/order). |
| order.status_changed | Sie haben eine Bestellung in der Konsole in einen anderen Status versetzt. |
Der Body
Der Body ist kanonisches JSON (RFC 8785) und bei jedem Versuch byteidentisch. data.order ist das vollständige signierte Dokument order.v1; data.fromStatus gibt es nur bei order.status_changed.
{
"createdAt": "2026-10-19T09:30:00.000Z",
"data": {
"fromStatus": "received",
"order": {
"…": "order.v1"
},
"orderId": "<order id>",
"status": "confirmed"
},
"id": "<event id>",
"schemaVersion": 1,
"tenantId": "<tenant id>",
"type": "order.status_changed"
}Header
Jede Zustellung trägt fünf Header. Deduplizieren Sie anhand von CutOptim-Event-Id: Eine Wiederholung und eine erneute Zustellung wiederholen sie, während sich CutOptim-Delivery-Id unterscheidet.
| Header-Feld | Beschreibung |
|---|---|
| CutOptim-Signature | Der Zeitstempel und eine oder zwei HMAC-SHA256-Signaturen des rohen Bodys. |
| CutOptim-Event-Id | Die Ereignis-ID, gleich der ID des Bodys. Deduplizieren Sie anhand davon. |
| CutOptim-Event-Type | Der Ereignisname. |
| CutOptim-Delivery-Id | Diese Zustellung; eine Wiederholung oder erneute Zustellung erhält eine neue. |
| CutOptim-Delivery-Attempt | Die Nummer des Versuchs, beginnend bei 1. |
Die Signatur prüfen
Die Signatur lautet t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. Nehmen Sie die Zustellung an, wenn irgendein v1-Wert zu einem Ihrer Secrets passt und t höchstens 300 Sekunden von Ihrer Uhr abweicht; vergleichen Sie in konstanter Zeit. Prüfen Sie die rohen Bytes, bevor Sie sie parsen: Ein Framework, das den Body parst und neu serialisiert, verändert ihn, und jede Prüfung schlägt fehl.
Die zwei Empfänger unten sind die unveränderten Referenzbeispiele: Node 18+ nur mit node:crypto und PHP 7.4+ ohne Composer. Beide lesen das Secret aus CUTOPTIM_WEBHOOK_SIGNING.
configurator/adapters/reference/node/example-server.mjs
// example-server.mjs - a webhook endpoint on node:http, to read and adapt. CUTOPTIM_WEBHOOK_SIGNING=whsec_... node example-server.mjs
// (whsec_REPLACE_ME is a placeholder: the real secret comes from the CutOptim admin route.)
import { createServer } from 'node:http';
import { createWebhookReceiver } from './cutoptim-verify.mjs';
const secret = process.env.CUTOPTIM_WEBHOOK_SIGNING;
if (!secret) { console.error('set CUTOPTIM_WEBHOOK_SIGNING (whsec_REPLACE_ME is a placeholder)'); process.exit(1); }
const receive = createWebhookReceiver({
secrets: [secret],
async onEvent(event) {
// event.type === 'order.created'; event.data.order is the signed order document. Store it in YOUR order table here,
// keyed by event.data.orderId, in the same transaction that records the event id as seen.
console.log('order.created', event.data.orderId);
},
});
createServer(async (req, res) => {
if (req.method !== 'POST') { res.writeHead(405).end(); return; }
const chunks = []; let size = 0;
for await (const c of req) { size += c.length; if (size > 2_000_000) { res.writeHead(413).end(); return; } chunks.push(c); }
const out = await receive({ rawBody: Buffer.concat(chunks), headers: req.headers }); // the RAW bytes, not parsed JSON
res.writeHead(out.status, { 'Content-Type': 'application/json' }).end(JSON.stringify({ result: out.reason }));
}).listen(Number(process.env.PORT ?? 8787));
configurator/adapters/reference/php/example-webhook.php
<?php
/**
* example-webhook.php - a webhook endpoint in plain PHP, to read and adapt.
* CUTOPTIM_WEBHOOK_SIGNING=whsec_... is read from the environment (whsec_REPLACE_ME is a placeholder; the real secret comes from the
* CutOptim admin route). Replace MemorySeen with a table that has a UNIQUE key on the event id.
*/
require __DIR__ . '/CutOptimVerify.php';
use CutOptim\CutOptimVerify;
use CutOptim\MemorySeen;
$secret = getenv('CUTOPTIM_WEBHOOK_SIGNING');
if ($secret === false || $secret === '') {
http_response_code(500);
exit('CUTOPTIM_WEBHOOK_SIGNING is not set');
}
$rawBody = file_get_contents('php://input'); // the RAW bytes, not decoded JSON
$headers = function_exists('getallheaders') ? getallheaders() : [];
$out = CutOptimVerify::handleWebhook($rawBody, $headers, [$secret], new MemorySeen(), static function (array $event): void {
// $event['type'] === 'order.created'; $event['data']['order'] is the signed order document. Store it in YOUR order table
// keyed by $event['data']['orderId'], in the same transaction that records the event id as seen.
});
http_response_code($out['status']);
header('Content-Type: application/json');
echo json_encode(['result' => $out['reason']]);
Wiederholungen, Dead Letters und erneute Zustellung
Eine Antwort außerhalb von 2xx, ein Timeout und eine Weiterleitung gelten als fehlgeschlagene Versuche. Eine Zustellung wird insgesamt 6 Mal versucht, mit wachsenden Wartezeiten; nach dem letzten Fehlschlag ist sie ein Dead Letter. Eine URL, die auf eine private Adresse auflöst, wird sofort zum Dead Letter. Ihr Server kann das Zustellprotokoll lesen und mit dem geheimen Schlüssel eine erneute Zustellung anfordern (/cfg/v1/admin/deliveries); eine erneute Zustellung behält die Ereignis-ID.
| Nach Versuch | Nächster Versuch in |
|---|---|
| 1 | 1 min |
| 2 | 5 min |
| 3 | 25 min |
| 4 | 2.08 h |
| 5 | 6 h |
Testbestellungen
Eine mit einem Testschlüssel aufgegebene Bestellung reiht weder Webhook noch E-Mail ein: Ihr ERP und Ihre Kunden sehen nie Testverkehr.
Eine Schaltfläche "Ausprobieren" gibt es hier mit Absicht nicht: Der geheime Schlüssel gehört auf Ihren Server, nie in einen Browser. Der Demo-Shop zeigt Einbettung und Bestellübergabe von Anfang bis Ende.
RechtlichesKonfigurator-Bedingungen (EN) ·Auftragsverarbeitungsvertrag (EN) ·Datenschutzhinweis für Käufer ·Drittanbieter-Lizenzen (EN)