Webhooks
Recevoir les tâches terminées sur votre propre URL, vérifier la signature et gérer les nouvelles tentatives et les doublons.
Un webhook permet à PopNow de vous prévenir quand une tâche est terminée : plus besoin de demander encore et encore. Il en existe deux sortes :
| Callback de tâche | Endpoint de compte | |
|---|---|---|
| Mise en place | callBackUrl à chaque createTask | Une seule fois, dans la console API |
| Envoyé pour | Cette tâche-là | Chaque tâche du compte qui correspond aux événements choisis |
| Signature | X-Webhook-Signature (voir ci-dessous) | En-têtes Standard Webhooks (voir ci-dessous) |
La plupart des intégrations n’ont besoin que du callback de tâche.
1. Obtenir votre secret de signature
Chaque livraison est signée avec le secret de signature des webhooks de votre compte. Ouvrez la page Webhooks de la console API, générez (ou renouvelez) le secret et stockez-le sur votre serveur sous POPNOW_WEBHOOK_SECRET. Il n’est affiché qu’une fois. Un renouvellement prend effet immédiatement, y compris pour les livraisons encore en cours de nouvelles tentatives : mettez vos récepteurs à jour juste après.
2. Passer 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 doit être en HTTPS public sur le port 443 (ou sans port), sans nom d’utilisateur ni mot de passe, et ne se résoudre qu’en adresses publiques — vérifié à la création de la tâche puis à nouveau à la connexion. http://localhost ne peut pas fonctionner : en développement, utilisez un tunnel comme Cloudflare Tunnel ou ngrok, ou interrogez la tâche. Une URL invalide renvoie 400 avec msg: "Invalid callback URL".
3. Ce qui arrive
Quand la tâche atteint un état final, PopNow envoie 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: 2Le corps est exactement ce que recordInfo renverrait pour cette tâche :
{
"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…"
}codevaut200même pour une tâche en échec : il décrit la livraison, pas la tâche. Lisezdata.state.request_idest l’identifiant de livraison. Il reste identique d’une tentative à l’autre : servez-vous-en pour ignorer les doublons.- Les liens signés de
outputexpirent au bout d’une vingtaine de minutes. Conservez letaskId, pas le lien.
4. Vérifier la signature
signature = base64( HMAC-SHA256( secret, "<taskId>.<timestamp>.<sha256_hex(raw_body)>" ) )taskId est data.taskId dans le corps, timestamp est l’en-tête X-Webhook-Timestamp, et la dernière partie est le SHA-256 du corps brut de la requête, en hexadécimal minuscule. Hachez les octets reçus, pas un objet resérialisé : l’ordre des clés et les espaces changeraient le résultat.
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
});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 "", 200X-Webhook-Payload-Version vaut 2 pour toute nouvelle intégration. La version 1 est un ancien format conservé pour les récepteurs existants ; si vous la rencontrez, l’identifiant de tâche se trouve dans task_id au premier niveau.
Répondre vite
Toute réponse 2xx en moins de 15 secondes vaut accusé de réception. Tout le reste — un autre statut, un délai dépassé, une connexion refusée — déclenche une nouvelle tentative. Accusez réception d’abord et faites le travail lent (téléchargement, transcodage, notification des utilisateurs) dans une tâche de fond.
Nouvelles tentatives et doublons
Une livraison échouée est retentée 7 fois de plus, après 10 s, 30 s, 2 min, 10 min, 30 min, 2 h et 6 h. Chaque nouvelle tentative est re-signée avec un horodatage neuf : comparez donc toujours l’horodatage à l’heure actuelle, pas à la première tentative. Après la dernière, la livraison est marquée en échec dans la console API, où vous pouvez la renvoyer à la main.
Une même tâche peut donc arriver plusieurs fois. Rendez votre gestionnaire idempotent : stockez request_id avec un index unique, et si vous le revoyez, répondez 200 sans rien faire.
Si votre serveur a été indisponible plus longtemps que la fenêtre de nouvelles tentatives, listez les tâches récentes avec GET /api/v1/jobs et lisez celles que vous avez manquées avec recordInfo.
Endpoints de compte
Un endpoint de compte est une URL enregistrée une fois dans la console API, avec les événements qu’elle souhaite recevoir. Il reçoit les événements de toutes les tâches du compte : vous n’avez plus à passer callBackUrl.
| Événement | Envoyé quand |
|---|---|
task.completed | L’un des quatre ci-dessous survient. Abonnez-vous à celui-ci, sauf besoin de les distinguer |
task.succeeded | Une tâche a réussi |
task.failed | Une tâche a échoué ; la réservation a été libérée |
task.expired | Une tâche a dépassé son échéance ; la réservation a été libérée |
task.canceled | Conservé pour les anciens enregistrements uniquement |
Les livraisons suivent le format Standard Webhooks : les bibliothèques existantes fonctionnent telles quelles.
webhook-id: msg_01k3m8x9q2z4v7n5p6r8s0t1w3
webhook-timestamp: 1787045531
webhook-signature: v1,base64( HMAC-SHA256( key, "<webhook-id>.<webhook-timestamp>.<raw_body>" ) )Deux différences avec le callback de tâche : c’est le corps lui-même qui est signé (pas d’étape SHA-256), et la clé HMAC correspond aux octets obtenus en décodant en Base64 la partie du secret qui suit whsec_ — pas à la chaîne elle-même. Pendant un renouvellement de secret, l’en-tête peut contenir deux signatures séparées par un espace ; acceptez la requête si l’une d’elles correspond. webhook-id reste identique d’une tentative à l’autre : utilisez-le pour ignorer les doublons. Le data.object de l’événement est l’enregistrement de la tâche au moment où l’événement a été créé.

