PopNowДокументация API
Работа с API
Основное содержимое

Вебхуки

Как получать завершённые задачи на свой URL, проверять подпись и обрабатывать повторы и дубликаты.

Вебхук позволяет PopNow самому сообщить вам, что задача готова, — не нужно спрашивать снова и снова. Есть два вида:

Колбэк задачиЭндпоинт аккаунта
НастройкаcallBackUrl в каждом createTaskОдин раз в API-консоли
Для чего отправляетсяДля этой одной задачиДля каждой задачи аккаунта, подходящей под выбранные события
ПодписьX-Webhook-Signature (ниже)Заголовки Standard Webhooks (ниже)

Большинству интеграций хватает колбэка задачи.

1. Получите секрет подписи

Каждая доставка подписывается секретом подписи вебхуков вашего аккаунта. Откройте страницу Webhooks в API-консоли, создайте (или обновите) секрет и сохраните его на сервере как POPNOW_WEBHOOK_SECRET. Он показывается один раз. Обновление действует сразу, в том числе для доставок, которые ещё повторяются, поэтому сразу после него обновите получателей.

2. Передайте 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"
}

URL должен быть публичным HTTPS на порту 443 (или без порта), без имени пользователя и пароля, и разрешаться только в публичные адреса — это проверяется при создании задачи и ещё раз при подключении. http://localhost работать не может; при разработке используйте туннель вроде Cloudflare Tunnel или ngrok либо опрос. Неверный URL даёт 400 с msg: "Invalid callback URL".

3. Что приходит

Когда задача доходит до конечного состояния, PopNow отправляет один 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

Тело в точности такое же, как вернул бы recordInfo для этой задачи:

{
  "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 даже для упавшей задачи: он описывает доставку, а не задачу. Смотрите на data.state.
  • request_id — это ID доставки. Он не меняется между повторами, по нему удобно отсекать дубликаты.
  • Подписанные ссылки в output истекают примерно через 20 минут. Храните taskId, а не ссылку.

4. Проверьте подпись

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

taskId — это data.taskId из тела, timestamp — заголовок X-Webhook-Timestamp, а последняя часть — SHA-256 сырого тела запроса в нижнем регистре hex. Хешируйте полученные байты, а не заново сериализованный объект: порядок ключей и пробелы изменят результат.

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. Версия 1 — старый формат, оставленный для существующих получателей; если он вдруг придёт, ID задачи лежит в поле верхнего уровня task_id.

Отвечайте быстро

Любой ответ 2xx в течение 15 секунд считается доставкой. Всё остальное — другой статус, таймаут, отказ в соединении — повторяется. Сначала подтвердите получение, а медленную работу (скачивание, перекодирование, уведомления пользователей) делайте в фоновой задаче.

Повторы и дубликаты

Неудачная доставка повторяется ещё 7 раз — через 10 с, 30 с, 2 мин, 10 мин, 30 мин, 2 ч и 6 ч. Каждый повтор подписывается заново со свежей отметкой времени, поэтому всегда сравнивайте её с текущим временем, а не с первой попыткой. После последнего повтора доставка помечается в API-консоли как неудачная, и её можно отправить вручную.

Значит, одна и та же задача может прийти не один раз. Сделайте обработчик идемпотентным: храните request_id с уникальным индексом и, увидев его снова, отвечайте 200 и ничего не делайте.

Если ваш сервер лежал дольше, чем длится окно повторов, получите последние задачи через GET /api/v1/jobs и дочитайте пропущенные через recordInfo.

Эндпоинты аккаунта

Эндпоинт аккаунта — это URL, который один раз регистрируется в API-консоли вместе с нужными событиями. Он получает события по всем задачам аккаунта, так что передавать callBackUrl больше не нужно.

СобытиеКогда отправляется
task.completedНаступило любое из четырёх ниже. Подписывайтесь на него, если различать их не нужно
task.succeededЗадача успешно завершилась
task.failedЗадача упала; резерв возвращён
task.expiredЗадача не уложилась в срок; резерв возвращён
task.canceledОставлено только для старых записей

Доставки следуют формату Standard Webhooks, поэтому готовые библиотеки работают как есть:

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

Два отличия от колбэка задачи: подписывается само тело (без шага SHA-256), а ключ HMAC — это байты, полученные декодированием из Base64 части секрета после whsec_, а не сама строка. Во время смены секрета в заголовке может быть две подписи через пробел; принимайте запрос, если совпала любая. webhook-id между повторами не меняется — по нему отсекайте дубликаты. data.object в событии — это запись задачи на момент создания события.

На этой странице