Blog/developers·1. Sept. 2026·5 Min.·vom eroq-Team
Asynchrone Video-API – Jobs, Polling und signierte Webhooks
Asynchrone Video-API integrieren, ohne Geld oder Clips zu verlieren: Job absenden, pollen oder signierter Webhook, Erstattungen und Retries sicher handhaben.
Ein Video-Render dauert ein bis fünf Minuten. So lange sollte keine HTTP-Anfrage offen bleiben – Load Balancer laufen in Timeouts, Mobilfunkverbindungen brechen ab, und deine Nutzer laden die Seite neu. Deshalb ist jede ernstzunehmende API zur Videogenerierung asynchron: Du schickst einen Job ab, bekommst eine ID und erfährst das Ergebnis später. In den Details dieses „später“ verlieren Integrationen Geld und Clips. So ist der Video-Endpunkt von eroq aufgebaut, und so nutzt du ihn richtig.
Der Vertrag in einem Absatz
POST /v1/videos/generations bucht den festen Credit-Preis ab, startet den Render und antwortet mit 202 und einer Job-ID. Dann fragst du entweder GET /v1/videos/generations/{id} ab (kostenlos, alle paar Sekunden) oder bekommst einen signierten Webhook, wenn der Job fertig ist. Ein Render, der fehlschlägt – oder nicht innerhalb von 10 Minuten fertig wird –, erstattet sich selbst; ein Prompt außerhalb der Nutzungsrichtlinie gibt content_blocked zurück und wird nie berechnet. Du zahlst nur für einen gelieferten Clip. Job-IDs leben 24 Stunden.
Einen Job absenden
curl https://eroq.ai/v1/videos/generations \
-H "Authorization: Bearer $EROQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "eroq-motion-one",
"prompt": "Slow push-in on a lighthouse keeper in a yellow raincoat climbing a spiral stone staircase at night, lantern swinging, warm practical light against cold blue storm windows, rain streaking the glass, tense and patient mood, 35mm film grain.",
"seconds": 10,
"aspect": "9:16",
"store": true
}'
Die Antwort:
{
"id": "b7e6c2d4-…",
"status": "processing",
"usage": { "credits_spent": 60, "credits_remaining": 887 }
}
Zwei Parameter verdienen eine Anmerkung.
seconds ist die Cliplänge, 1 bis 30. Dein Plan begrenzt sie – 10 s bei Pay-as-you-go, 15 bei Hobby, 20 bei Creator, 30 ab Studio –, und das Raster der Engine rundet sie: Motion One rendert 5 oder 10 s, Seedance 2.5 alles von 4 bis 30. Das Einrasten passiert vor der Abrechnung, der Preis passt also immer zu dem Clip, der tatsächlich gerendert wird. Schickst du seconds: 8 an Motion One, rastet der Wert auf das Raster der Engine ein, und berechnet wird der Clip, der wirklich entsteht; frag /v1/engines ab, wenn du das Raster vorher kennen willst.
aspect akzeptiert 16:9, 9:16, 1:1, 21:9, 4:3 oder 3:4. Wo die Engine ein Seitenverhältnis nativ annimmt, wird es durchgereicht; sonst fließt es in den Prompt ein – eine deutliche Bitte an die Engine, keine harte Garantie. Der Eintrag zum Seitenverhältnis hat die Details pro Engine.
store: true ist optional und lohnt sich in Produktion: Der fertige Clip landet im eroq Store, und der Job enthält eine dauerhafte CDN-URL statt Inline-Base64, das mit dem Job verfällt. Das kostet zusätzlich 2 Credits je angefangene 10 MB (Preise).
Option A – Polling
Polling ist kostenlos, und der vorgesehene Takt ist alle paar Sekunden. Setz eine Deadline knapp hinter dem 10-Minuten-Erstattungsfenster, damit sich ein hängender Job selbst auflöst, bevor deine Schleife aufgibt.
const BASE = 'https://eroq.ai/v1'
const headers = { Authorization: `Bearer ${process.env.EROQ_API_KEY}` }
async function waitForClip(jobId, { every = 4000, deadline = 11 * 60_000 } = {}) {
const started = Date.now()
while (Date.now() - started < deadline) {
const job = await fetch(`${BASE}/videos/generations/${jobId}`, { headers }).then(r => r.json())
if (job.status === 'succeeded') return job.data[0].url // with store: true
if (job.status === 'failed') throw new Error(job.error?.code ?? 'generation_failed')
await new Promise(r => setTimeout(r, every))
}
throw new Error('timeout')
}
status geht von processing zu succeeded oder failed. Bei failed sind die Credits schon wieder im Guthaben – „Retry“ heißt also „neu absenden“, nicht „über die Rechnung streiten“.
Option B – signierter Webhook
Registriere einen Endpunkt im Entwickler-Dashboard, abonniere damit video.generation.succeeded und video.generation.failed und kopiere das whsec_-Secret – es wird nur einmal angezeigt. Jede Zustellung ist ein JSON-POST:
{
"id": "evt_9f1c…",
"type": "video.generation.succeeded",
"data": {
"id": "b7e6c2d4-…",
"result_url": "https://…/b7e6c2d4.mp4"
}
}
Ein fehlgeschlagener Job enthält statt einer result_url ein error-Objekt. Medien reisen nie im Payload mit; du bekommst eine URL oder holst den Job ab.
Die Signatur ist im Stripe-Stil aufgebaut und steht im Header eroq-signature: t=<unix timestamp>,v1=<hex>, wobei v1 der HMAC-SHA256 von ${t}.${rawBody} mit deinem Secret ist. Prüf gegen die rohen Bytes – parse das JSON erst nach der Prüfung.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < toleranceSec
const a = Buffer.from(expected)
const b = Buffer.from(String(parts.v1 ?? ''))
return fresh && a.length === b.length && timingSafeEqual(a, b)
}
Die Zustellung ist ein einziger Versuch mit einem Zeitbudget von 10 Sekunden, und ein ausgefallener Server beim Kunden lässt nie dessen Render scheitern. Also: Antworte sofort mit 2xx, erledige die Arbeit danach und behalte Polling als Rückfallebene für alles, wovon du nichts gehört hast. Die Warteschlangenliste der letzten 24 Stunden unter GET /v1/videos/generations gibt es genau für diesen Abgleich.
Idempotenz – der Teil, bei dem wirklich Geld verloren geht
Jeder Fehlerfall hier ist ein Duplikat: ein Webhook, der ankommt, nachdem dein Poller den Erfolg schon gesehen hat, ein Retry nach einem Netzwerkfehler beim Absenden, ein Worker, der zwischen dem 202 und dem Schreiben in die Datenbank abstürzt.
- Speichere die Job-ID in derselben Transaktion wie den Zustand, den der Nutzer sieht. Mit dem
202gehört dir eine Belastung; liegt diese ID nicht auf der Platte, bevor du dem Nutzer „wird gerendert“ anzeigst, kostet dich ein Absturz einen Clip, den du nicht mehr findest. Gleich beim Neustart mit der Warteschlangenliste ab. - Upserte nach Job-ID und dedupliziere Events nach
evt_-ID. Beide Kanäle können denselben Abschluss melden. Der erste gewinnt, der zweite ist ein No-op. - Schick bei einem Netzwerkfehler beim Absenden nie blind erneut ab. Die Anfrage ist vielleicht durchgegangen. Prüf in der Warteschlangenliste, ob es in der letzten Minute einen Job mit demselben Prompt gibt, bevor du dir selbst doppelt etwas berechnest.
- Wiederhole nur bei
failed. Die Erstattung ist dann schon passiert, ein neues Absenden ist also eine neue, einzelne Belastung. Beachte das Rate-Limit pro Schlüssel – Video liegt im Basistarif bei 6 Anfragen pro Minute, multipliziert mit deinem Plan.
Feste Credit-Preise machen all das leicht prüfbar: Jeder Job entspricht einer bekannten Belastung, und der usage-Block in der Antwort nennt dir das Guthaben in genau diesem Moment.
Häufige Fragen
Wie lange habe ich Zeit, den Clip abzuholen?
Job-IDs laufen 24 Stunden nach ihrer Erstellung ab, und das Inline-Base64 verschwindet mit ihnen. Nutz store: true für eine CDN-URL, die bleibt, oder lade den Clip herunter, sobald er fertig ist.
Was passiert, wenn mein Webhook-Endpunkt nicht erreichbar ist?
Ein Versuch, dann nichts mehr. Frag GET /v1/videos/generations/{id} ab oder liste die letzten 24 Stunden auf, um aufzuholen. Webhooks sind eine Bequemlichkeit; Polling ist die verlässliche Quelle.
Bekomme ich eine Tonspur?
Nur mit veo-3-fast, das native Atmo, Effekte und Dialoge in festen 8-Sekunden-Clips rendert; es wird vorgelagert von seinem Anbieter moderiert und braucht einen Creator-Plan. Alle anderen Engines rendern stummes Video – siehe die Engine-Übersicht.
Fang mit den 50 Gratis-Credits an: Hol dir einen API-Schlüssel und schick einen Job ab.
Mach das mit den Modellen hinter diesem Artikel – starte mit 50 Gratis-Credits oder sieh dir alle Engines und ihre Preise an.