Задачи и вебхуки
Генерация медиа (видео, изображения, музыка) — асинхронная: вы создаёте задачу (job) и забираете результат, когда он готов. Есть два способа узнать, что задача завершилась: опрос (polling) и вебхук (push). Ниже — как устроена задача и как получать результат надёжно.
Жизненный цикл задачи
POST /media/generate → status: processing ──► done (result_url готов)
│ └─► failed (error, средства возвращены)
└── (опционально) callback_url → пуш-вебхук на done/failed- Создание.
POST /media/generateс моделью и параметрами — возвращаетidи ссылку для опроса:
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }Ожидание. Задача проходит
processing→done(илиfailed). Изображения — секунды, видео и музыка — от десятков секунд до пары минут.Результат. У завершённой задачи заполнен
result_url(ссылка наnordrouter.com); файл хранится ограниченное время (retention_daysизGET /media/models) — скачайте, чтобы сохранить.
Формат ответа GET /media/job/:id (и тела вебхука — см. ниже) одинаковый:
{ "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }status—processing|done|failed.result_url— заполнен только приdone, иначеnull.result_url_2— второй трек у музыкальных моделей (Suno), иначеnull.cost_usd— фактическая стоимость (списывается по факту; неудачные генерации не тарифицируются — резерв возвращается).error— текст ошибки приfailed, иначеnull.
Способ 1. Опрос (polling)
Самый простой путь — периодически запрашивать GET /media/job/:id, пока status не станет done/failed:
curl https://nordrouter.com/media/job/beefb531-…Опрашивайте раз в 2–3 секунды. Опрос работает всегда и не требует публичного адреса — подходит для скриптов, ноутбуков, CI.
Способ 2. Вебхуки (push)
Чтобы не опрашивать — передайте callback_url в POST /media/generate, и мы сами отправим POST на ваш адрес, когда задача завершится:
curl https://nordrouter.com/media/generate \
-H "Authorization: Bearer sk-nr-YOUR-KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "image/nano-banana-2",
"input": { "prompt": "…" },
"callback_url": "https://your-server.com/webhooks/nordrouter",
"callback_secret": "любая-ваша-строка"
}'Если callback_url принят, ответ на создание содержит "webhook": "registered". callback_url проверяется сразу при создании задачи: если он не подходит (не https, приватный/локальный адрес) — запрос вернёт 400 с причиной, задача не создаётся.
Тело вебхука
Когда задача доходит до done/failed, на ваш callback_url приходит POST с тем же телом, что и GET /media/job/:id, плюс поле event:
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }event—media.completed(успех) илиmedia.failed(ошибка).
Проверка подписи
Если вы задали callback_secret, заголовок X-NR-Signature содержит sha256=<hmac> — HMAC-SHA256 от сырого тела запроса, с ключом = ваш секрет. Проверьте его, чтобы убедиться, что запрос от нас, а не подделка:
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header) # сравнение за константное времяimport crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}Считайте HMAC от байтов ровно как пришли, до JSON-парсинга — иначе подпись не сойдётся.
Готовый приёмник за 10 строк
Рабочий сервер, который принимает вебхук, проверяет подпись и отвечает 2xx. Секрет — тот же, что вы передали в callback_secret.
Node.js / Express:
import express from "express";
import crypto from "node:crypto";
const SECRET = "любая-ваша-строка"; // = ваш callback_secret
const app = express();
app.post("/webhooks/nordrouter",
express.raw({ type: "application/json" }), // сырое тело — обязательно для подписи
(req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const got = req.headers["x-nr-signature"] || "";
if (expected.length !== got.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got)))
return res.sendStatus(401); // подпись не сошлась — не наш запрос
const job = JSON.parse(req.body);
if (job.event === "media.completed") console.log("готово:", job.result_url, "$" + job.cost_usd);
res.sendStatus(200); // ответьте 2xx, иначе будет повтор
});
app.listen(3000);Python / FastAPI:
import hmac, hashlib
from fastapi import FastAPI, Request, Response
SECRET = b"любая-ваша-строка" # = ваш callback_secret
app = FastAPI()
@app.post("/webhooks/nordrouter")
async def hook(request: Request):
raw = await request.body() # сырые байты — обязательно для подписи
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-NR-Signature", "")):
return Response(status_code=401) # подпись не сошлась — не наш запрос
job = await request.json()
if job["event"] == "media.completed":
print("готово:", job["result_url"], "$", job["cost_usd"])
return Response(status_code=200) # ответьте 2xx, иначе будет повторПроверить локально
callback_url должен быть публичным https. Для разработки на ноутбуке поднимите туннель — например ngrok http 3000 — и передайте выданный им https-адрес как callback_url.
Доставка и надёжность
- До 4 попыток с нарастающей паузой (≈3 → 6 → 9 c), таймаут каждой — 15 c. Ответьте
2xx, иначе будет повтор. - Редиректы не проходятся — отвечайте напрямую по
callback_url. - Доставка — best-effort: если все 4 попытки не удались (ваш сервер лежал), вебхук не переотправляется позже. Поэтому:
Вебхуки не заменяют опрос
Вебхук — это ускорение, а не гарантия. Держите GET /media/job/:id как запасной путь: если пуш не пришёл за разумное время — опросите статус сами. Идемпотентность на вашей стороне: одна и та же задача может прийти как вебхук и быть замечена опросом — ориентируйтесь на id.
Требования к callback_url
- Только
https://. - Только публичный адрес. Локальные и приватные адреса (
localhost,*.local,10.x,172.16–31.x,192.168.x,127.x,169.254.x,100.64–127.x, IPv6 loopback/ULA/link-local) отклоняются — это защита от SSRF. Адрес перепроверяется и перед каждой отправкой (защита от DNS-rebinding).
Что выбрать
| Опрос | Вебхук | |
|---|---|---|
Публичный https-адрес | не нужен | нужен |
| Задержка получения | до интервала опроса | почти мгновенно |
| Гарантия | вы контролируете | best-effort + опрос как запас |
| Где удобно | скрипты, CI, ноутбук | серверы, продакшн-бэкенды |
Рекомендация: на бэкенде — вебхук + опрос как страховка; в скрипте/ноутбуке — просто опрос.