Webhooks
Le service informe votre serveur des commandes par deux webhooks signés. La livraison est au moins une fois, donc votre récepteur vérifie la signature, déduplique sur l’identifiant d’événement et répond rapidement.
Les chemins, noms de champs, codes d’erreur, noms d’en-têtes et exemples de code restent en anglais : ils font partie du contrat.
Événements
Abonnez-vous en publiant channels.webhooks[] dans la configuration de votre boutique (un id, une url et les events). L’URL doit être un https public ; les adresses privées, de bouclage et internes sont refusées à l’enregistrement de la configuration, puis de nouveau à l’envoi.
| Événement | Envoyé quand |
|---|---|
| order.created | Une commande réelle a été enregistrée (POST /cfg/v1/order). |
| order.status_changed | Vous avez fait passer une commande à un autre statut dans la console. |
Le corps
Le corps est du JSON canonique (RFC 8785) et identique octet pour octet à chaque tentative. data.order est le document order.v1 signé complet ; data.fromStatus n’existe que sur 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"
}En-têtes
Chaque envoi porte cinq en-têtes. Dédupliquez sur CutOptim-Event-Id : une nouvelle tentative et un réenvoi le répètent, alors que CutOptim-Delivery-Id diffère.
| En-tête | Description |
|---|---|
| CutOptim-Signature | L’horodatage et une ou deux signatures HMAC-SHA256 du corps brut. |
| CutOptim-Event-Id | L’identifiant de l’événement, égal à l’identifiant du corps. Dédupliquez dessus. |
| CutOptim-Event-Type | Le nom de l’événement. |
| CutOptim-Delivery-Id | Cet envoi ; une nouvelle tentative ou un réenvoi reçoit un nouvel identifiant. |
| CutOptim-Delivery-Attempt | Le numéro de la tentative, à partir de 1. |
Vérifier la signature
La signature est t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. Acceptez l’envoi lorsqu’une valeur v1 correspond à l’un de vos secrets et que t est à moins de 300 secondes de votre horloge ; comparez en temps constant. Vérifiez les octets bruts avant de les analyser : un framework qui analyse puis resérialise le corps le modifie, et toutes les vérifications échouent.
Les deux récepteurs ci-dessous sont les exemples de référence, tels quels : Node 18+ avec node:crypto uniquement, et PHP 7.4+ sans Composer. Les deux lisent le secret dans 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']]);
Nouvelles tentatives, dead letters et réenvoi
Une réponse non 2xx, un délai dépassé et une redirection sont des tentatives échouées. Un envoi est essayé 6 fois au total, avec des attentes croissantes ; après le dernier échec, il devient une dead letter. Une URL qui se résout vers une adresse privée est immédiatement une dead letter. Votre serveur peut lire le journal des envois et demander un réenvoi avec la clé secrète (/cfg/v1/admin/deliveries) ; un réenvoi conserve l’identifiant d’événement.
| Après la tentative | Prochain essai dans |
|---|---|
| 1 | 1 min |
| 2 | 5 min |
| 3 | 25 min |
| 4 | 2.08 h |
| 5 | 6 h |
Commandes de test
Une commande passée avec une clé de test ne met en file aucun webhook ni e-mail : votre ERP et vos clients ne voient jamais le trafic de test.
Il n’y a volontairement pas de bouton « essayer » ici : la clé secrète appartient à votre serveur, jamais à un navigateur. La boutique de démonstration montre l’intégration et la transmission de la commande de bout en bout.
Mentions légalesConditions du configurateur (EN) ·Accord de traitement des données (EN) ·Notice de confidentialité pour les acheteurs ·Licences tierces (EN)