PopNowDokumentacja API
Budowanie z API
Treść główna

Webhooki

Odbieraj gotowe zadania na własny adres URL, weryfikuj podpis i obsługuj ponowienia oraz duplikaty.

Dzięki webhookowi PopNow samo da ci znać, że zadanie się skończyło — nie musisz pytać w kółko. Są dwa rodzaje:

Callback zadaniaEndpoint konta
KonfiguracjacallBackUrl przy każdym createTaskRaz, w konsoli API
Wysyłany dlaTego jednego zadaniaKażdego zadania na koncie pasującego do wybranych zdarzeń
PodpisX-Webhook-Signature (niżej)Nagłówki Standard Webhooks (niżej)

Większości integracji wystarcza callback zadania.

1. Zdobądź sekret podpisu

Każde doręczenie jest podpisane sekretem podpisu webhooków twojego konta. Otwórz stronę Webhooks w konsoli API, wygeneruj (albo wymień) sekret i zapisz go na serwerze jako POPNOW_WEBHOOK_SECRET. Wyświetla się tylko raz. Wymiana działa od razu, także dla doręczeń, które są jeszcze ponawiane, więc zaraz potem zaktualizuj odbiorców.

2. Przekaż callBackUrl

{
  "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"
}

Adres musi być publicznym HTTPS na porcie 443 (albo bez portu), bez nazwy użytkownika i hasła, i rozwiązywać się wyłącznie na publiczne adresy — sprawdzamy to przy tworzeniu zadania i ponownie przy połączeniu. http://localhost nie zadziała; podczas pracy lokalnej użyj tunelu w rodzaju Cloudflare Tunnel lub ngrok albo odpytuj. Nieprawidłowy adres zwraca 400 z msg: "Invalid callback URL".

3. Co przychodzi

Gdy zadanie osiągnie stan końcowy, PopNow wysyła jeden 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

Treść jest dokładnie taka, jaką recordInfo zwróciłoby dla tego zadania:

{
  "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 ma wartość 200 nawet dla nieudanego zadania: opisuje doręczenie, a nie zadanie. Czytaj data.state.
  • request_id to ID doręczenia. Nie zmienia się między ponowieniami, więc służy do pomijania duplikatów.
  • Podpisane linki w output wygasają po około 20 minutach. Zapisuj taskId, nie link.

4. Zweryfikuj podpis

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

taskId to data.taskId z treści, timestamp to nagłówek X-Webhook-Timestamp, a ostatnia część to SHA-256 surowej treści zapytania, zapisany małymi literami w hex. Hashuj bajty, które dostałeś, a nie ponownie zserializowany obiekt: kolejność kluczy i białe znaki zmieniłyby wynik.

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

W każdej nowej integracji X-Webhook-Payload-Version ma wartość 2. Wersja 1 to starszy format zachowany dla istniejących odbiorców; jeśli kiedyś się pojawi, ID zadania jest w polu task_id na najwyższym poziomie.

Odpowiadaj szybko

Każda odpowiedź 2xx w ciągu 15 sekund oznacza odbiór. Wszystko inne — inny status, przekroczony czas, odrzucone połączenie — jest ponawiane. Najpierw potwierdź, a powolną pracę (pobieranie, transkodowanie, powiadamianie użytkowników) wykonuj w zadaniu w tle.

Ponowienia i duplikaty

Nieudane doręczenie jest ponawiane jeszcze 7 razy: po 10 s, 30 s, 2 min, 10 min, 30 min, 2 h i 6 h. Każde ponowienie jest podpisywane od nowa ze świeżym znacznikiem czasu, więc zawsze porównuj go z bieżącą godziną, a nie z pierwszą próbą. Po ostatniej próbie doręczenie jest oznaczane w konsoli API jako nieudane i tam możesz wysłać je ponownie ręcznie.

To samo zadanie może więc dotrzeć więcej niż raz. Zrób obsługę idempotentną: zapisuj request_id z unikalnym indeksem, a gdy zobaczysz go znowu, odpowiedz 200 i nic nie rób.

Jeśli twój serwer leżał dłużej niż okno ponowień, wylistuj ostatnie zadania przez GET /api/v1/jobs i doczytaj przegapione przez recordInfo.

Endpointy konta

Endpoint konta to adres URL rejestrowany raz w konsoli API razem ze zdarzeniami, które ma odbierać. Dostaje zdarzenia o wszystkich zadaniach na koncie, więc nie musisz już przekazywać callBackUrl.

ZdarzenieKiedy jest wysyłane
task.completedZaszło którekolwiek z czterech poniżej. Subskrybuj to, chyba że musisz je rozróżniać
task.succeededZadanie zakończyło się sukcesem
task.failedZadanie się nie powiodło; blokada została zwolniona
task.expiredZadanie przekroczyło termin; blokada została zwolniona
task.canceledZachowane tylko dla starych rekordów

Doręczenia są zgodne z formatem Standard Webhooks, więc gotowe biblioteki działają bez zmian:

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

Dwie różnice względem callbacku zadania: podpisywana jest sama treść (bez kroku SHA-256), a kluczem HMAC są bajty uzyskane przez zdekodowanie z Base64 części sekretu po whsec_ — nie sam ciąg znaków. W trakcie wymiany sekretu nagłówek może zawierać dwa podpisy oddzielone spacją; przyjmij zapytanie, jeśli pasuje którykolwiek. webhook-id nie zmienia się między ponowieniami: używaj go do pomijania duplikatów. data.object w zdarzeniu to rekord zadania z chwili utworzenia zdarzenia.

Na tej stronie