CheaperGen

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.output contient les URL des médias et data.cost le montant final débité.
generation.failedevent
La génération a échoué. data.error indique la raison (reason, message) et la marche à suivre (hint, retryable) — voir motifs d’échec ; le montant réservé a été remboursé et data.cost.amount_usd vaut 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}.

POST https://your-app.com/hooks/cheapergen
{
  "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-Typestring
application/json
User-Agentstring
CheaperGen-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
  1. Séparez l’en-tête au niveau de , et récupérez les valeurs t (horodatage) et v1 (signature).
  2. 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.
  3. Calculez le HMAC-SHA256 de cette chaîne avec votre secret de signature comme clé, puis encodez le résultat en hexadécimal.
  4. Comparez-les à v1 en 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 Gone arrête aussitôt les nouvelles tentatives.
  • Chaque tentative est signée à nouveau avec un t ré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-Id comme 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.