Webhooks : recevoir automatiquement les mises à jour de commande
12 min de lectureMis à jour
Avec les webhooks, Printen.eu vous envoie un message dès qu’il se passe quelque chose sur une commande : en attente de solde, en production, expédiée avec suivi, livrée ou annulée. Vous n’avez donc plus à interroger l’API en permanence. Dans cet article, vous ajoutez un endpoint, vérifiez la signature et construisez un traitement qui supporte les répétitions.
Dans cet article
Comment fonctionnent les webhooks ?
Vous nous donnez une URL https de votre propre serveur. À chaque événement, nous y envoyons une requête POST avec un message JSON. Votre serveur répond avec un statut 2xx, et nous savons alors que le message est arrivé.
X-Printen-Event: le type d’événement, par exempleorder.shipped.X-Printen-Delivery: un id de cette livraison. Pratique dans vos journaux.X-Printen-Timestamp: l’heure d’envoi, en secondes depuis 1970 (temps Unix).X-Printen-Signature: la signature (v1=suivi d’un code hexadécimal), qui vous permet de vérifier que le message vient bien de nous.
Comment ajouter un endpoint ?
Ouvrez l’onglet
Dans votre tableau de bord, allez dans Webhooks.
Saisissez votre URL
Dans URL (https), saisissez l’adresse de votre propre serveur, par exemple
https://votredomaine.fr/printen-webhook.Choisissez les événements
Cochez les événements que vous voulez recevoir. Par défaut,
order.shipped,order.payment_failedetorder.awaiting_fundssont cochés.Ajoutez le webhook
Cliquez sur Ajouter un webhook. Le secret s’affiche, avec le message Conservez ce secret maintenant. Il ne s’affichera qu’une fois.
Conservez le secret
Placez aussitôt le secret dans la configuration de votre serveur, par exemple dans une variable d’environnement. Vous en avez besoin pour vérifier la signature.
Vous pouvez aussi ajouter un endpoint via l’API avec POST /webhooks. Si vous omettez events ou envoyez une liste vide, vous recevez tous les événements. Le secret ne figure que dans cette réponse.
curl -X POST https://www.printen.eu/api/reseller/v1/webhooks \
-H "Authorization: Bearer prk_live_..." -H "Content-Type: application/json" \
-d '{"url":"https://example.com/printen-webhook","events":["order.shipped","order.delivered"]}'{
"data": {
"id": "WEBHOOK_ID",
"url": "https://example.com/printen-webhook",
"events": ["order.shipped", "order.delivered"],
"allEvents": false,
"active": true,
"createdAt": "2026-10-10T09:00:00.000Z",
"updatedAt": "2026-10-10T09:00:00.000Z",
"secret": "WEBHOOK_SECRET"
}
}- L’URL doit utiliser https et être accessible publiquement. Les adresses comme
localhost, les adresses de réseau interne et les noms se terminant par.local,.internalou.lansont refusés. - Vous pouvez avoir au maximum 10 endpoints par compte.
- Chaque endpoint a son propre secret.
Quels événements existent ?
| Événement | Quand | Ce que vous faites en général |
|---|---|---|
order.created | Une commande a été créée, via l’API, le tableau de bord ou une boutique connectée. | Enregistrer notre numéro de commande avec votre propre commande. |
order.submitted | La commande est payée et part en production. | Passer la commande à « en cours » dans votre système. |
order.awaiting_funds | Votre solde est insuffisant ; la commande attend. | Vous alerter et recharger. |
order.payment_failed | Le paiement avec votre carte enregistrée a échoué. | Vérifier votre carte et réessayer la commande dans le tableau de bord. |
order.in_production | La production a commencé. | Afficher le statut. L’annulation n’est plus possible à partir de maintenant. |
order.shipped | Le colis est expédié ; tracking contient le numéro et le lien. | Envoyer le suivi à votre client ou l’ajouter dans votre boutique. |
order.delivered | Le colis a été livré. | Clôturer la commande. |
order.cancelled | La commande a été annulée. | Annuler la commande dans votre système et traiter un éventuel remboursement. |
order.file_problem | Il y a un problème avec le fichier d’impression. Nous vous contactons. | Vérifier votre fichier et répondre à notre message. |
À quoi ressemble un message ?
Chaque message a la même structure. Voici un exemple de order.shipped.
{
"id": "EVENT_ID",
"type": "order.shipped",
"created": "2026-10-10T14:32:05.000Z",
"data": {
"order": {
"id": "ORDER_ID",
"orderNumber": "RS-3F9A1C7B2E",
"externalOrderId": "1001",
"externalOrderNumber": "1001",
"state": "SUBMITTED",
"status": "SHIPPED",
"tracking": {
"code": "3SABCD1234567",
"url": "https://example.com/track/3SABCD1234567"
},
"items": [
{
"productId": "PRODUCT_ID",
"name": "Poster",
"quantity": 1,
"options": { "size": "A2" }
}
]
}
}
}| Champ | Signification |
|---|---|
id | Id unique de l’événement. Traitez-le comme un texte sans structure fixe et utilisez-le pour reconnaître les doublons. |
type | Le type d’événement, identique à l’en-tête X-Printen-Event. |
created | Le moment où l’événement s’est produit (ISO 8601). |
data.order.orderNumber | Notre numéro de commande, tel qu’affiché dans le tableau de bord. |
data.order.externalOrderId | Votre propre id de commande, si vous l’avez envoyé lors de la commande. |
data.order.state et status | Le statut de votre côté (par exemple SUBMITTED) et le statut de production (par exemple SHIPPED). Voir les statuts de commande. |
data.order.tracking | Numéro et lien de suivi, dès que la commande est expédiée. |
Champs supplémentaires dans data | Pour order.awaiting_funds et order.payment_failed, la raison figure dans reason. order.cancelled peut contenir refundedCents. Dans le bac à sable, order.submitted contient un champ test. |
Comment vérifier la signature ?
N’importe qui peut envoyer une requête à votre URL. Vérifiez donc la signature de chaque message avant d’agir. La signature est un HMAC-SHA256 calculé avec le secret de votre endpoint.
Lisez les en-têtes
Récupérez
X-Printen-TimestampetX-Printen-Signaturedans la requête.Vérifiez l’heure
Rejetez le message si l’horodatage s’écarte de plus de 5 minutes (300 secondes) de l’heure actuelle. Personne ne peut ainsi rejouer un ancien message.
Calculez la signature attendue
Calculez un HMAC-SHA256 avec votre secret sur le texte
timestamp.rawBody: l’horodatage, un point puis le corps brut. Ajoutezv1=devant.Comparez en sécurité
Comparez le résultat à
X-Printen-Signatureavec une comparaison à temps constant. S’ils ne correspondent pas, répondez 401 et ne traitez rien.
import crypto from 'node:crypto';
export function verifyPrintenWebhook(rawBody, headers, secret) {
const timestamp = headers['x-printen-timestamp'];
const signature = headers['x-printen-signature'] || '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v1=' + crypto.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`).digest('hex');
return signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}<?php
function verify_printen_webhook(string $rawBody, string $timestamp, string $signature, string $secret): bool {
if (abs(time() - (int) $timestamp) > 300) return false;
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}import hashlib, hmac, time
def verify_printen_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)Piège : vérifiez sur le corps brut
La signature est calculée sur le corps exactement tel que nous l’envoyons, octet par octet. Beaucoup de frameworks lisent automatiquement le corps comme du JSON. Si vous reconvertissez ensuite ce JSON en texte, les espaces, l’ordre ou certains caractères peuvent différer légèrement, et la signature ne correspond plus.
import express from 'express';
import { verifyPrintenWebhook } from './verify-printen-webhook.js';
const app = express();
// express.raw (not express.json) keeps the body exactly as it was sent
app.post('/printen-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyPrintenWebhook(rawBody, req.headers, process.env.PRINTEN_WEBHOOK_SECRET)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(rawBody);
await saveEventForProcessing(event); // store first, process later
res.status(200).send('ok');
});- PHP : lisez le corps avec
file_get_contents("php://input"), pas avec$_POST. - Python (Flask) : utilisez
request.get_data()pour les octets bruts, pasrequest.json. - Next.js (App Router) : utilisez
await request.text()et seulement ensuiteJSON.parse.
Et si votre endpoint ne répond pas ?
Une livraison réussit si vous répondez en moins de 10 secondes avec un statut 2xx. Tout autre résultat compte comme un échec : un délai dépassé, un statut d’erreur, mais aussi une redirection (3xx). Nous réessayons alors selon ce calendrier.
| Tentative | Attente après la tentative précédente | Environ après la première tentative |
|---|---|---|
| 1 | Immédiatement | Départ |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 15 minutes | 21 minutes |
| 5 | 30 minutes | 51 minutes |
| 6 | 1 heure | 2 heures |
| 7 | 2 heures | 4 heures |
| 8 | 4 heures | 8 heures |
| 9 | 8 heures | 16 heures |
| 10 | 8 heures | 24 heures |
Après la dixième tentative, nous abandonnons pour cet événement. Votre endpoint n’est jamais désactivé automatiquement : nous continuons d’envoyer les nouveaux événements.
Traiter les événements en double ou dans le désordre
À cause des répétitions, un même événement peut arriver plusieurs fois, et les événements peuvent arriver dans un autre ordre que celui où ils se sont produits. Concevez votre traitement en conséquence.
- Dédoublonnez sur `id` : enregistrez l’
idde chaque événement traité. Si le mêmeidrevient, répondez 2xx et ne faites rien d’autre. - Surveillez l’ordre avec `created` : si vous avez déjà traité un événement plus récent pour cette commande, n’écrasez pas un statut récent par un plus ancien.
- Ne faites jamais reculer un statut : si
order.shippedarrive aprèsorder.delivered, laissez la commande au statut livrée. - En cas de doute, consultez la commande :
GET /orders/{id}vous donne toujours l’état actuel.
Comment tester votre webhook ?
Préparez votre endpoint
Assurez-vous que votre URL est accessible publiquement en https. Si vous travaillez en local, utilisez un service de tunnel qui redirige une URL https publique vers votre ordinateur.
Ajoutez l’endpoint
Ajoutez-le dans Webhooks et cochez
order.created,order.submittedetorder.cancelled(ou omettez les événements via l’API pour tout recevoir).Passez une commande de test
Passez une commande via l’API avec une clé de test (
prk_test_). En quelques minutes, vous recevezorder.createdetorder.submitted(avectestdansdata).Annulez la commande de test
Annulez la commande avec
POST /orders/{id}/cancel. Vous recevez alorsorder.cancelled.Vérifiez vos journaux
Vérifiez que la signature était correcte, que vous avez répondu 2xx et qu’un message répété avec le même
ida bien été ignoré.
Résoudre les problèmes
Je ne reçois rien du tout.
Vérifiez que l’URL est correcte, utilise https et est accessible depuis internet (pas localhost ni une adresse interne). Vérifiez que vous avez coché les bons événements. Gardez à l’esprit qu’un événement peut mettre quelques minutes à arriver.
La signature ne correspond jamais.
Vérifiez que vous utilisez le secret de cet endpoint (chaque endpoint a le sien) et que vous vérifiez sur le corps brut, pas sur du JSON reconstruit. Voir le corps brut.
Je reçois des messages en double.
C’est normal après un délai dépassé ou un statut d’erreur. Dédoublonnez sur l’id de l’événement. Voir les doublons.
Mon serveur renvoie une redirection.
Nous ne suivons pas les redirections ; un 301 ou 302 compte comme un échec. Utilisez l’URL finale, en faisant attention au www, à la barre oblique finale et au passage de http à https.
Je n’ai pas conservé le secret.
Supprimez l’endpoint et ajoutez-le à nouveau. Vous obtenez un nouveau secret ; mettez-le à jour dans la configuration de votre serveur.
Je ne peux plus ajouter d’endpoint.
Vous avez atteint le maximum de 10 endpoints. Supprimez d’abord un endpoint que vous n’utilisez plus.
Articles associés
- L’API : commander depuis votre propre systèmeCréez une clé API, demandez des prix, passez des commandes avec une Idempotency-Key, gérez vos propres produits et testez tout en sécurité dans le bac à sable.
- Suivre les commandes, les modifier et résoudre les problèmesSuivez vos commandes, comprenez chaque statut, passez une commande manuellement, annulez à temps et sachez réagir aux problèmes de paiement, d’adresse ou de dommages.
Besoin d’aide ?
Nous vous aidons volontiers, en personne les jours ouvrés et généralement le jour même.