10Основы API
Вебхуки
Мы пришлём подписанный POST на ваш адрес, когда задание станет completed или failed. Постоянно опрашивать статус не нужно.
Эта модель рисует не все разрешения и форматы кадра: в примерах стоят те, что ей доступны.
Подготовка
Получите секрет подписи
В кабинете, в разделе «Вебхуки», скопируйте секрет подписи аккаунта. Он начинается с
whsec_и один на все ваши API-ключи. Секрет должен существовать к моменту, когда задание завершится. Это не API-ключ: значения разные.Подготовьте адрес
Нужен HTTPS-адрес, доступный из интернета, с действительным сертификатом. Localhost, адреса внутренних сетей и другие запрещённые назначения не подходят. Длина адреса — до 2048 знаков. Переадресации при доставке не выполняются, поэтому укажите конечный адрес.
Добавьте
webhook_urlв запросУказывайте адрес в каждом запросе генерации, для которого нужно уведомление. Неверный или запрещённый адрес вернёт
400до списания.{ "model": "nano-banana-pro", "prompt": "Керамическая кружка на мраморе", "webhook_url": "https://example.com/ webhooks/ rendergrid" }
Что приходит на ваш адрес
| Заголовок | Значение |
|---|---|
Content-Type | application/json |
X-Webhook-Event | creation.completed или creation.failed. В логике опирайтесь на поле event из подписанного тела |
X-Webhook-Delivery-ID | Идентификатор доставки. При повторах той же доставки не меняется |
X-Timestamp | Время подписи в секундах Unix. Для каждой попытки считается заново |
X-Webhook-Signature-256 | sha256= и подпись HMAC-SHA256 строчными шестнадцатеричными знаками |
События
Тело события — JSON с полями event, id, status, result_urls и cost. Событие ошибки добавляет error и failure.
{
"event": "creation.completed",
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "completed",
"result_urls": [
"https:// cdn.rendergrid.io/ images/ example/ result.jpg"
],
"cost": 0.04
}{
"event": "creation.failed",
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "failed",
"result_urls": [],
"cost": 0.04,
"error": "Generation failed.",
"failure": {
"type": "error",
"code": "internal",
"retryable": true
}
}{
"event": "creation.completed",
"id": "test",
"test": true
}- cost
- Исходное списание в USD. Это не подтверждение возврата и не баланс. Если суммы нет, придёт
null. - error
- Текст для показа или
null. Успех и неудачу определяйте поstatus, а не по тексту. - failure
- Причина неудачи для программы:
type,code,retryable. Что они значат. - result_urls
- Ссылки на файлы. У неудачного события массив пуст. Файлы живут сутки: скачайте их сразу.
Новые поля могут появиться позже. Если вы проверяете тело по строгой схеме, разрешите неизвестные поля, иначе доставка начнёт отклоняться.
Проверка подписи
Подпись нужна, чтобы убедиться: письмо пришло от нас и его не меняли. Считайте её так:
- Возьмите исходные байты тела — до того, как фреймворк разберёт JSON.
- Склейте значение
X-Timestamp, точку и тело. - Посчитайте HMAC-SHA256 от этой строки. Ключ — секрет целиком, вместе с
whsec_, в байтах UTF-8. - Сравните
sha256=и результат с заголовкомX-Webhook-Signature-256за постоянное время. - Отклоните запрос, если метка времени вне окна. В примерах окно — 300 секунд; это правило получателя, а не срок повторной доставки.
import hashlib import hmac import re import time TOLERANCE_SECONDS = 300 def verify_webhook( secret: str, timestamp: str, raw_body: bytes, signature: str ) -> bool: if not isinstance(secret, str) or not secret: return False if not isinstance(raw_body, bytes): return False if not re.fullmatch(r"[0-9]{1,12}", timestamp or ""): return False if not re.fullmatch(r"sha256=[0-9a-f]{64}", signature or ""): return False if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False expected = "sha256=" + hmac.new( secret.encode("utf-8"), timestamp.encode("ascii") + b"." + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature)
import { createHmac, timingSafeEqual } from "node:crypto"; const TOLERANCE_SECONDS = 300; const TS_RE = /^[0-9]{1,12}$/; const SIG_RE = /^sha256=[0-9a-f]{64}$/; export function verifyWebhook( secret, timestamp, rawBody, signature, ) { if (typeof secret !== "string" || !secret) return false; if (!Buffer.isBuffer(rawBody)) return false; if (typeof timestamp !== "string" || !TS_RE.test(timestamp)) { return false; } if (typeof signature !== "string" || !SIG_RE.test(signature)) { return false; } const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (age > TOLERANCE_SECONDS) return false; const expected = "sha256=" + createHmac("sha256", Buffer.from(secret, "utf8")) .update(timestamp + ".", "ascii") .update(rawBody) .digest("hex"); return timingSafeEqual( Buffer.from(expected, "ascii"), Buffer.from(signature, "ascii"), ); }
Не разбирайте и не собирайте JSON заново до проверки: меняются пробелы, экранирование и порядок ключей, и подпись перестаёт сходиться. Не декодируйте секрет и не отрезайте у него whsec_. Синхронизируйте часы сервера.
Разбирайте JSON и выполняйте действия только после успешной проверки.
Проверьте на примере
Три готовых запроса, подписанных по-настоящему. Выберите один и нажмите кнопку.
Подтверждение и повторы
Любой ответ 2xx — это «получили». Остальные ответы и сетевые ошибки мы считаем неудачей и повторяем доставку: по умолчанию до пяти попыток всего.
| Попытка | Когда |
|---|---|
| 1 | сразу, как задание завершилось |
| 2 | через 60 секунд после первой |
| 3 | через 120 секунд после второй |
| 4 | через 240 секунд после третьей |
| 5 | через 480 секунд после четвёртой |
- Ждём ответа 10 секунд. Подтверждайте приём быстро, а скачивание картинок и долгую работу делайте после.
- Сначала надёжно сохраните событие или поставьте его в очередь, потом отвечайте
2xx. Не получилось принять — ответьте другим статусом. - Выполняйте действие один раз на пару
(id, event).X-Webhook-Delivery-IDпомогает убирать дубли. Повтор должен быть безопасен, даже если ваше подтверждение потерялось. - Ошибка доставки не переводит задание в
failed. Состояние всегда можно узнать уGET /creations/, а историю доставок — посмотреть в кабинете.{id}
Секрет подписи и его замена
Все API-ключи аккаунта подписывают вебхуки одним секретом. Храните его на сервере, в хранилище секретов.
После замены в кабинете новый секрет сразу становится текущим: им подписываются все следующие попытки, включая повторы. Обновите обработчик сразу и ещё 24 часа принимайте предыдущий секрет, продолжая проверять свежесть метки времени. Хранится только один предыдущий секрет: следующая замена его вытеснит.
Тестовая отправка
В кабинете можно отправить подписанный тест на разрешённый HTTPS-адрес. Не чаще раза в 30 секунд на аккаунт; тест уходит один раз, без повторов. Тело короче настоящего события:
{
"event": "creation.completed",
"id": "test",
"test": true
}Проверьте подпись, увидьте test: true, ответьте 2xx и не обрабатывайте результат. В тесте нет status, result_urls и cost: не требуйте их. Журнал последних доставок и их исходов — там же, в кабинете, самые новые сверху.
Частые вопросы
Нужно ли указывать webhook_url в каждом запросе?
Что делать, если вебхук не пришёл?
GET /creations/ {id}. Доставка не гарантируется на сто процентов, поэтому опрос — запасной путь.Почему подпись не сходится?
whsec_ или считают подпись не от тех байт. Берите исходное тело и значение X-Timestamp как есть.