Практика API · обновлено 20 октября 2026 г. · 20 минут

Видеогенерация через API: пошаговая инструкция с cURL, Python и JavaScript

Сделаем один короткий ролик по тексту и получим ссылку на видео вместе с фактической стоимостью. Сначала проверим формат запроса и цену через cURL, затем разберём две готовые программы: на Python и JavaScript. Для первого запуска выберите один язык: запуск обоих примеров как отдельных заказов может создать два платных ролика.

Пошаговое подключение видеогенерации: JSON, ожидание и готовый ролик на экране
Одна учебная сцена и один жизненный цикл задания для cURL, Python и Node.js.

Что вы получите и за что будет оплата

В конце у вас появится JSON завершённого задания и ссылка на видео. В том же ответе будут выбранный маршрут provider, фактическое списание billing.cost_rub и подтверждённый расход провайдера billing.upstream_cost_usd. Это удобнее, чем угадывать цену по изменению доступного баланса: часть суммы во время работы могла быть резервом.

Учебная сцена — лиса, медленно идущая по осеннему лесу. Закажем 5 секунд, горизонтальный кадр 16:9 и выключим звук. Используем каноническую модель kwaivgi/kling-v3.0-pro и общий маршрут /v1/media. Этот формат запроса проходил проверку схемы и оценки Gate без платной генерации; приведённые в статье программы при вашем запуске создают настоящий оплачиваемый заказ.

На схеме ниже показан порядок действий. Это раскадровка работы приложения: сначала текст задания, затем очередь, потом файл и учёт. Художественная обложка и схема не являются скриншотами сгенерированного результата и не обещают точного совпадения ролика с иллюстрацией.

Как это устроено
  1. Подготовить: Ключ и JSON на вашем сервере
  2. Проверить: Схема и предварительная оценка
  3. Отправить: Один заказ со стабильным ключом
  4. Получить: Файл и окончательное списание
Этапы программы: ключ на сервере, схема и оценка, одно задание, опрос и готовый файл с итоговой стоимостью
Обучающая раскадровка интеграции. Платная операция — создание задания; ожидание продолжается по сохранённому ID.

Шаг 1. Подготовьте ключ и отдельную папку

Создайте папку для примера и откройте в ней терминал. В кабинете Celestria Gate создайте ключ в разделе «Ключи», проверьте баланс и лимиты. Ключ даёт право расходовать баланс: храните его в окружении серверного процесса, не вставляйте в публичный HTML, мобильное приложение, репозиторий или скриншот. Отдельный ключ fal для этих примеров не требуется.

Для Python нужен Python 3.10 или новее и библиотека requests. Для JavaScript используйте Node.js 22 или новее: fetch и ограничение ожидания HTTP-запроса доступны без установки отдельного SDK. Вам нужен один из этих вариантов. Вызовы проверок схемы и оценки не создают платный ролик; POST /generations создаёт.

  1. Выберите язык

    Если уже используете Python, берите Python. Для проекта на JavaScript подойдёт Node.js. Не запускайте оба примера ради проверки одного заказа.

  2. Подготовьте папку

    Создайте файлы обычным редактором текста. Сохраняйте расширение .py для Python или .mjs для JavaScript; не добавляйте в конец .txt.

  3. Проверьте баланс

    До отправки сравните оценку со своим бюджетом. В ответе оценки нет обещания постоянного тарифа.

Шаг 2. Задайте переменные окружения

Для macOS и Linux откройте bash командой bash, затем выполните строки ниже. read попросит ключ скрытым вводом: вставьте его и нажмите Enter. Он не появится в тексте команды. IDEMPOTENCY_KEY — имя одного заказа, а не секрет. Подставьте свой уникальный номер и сохраните его. Значение не должно быть длиннее 200 символов.

Для продолжения того же заказа после обрыва связи оставляйте прежний IDEMPOTENCY_KEY. Для нового ролика задайте новое значение. Код сохраняет ID и полное тело заказа в файл, имя которого зависит от ключа. При повторном запуске он сначала сравнит сохранённое тело с текущим и только после совпадения продолжит прежнее задание. Изменённый prompt или параметры с тем же ключом вызовут локальную ошибку до обращения к API.

bash
read -r -s -p "Ключ Celestria Gate: " CELESTRIA_API_KEY
echo
export CELESTRIA_API_KEY
export IDEMPOTENCY_KEY="fox-demo-20261020-your-order-001"

В Windows PowerShell используйте отдельный вариант ниже. curl.exe обозначает настоящую программу cURL; примеры с переносами через обратную косую черту далее рассчитаны на bash. Полные Python- и Node.js-программы работают на обеих системах.

Windows: безопасный ввод ключа в PowerShell

Переменные действуют в текущем окне терминала и наследуются запущенной из него программой. Если открыть другое окно, задайте их заново. В рабочем приложении используйте штатное секретное хранилище своего сервера, а не такие учебные команды запуска.

$secureKey = Read-Host "Ключ Celestria Gate" -AsSecureString
$env:CELESTRIA_API_KEY = [System.Net.NetworkCredential]::new("", $secureKey).Password
$env:IDEMPOTENCY_KEY = "fox-demo-20261020-your-order-001"

Шаг 3. Разберите запрос и получите его схему

Сохраните JSON ниже как request.json. В общем контракте duration — число; generate_audio — boolean. Поле operation выбирает режим создания по тексту. Название модели копируйте полностью: семейство Kling включает разные версии и режимы, которые нельзя незаметно подменять друг другом.

resolution в этом примере намеренно не указан. Нельзя обещать точный 1080p только по слову Pro. Текущий общий контракт Kling 3 Pro отклоняет неподдерживаемые явные параметры; подходящий размер нужно отдельно подтвердить схемой и оценкой. При смене модели снова прочитайте её схему, даже если prompt остался тем же.

{
  "model": "kwaivgi/kling-v3.0-pro",
  "input": {
    "operation": "text-to-video",
    "prompt": "A red fox walks slowly through an autumn forest. One continuous shot, a fixed camera, soft morning light. No text or logos.",
    "duration": 5,
    "aspect_ratio": "16:9",
    "generate_audio": false
  }
}

Шаг 4. Проверьте схему и цену через cURL

Первая команда получает схему, вторая оценивает содержимое файла. Если схема не загрузилась или оценка вернула ошибку, не запускайте генерацию до исправления причины. Параметры API должны соответствовать выбранному режиму, а не только выглядеть как корректный JSON.

Ответ /estimate сообщает model, provider, reason, compared и estimate. cost_rub — ориентир в рублях, upstream_cost_usd — предварительный расход провайдера. null не означает нулевую стоимость. Решение оценки не фиксирует цену на неограниченное время: при создании заказа сервер проверит актуальный маршрут.

curl --fail-with-body --get \
  "https://api.celestriagate.com/v1/media/models/schema" \
  -H "Authorization: Bearer $CELESTRIA_API_KEY" \
  --data-urlencode "model=kwaivgi/kling-v3.0-pro" \
  --data-urlencode "operation=text-to-video"

curl --fail-with-body \
  "https://api.celestriagate.com/v1/media/estimate" \
  -H "Authorization: Bearer $CELESTRIA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @request.json

Как выглядят платная отправка и ожидание

Для понимания протокола ниже показаны два вызова. POST создаёт платный заказ и отвечает HTTP 202. Сохраните id из JSON или путь Location из заголовка ответа. Затем GET проверяет существующий заказ. В учебной команде MEDIA_JOB_ID — заменяемое обозначение, а не настоящий идентификатор.

Выберите способ запуска: либо отправьте cURL вручную и продолжайте GET, либо запускайте одну из программ ниже. Не выполняйте все платные примеры подряд. Если cURL уже создал задание, передайте его ID в программу через её файл состояния или продолжайте ручной опрос; новая независимая отправка с новым ключом будет другим заказом.

Пока видитеДействие клиента
queued, submitting, pendingПродолжать опрос по ID
in_progressДождаться провайдера; не создавать новый заказ
awaiting_billingДождаться подтверждения итогового расхода
completedПрочитать files и billing, показать результат
failed, cancelledОстановить ожидание, сохранить error и billing
curl --fail-with-body \
  "https://api.celestriagate.com/v1/media/generations" \
  -H "Authorization: Bearer $CELESTRIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data-binary @request.json

curl --fail-with-body \
  "https://api.celestriagate.com/v1/media/generations/MEDIA_JOB_ID" \
  -H "Authorization: Bearer $CELESTRIA_API_KEY"

Вариант A. Установите requests для Python

В той же папке создайте отдельное окружение, чтобы учебная библиотека не меняла зависимости других проектов. На macOS/Linux выполните команды ниже. После активации окружения команда python указывает на его интерпретатор. В PowerShell создайте окружение через py -m venv .venv; активация не обязательна: библиотеку можно установить командой .venv\Scripts\python.exe -m pip install requests.

Следующий полный пример сохраните как video.py. Он сохраняет тело запроса перед отправкой, затем дописывает ID и опрашивает задание с паузой 5 секунд. При сетевой ошибке повтор HTTP-запроса ограничен тремя попытками. POST повторяется с тем же Idempotency-Key и тем же телом; это восстановление одного заказа. При запуске с изменённым телом и прежним ключом программа остановится до сетевых вызовов.

python3 -m venv .venv
source .venv/bin/activate
python -m pip install requests

Полная программа Python: запуск, ожидание и списание

import hashlib
import json
import os
from pathlib import Path
import time
from urllib.parse import quote, urljoin, urlsplit
import re

import requests

BASE = "https://api.celestriagate.com"
TOKEN = os.environ["CELESTRIA_API_KEY"]
KEY = os.environ["IDEMPOTENCY_KEY"]
if not 1 <= len(KEY) <= 200:
    raise SystemExit("IDEMPOTENCY_KEY: от 1 до 200 символов")
REQUEST = {
    "model": "kwaivgi/kling-v3.0-pro",
    "input": {
        "operation": "text-to-video",
        "prompt": "A red fox walks slowly through an autumn forest. One continuous shot, a fixed camera, soft morning light. No text or logos.",
        "duration": 5,
        "aspect_ratio": "16:9",
        "generate_audio": False,
    },
}
tag = hashlib.sha256(KEY.encode()).hexdigest()[:16]
state_file = Path(f"gate-job-{tag}.json")
result_file = Path(f"gate-result-{tag}.json")
session = requests.Session()
session.headers["Authorization"] = f"Bearer {TOKEN}"


def canonical(value):
    return json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"))


def safe_media_url(value):
    if not isinstance(value, str) or len(value) > 16_384:
        return None
    try:
        parsed = urlsplit(urljoin(BASE, value))
        host = (parsed.hostname or "").lower()
        trusted_cdn = host == "fal.media" or host.endswith(".fal.media")
        gate_video = host == "api.celestriagate.com" and bool(
            re.fullmatch(r"/(?:api/)?v1/videos/[a-zA-Z0-9_-]+/content", parsed.path)
        )
        if (parsed.scheme != "https" or parsed.username or parsed.password
                or parsed.fragment or parsed.port not in (None, 443)
                or not (trusted_cdn or gate_video)):
            return None
        return parsed.geturl()
    except ValueError:
        return None


def video_candidates(job):
    found = []

    def add(raw, video_context=False):
        item = {"url": raw} if isinstance(raw, str) else raw
        if not isinstance(item, dict):
            return
        url = safe_media_url(item.get("url"))
        if not url:
            return
        mime = str(item.get("content_type") or "").lower().split(";")[0]
        name = str(item.get("file_name") or "")
        path = urlsplit(url).path
        extension = any(re.search(r"\.(mp4|webm|mov|m4v)
quot;, value, re.I) for value in (name, path)) if (video_context or mime.startswith("video/") or extension) and url not in found: found.append(url) for item in job.get("files", []): add(item) result = job.get("result") if isinstance(result, dict): add(result.get("video"), video_context=True) add(result.get("video_url"), video_context=True) return found def api(method, path, body=None, key=None): headers = {"Idempotency-Key": key} if key else {} for attempt in range(3): try: response = session.request( method, BASE + path, json=body, headers=headers, timeout=(10, 30), ) except (requests.Timeout, requests.ConnectionError): if attempt == 2: raise RuntimeError( "Связь оборвалась. Повторите программу с тем же " "IDEMPOTENCY_KEY и тем же REQUEST." ) from None time.sleep(2 * (attempt + 1)) continue if response.status_code == 429 or response.status_code >= 500: if attempt < 2: time.sleep(2 * (attempt + 1)) continue try: data = response.json() except ValueError: raise RuntimeError( f"HTTP {response.status_code}: ожидался JSON; " "сохраните прежний IDEMPOTENCY_KEY." ) from None if not response.ok: error = data.get("error", {}) if isinstance(data, dict) else {} raise RuntimeError( f"HTTP {response.status_code}: " f"{error.get('code', 'api_error')}: " f"{error.get('message', 'запрос отклонён')}" ) return data raise RuntimeError("Не удалось получить ответ") def main(): state = {"request": REQUEST, "id": None} if state_file.exists(): state = json.loads(state_file.read_text()) if canonical(state.get("request")) != canonical(REQUEST): raise RuntimeError( "Тело заказа изменено при прежнем IDEMPOTENCY_KEY. " "Для нового заказа задайте новый ключ; " "для продолжения восстановите исходный REQUEST." ) job_id = state.get("id") if job_id: print("Продолжаем существующее задание:", job_id) else: api("GET", "/v1/media/models/schema?model=" + quote(REQUEST["model"], safe="") + "&operation=text-to-video") estimate = api("POST", "/v1/media/estimate", REQUEST) print("Оценка, НЕ итоговое списание:", estimate["estimate"]) print("Следующий вызов создаёт платное задание.") state_file.write_text(json.dumps(state, ensure_ascii=False, indent=2)) job = api("POST", "/v1/media/generations", REQUEST, KEY) job_id = job["id"] state["id"] = job_id state_file.write_text(json.dumps(state, ensure_ascii=False, indent=2)) print("Сохранён ID:", job_id) path = "/v1/media/generations/" + quote(job_id, safe="") deadline = time.monotonic() + 20 * 60 while time.monotonic() < deadline: job = api("GET", path) print(job["id"], job["status"]) if job["status"] == "completed": result_file.write_text(json.dumps(job, ensure_ascii=False, indent=2)) print("Провайдер:", job.get("provider")) print("Списано Gate, RUB:", job["billing"]["cost_rub"]) print("Расход провайдера, USD:", job["billing"]["upstream_cost_usd"]) print("Текущий резерв, RUB:", job["billing"]["reserved_rub"]) videos = video_candidates(job) for url in videos: print("Видео (по метаданным):", url) if not videos: print("Видео не определено безопасно. Проверьте result и output_schema модели.") print("Полный результат:", result_file) return if job["status"] in {"failed", "cancelled"}: print("Ошибка:", job.get("error")) print("Учёт задания:", job.get("billing")) raise SystemExit(1) time.sleep(5) raise SystemExit( f"20 минут ожидания истекли. Задание {job_id} не отменено. " "Запустите программу позже с тем же IDEMPOTENCY_KEY." ) if __name__ == "__main__": main()

Запуск в активированном окружении: python video.py. В Windows без активации: .venv\Scripts\python.exe video.py. Переменные CELESTRIA_API_KEY и IDEMPOTENCY_KEY должны быть заданы в том же терминале. В код не нужно вставлять сам ключ.

Программа ограничивает один HTTP-вызов и прекращает основной цикл ожидания примерно через 20 минут; несколько последних сетевых попыток могут добавить время. Истечение времени программы не отменяет задание на сервере. Повторный запуск с прежним ключом и сохранённым файлом состояния продолжит опрос, а не создаст новый ролик.

Вариант B. Полная программа JavaScript для Node.js

Сохраните следующий код как video.mjs. Расширение .mjs позволяет использовать import без дополнительных настроек проекта. Установите Node.js 22 или новее и проверьте node --version. Дополнительные npm-пакеты этому примеру не нужны. Запуск: node video.mjs.

Этот вариант делает тот же заказ, что Python. Он использует то же имя файла состояния для одного ключа: в одной папке можно продолжить ожидание известного задания после смены языка. Если хотите отдельный новый ролик, задайте новый IDEMPOTENCY_KEY. Не запускайте два экземпляра программы одновременно ради одного заказа.

import { createHash } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";

const BASE = "https://api.celestriagate.com";
const token = process.env.CELESTRIA_API_KEY;
const key = process.env.IDEMPOTENCY_KEY;
if (!token || !key || key.length > 200) {
  throw new Error("Нужны CELESTRIA_API_KEY и IDEMPOTENCY_KEY (до 200 символов)");
}
const request = {
  model: "kwaivgi/kling-v3.0-pro",
  input: {
    operation: "text-to-video",
    prompt: "A red fox walks slowly through an autumn forest. One continuous shot, a fixed camera, soft morning light. No text or logos.",
    duration: 5,
    aspect_ratio: "16:9",
    generate_audio: false,
  },
};
const tag = createHash("sha256").update(key).digest("hex").slice(0, 16);
const stateFile = `gate-job-${tag}.json`;
const resultFile = `gate-result-${tag}.json`;

function canonical(value) {
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
  if (value && typeof value === "object") {
    return `{${Object.keys(value).sort().map((name) => `${JSON.stringify(name)}:${canonical(value[name])}`).join(",")}}`;
  }
  return JSON.stringify(value);
}

function safeMediaUrl(value) {
  if (typeof value !== "string" || value.length > 16_384) return null;
  try {
    const url = new URL(value, BASE);
    const host = url.hostname.toLowerCase();
    const trustedCdn = host === "fal.media" || host.endsWith(".fal.media");
    const gateVideo = host === "api.celestriagate.com"
      && /^\/(?:api\/)?v1\/videos\/[a-zA-Z0-9_-]+\/content$/.test(url.pathname);
    if (url.protocol !== "https:" || url.username || url.password || url.hash
        || (url.port && url.port !== "443") || !(trustedCdn || gateVideo)) return null;
    return url.href;
  } catch {
    return null;
  }
}

function videoCandidates(job) {
  const found = new Set();
  function add(raw, videoContext = false) {
    const item = typeof raw === "string" ? { url: raw } : raw;
    if (!item || typeof item !== "object") return;
    const url = safeMediaUrl(item.url);
    if (!url) return;
    const mime = String(item.content_type ?? "").toLowerCase().split(";")[0];
    const name = String(item.file_name ?? "");
    const extension = /\.(mp4|webm|mov|m4v)$/i.test(name)
      || /\.(mp4|webm|mov|m4v)$/i.test(new URL(url).pathname);
    if (videoContext || mime.startsWith("video/") || extension) found.add(url);
  }
  for (const item of job.files ?? []) add(item);
  add(job.result?.video, true);
  add(job.result?.video_url, true);
  return [...found];
}


async function api(method, path, body, idempotencyKey) {
  for (let attempt = 0; attempt < 3; attempt++) {
    let response;
    try {
      response = await fetch(BASE + path, {
        method,
        headers: {
          Authorization: `Bearer ${token}`,
          ...(body ? { "Content-Type": "application/json" } : {}),
          ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: AbortSignal.timeout(30_000),
      });
    } catch {
      if (attempt === 2) {
        throw new Error("Связь оборвалась. Повторите программу с тем же IDEMPOTENCY_KEY и тем же request.");
      }
      await sleep(2_000 * (attempt + 1));
      continue;
    }
    if ((response.status === 429 || response.status >= 500) && attempt < 2) {
      await response.arrayBuffer();
      await sleep(2_000 * (attempt + 1));
      continue;
    }
    let data;
    try {
      data = await response.json();
    } catch {
      throw new Error(`HTTP ${response.status}: ожидался JSON. Сохраните прежний IDEMPOTENCY_KEY.`);
    }
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${data.error?.code ?? "api_error"}: ${data.error?.message ?? "запрос отклонён"}`);
    }
    return data;
  }
  throw new Error("Не удалось получить ответ");
}

async function main() {
  let state = { request, id: null };
  if (existsSync(stateFile)) {
    state = JSON.parse(readFileSync(stateFile, "utf8"));
    if (canonical(state.request) !== canonical(request)) {
      throw new Error("Тело заказа изменено при прежнем IDEMPOTENCY_KEY. Для нового заказа задайте новый ключ; для продолжения восстановите исходный request.");
    }
  }
  let jobId = state.id;
  if (jobId) {
    console.log("Продолжаем существующее задание:", jobId);
  } else {
    const model = encodeURIComponent(request.model);
    await api("GET", `/v1/media/models/schema?model=${model}&operation=text-to-video`);
    const estimate = await api("POST", "/v1/media/estimate", request);
    console.log("Оценка, НЕ итоговое списание:", estimate.estimate);
    console.log("Следующий вызов создаёт платное задание.");
    writeFileSync(stateFile, JSON.stringify(state, null, 2), { mode: 0o600 });
    const job = await api("POST", "/v1/media/generations", request, key);
    jobId = job.id;
    state.id = jobId;
    writeFileSync(stateFile, JSON.stringify(state, null, 2), { mode: 0o600 });
    console.log("Сохранён ID:", jobId);
  }
  const path = `/v1/media/generations/${encodeURIComponent(jobId)}`;
  const deadline = Date.now() + 20 * 60_000;
  while (Date.now() < deadline) {
    const job = await api("GET", path);
    console.log(job.id, job.status);
    if (job.status === "completed") {
      writeFileSync(resultFile, JSON.stringify(job, null, 2), { mode: 0o600 });
      console.log("Провайдер:", job.provider);
      console.log("Списано Gate, RUB:", job.billing.cost_rub);
      console.log("Расход провайдера, USD:", job.billing.upstream_cost_usd);
      console.log("Текущий резерв, RUB:", job.billing.reserved_rub);
      const videos = videoCandidates(job);
      for (const url of videos) console.log("Видео (по метаданным):", url);
      if (!videos.length) console.log("Видео не определено безопасно. Проверьте result и output_schema модели.");
      console.log("Полный результат:", resultFile);
      return;
    }
    if (["failed", "cancelled"].includes(job.status)) {
      console.error("Ошибка:", job.error);
      console.error("Учёт задания:", job.billing);
      process.exitCode = 1;
      return;
    }
    await sleep(5_000);
  }
  throw new Error(`20 минут ожидания истекли. Задание ${jobId} не отменено. Запустите программу позже с тем же IDEMPOTENCY_KEY.`);
}

main().catch((error) => {
  console.error(error.message);
  process.exitCode = 1;
});

Как прочитать результат программы

После completed программа печатает провайдера, сумму в рублях, расход провайдера в долларах и текущий резерв. Полный ответ сохраняется в gate-result-…json. Массив files не гарантирует нормализованный MIME: content_type может быть null или application/octet-stream. Примеры находят ссылки на видео по video/ MIME, расширению URL или file_name, затем проверяют result.video и result.video_url. Это определение по метаданным, а не проверка содержимого файла. При неизвестной структуре сверяйте result с output_schema выбранной модели.

Перед выводом ссылок примеры проверяют HTTPS, отсутствие пароля в URL и разрешённый хост: fal.media и его поддомены либо путь выдачи видео api.celestriagate.com. Это учебный список доверенных адресов, а не обещание, что все будущие модели будут использовать тот же CDN. Если модель возвращает другой хост или неизвестный формат, сначала проверьте документацию и правила своего приложения, затем обновите обработчик. Не отключайте проверки целиком. Ссылка может иметь срок действия: сохранённый ID позволяет запросить свежий ответ. Для долгого хранения перенесите файл в своё хранилище с подходящими правами.

Файл gate-job-…json содержит тело заказа и ID, но не API-ключ. Не редактируйте его вручную для обхода проверки. Для нового содержания задайте новый IDEMPOTENCY_KEY. Для старого заказа восстановите исходное тело и прежний ключ. Результаты, подписанные ссылки и исходные материалы тоже не стоит добавлять в публичный репозиторий.

Ниже — сокращённый условный ответ, который объясняет названия полей. ID учебный, стоимости намеренно не заданы: это не отчёт о платном запуске и не обещание нулевого расхода. В настоящем completed используйте подтверждённые значения, вернувшиеся от сервера.

{
  "id": "media_generation_EXAMPLE",
  "object": "media_generation",
  "provider": "fal-native",
  "status": "queued",
  "files": [],
  "billing": {
    "currency": "RUB",
    "estimated_cost_rub": null,
    "reserved_rub": 0,
    "cost_rub": null,
    "upstream_cost_usd": null
  }
}

queued в этом фрагменте выбран специально: до завершения файлы отсутствуют, а окончательное списание ещё не подтверждено. Не показывайте такой ответ пользователю как готовый результат.

Что делать при ошибке: короткая памятка

Отсутствующий ключ даёт ошибку авторизации; нехватка доступного баланса или ограничение аккаунта требует проверки кабинета. Неверные поля и неподдерживаемые режимы исправляйте по сообщению error и схеме. Эти причины не лечатся многократным нажатием «Отправить». При конфликте 409 проверьте, не использован ли Idempotency-Key для другого тела.

Если сервер не ответил на POST или вернул временную ошибку, исход мог остаться неизвестным. Примеры сохраняют прежний ключ при ограниченных повторах. Не меняйте ключ до выяснения состояния старого заказа. Если ID уже есть, работайте через GET. Для истории собственной организации доступен GET /v1/media/generations.

Задание failed не даёт права предполагать любую произвольную сумму: сохраняйте error, billing и ID для разбора. Отмена POST /v1/media/generations/:id/cancel возможна только на безопасном для отмены этапе. После отправки провайдеру нельзя обещать бесплатную отмену; локальное завершение программы тоже не является отменой.

СитуацияПравильный следующий шаг
400: схема или маршрут не подходитПроверить модель, operation и поля; затем сформировать новый осмысленный заказ
401: ключ не принятПроверить CELESTRIA_API_KEY в текущем процессе
409: ключ уже связан с другим теломВосстановить прежнее тело для прежнего заказа; новому заказу дать другой ключ
429 или временная 5xxОграниченный повтор того же запроса с прежним ключом
Сетевой тайм-аут после отправкиСчитать исход неизвестным; получить известный ID или повторить то же тело с прежним ключом
Долгая очередь / awaiting_billingПродолжить GET позже; новый платный POST не нужен

Как перейти от примера к реальному приложению

В рабочем сервисе храните ID задания и ключ идемпотентности в базе вместе с заказом пользователя. Возвращайте браузеру номер заказа и статус; длительное ожидание выполняйте серверным процессом. Ограничьте число параллельных задач по своему бюджету и разрешениям аккаунта. Это сохраняет понятную связь между нажатием пользователя, результатом и одним списанием.

При добавлении фотографии или видео не подменяйте задачу только новой строкой prompt. Смените operation, получите соответствующую схему и передайте доступные провайдеру URL материалов. Локальный путь C:\video.mp4 или /Users/name/photo.png не является интернет-ссылкой для модели. Для замены персонажа отдельно описывайте, что меняется и что должно сохраниться.

Различайте доказательство работы API и оценку качества. В отдельном тесте обычного чата 20 октября 2026 года Kling O1 заменил персонажа по видео и фото через fal: подтверждено $0,84 у провайдера и 93,79 ₽ списания Gate. Это другой режим и конкретное выполненное задание; переносить его цену на данный пример Kling 3 Pro нельзя.

Частые вопросы

Можно ли просто скопировать программу и запустить?

Да: установите выбранный язык, для Python добавьте requests, задайте CELESTRIA_API_KEY и уникальный IDEMPOTENCY_KEY. Сначала проверьте оценку отдельным cURL-вызовом: программа затем создаёт настоящий платный заказ. Запускайте один вариант, а не оба как разные заказы.

Что сохранится после закрытия терминала?

Файл gate-job-…json хранит полное тело заказа и ID принятого задания. С тем же IDEMPOTENCY_KEY программа сначала проверит совпадение тела и продолжит GET. Если ответ создания потерялся, сохранённое исходное тело и прежний ключ восстанавливают тот же заказ в пределах организации. Изменённое тело с прежним ключом будет отклонено локально.

Можно ли пользоваться ключом API прямо в React-приложении?

Ключ не должен попадать в публичный клиентский код. Браузер обращается к вашему серверу, а сервер хранит CELESTRIA_API_KEY и вызывает Gate. Пример JavaScript предназначен для Node.js на сервере.

Почему оценка отличается от суммы в результате?

Оценка отражает предварительный расчёт до отправки. billing.cost_rub после completed — окончательное списание для конкретного заказа. Резерв reserved_rub учитывается отдельно и не означает вторую оплату.

Как добавить звук или другую модель?

Сначала прочитайте схему новой модели и режима. Только если generate_audio поддерживается, задавайте его явно и повторно оценивайте запрос. Звук, длительность, размер и версия модели меняют совместимые маршруты и цену; произвольно добавлять неизвестные параметры нельзя.

Источники

Канонический адрес материала · Получить API-ключ