CheaperGen

Anleitungen

Webhooks

generation.succeeded- und generation.failed-Webhooks empfangen und den HMAC-SHA256-Header CheaperGen-Signature mit Node.js oder Python prüfen.

Statt den Status abzufragen, übergeben Sie beim Start einer Generierung webhook_url. Sobald der Auftrag abgeschlossen ist, senden wir ein HTTPS-POST mit dem vollständigen Generierungsobjekt an diese URL. Die Anfrage wird mit dem geheimen Schlüssel Ihres Kontos signiert.

Webhook anfordern#

Fügen Sie webhook_url in den Text jeder POST /api/v1/run/{endpoint}-Anfrage ein. Dabei muss es sich um eine HTTPS-URL handeln, die aus dem Internet erreichbar ist.

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": "Langsame Kamerafahrt auf einen Keramiker, der zur goldenen Stunde eine Vase formt, Staub im Licht, geringe Tiefenschärfe",
  "aspect_ratio": "16:9",
  "resolution": "720p",
  "duration": 5,
  "webhook_url": "https://your-app.com/hooks/cheapergen"
}'

Ereignisse#

generation.succeededevent
Die Generierung wurde abgeschlossen. data.output enthält die Medien-URLs und data.cost den endgültigen Betrag.
generation.failedevent
Die Generierung ist fehlgeschlagen. data.error nennt den Grund (reason, message) und die nächsten Schritte (hint, retryable) – siehe Fehlerursachen; der reservierte Betrag wurde erstattet und data.cost.amount_usd beträgt 0.

Bei abgebrochenen Generierungen wird kein Webhook ausgelöst – Sie haben den Auftrag selbst abgebrochen und wissen bereits Bescheid.

Nutzlast#

Der Text ist JSON mit dem Ereignis type und der Generierung im Status data – dasselbe Objekt, das Sie auch über GET /generations/{id} erhalten.

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": "Langsame Kamerafahrt auf einen Keramiker, der zur goldenen Stunde eine Vase formt, Staub im Licht, geringe Tiefenschärfe",
      "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
  }
}

Header#

CheaperGen-Signaturestring
Zeitstempel und Signatur: t=<unix seconds>,v1=<hex HMAC-SHA256>.
CheaperGen-Webhook-Idstring
Eine eindeutige ID dieser Benachrichtigung, etwa whd_3f9c2a…. Sie bleibt bei jedem erneuten Versuch gleich: Deduplizieren Sie anhand dieser ID.
Content-Typestring
application/json
User-Agentstring
CheaperGen-Webhooks/1.0

Signaturen prüfen#

Prüfen Sie immer die Signatur, bevor Sie einem Webhook vertrauen – an eine öffentliche URL kann jeder POST-Anfragen senden. Der Header sieht so aus:

CheaperGen-Signature: t=1760520151,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  1. Teilen Sie den Header anhand von , auf und lesen Sie die Werte t (Zeitstempel) und v1 (Signatur) aus.
  2. Erstellen Sie die signierte Zeichenfolge aus dem Zeitstempel, dem wörtlichen Wert . und anschließend dem unverändert übernommenen rohen Anfrage-Body.
  3. Berechnen Sie den HMAC-SHA256 dieser Zeichenfolge mit Ihrem Signaturgeheimnis als Schlüssel und geben Sie das Ergebnis hexadezimal aus.
  4. Vergleichen Sie den Wert mit v1 in konstanter Zeit und lehnen Sie Zeitstempel ab, die mehrere Minuten alt sind, um Replay-Angriffe zu verhindern.

Ihr Signaturschlüssel beginnt mit whsec_ und wird in Dashboard neben Ihren API-Schlüsseln angezeigt. Bewahren Sie ihn wie einen API-Schlüssel ausschließlich auf Ihrem Server auf.

Ihr Code kann ihn auch mit einem API-Schlüssel abrufen: GET /api/v1/webhooks/secret liefert { "key": "whsec_…" }. Hat das Konto noch keinen Schlüssel, erstellt ihn der erste Aufruf.

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

Den unveränderten Body verwenden

Prüfen Sie die empfangenen Bytes. Wenn Sie das JSON parsen und erneut serialisieren, ändern sich Leerraum und Schlüsselreihenfolge, sodass die Signatur nicht mehr übereinstimmt.

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)

Zustellung und erneute Versuche#

  • Antworten Sie innerhalb von 10 Sekunden mit einem beliebigen 2xx-Status, um den Empfang zu bestätigen. Weiterleitungen werden nicht verfolgt.
  • Wenn Ihr Endpunkt in ein Timeout läuft oder nicht mit 2xx antwortet, versuchen wir es bis zu einer Stunde lang erneut: etwa 1, 3, 8, 18, 38 und 60 Minuten nach dem ersten Versuch (insgesamt 7 Versuche), danach nicht mehr. Eine 410 Gone-Antwort beendet die Wiederholungen sofort.
  • Jeder Versuch wird mit einem neuen t neu signiert, sodass auch ein erneuter Versuch die 5-Minuten-Prüfung besteht wie die erste Zustellung.
  • Zustellungen können mehrfach und in seltenen Fällen in anderer Reihenfolge eintreffen. Machen Sie Ihren Handler idempotent: Verwenden Sie den Header CheaperGen-Webhook-Id als Schlüssel (bei jedem erneuten Versuch einer Benachrichtigung gleich) und ignorieren Sie bereits verarbeitete Benachrichtigungen.
  • Webhooks sind Benachrichtigungen und nicht die einzige maßgebliche Quelle. Wenn Sie eine Benachrichtigung verpassen, können Sie die Generierung jederzeit über GET /api/v1/generations/{id} abrufen – ein regelmäßiger Abgleich der bei Ihnen noch ausstehenden Aufträge deckt alle Sonderfälle ab.

Lokal testen#

Webhook-URLs müssen öffentlich über HTTPS erreichbar sein. Während der Entwicklung können Sie Ihren lokalen Server über einen Tunnel freigeben (zum Beispiel cloudflared tunnel oder ngrok http 3000) und dessen HTTPS-URL als webhook_url verwenden.

Ist etwas unklar oder fehlt etwas? Schreiben Sie an support@cheapergen.com – wir lesen jede Nachricht.