NezhaGateNezhaGate
API Documentation

Вебхуки: уведомления о завершении задач

Генерация видео и изображений выполняется асинхронно: вы отправляете запрос, получаете ID задачи, а результат приходит чуть позже. Вместо постоянного опроса API мы можем отправить результат на ваш сервер, как только задача завершится, и предупредить, когда баланс будет подходить к концу.

Где пригодятся вебхуки и как настроить их за три шага — читайте руководство →

Два способа получать события

  1. Адрес аккаунта по умолчанию: в консоли откройте «Настройки → Вебхуки», укажите URL эндпоинта и отметьте нужные события. После этого о завершении каждой задачи, отправленной через API, будет приходить уведомление. Открыть настройки вебхуков →
  2. callback_url в запросе: добавьте поле callback_url при отправке задачи на генерацию видео или изображения. Результат этой задачи (успех или ошибка) придёт только на этот URL, независимо от того, что отмечено в настройках.

callback_url должен быть общедоступным адресом http(s) длиной не более 2000 символов; написание callBackUrl тоже принимается. Оба способа используют один и тот же секрет подписи — он показан в разделе «Настройки → Вебхуки».

curl · Отправка задачи с callback_url:
curl https://nezhagate.com/v1/videos/generations -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' -d '{"model": "seedance-2.5", "prompt": "a paper boat drifting down a rainy street", "duration": 5, "callback_url": "https://example.com/webhooks/nezhagate"}'

События

typeКогда отправляетсяСодержимое data
video.completedвидео успешно созданов точности тот же JSON, что возвращают эндпоинты задач (GET /v1/videos/jobs/{id}, GET /v1/images/jobs/{id})
video.failedне удалось создать видео (кредиты возвращены полностью)
image.completedизображение успешно создано
image.failedне удалось создать изображение (кредиты возвращены полностью)
balance.lowбаланс опустился ниже заданного вами числа кредитов (один раз при каждом снижении; после пополнения уведомление снова включается)balance_credits (текущий баланс в кредитах) и threshold_credits (заданный вами порог)
webhook.testвы нажали «Отправить тестовое событие» в настройкахкороткое сообщение — чтобы убедиться, что события доходят и подпись проверяется

Формат запроса

Мы отправляем на ваш URL POST-запрос с телом в формате JSON и такими заголовками:

webhook-idID события. При повторных отправках не меняется — используйте его для дедупликации.
webhook-timestampВремя отправки этой попытки (секунды Unix). Обновляется при каждой попытке.
webhook-signatureПодпись: префикс v1, и затем HMAC-SHA256 в кодировке Base64.
Content-TypeВсегда application/json.

Примеры тела запроса

Видео готово:

video.completed
{
  "id": "evt_5f0c3d2e8a9b4c71d2e3f405",
  "object": "event",
  "type": "video.completed",
  "created_at": 1791200000,
  "data": {
    "id": "img_3f9a...c2",
    "object": "video.generation.job",
    "status": "succeeded",
    "created": 1791199400,
    "model": "seedance-2.5",
    "data": [{ "url": "https://img.nezhagate.com/i/9f86d081a8....mp4" }],
    "usage": { "clips": 1, "model": "seedance-2.5" }
  }
}

Если задача не удалась, в data передаётся error (как и в эндпоинтах задач), а списанные кредиты уже полностью возвращены:

image.failed
{
  "id": "evt_8b1e47c09d3a2f6e5b7c1d90",
  "object": "event",
  "type": "image.failed",
  "created_at": 1791200120,
  "data": {
    "id": "img_7c21...e4",
    "object": "image.generation.job",
    "status": "failed",
    "created": 1791200050,
    "model": "gpt-image-2",
    "error": { "message": "...", "type": "upstream_error", "code": "upstream_error" }
  }
}
balance.low
{
  "id": "evt_1a2b3c4d5e6f708192a3b4c5",
  "object": "event",
  "type": "balance.low",
  "created_at": 1791203000,
  "data": { "balance_credits": 412.5, "threshold_credits": 500 }
}

Проверка подписи

События подписываются по спецификации Standard Webhooks (её же используют вебхуки OpenAI). Как проверить подпись:

  1. Считайте заголовки webhook-id, webhook-timestamp и webhook-signature и исходное тело запроса (не разбирайте его в JSON, чтобы потом сериализовать заново).
  2. Соберите подписываемую строку: webhook-id + "." + webhook-timestamp + "." + исходное тело.
  3. Ключ — это часть после whsec_, декодированная из Base64. Вычислите с ним HMAC-SHA256 и закодируйте результат в Base64.
  4. В webhook-signature может быть несколько подписей (через пробел, каждая начинается с v1,); проверка пройдена, если совпадает любая из них. Используйте сравнение за постоянное время.
  5. Чтобы защититься от атак повторного воспроизведения, отклоняйте запросы, метка времени которых расходится с вашими часами более чем на 5 минут.
Python · Flask
import base64, hashlib, hmac, time
from flask import Flask, abort, request

SECRET = "whsec_..."   # Settings -> Webhook -> Signing secret
KEY = base64.b64decode(SECRET[len("whsec_"):])

def verify(headers, body: bytes) -> bool:
    msg_id = headers.get("webhook-id", "")
    ts = headers.get("webhook-timestamp", "")
    if not msg_id or not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    signed = f"{msg_id}.{ts}.".encode() + body
    expected = base64.b64encode(hmac.new(KEY, signed, hashlib.sha256).digest()).decode()
    return any(sig.startswith("v1,") and hmac.compare_digest(sig[3:], expected)
               for sig in headers.get("webhook-signature", "").split())

app = Flask(__name__)

@app.post("/webhooks/nezhagate")
def webhook():
    if not verify(request.headers, request.get_data()):
        abort(401)
    event = request.get_json()
    if event["type"] == "video.completed":
        print(event["data"]["id"], event["data"]["data"][0]["url"])
    return "", 204
Node.js · Express
import crypto from "node:crypto";
import express from "express";

const SECRET = "whsec_...";   // Settings -> Webhook -> Signing secret
const KEY = Buffer.from(SECRET.slice("whsec_".length), "base64");

const app = express();
app.post("/webhooks/nezhagate", express.raw({ type: "application/json" }), (req, res) => {
  const id = req.header("webhook-id"), ts = req.header("webhook-timestamp");
  if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
  const expected = Buffer.from(
    crypto.createHmac("sha256", KEY).update(`${id}.${ts}.`).update(req.body).digest("base64"));
  const ok = (req.header("webhook-signature") || "").split(" ").some((sig) => {
    const got = Buffer.from(sig.slice(3));
    return sig.startsWith("v1,") && got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
  if (!ok) return res.sendStatus(401);
  const event = JSON.parse(req.body.toString("utf8"));
  console.log(event.type, event.data.id);
  res.sendStatus(204);
});
app.listen(3000);

Можно также использовать официальную библиотеку Standard Webhooks для вашего языка (Python: pip install standardwebhooks; Node: npm install standardwebhooks).

Ответы и повторные отправки

  • Любой ответ 2xx в течение 10 секунд считается доставкой; тело ответа не важно. Сначала ответьте 2xx, а затем обрабатывайте событие.
  • Если ответа 2xx нет (тайм-аут, ошибка соединения, 3xx, 4xx, 5xx), мы повторяем отправку через 1 мин, 5 мин, 30 мин, 2 ч и 6 ч, после чего помечаем её как неудачную.
  • Одно и то же событие может прийти несколько раз и в любом порядке: убирайте дубликаты по webhook-id и ориентируйтесь на data.status.
  • В разделе «Настройки → Вебхуки» показаны отправки за последние 30 дней с результатом каждой; любую из них можно отправить повторно.

Безопасность

  • Только публичные адреса: частные, локальные и зарезервированные адреса отклоняются.
  • Перенаправления не выполняются: ответ 3xx считается неудачей.
  • Храните секрет подписи так же надёжно, как API Key. Если он утёк, смените его в настройках — старый сразу перестанет работать.

Тестирование

Сохраните URL в разделе «Настройки → Вебхуки» и нажмите «Отправить тестовое событие». Мы сразу отправим событие webhook.test и покажем код состояния, которым ответил ваш сервер, и время ответа.

Открыть настройки вебхуков