Guides
Webhooks
Recevez les webhooks generation.succeeded et generation.failed et vérifiez l’en-tête HMAC-SHA256 CheaperGen-Signature en Node.js ou Python.
Au lieu d’interroger le statut, indiquez un webhook_url lors de l’envoi d’une génération. Lorsque la tâche atteint un état final, nous envoyons un POST HTTPS à cette URL, contenant l’objet de génération complet, signé avec le secret de votre compte.
Demander un webhook#
Ajoutez webhook_url au corps de toute requête POST /api/v1/run/{endpoint}. L’URL doit être en HTTPS et accessible depuis Internet.
curl -X POST https://cheapergen.com/api/v1/run/bytedance/seedance-2.0-fast/text-to-video \
-H "Authorization: Bearer $CHEAPERGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Travelling avant lent sur un céramiste qui façonne un vase à l’heure dorée, poussière dans la lumière, faible profondeur de champ",
"aspect_ratio": "16:9",
"resolution": "720p",
"duration": 5,
"webhook_url": "https://your-app.com/hooks/cheapergen"
}'Événements#
generation.succeededevent- La génération est terminée.
data.outputcontient les URL des médias etdata.costle montant final débité. generation.failedevent- La génération a échoué.
data.errorindique la raison (reason,message) et la marche à suivre (hint,retryable) — voir motifs d’échec ; le montant réservé a été remboursé etdata.cost.amount_usdvaut 0.
Les générations annulées ne déclenchent pas de webhook : vous les avez annulées, vous le savez donc déjà.
Charge utile#
Le corps de la requête est un JSON contenant l’événement type et la génération dans data — le même objet que celui renvoyé par GET /generations/{id}.
{
"type": "generation.succeeded",
"data": {
"id": "gen_8c2QmX1nq0k4Lw7Z",
"object": "generation",
"model": "bytedance/seedance-2.0-fast",
"endpoint": "bytedance/seedance-2.0-fast/text-to-video",
"mode": "text-to-video",
"status": "succeeded",
"input": {
"prompt": "Travelling avant lent sur un céramiste qui façonne un vase à l’heure dorée, poussière dans la lumière, faible profondeur de champ",
"aspect_ratio": "16:9",
"resolution": "720p",
"duration": 5,
"generate_audio": true
},
"output": {
"video": {
"url": "https://cdn.cheapergen.com/outputs/gen_8c2QmX1nq0k4Lw7Z.mp4",
"content_type": "video/mp4"
}
},
"error": null,
"cost": {
"amount_usd": 0.4494,
"currency": "usd",
"estimated": false
},
"source": "api",
"created_at": "2026-10-15T09:21:04.512Z",
"started_at": "2026-10-15T09:21:09.031Z",
"completed_at": "2026-10-15T09:22:31.870Z",
"retention": "1d",
"expires_at": "2026-10-16T09:22:31.870Z",
"purged_at": null
}
}En-têtes#
CheaperGen-Signaturestring- Horodatage et signature :
t=<unix seconds>,v1=<hex HMAC-SHA256>. CheaperGen-Webhook-Idstring- Un identifiant unique de cette notification, par exemple
whd_3f9c2a…. Il reste le même à chaque nouvelle tentative : dédupliquez sur cet identifiant. Content-Typestringapplication/jsonUser-AgentstringCheaperGen-Webhooks/1.0
Vérifier les signatures#
Vérifiez toujours la signature avant de faire confiance à un webhook : n’importe qui peut envoyer une requête POST à une URL publique. L’en-tête ressemble à ceci :
CheaperGen-Signature: t=1760520151,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd- Séparez l’en-tête au niveau de
,et récupérez les valeurst(horodatage) etv1(signature). - Construisez la chaîne signée : l’horodatage, le caractère littéral
., puis le corps brut de la requête exactement tel qu’il a été reçu. - Calculez le HMAC-SHA256 de cette chaîne avec votre secret de signature comme clé, puis encodez le résultat en hexadécimal.
- Comparez-les à
v1en temps constant et rejetez les horodatages datant de plus de quelques minutes afin d’éviter les rejeux.
Votre secret de signature commence par whsec_ et s’affiche dans le tableau de bord, à côté de vos clés API. Conservez-le côté serveur comme une clé API.
Votre code peut aussi le récupérer avec une clé API : GET /api/v1/webhooks/secret renvoie { "key": "whsec_…" }. Si le compte n’a pas encore de secret, le premier appel le crée.
curl https://cheapergen.com/api/v1/webhooks/secret \
-H "Authorization: Bearer $CHEAPERGEN_API_KEY"Utilisez le corps brut
Vérifiez la signature à partir des octets reçus. Analyser le JSON puis le sérialiser à nouveau modifie les espaces et l’ordre des clés ; la signature ne correspondra plus.Node.js#
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300; // reject deliveries older than 5 minutes
/**
* Verify a CheaperGen webhook.
* @param rawBody the request body exactly as received (string or Buffer) — not re-serialized JSON
* @param header the value of the "CheaperGen-Signature" header
* @param secret your signing secret (whsec_…)
*/
export function verifyCheaperGenSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header ?? "")
.split(",")
.map((kv) => kv.trim().split("=", 2)),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!Number.isFinite(t) || !v1) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Python#
import hashlib, hmac, time
TOLERANCE_SECONDS = 300 # reject deliveries older than 5 minutes
def verify_cheapergen_signature(raw_body: bytes, header: str, secret: str) -> bool:
"""Verify a CheaperGen webhook. raw_body must be the exact bytes received."""
try:
parts = dict(kv.strip().split("=", 1) for kv in (header or "").split(","))
t, v1 = int(parts["t"]), parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE_SECONDS:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Livraison et nouvelles tentatives#
- Répondez avec n’importe quel code d’état 2xx sous 10 secondes pour confirmer la réception. Les redirections ne sont pas suivies.
- Si votre endpoint expire ou répond autre chose qu’un 2xx, nous réessayons pendant une heure au maximum : environ 1, 3, 8, 18, 38 et 60 minutes après la première tentative (7 tentatives au total), puis nous arrêtons. Une réponse
410 Gonearrête aussitôt les nouvelles tentatives. - Chaque tentative est signée à nouveau avec un
trécent : une nouvelle tentative passe donc la vérification des 5 minutes comme la première livraison. - Une livraison peut arriver plusieurs fois et, rarement, dans le désordre. Rendez votre handler idempotent : utilisez l’en-tête
CheaperGen-Webhook-Idcomme clé (identique à chaque nouvelle tentative d’une notification) et ignorez les notifications déjà traitées. - Les webhooks sont des notifications, pas l’unique source de vérité. Si vous en manquez un, la génération reste accessible via
GET /api/v1/generations/{id}; une réconciliation périodique des tâches encore en attente de votre côté couvre tous les cas limites.
Tester en local#
Les URL de webhook doivent être accessibles publiquement en HTTPS. En développement, exposez votre serveur local avec un tunnel (par exemple cloudflared tunnel ou ngrok http 3000) et indiquez son URL HTTPS comme valeur de webhook_url.
Quelque chose n’est pas clair ou manque ? Écrivez à support@cheapergen.com : nous lisons tous les messages.