PopNowAPI-Doku
Mit der API bauen
Hauptinhalt

Webhooks

Fertige Tasks an deiner eigenen URL empfangen, die Signatur prüfen und mit Wiederholungen und Duplikaten umgehen.

Mit einem Webhook sagt dir PopNow Bescheid, wenn ein Task fertig ist – du musst nicht ständig nachfragen. Es gibt zwei Arten:

Task-CallbackKonto-Endpunkt
EinrichtungcallBackUrl bei jedem createTaskEinmal in der API-Konsole
Gesendet fürDiesen einen TaskJeden Task des Kontos, der zu den gewählten Ereignissen passt
SignaturX-Webhook-Signature (siehe unten)Standard-Webhooks-Header (siehe unten)

Den meisten Integrationen genügt der Task-Callback.

1. Signatur-Secret holen

Jede Zustellung wird mit dem Webhook-Signatur-Secret deines Kontos signiert. Öffne die Seite Webhooks in der API-Konsole, erzeuge (oder rotiere) das Secret und leg es auf deinem Server als POPNOW_WEBHOOK_SECRET ab. Es wird nur einmal angezeigt. Eine Rotation wirkt sofort, auch für Zustellungen, die gerade wiederholt werden – aktualisiere deine Empfänger also direkt danach.

2. callBackUrl übergeben

{
  "model": "alibaba/wan-3.0/image-to-video",
  "input": { "image_url": "https://media.popnow.ai/movie/lighthouse.webp" },
  "callBackUrl": "https://your-app.example.com/hooks/popnow"
}

Die URL muss öffentliches HTTPS auf Port 443 (oder ohne Port) sein, ohne Benutzername oder Passwort, und darf nur zu öffentlichen Adressen auflösen – geprüft beim Erstellen des Tasks und noch einmal beim Verbinden. http://localhost kann nicht funktionieren; nimm beim Entwickeln einen Tunnel wie Cloudflare Tunnel oder ngrok oder polle. Eine ungültige URL liefert 400 mit msg: "Invalid callback URL".

3. Was ankommt

Erreicht der Task einen Endzustand, schickt PopNow einen POST:

POST /hooks/popnow HTTP/1.1
Content-Type: application/json
User-Agent: PopNow-Webhook/1
X-Webhook-Timestamp: 1787045531
X-Webhook-Signature: 9tXk3…=
X-Webhook-Payload-Version: 2

Der Body ist genau das, was recordInfo für den Task liefern würde:

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01JH8Z4Q5W2N7R3K9M6P1C0VXD",
    "model": "alibaba/wan-3.0/image-to-video",
    "state": "succeeded",
    "output": { "assets": [{ "url": "https://…", "mime": "video/mp4", "durationSeconds": 5 }] },
    "credits": 34,
    "cost": "0.34",
    "settled": true
  },
  "request_id": "whd_01JH8Z…"
}
  • code ist auch bei einem gescheiterten Task 200: Es beschreibt die Zustellung, nicht den Task. Lies data.state.
  • request_id ist die Zustellungs-ID. Sie bleibt bei Wiederholungen gleich – nutz sie, um Duplikate zu ignorieren.
  • Die signierten Links in output laufen nach etwa 20 Minuten ab. Speichere die taskId, nicht den Link.

4. Signatur prüfen

signature = base64( HMAC-SHA256( secret, "<taskId>.<timestamp>.<sha256_hex(raw_body)>" ) )

taskId ist data.taskId aus dem Body, timestamp der Header X-Webhook-Timestamp, und der letzte Teil ist der SHA-256 des rohen Request-Bodys in kleingeschriebenem Hex. Hashe die Bytes, die du bekommen hast, nicht ein neu serialisiertes Objekt: Schlüsselreihenfolge und Leerraum würden das Ergebnis ändern.

Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.POPNOW_WEBHOOK_SECRET;

// express.raw keeps the exact bytes; express.json would throw them away.
app.post('/hooks/popnow', express.raw({ type: 'application/json', limit: '10mb' }), (req, res) => {
  const ts = Number(req.headers['x-webhook-timestamp']);
  const sig = String(req.headers['x-webhook-signature'] ?? '');
  // Refuse anything older than five minutes, however valid its signature.
  if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).end();

  let event;
  try { event = JSON.parse(req.body.toString('utf8')); } catch { return res.status(400).end(); }
  const digest = crypto.createHash('sha256').update(req.body).digest('hex');
  const expected = crypto.createHmac('sha256', SECRET)
    .update(`${event.data?.taskId}.${ts}.${digest}`).digest('base64');
  const ok = expected.length === sig.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  if (!ok) return res.status(401).end();

  res.status(200).end();          // acknowledge first…
  enqueue(event.request_id, event.data); // …then do the work
});
Python (Flask)
import hashlib, hmac, base64, json, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["POPNOW_WEBHOOK_SECRET"].encode()

@app.post("/hooks/popnow")
def popnow_hook():
    raw = request.get_data()  # the exact bytes
    ts = request.headers.get("X-Webhook-Timestamp", "")
    sig = request.headers.get("X-Webhook-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(401)
    event = json.loads(raw)
    digest = hashlib.sha256(raw).hexdigest()
    message = f'{event["data"]["taskId"]}.{ts}.{digest}'.encode()
    expected = base64.b64encode(hmac.new(SECRET, message, hashlib.sha256).digest()).decode()
    if not hmac.compare_digest(expected, sig):
        abort(401)
    enqueue(event["request_id"], event["data"])  # do the slow work elsewhere
    return "", 200

X-Webhook-Payload-Version ist bei jeder neuen Integration 2. Version 1 ist ein älteres Format für bestehende Empfänger; falls du es je siehst, steht die Task-ID im obersten Feld task_id.

Schnell antworten

Jede 2xx-Antwort innerhalb von 15 Sekunden gilt als empfangen. Alles andere – ein anderer Status, ein Timeout, eine abgelehnte Verbindung – wird wiederholt. Bestätige zuerst und erledige die langsame Arbeit (Herunterladen, Umwandeln, Nutzer benachrichtigen) in einem Hintergrundjob.

Wiederholungen und Duplikate

Eine fehlgeschlagene Zustellung wird noch 7-mal wiederholt, nach 10 s, 30 s, 2 min, 10 min, 30 min, 2 h und 6 h. Jede Wiederholung wird mit frischem Zeitstempel neu signiert – vergleiche den Zeitstempel also immer mit der aktuellen Zeit, nicht mit dem ersten Versuch. Nach der letzten Wiederholung wird die Zustellung in der API-Konsole als fehlgeschlagen markiert, wo du sie von Hand erneut senden kannst.

Derselbe Task kann also mehr als einmal ankommen. Mach deinen Handler idempotent: Speichere request_id mit eindeutigem Index, und wenn du sie wiedersiehst, antworte 200 und tu nichts.

War dein Server länger als das Wiederholungsfenster weg, liste die letzten Tasks mit GET /api/v1/jobs und lies die verpassten mit recordInfo.

Konto-Endpunkte

Ein Konto-Endpunkt ist eine URL, die du einmal in der API-Konsole zusammen mit den gewünschten Ereignissen registrierst. Er erhält Ereignisse zu jedem Task des Kontos, du musst callBackUrl also nie wieder mitschicken.

EreignisGesendet, wenn
task.completedEins der vier folgenden eintritt. Abonniere das, sofern du sie nicht unterscheiden musst
task.succeededEin Task erfolgreich fertig ist
task.failedEin Task gescheitert ist; die Reservierung wurde freigegeben
task.expiredEin Task seine Frist überschritten hat; die Reservierung wurde freigegeben
task.canceledNur für alte Einträge erhalten

Zustellungen folgen dem Format Standard Webhooks, sodass fertige Bibliotheken funktionieren:

webhook-id: msg_01k3m8x9q2z4v7n5p6r8s0t1w3
webhook-timestamp: 1787045531
webhook-signature: v1,base64( HMAC-SHA256( key, "<webhook-id>.<webhook-timestamp>.<raw_body>" ) )

Zwei Unterschiede zum Task-Callback: Signiert wird der Body selbst (kein SHA-256-Schritt), und der HMAC-Schlüssel sind die Bytes, die du durch Base64-Dekodieren des Teils nach whsec_ erhältst – nicht der String. Während einer Secret-Rotation kann der Header zwei durch Leerzeichen getrennte Signaturen enthalten; akzeptiere die Anfrage, wenn eine davon passt. webhook-id bleibt bei Wiederholungen gleich: nutz sie, um Duplikate zu ignorieren. data.object im Ereignis ist der Task-Eintrag zum Zeitpunkt, als das Ereignis entstand.

Auf dieser Seite