CheaperGen

Guide

Webhook

Ricevi i webhook generation.succeeded e generation.failed e verifica l’header CheaperGen-Signature HMAC-SHA256 con Node.js o Python.

Invece del polling, passa webhook_url quando invii una generazione. Quando il job raggiunge uno stato finale, inviamo a quell’URL una richiesta HTTPS POST con l’intero oggetto di generazione, firmata con il segreto del tuo account.

Richiedere un webhook#

Aggiungi webhook_url al corpo di qualsiasi richiesta POST /api/v1/run/{endpoint}. Deve essere un URL HTTPS raggiungibile da 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": "Lento avvicinamento a un ceramista che modella un vaso all’ora dorata, polvere sospesa nella luce, profondità di campo ridotta",
  "aspect_ratio": "16:9",
  "resolution": "720p",
  "duration": 5,
  "webhook_url": "https://your-app.com/hooks/cheapergen"
}'

Eventi#

generation.succeededevent
La generazione è terminata. data.output contiene gli URL dei media e data.cost l’addebito finale.
generation.failedevent
La generazione non è riuscita. data.error spiega il motivo (reason, message) e come procedere (hint, retryable) — consulta motivi dell'errore; l’importo trattenuto è stato rimborsato e data.cost.amount_usd è pari a 0.

Le generazioni annullate non attivano un webhook: le hai annullate tu, quindi lo sai già.

Payload#

Il corpo è JSON e include l’evento type e la generazione in data — lo stesso oggetto restituito da 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": "Lento avvicinamento a un ceramista che modella un vaso all’ora dorata, polvere sospesa nella luce, profondità di campo ridotta",
      "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
  }
}

Intestazioni#

CheaperGen-Signaturestring
Timestamp e firma: t=<unix seconds>,v1=<hex HMAC-SHA256>.
CheaperGen-Webhook-Idstring
Un ID univoco di questa notifica, ad esempio whd_3f9c2a…. Resta lo stesso a ogni nuovo tentativo: usalo per eliminare i duplicati.
Content-Typestring
application/json
User-Agentstring
CheaperGen-Webhooks/1.0

Verificare le firme#

Verifica sempre la firma prima di fidarti di un webhook: chiunque può inviare una richiesta POST a un URL pubblico. L'intestazione ha questo formato:

CheaperGen-Signature: t=1760520151,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  1. Separa l’header in corrispondenza di , e leggi i valori di t (timestamp) e v1 (firma).
  2. Crea la stringa da firmare: il timestamp, un carattere . letterale e poi corpo grezzo della richiesta esattamente come ricevuto.
  3. Calcola l'HMAC-SHA256 della stringa usando il segreto di firma come chiave e convertilo in esadecimale.
  4. Confrontalo con v1 usando un confronto a tempo costante e rifiuta i timestamp più vecchi di alcuni minuti per impedire i replay.

Il tuo segreto di firma inizia con whsec_ ed è mostrato nella dashboard accanto alle chiavi API. Conservalo lato server, come una chiave API.

Il tuo codice può anche recuperarlo con una chiave API: GET /api/v1/webhooks/secret restituisce { "key": "whsec_…" }. Se l’account non ha ancora un segreto, la prima chiamata lo crea.

curl https://cheapergen.com/api/v1/webhooks/secret \
  -H "Authorization: Bearer $CHEAPERGEN_API_KEY"

Usa il corpo grezzo

Verifica i byte ricevuti. Analizzare il JSON e serializzarlo di nuovo modifica spazi e ordine delle chiavi, quindi la firma non corrisponde più.

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)

Consegna e nuovi tentativi#

  • Rispondi con qualsiasi stato 2xx entro 10 secondi per confermare la consegna. I reindirizzamenti non vengono seguiti.
  • Se il tuo endpoint va in timeout o risponde con qualcosa di diverso da 2xx, riproviamo per un massimo di un’ora: circa 1, 3, 8, 18, 38 e 60 minuti dopo il primo tentativo (7 tentativi in tutto), poi ci fermiamo. Una risposta 410 Gone interrompe subito i tentativi.
  • Ogni tentativo viene firmato di nuovo con un t nuovo, quindi un nuovo tentativo supera il controllo dei 5 minuti come la prima consegna.
  • Le consegne possono arrivare più di una volta e, raramente, fuori ordine. Rendi il tuo handler idempotente: usa come chiave l’header CheaperGen-Webhook-Id (uguale a ogni nuovo tentativo di una notifica) e ignora le notifiche già elaborate.
  • I webhook inviano notifiche, ma non sono l’unica fonte attendibile. Se te ne perdi uno, la generazione è sempre disponibile tramite GET /api/v1/generations/{id}; una riconciliazione periodica dei job ancora in sospeso sul tuo lato copre ogni caso limite.

Test in locale#

Gli URL dei webhook devono essere raggiungibili pubblicamente tramite HTTPS. Durante lo sviluppo, esponi il server locale con un tunnel (per esempio cloudflared tunnel o ngrok http 3000) e usa l’URL HTTPS del tunnel come webhook_url.

Qualcosa non è chiaro o manca? Scrivi a support@cheapergen.com: leggiamo ogni messaggio.