PopNowDocs API
Costruire con l’API
Contenuto principale

Webhook

Ricevi le attività completate sul tuo URL, verifica la firma e gestisci nuovi tentativi e duplicati.

Con un webhook è PopNow ad avvisarti quando un’attività è finita, senza che tu debba chiedere di continuo. Ce ne sono di due tipi:

Callback dell’attivitàEndpoint dell’account
ConfigurazionecallBackUrl in ogni createTaskUna volta sola, nella console API
Inviato perQuella singola attivitàOgni attività dell’account che corrisponde agli eventi scelti
FirmaX-Webhook-Signature (sotto)Header Standard Webhooks (vedi sotto)

Alla maggior parte delle integrazioni basta il callback dell’attività.

1. Ottieni il segreto di firma

Ogni consegna è firmata con il segreto di firma dei webhook del tuo account. Apri la pagina Webhooks della console API, genera (o ruota) il segreto e salvalo sul server come POPNOW_WEBHOOK_SECRET. Viene mostrato una volta sola. La rotazione ha effetto subito, anche per le consegne ancora in fase di nuovo tentativo, quindi aggiorna i ricevitori appena dopo.

2. Passa 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"
}

L’URL deve essere HTTPS pubblico sulla porta 443 (o senza porta), senza nome utente né password, e risolversi solo in indirizzi pubblici: viene controllato alla creazione dell’attività e di nuovo alla connessione. http://localhost non può funzionare; in sviluppo usa un tunnel come Cloudflare Tunnel o ngrok, oppure fai polling. Un URL non valido restituisce 400 con msg: "Invalid callback URL".

3. Cosa arriva

Quando l’attività raggiunge uno stato finale, PopNow invia un 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

Il corpo è esattamente ciò che recordInfo restituirebbe per quell’attività:

{
  "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 è 200 anche per un’attività fallita: descrive la consegna, non l’attività. Leggi data.state.
  • request_id è l’ID della consegna. Non cambia tra un tentativo e l’altro, quindi usalo per ignorare i duplicati.
  • I link firmati in output scadono dopo circa 20 minuti. Salva il taskId, non il link.

4. Verifica la firma

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

taskId è data.taskId del corpo, timestamp è l’header X-Webhook-Timestamp e l’ultima parte è lo SHA-256 del corpo grezzo della richiesta, in esadecimale minuscolo. Calcola l’hash dei byte ricevuti, non di un oggetto riserializzato: ordine delle chiavi e spazi cambierebbero il risultato.

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 è 2 per ogni nuova integrazione. La versione 1 è un formato più vecchio mantenuto per i ricevitori esistenti; se mai la vedi, l’ID dell’attività è nel campo task_id al primo livello.

Rispondi in fretta

Qualsiasi 2xx entro 15 secondi vale come ricevuto. Tutto il resto — un altro stato, un timeout, una connessione rifiutata — viene ritentato. Conferma subito e fai il lavoro lento (download, transcodifica, notifiche agli utenti) in un job in background.

Nuovi tentativi e duplicati

Una consegna fallita viene ritentata altre 7 volte, dopo 10 s, 30 s, 2 min, 10 min, 30 min, 2 h e 6 h. Ogni tentativo viene firmato di nuovo con un timestamp nuovo, quindi confronta sempre il timestamp con l’ora attuale, non con il primo tentativo. Dopo l’ultimo, la consegna viene segnata come fallita nella console API, da dove puoi reinviarla a mano.

La stessa attività può quindi arrivare più di una volta. Rendi il tuo handler idempotente: salva request_id con un indice univoco e, se lo rivedi, rispondi 200 senza fare nulla.

Se il tuo server è rimasto giù più a lungo della finestra dei tentativi, elenca le attività recenti con GET /api/v1/jobs e leggi quelle perse con recordInfo.

Endpoint dell’account

Un endpoint dell’account è un URL registrato una volta nella console API insieme agli eventi che vuole ricevere. Riceve gli eventi di ogni attività dell’account, così non devi più passare callBackUrl.

EventoInviato quando
task.completedSi verifica uno qualsiasi dei quattro sotto. Iscriviti a questo, a meno che tu non debba distinguerli
task.succeededUn’attività è riuscita
task.failedUn’attività è fallita; la trattenuta è stata rilasciata
task.expiredUn’attività ha superato la scadenza; la trattenuta è stata rilasciata
task.canceledConservato solo per i vecchi record

Le consegne seguono il formato Standard Webhooks, quindi le librerie già pronte funzionano:

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

Due differenze rispetto al callback dell’attività: si firma il corpo stesso (senza il passaggio SHA-256) e la chiave HMAC sono i byte ottenuti decodificando in Base64 la parte del segreto dopo whsec_, non la stringa. Durante una rotazione del segreto l’header può contenere due firme separate da uno spazio; accetta la richiesta se una delle due corrisponde. webhook-id resta uguale tra i tentativi: usalo per ignorare i duplicati. Il data.object dell’evento è il record dell’attività nel momento in cui l’evento è stato creato.

In questa pagina