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.outputcontiene gli URL dei media edata.costl’addebito finale. generation.failedevent- La generazione non è riuscita.
data.errorspiega il motivo (reason,message) e come procedere (hint,retryable) — consulta motivi dell'errore; l’importo trattenuto è stato rimborsato edata.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}.
{
"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-Typestringapplication/jsonUser-AgentstringCheaperGen-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- Separa l’header in corrispondenza di
,e leggi i valori dit(timestamp) ev1(firma). - Crea la stringa da firmare: il timestamp, un carattere
.letterale e poi corpo grezzo della richiesta esattamente come ricevuto. - Calcola l'HMAC-SHA256 della stringa usando il segreto di firma come chiave e convertilo in esadecimale.
- Confrontalo con
v1usando 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 Goneinterrompe subito i tentativi. - Ogni tentativo viene firmato di nuovo con un
tnuovo, 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.