genmux API теперь доступен всем. Читать документацию →
genmux API

Блог

Генерация изображений в Node.js: вебхуки вместо опроса

2 мин чтенияАвтор Unified Image API Team


Опрос работает, но на масштабе тратит запросы и добавляет секунды задержки. Передайте webhook_url при создании задания — и мы отправим POST в момент завершения: с подписью, повторами и ровно тем payload, который нужен. Туториал на чистом Node 20+, без SDK.

Отправка с вебхуком

ts
const BASE = process.env.IMAGE_API_BASE!;
const KEY = process.env.IMAGE_API_KEY!;

export async function submit(prompt: string) {
  const res = await fetch(`${BASE}/v1/generations`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "nanobanana",
      prompt,
      resolution: "1K",
      ratio: "1:1",
      webhook_url: "https://yourapp.com/hooks/image-api",
    }),
  });
  if (!res.ok) throw new Error(`submit ${res.status}: ${await res.text()}`);
  return res.json(); // { id, status: "queued", cost, ... }
}

Приём и проверка

Каждая доставка подписана HMAC-SHA256 от строки timestamp + "." + rawBody секретом вебхуков вашего аккаунта (Кабинет → Настройки). Проверяйте до того, как чему-либо доверять:

ts
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();

app.post("/hooks/image-api", express.raw({ type: "application/json" }), (req, res) => {
  const ts = req.header("X-Webhook-Timestamp")!;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); // устарело

  const expected = createHmac("sha256", process.env.WEBHOOK_SECRET!)
    .update(`${ts}.${req.body}`).digest("hex");
  const ok = (req.header("X-Webhook-Signature") ?? "").split(",").some((s) => {
    const v = s.replace(/^v1=/, "");
    return v.length === expected.length && timingSafeEqual(Buffer.from(v), Buffer.from(expected));
  });
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString());
  // { type: "generation.succeeded" | "generation.failed", id, status, error_code, ... }
  queueMicrotask(() => handle(event)); // быстрый ack, работа потом
  res.sendStatus(200);
});

Три привычки, которые делают это продакшен-решением:

  1. Проверяйте подпись на сыром теле. Любая пересериализация JSON ломает подпись — обратите внимание на express.raw.
  2. Отвечайте быстро, обрабатывайте асинхронно. Мы повторяем доставку до 8 раз с бэкоффом, пока не получим 2xx; медленный обработчик создаёт вам дубли.
  3. Будьте идемпотентны. Повторы означают, что событие может прийти дважды — ключуйте обработку по id задания.

Получение изображения

В payload вебхука намеренно нет ссылки на изображение (payload должен быть маленьким и без чувствительных данных). На generation.succeeded запросите задание — придут свежие подписанные ссылки:

ts
async function handle(event: { id: string; type: string }) {
  if (event.type !== "generation.succeeded") return;
  const job = await (await fetch(`${BASE}/v1/generations/${event.id}`, {
    headers: { Authorization: `Bearer ${KEY}` },
  })).json();
  const buf = Buffer.from(await (await fetch(job.images[0].url)).arrayBuffer());
  // сохраните buf туда, где ваше приложение держит ассеты
}

Для локальной разработки выведите обработчик наружу туннелем (cloudflared tunnel --url http://localhost:3000) — URL вебхука должен быть публичным https. Полный справочник: документация по вебхукам.

Об авторе

Автор Unified Image API Team — genmux API.

Подробнее о нас