Webhooks
Les webhooks préviennent votre serveur quand quelque chose se produit : un travail imprimé ou en échec, une imprimante sans papier, un appareil hors ligne. Les webhooks sont inclus à partir du forfait Business.
Ajouter une adresse
Tableau de bord → Développeurs → Webhooks → Ajouter l'adresse, ou avec l'API (portée webhooks:write) :
curl https://api.jodaprint.dev/v1/webhooks \
-H "Authorization: Bearer $JODAPRINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://shop.example.com/jodaprint/webhook","events":["job.printed","job.failed"]}'
La réponse contient le secret de l'adresse (whsec_…), affiché une seule fois. Enregistrez-le dans JODAPRINT_WEBHOOK_SECRET.
En production, l'URL doit utiliser https et pointer vers une adresse publique. La liste des événements et leur contenu se trouvent dans Événements webhook.
Livraison
- Chaque événement est un
POSTavec un corps JSON, envoyé en quelques secondes. - Répondez avec n'importe quel statut
2xxdans les 10 secondes. Faites le travail long après avoir répondu. - Les livraisons en échec font l'objet d'une nouvelle tentative après 1 minute, 5 minutes, 30 minutes, 2 heures, 6 heures et 12 heures (environ 21 heures au total).
- Une adresse qui échoue sans interruption pendant 3 jours est désactivée ; le tableau de bord en indique la raison, et vous pouvez la réactiver.
- Une nouvelle tentative garde le même
idd'événement (aussi présent dans l'en-têteJodaPrint-Event-Id). Enregistrez les identifiants déjà traités et ignorez les doublons. - Le tableau de bord liste chaque livraison avec son code de réponse, et peut renvoyer un événement ou envoyer un événement
webhook.test.
Vérifier la signature
Chaque requête contient un en-tête :
JodaPrint-Signature: t=1758800000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
v1 est le HMAC-SHA256 en hexadécimal de t + "." + corps brut, avec le secret de l'adresse comme clé. Pour vérifier :
- Lisez les octets bruts du corps, avant tout décodage JSON.
- Calculez le HMAC et comparez-le à
v1en temps constant. - Refusez la requête quand
ts'écarte de plus de 300 secondes de votre horloge (rejeux).
Node.js
Avec le SDK :
import express from "express";
import { verifyWebhook } from "@jodaprint/node";
app.post("/jodaprint/webhook", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await verifyWebhook(
req.body, // Buffer: the raw body
req.headers["jodaprint-signature"],
process.env.JODAPRINT_WEBHOOK_SECRET!,
);
res.sendStatus(200);
if (event.type === "job.failed") notifyStaff(event.data);
} catch {
res.sendStatus(400);
}
});
Sans le SDK :
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
PHP
<?php
function jodaprint_verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $p) {
[$k, $v] = array_pad(explode('=', $p, 2), 2, '');
$parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
if ($t === 0 || abs(time() - $t) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}
$raw = file_get_contents('php://input');
if (!jodaprint_verify($raw, $_SERVER['HTTP_JODAPRINT_SIGNATURE'] ?? '', getenv('JODAPRINT_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($raw, true);
http_response_code(200);
Python
import hashlib, hmac, json, os, time
def jodaprint_verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts.get("t", "0"))
except ValueError:
return False
if t == 0 or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
# Flask
@app.post("/jodaprint/webhook")
def webhook():
raw = request.get_data()
if not jodaprint_verify(raw, request.headers.get("JodaPrint-Signature", ""), os.environ["JODAPRINT_WEBHOOK_SECRET"]):
return "", 400
event = json.loads(raw)
return "", 200
Renouveler le secret
Tableau de bord → Webhooks → adresse → Nouveau secret crée un nouveau secret. Mettez votre serveur à jour immédiatement : à partir de ce moment, les livraisons sont signées avec le nouveau secret.