6ioAPI

Уведомления (вебхуки)

Картинки, видео, звук и фоновые задания делаются очередью: запрос сразу возвращает номер, а результат готов позже. Чтобы не спрашивать статус каждые несколько секунд, укажите адрес вашего сервера. Когда запрос закончится, мы отправим на него POST с итогом.

Опрос от этого не отменяется. Уведомление может не дойти (сервер был недоступен, сеть), а статус по номеру всегда можно получить запросом GET. Считайте уведомление быстрым способом узнать о конце, а GET — способом проверить.

Как передать адрес

Что запускаете Куда писать адрес
модель Sixio (POST /native/sixio/tasks) поле webhook_url в теле, рядом с model и input
фоновое задание (POST /v1/jobs) поле webhook_url в теле
модель fal (POST /native/fal/queue/...) параметр строки запроса fal_webhook, как у самого fal

Чат POST /v1/chat/completions отвечает в том же соединении, уведомлений у него нет.

curl -X POST "https://api.6io.io/native/sixio/tasks" \
  -H "Authorization: Bearer sixio_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "sixio/flux-3/klein-text-to-image",
    "input": {"prompt": "a red circle on a white background", "aspectRatio": "1:1", "resolution": "1K"},
    "webhook_url": "https://example.com/hooks/6io"
  }'

Для fal адрес идёт в строке запроса и должен быть закодирован: ?fal_webhook=https%3A%2F%2Fexample.com%2Fhooks%2F6io.

Какой адрес подходит

Адрес проверяется до того, как резервируются деньги. Неподходящий даёт 400 invalid_webhook_url, и ничего не запускается.

  • Только https. Адрес не длиннее 2048 знаков и без логина и пароля.
  • Только публичный интернет. Адреса из закрытых сетей (10.x, 192.168.x, localhost), служебные и зарезервированные адреса не принимаются. Проверка идёт ещё раз при соединении, поэтому подменить адрес через DNS не получится.
  • Не адрес этого API и не 6io.
  • Редиректы мы не выполняем. Укажите конечный адрес, без перенаправления с http на https и без слэша, который добавит сервер.

Чтобы проверить приём на своём компьютере, откройте на нём временный публичный адрес (любой туннель, например ngrok или cloudflared) и передайте его.

Что приходит

POST на ваш адрес, тело — JSON, Content-Type: application/json. Формат — Standard Webhooks: по нему есть готовые библиотеки проверки подписи для многих языков.

Заголовки:

Заголовок Что значит
webhook-id номер уведомления, вида msg_req_…; при повторах не меняется
webhook-timestamp время отправки, секунды Unix
webhook-signature подпись, вида v1,<base64> (если подписей несколько, через пробел)
X-Sixio-Request-Id номер вашего запроса

Тело:

{
  "type": "request.finished",
  "timestamp": "2026-10-27T09:12:44Z",
  "data": { "...": "то же, что вернёт GET статуса этого запроса" }
}

type всегда request.finished. В data лежит то же, что отдаёт запрос статуса.

Sixio — как GET /native/sixio/tasks/{id}:

{
  "id": "req_8fK2xQ1mZpVwT9sJdL4nAg",
  "object": "task",
  "model": "sixio/flux-3/klein-text-to-image",
  "status": "succeeded",
  "created_at": "2026-10-27T09:12:10Z",
  "finished_at": "2026-10-27T09:12:44Z",
  "billing": { "status": "settled", "charged_credits": "3.200000000000" },
  "status_url": "https://api.6io.io/native/sixio/tasks/req_8fK2xQ1mZpVwT9sJdL4nAg",
  "result": { "urls": ["https://api.6io.io/api/download/…"] }
}

Фоновое задание — как GET /v1/jobs/{id} (сам ответ чата забирается по result_url).

fal — в форме вебхука fal:

{
  "request_id": "req_8fK2xQ1mZpVwT9sJdL4nAg",
  "status": "OK",
  "outcome": "succeeded",
  "payload": { "images": [{ "url": "https://api.6io.io/api/download/…" }] },
  "status_url": "https://api.6io.io/native/fal/requests/req_8fK2xQ1mZpVwT9sJdL4nAg/status",
  "response_url": "https://api.6io.io/native/fal/requests/req_8fK2xQ1mZpVwT9sJdL4nAg"
}

status — OK или ERROR (при ERROR есть error, payload — null). outcome — наше слово состояния. Результат больше 256 КБ в уведомление не кладётся: payload будет null, а payload_error скажет забрать его по response_url.

Состояние в status (outcome у fal): succeeded — выполнено, failed — не выполнено, unknown — исход пока не подтверждён, деньги проверяются: уведомление уйдёт один раз с этим словом, а итог смотрите позже в GET /v1/requests/{id}.

Как проверить подпись

Не доверяйте запросу, пока не проверили подпись: на ваш адрес может написать кто угодно.

Секрет подписи лежит в консоли, на вкладке «Ключи», блок «Подпись уведомлений». Он начинается с whsec_, общий для всех ключей аккаунта и меняется кнопкой «Сменить секрет» (старый перестаёт работать сразу).

Подписывается строка webhook-id + . + webhook-timestamp + . + тело. Подпись — HMAC-SHA256 с ключом из секрета (часть после whsec_ — это base64), результат в base64. Тело берите байтами, как пришло: если разобрать JSON и собрать заново, подпись не совпадёт. Отвергайте уведомления, у которых время больше чем на 5 минут расходится с вашим.

Python:

import base64, hashlib, hmac, time


def verify(secret: str, headers: dict, body: bytes) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        return False  # слишком старое (или из будущего) уведомление
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(
        hmac.compare_digest(expected, s.removeprefix("v1,"))
        for s in headers["webhook-signature"].split()
        if s.startswith("v1,")
    )

Приёмник на Flask:

from flask import Flask, request

app = Flask(__name__)
SECRET = "whsec_..."  # из консоли


@app.post("/hooks/6io")
def hook():
    if not verify(SECRET, request.headers, request.get_data()):
        return "bad signature", 400
    event = request.get_json()
    data = event["data"]
    print(data.get("id") or data.get("request_id"), data.get("status"))
    return "", 204

Node.js:

import crypto from "node:crypto";

export function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // слишком старое (или из будущего) уведомление
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.`).update(rawBody).digest("base64");
  return headers["webhook-signature"].split(" ").some((s) => {
    if (!s.startsWith("v1,")) return false;
    const got = Buffer.from(s.slice(3));
    const want = Buffer.from(expected);
    return got.length === want.length && crypto.timingSafeEqual(got, want);
  });
}

В Express возьмите тело как Buffer (express.raw({ type: "application/json" })), а не разобранный JSON.

Что должен делать ваш приёмник

  • Ответить любым кодом 2xx в течение 10 секунд. Тело ответа игнорируется. Всю тяжёлую работу делайте после ответа.
  • Быть готовым к повтору. Одно и то же уведомление (тот же webhook-id) может прийти дважды: сервер мог не успеть ответить, а мы повторили. Запоминайте обработанные webhook-id или делайте обработку безопасной для повтора.
  • Не считать тело снимком момента конца. При повторе оно строится заново и отражает состояние на момент отправки.

Повторы

Если ответа 2xx нет (ошибка, 4xx, 5xx, таймаут, адрес недоступен), мы повторяем отправку. Всего семь попыток, паузы между ними растут:

Попытка Через сколько после предыдущей
1 сразу, как запрос закончился
2 5 секунд
3 5 минут
4 30 минут
5 2 часа
6 5 часов
7 10 часов

Всего около 17,5 часа. Ответ 410 Gone значит «больше не присылайте»: повторов не будет. После последней неудачной попытки уведомление не отправляется, и итог нужно забрать запросом GET.

Если уведомление не пришло

В ответе статуса (GET /native/sixio/tasks/{id}, GET /v1/jobs/{id}, GET /native/fal/requests/{id}/status) у запроса с адресом есть поле webhook:

"webhook": { "status": "pending", "attempts": 2, "last_code": 503 }

status — pending (ещё не доставлено, идут попытки), delivered (приёмник ответил 2xx) или failed (попытки кончились). attempts — сколько попыток сделано, last_code — код последнего ответа приёмника (null, если ответа не было).

Ограничения

  • Адрес задаётся в каждом запросе отдельно, общего адреса на аккаунт пока нет.
  • Уведомление приходит один раз на запрос, в момент, когда он вышел из «в работе». Промежуточных событий (например, «началось») нет.
  • Для одного запроса один адрес.