spicyapiДокументация
SDK
Основное содержимое

TypeScript SDK

Пошаговое руководство по TypeScript SDK @spicyapi/sdk — как установить его, получить первый результат и найти любой метод, параметр и ошибку с объяснением простыми словами. SDK для Python, Go, PHP и Java описаны на собственных страницах.

@spicyapi/sdk — официальная библиотека SpicyAPI для TypeScript и JavaScript. Эта страница рассчитана на тех, кто ни разу ей не пользовался. Сначала она проведёт вас от пустой папки до сохранённого изображения, а затем опишет каждую функцию пакета: для чего она нужна, что ей передать, что она возвращает, стоит ли вызов денег и что может пойти не так.

Что такое SDK

SDK — это готовая библиотека кода, которую вы подключаете к своей собственной программе. Без неё программе приходится вручную собирать каждый HTTP-запрос: прикладывать API-ключ, превращать ответ из JSON в данные, снова и снова спрашивать, завершилась ли задача, повторять запрос при обрыве сети, загружать файлы в три отдельных шага и проверять подпись каждого вебхука. SDK делает всё это за вас, поэтому целая генерация умещается в один вызов:

const task = await client.run({ model: modelId, input: { prompt: "A paper lantern at night" } });

SDK вам подойдёт, если вы пишете или собираетесь писать программу на Node.js (JavaScript или TypeScript), которая работает на сервере, в виде скрипта на вашем компьютере или в фоновом задании. В TypeScript вы к тому же получаете автодополнение и проверку типов для каждого поля.

Тот ли это инструмент?

Вы хотите…Что использовать
Вызывать SpicyAPI из собственного кода на Node.js или TypeScriptЭтот SDK
Генерировать без написания кода, из терминалаCLI
Чтобы ИИ-ассистент вроде Claude Code или Cursor вызывал SpicyAPI за васMCP-сервер и Agent Skill
Пользоваться чат-моделью или текстовой модельюОфициальный клиент OpenAI, направленный на SpicyAPI, — см. Текст и потоковые ответы
Писать на Python, Go, PHP или JavaОфициальный Python SDK, Go SDK, PHP SDK или Java SDK
Писать на любом другом языкеОбычные HTTP-вызовы или клиент, сгенерированный по описанию API

Чего SDK не делает

  • Он не работает в браузере и в мобильном приложении. Ему нужен Node.js, а ваш API-ключ никогда не должен покидать ваш сервер: из веб-страницы или приложения ключ может вытащить кто угодно. Пусть ваш сайт обращается к вашему собственному бэкенду, а бэкенд — к SpicyAPI.
  • Он не работает с чат-моделями. Генерация текста идёт через эндпоинты, совместимые с OpenAI, и официальный пакет openai уже хорошо с ними справляется. См. Текст и потоковые ответы.
  • Он не выбирает модель и не заполняет поля за вас. Каждая модель принимает свои поля. SDK даёт вам список моделей и схему каждой модели (список полей, которые она принимает), а что отправлять, решаете вы.
  • Он не умеет отменять задачу. Если SpicyAPI принял задачу, она выполняется до конца. Остановка вашей программы останавливает только ваше ожидание.
  • Он не управляет вашим аккаунтом. Пополнение баланса, создание ключей, лимиты расходов и ключи подписи вебхуков настраиваются в консоли.

Ваш API-ключ остаётся на вашем сервере

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

Слова, которые встречаются на этой странице

СловоЧто оно означает здесь
ID моделиТочное имя модели в виде publisher/model/task. Всегда копируйте его из актуального каталога (listModels()), а не по памяти
Входные данные (input)Настройки, которые вы отправляете вместе с задачей, например prompt или image_url. У каждой модели свой список допустимых полей, он называется схемой
ЗадачаОдна работа по генерации. Вы её создаёте, SpicyAPI выполняет её в фоне, а вы забираете результат. У каждой задачи есть ID, который начинается с job_
РезервКогда задача принята, её максимальная цена откладывается с вашего баланса. При ошибке или истечении срока задачи резерв обычно освобождается полностью. Некоторые отказы по содержанию оплачиваются по принятой цене, только если это исключение прямо указано на странице модели. Окончательную сумму проверяйте по settled и cost.
Ключ идемпотентностиМетка, которую вы придумываете для одной единицы работы, например order-8814-image-1. Если отправить один и тот же запрос дважды с одной и той же меткой, SpicyAPI вернёт первую задачу, а не создаст и не оплатит вторую
ВебхукВаш веб-адрес, на который SpicyAPI отправляет запрос, когда задача завершается, чтобы вам не приходилось постоянно спрашивать

Что можно делать

Каждая функция ниже — метод клиента (client.listModels()), если не сказано иное. «Стоит денег» означает, что вызов может привести к списанию с вашего баланса.

Что вы хотите сделатьМетодСтоит денег?
Проверить, доступен ли SpicyAPI (ключ не нужен)getStatusНет
Посмотреть актуальный список моделей, ваши цены и поля каждой моделиlistModels, getModelНет
Узнать точную цену запроса, ничего не тратяquoteTaskНет
Запустить генерациюcreateTaskДа
Запустить генерацию и дождаться результата одним вызовомrunДа
Один раз посмотреть на задачуgetTaskНет
Дождаться завершения задачиwaitForTaskНет
Повторить задачу, завершившуюся ошибкойretryTaskДа (новая задача)
Получить список задач, созданных вашим ключомlistTasksНет
Проверить балансgetBalanceНет
Посмотреть расходы по дням и по моделямgetUsageНет
Загрузить локальный файл изображения, видео или аудиоuploadFile, uploadBytes, uploadBase64Нет
Получить свежую ссылку для скачивания результатаcreateDownloadUrlНет
Удалить сохранённые файлы и промпт завершённой задачиpurgeTaskНет (и деньги не возвращаются)
Проверить, что вебхук действительно пришёл от SpicyAPIverifyWebhook (обычная функция)Нет
Искать по документации или читать контракт API без сетиsearchDocumentation, readOpenApiContract (обычные функции)Нет

Перед началом

Вам понадобится минут десять. У каждого шага объяснено, что делает команда, так что ничего не запускается вслепую.

Установите Node.js 22.13 или новее

Node.js — это программа, которая выполняет JavaScript вне браузера. Установите версию LTS с nodejs.org; подойдёт любая версия начиная с 22.13. Затем откройте терминал («Терминал» на macOS, PowerShell на Windows) и проверьте:

node -v

Команда выводит установленную версию, например v22.13.0 или v24.8.0. Если номер меньше 22.13 или команда не найдена, установите Node.js заново и откройте новое окно терминала.

Создайте папку проекта и установите SDK

Выполняйте эти команды по одной. Они одинаково работают на macOS, Linux и в Windows PowerShell.

mkdir spicy-demo
cd spicy-demo
npm init -y
npm pkg set type=module
npm install @spicyapi/sdk
КомандаЧто она делает
mkdir spicy-demoСоздаёт пустую папку для вашего проекта
cd spicy-demoПереводит терминал в эту папку; следующие команды действуют в ней
npm init -yСоздаёт package.json — файл, где перечислены настройки и библиотеки вашего проекта
npm pkg set type=moduleСообщает Node.js, что ваши файлы используют современный синтаксис import, который нужен этому SDK
npm install @spicyapi/sdkСкачивает SDK в папку node_modules и записывает его в package.json

SDK публикуется только как современный ES-модуль: import { SpicyClient } from "@spicyapi/sdk" работает, а старый вариант require("@spicyapi/sdk") завершается ошибкой ERR_PACKAGE_PATH_NOT_EXPORTED. Если вы пропустили npm pkg set type=module, давайте файлам расширение .mjs.

Создайте API-ключ

  1. Войдите на spicyapi.ai. Аккаунта ещё нет? Зарегистрируйтесь на spicyapi.ai/register; если регистрация новых аккаунтов приостановлена, на этой странице можно записаться в лист ожидания.
  2. Откройте в консоли страницу Ключи API и нажмите Создать ключ. Обязательно указать только название.
  3. Сразу скопируйте ключ. Он начинается с sk-spicy- и показывается только один раз. Если вы его потеряли, создайте новый.

На той же странице можно задать дневной лимит расходов; у новых ключей его нет, пока вы его не добавите. Чтобы запускать платные задачи, нужен ещё и баланс; пополнить его можно в разделе Биллинг.

Сохраните ключ в переменной окружения

Переменная окружения — это именованное значение, которое терминал передаёт каждой запущенной из него программе. SDK ищет переменную с именем SPICY_API_KEY, поэтому ключ не приходится вписывать в код.

# Задать для этого окна терминала (вставьте свой ключ)
export SPICY_API_KEY="sk-spicy-paste-your-key-here"

# Проверка: должно вывести sk-spicy-
echo "${SPICY_API_KEY:0:9}"

Значение пропадёт, когда вы закроете окно. Чтобы сохранить его на своём компьютере, добавьте строку export в ~/.zshrc (macOS) или ~/.bashrc (Linux) и откройте новое окно.

Удобнее файл .env? Создайте в папке проекта файл .env с одной строкой SPICY_API_KEY=sk-spicy-paste-your-key-here и запускайте программу командой node --env-file=.env yourfile.mjs. Сам SDK файлы .env не читает, это делает указанный флаг. Добавьте .env в файл .gitignore до того, как впервые выполните git add, чтобы ключ никогда не попал в коммит.

На настоящем сервере задайте SPICY_API_KEY в настройках секретов или переменных окружения у своего хостинг-провайдера либо передайте ключ из менеджера секретов через new SpicyClient({ apiKey }).

Запустите первый файл

В папке проекта создайте в любом текстовом редакторе файл check.mjs, вставьте в него этот код и сохраните:

check.mjs
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();
const status = await client.getStatus();

console.log(status.operational ? "SpicyAPI is up" : "SpicyAPI is not reachable right now");
console.log("Checked at", status.checkedAt);

Запустите его:

node check.mjs
SpicyAPI is up
Checked at 2026-09-17T08:00:00.000Z

Для этой проверки ключ не нужен, и она ничего не стоит, поэтому она подтверждает, что Node.js и SDK установлены правильно.

Пишете на TypeScript? Сохраните тот же код как check.ts. Свежие выпуски Node.js 22 и 24 запускают его напрямую командой node check.ts. Если ваша версия жалуется на расширение .ts, выполните node --experimental-strip-types check.ts или npx tsx check.ts.

Ваш первый результат

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

Запуск с --yes тратит деньги

Без --yes программа только читает данные: ничего не создаётся и ничего не списывается. С --yes она создаёт одну задачу на изображение, для которой резервируется рассчитанная сумма, и эта сумма списывается, если изображение создано успешно.

first-result.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

// Читает ваш ключ из переменной окружения SPICY_API_KEY.
const client = new SpicyClient();

async function main() {
  // Шаг 1 — доступен ли SpicyAPI? Бесплатно, ключ не нужен.
  const status = await client.getStatus();
  if (!status.operational) {
    throw new Error("SpicyAPI is not reachable right now. Try again in a few minutes.");
  }

  // Шаг 2 — какие модели для изображений может вызывать этот ключ? Бесплатно.
  const catalog = await client.listModels({ modality: "image", includeExamples: true });
  const model = catalog.items.find(
    (item) => item.enabled && item.available && item.examples?.length,
  );
  if (!model) throw new Error("No image model is available to this key right now.");
  console.log(`Model: ${model.displayName} (${model.model})`);

  // Шаг 3 — собираем входные данные. Начинаем с примера, который сервер уже
  // проверил, и подставляем свой промпт, если модель его принимает.
  const input = { ...model.examples?.[0]?.input };
  if ("prompt" in input) input.prompt = "A paper lantern glowing on a rainy street at night";

  // Шаг 4 — запрашиваем точную цену. Бесплатно: ничего не создаётся и не резервируется.
  const request = { model: model.model, input };
  const quote = await client.quoteTask(request);
  console.log(`Price: $${quote.estimatedCost} USD`);

  if (!process.argv.includes("--yes")) {
    console.log("Nothing was charged. Run again with --yes to generate the image.");
    return;
  }

  // Шаг 5 — создаём задачу и ждём её. Именно этот шаг стоит денег.
  const task = await client.run(
    { ...request, quoteId: quote.quoteId, expectedCost: quote.estimatedCost },
    {
      idempotencyKey: randomUUID(),
      onAccepted: (accepted) => console.log(`Task accepted: ${accepted.taskId}`),
      onUpdate: (update) => console.log(`State: ${update.state}`),
    },
  );

  // Шаг 6 — Неуспешная задача возвращается обычным ответом. Проверьте расчёт.
  if (task.state !== "succeeded") {
    console.log(`The task ended as ${task.state}: ${task.errorCode} ${task.errorMessage ?? ""}`);
    console.log("Check settled and cost for the final charge.");
    return;
  }

  // Шаг 7 — скачиваем результат сразу. Ссылка перестаёт работать примерно через 20 минут.
  const asset = task.output?.assets?.find((item) => item.url);
  if (!asset?.url) throw new Error("The task succeeded but its file is unavailable.");
  const download = await fetch(asset.url); // сюда никогда не отправляйте свой API-ключ
  if (!download.ok) throw new Error(`Download failed with HTTP ${download.status}`);
  const extension = asset.mime?.split("/")[1] ?? "png";
  await writeFile(`result.${extension}`, Buffer.from(await download.arrayBuffer()));
  console.log(`Saved result.${extension}. Charged: $${task.cost}${task.settled ? "" : " (not final yet)"}`);
}

main().catch((error) => {
  if (error instanceof SpicyApiError) {
    console.error(`SpicyAPI error ${error.code}: ${error.message} (request_id ${error.requestId})`);
  } else {
    console.error(error);
  }
  process.exitCode = 1;
});

Запустите её дважды:

node first-result.mjs
node first-result.mjs --yes
Model: <display name> (<model ID>)
Price: $<amount> USD
Nothing was charged. Run again with --yes to generate the image.

Model: <display name> (<model ID>)
Price: $<amount> USD
Task accepted: job_…
State: running
State: succeeded
Saved result.png. Charged: $<amount>

Что делает каждый шаг

  1. getStatus() спрашивает у эндпоинта проверки работоспособности SpicyAPI, работает ли сервис. При сетевых проблемах он никогда не выбрасывает исключение, а вместо этого сообщает operational: false.
  2. listModels() возвращает актуальный каталог моделей с ценами вашего аккаунта. Используйте только элементы, у которых и enabled, и available равны true. includeExamples: true добавляет примеры входных данных, которые сервер уже проверил: это самая надёжная отправная точка. Позже вы отправляете значение поля model элемента, то есть ID модели.
  3. Входные данные. Каждая модель принимает свои поля; поле, которого модель не знает, отклоняется с ошибкой 400. Если скопировать проверенный пример и поменять только промпт, этого не случится. Чтобы увидеть все поля, которые принимает модель, вызовите getModel() и прочитайте её inputSchema.
  4. quoteTask() рассчитывает стоимость именно этого запроса. Он ничего не создаёт и ничего не резервирует. Суммы указываются в долларах США и передаются текстом, например "0.04", чтобы не накапливались ошибки округления.
  5. run() создаёт задачу и затем проверяет её, пока она не завершится. Передать quoteId и expectedCost означает «принимай только по той цене, которую мне показали»: если цена изменилась, вызов завершится ошибкой 40901, а не спишет другую сумму. idempotencyKey делает повтор после сетевого сбоя безопасным; в настоящем приложении сохраняйте его до вызова, чтобы перезапущенная программа могла использовать его снова. onAccepted передаёт вам ID задачи в тот момент, когда задача появилась. Сохраните его, чтобы потом проверить задачу, даже если ваша программа остановится.
  6. Состояние задачи. run() возвращается обычным образом и для задач, завершившихся ошибкой. При ошибке или истечении срока задачи резерв обычно освобождается полностью. Некоторые отказы по содержанию оплачиваются по принятой цене, только если это исключение прямо указано на странице модели. Окончательную сумму проверяйте по settled и cost.
  7. Скачивание. Каждый результат приходит с временной ссылкой url. Скачивайте его без API-ключа и храните собственную копию: ссылка действует около 20 минут (чтобы получить свежую, снова вызовите getTask()), а сгенерированные файлы SpicyAPI удаляет самое позднее примерно через 14 дней. См. Хранение и удаление данных.

С видео всё работает так же, но генерация может занять несколько минут; передайте в run() более длинный timeoutMs (по умолчанию ожидание длится 10 минут). ID модели в примерах кода ниже записан как MODEL_ID_FROM_CATALOG: это заполнитель, а не настоящая модель. Замените его значением model элемента из актуального каталога (live catalog) — это listModels() в SDK или аутентифицированный эндпоинт GET /api/v1/models, — у которого и enabled, и available равны true.

Справочник по API

Для каждого метода ниже сказано, для чего он нужен, как его вызвать, что он возвращает, может ли вызов стоить денег и каких ошибок ожидать. Классы ошибок вроде SpicyApiError описаны в разделе Обработка ошибок. Все суммы указаны в долларах США и передаются текстом ("0.04"), все значения времени — строки ISO 8601 в UTC.

Создание клиента

new SpicyClient(options?: SpicyClientOptions)

Большинство программ ничего не передают: new SpicyClient() читает ключ из SPICY_API_KEY и работает с рабочим сервисом. Недопустимый параметр сразу выбрасывает TypeError, ещё до отправки какого-либо запроса.

Свойство

Тип

Автоматические повторы

SDK сам повторяет запрос только тогда, когда из-за этого с вас не могут списать деньги дважды:

ВызовПовторяется автоматически?
Чтение: getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage; а также quoteTask, purgeTask, commitUploadedFileДа
createTask, run (его шаг создания), retryTaskТолько если вы передали idempotencyKey. Без него сетевая ошибка сразу выбрасывается вам, потому что повторная отправка могла бы создать вторую задачу
createUploadUrl, сама отправка байтов, createDownloadUrlНет

Повтор выполняется, когда ответа нет совсем (сетевая ошибка или локальный тайм-аут) или пришёл HTTP 408, 429, 500, 502, 503 или 504. Пауза берётся из заголовка Retry-After ответа сервера, а если его нет — 250 мс, затем 500 мс, затем 1 с и так далее, каждый раз с разбросом до 25% и не дольше maxRetryDelayMs. Если все попытки не удались, вы получаете последнюю ошибку.

Остановка вызова через signal

Каждый метод принимает параметр signal — стандартный AbortSignal. Используйте AbortSignal.timeout(5_000), чтобы задать крайний срок, или AbortController, чтобы остановить вызов по требованию. Тогда вызов выбросит причину прерывания. Остановка завершает только ваш запрос или ожидание: задача, которую SpicyAPI уже принял, продолжает выполняться, и за неё всё равно могут списать деньги.

getStatus()

Проверяет, доступен ли SpicyAPI. Удобно поставить первой строкой скрипта или использовать как проверку работоспособности в собственном мониторинге.

getStatus(options?: { signal?: AbortSignal }): Promise<ServiceStatus>
Возвращаемое полеЗначение
operationaltrue, когда сервис отвечает. Ориентируйтесь именно на это поле
healthНеобработанный результат проверки работоспособности: path, ok, HTTP-статус status, первые 512 символов body и error, если ответ не пришёл
readinessТа же структура для второй проверки. На публичном сервисе она отвечает 404 — это нормально и не означает, что сервис не работает
checkedAtКогда выполнялась проверка

Стоит денег: нет. Нужен ключ: нет. Ошибки: при сетевых проблемах метод не выбрасывает исключение — вы получаете operational: false, а причину в health.error. Исключение будет, только если вы прервали вызов через signal. Пример см. в шаге «Запустите первый файл».

listModels(options?)

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

listModels(options?: ListModelsOptions): Promise<ModelList>

Свойство

Тип

Метод возвращает { total, items }. Самые полезные поля каждого элемента:

ПолеЗначение
modelID модели. При создании задачи отправляйте ровно это значение
displayNameНазвание, удобное для чтения
modality, tasksЧто модель создаёт и какую работу выполняет
provider, familyКто создал модель и к какой линейке продуктов она относится
enabled, availableПрежде чем пользоваться моделью, оба должны быть true
pricingПо одной записи на каждый ценовой уровень: price, unit (per_image, per_second, per_request или per_1k_tokens) и variant (какой это уровень, часто разрешение). Во время скидки regularPrice показывает цену без скидки
startingPriceСамый дешёвый уровень. Точную сумму даёт quoteTask
quantityFieldПоле входных данных, которое задаёт оплачиваемое количество, например длительность. Если пусто, один вызов — одна единица
taskTimeoutSecondsСколько задача может выполняться, прежде чем завершится как expired
maxOutputDurationSecondsМаксимальная длительность результата, если у модели есть длительность
inputSchema, examplesПрисутствуют, только если вы их запросили
updatedAt, versionМеняются при каждом изменении сведений о модели; по ним видно, когда пора обновить сохранённую схему

Стоит денег: нет. Нужен ключ: да. Ошибки: SpicyApiError (например, 401 при проблеме с ключом, 429 при слишком большом числе запросов), SpicyTransportError.

const { items } = await client.listModels({ modality: "video", task: "image-to-video" });

for (const item of items) {
  if (!item.enabled || !item.available) continue;
  const from = item.startingPrice ? `from $${item.startingPrice.price} ${item.startingPrice.unit}` : "";
  console.log(item.model, "|", item.displayName, "|", from);
}

Чтение каталога использует отдельный лимит чтения аккаунта и не расходует квоту создания задач. Сохраните список и время от времени обновляйте его, а также после ошибки 400 или 50301, вместо того чтобы читать каталог перед каждой задачей.

getModel(modelId)

Возвращает одну модель с полной схемой: списком полей входных данных, которые она принимает, с указанием, какие из них обязательны и какие значения допустимы.

getModel(modelId: string, options?: { signal?: AbortSignal }): Promise<ApiModel>

В результате есть все поля, описанные в разделе listModels, и всегда есть inputSchema и examples. Схема записана в стандартном формате JSON Schema: properties перечисляет каждое поле с его type и часто с enum (допустимыми значениями), minimum / maximum и default; required перечисляет поля, которые нужно отправить обязательно.

Стоит денег: нет. Нужен ключ: да. Ошибки: SpicyApiError с 404, если модели с таким ID нет; TypeError, если ID пустой.

const model = await client.getModel("MODEL_ID_FROM_CATALOG");

console.log("Required fields:", model.inputSchema?.required);
console.log(JSON.stringify(model.inputSchema?.properties, null, 2));
console.log("A request that is known to work:", model.examples?.[0]?.input);

quoteTask(request)

Сообщает точную цену запроса, ничего не создавая. Используйте его, чтобы показать цену человеку или отказаться от работы дороже бюджета.

quoteTask(request: { model: string; input: object; callBackUrl?: string }, options?: { signal?: AbortSignal }): Promise<TaskQuote>

Передайте те же model, input и необязательный callBackUrl, с которыми собираетесь создавать задачу.

Возвращаемое полеЗначение
quoteIdПередайте его в createTask или run, чтобы закрепить эту цену
estimatedCostСумма, которая будет зарезервирована, если создать задачу сейчас
maxChargeМаксимум, который может стоить этот запрос
currencyВсегда USD
quantity, unitОплачиваемое количество, например "5" и per_second
expiresAtРасчёт стоимости действует пять минут

Стоит денег: нет. Нужен ключ: да. Ошибки: SpicyApiError, например 400, если входные данные не соответствуют схеме модели, 50301, если модель недоступна, 429.

const request = { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } };
const quote = await client.quoteTask(request);

console.log(`This will cost $${quote.estimatedCost}. Quote valid until ${quote.expiresAt}.`);

createTask(request, options?)

Запускает генерацию и сразу возвращает ID задачи. Результата метод не ждёт: дальше используйте waitForTask или вебхук, либо вызовите run, который делает и то и другое за один вызов.

createTask(request: CreateTaskInput, options?: CreateTaskOptions): Promise<CreateTaskResult>

Запрос:

Свойство

Тип

Параметры:

Свойство

Тип

Возвращаемое полеЗначение
taskIdID новой задачи. Сразу сохраните его
statequeued для новой задачи. Если вы повторили ключ идемпотентности — текущее состояние исходной задачи
estimatedCostСумма, которая сейчас зарезервирована; это же максимум, который может быть списан за задачу
deadlineAtКогда SpicyAPI прекратит работу над задачей, если она не завершится

Стоит денег: да. estimatedCost резервируется на вашем балансе. Если задача выполнена успешно, списывается плата за фактическое использование, но не больше резерва, а остаток освобождается. При ошибке или истечении срока задачи резерв обычно освобождается полностью. Некоторые отказы по содержанию оплачиваются по принятой цене, только если это исключение прямо указано на странице модели. Окончательную сумму проверяйте по settled и cost.

Ошибки:

error.codeЧто это значитЧто делать
400Входные данные не соответствуют схеме модели, или callBackUrl недопустимСверьтесь с getModel().inputSchema и исправьте запрос
401Ключ отсутствует, имеет неверный формат или отозванПроверьте SPICY_API_KEY
40201Вашего баланса не хватает на резервПополните баланс в разделе «Биллинг» консоли
40202Достигнут дневной, месячный или общий лимит расходовПовысьте лимит на странице «Ключи API» или дождитесь его сброса
40301Этому ключу не разрешено пользоваться этой модельюИзмените список моделей ключа в консоли
409Этот ключ идемпотентности уже использовался для другого запросаДля новой работы берите новый ключ
40901Срок расчёта стоимости истёк, или изменилась цена или запросПолучите расчёт заново и подтвердите новую цену
413Тело запроса слишком большоеЗагружайте медиафайлы, а не вставляйте их в запрос
429Слишком много запросовПодождите error.retryAfterSeconds и попробуйте снова
50301Модель сейчас не может принимать задачиПопробуйте позже или выберите другую модель

SpicyTransportError или SpicyTimeoutError означают, что ответ не пришёл, поэтому неизвестно, создана ли задача. Отправьте тот же запрос с тем же ключом идемпотентности ещё раз: либо задача будет создана, либо вы получите уже существующую. TypeError означает, что параметр недопустим (например, отрицательный retentionSeconds), и ничего не было отправлено.

import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID(); // в настоящем приложении сначала сохраните его вместе с заказом
const accepted = await client.createTask(
  { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } },
  { idempotencyKey },
);

console.log(accepted.taskId, accepted.state, "on hold: $" + accepted.estimatedCost);

run(request, options?)

Самый простой способ что-нибудь сгенерировать: метод вызывает createTask, затем waitForTask и возвращает завершённую задачу.

run(request: CreateTaskInput, options?: RunTaskOptions): Promise<TaskRecord>

request такой же, как у createTask. Параметры — всё, что принимает createTask, и дополнительно:

Свойство

Тип

Метод возвращает завершённую задачу с полями, описанными в разделе getTask. Для успешной задачи он также ждёт, пока файлы результата будут скопированы, так что output.assets[].url уже можно скачивать.

Стоит денег: да, точно так же, как createTask. Задача, завершившаяся ошибкой, возвращается, а не выбрасывается — проверяйте task.state.

Ошибки: всё, что может выбросить createTask, а также:

  • SpicyWaitTimeoutError, когда timeoutMs истёк после того, как задача была принята. Задача продолжает выполняться в SpicyAPI. Позже вызовите waitForTask(error.taskId), чтобы забрать её.
  • SpicyTimeoutError, когда время истекло до того, как SpicyAPI подтвердил задачу. Чтобы узнать, что произошло, отправьте тот же запрос с тем же ключом идемпотентности.
  • Любая ошибка, выброшенная внутри вашего onAccepted или onUpdate, прекращает ожидание; сама задача продолжает выполняться.
  • TypeError при недопустимом timeoutMs или intervalMs. Они проверяются до того, как что-либо создано.

Полный пример см. в разделе Ваш первый результат.

getTask(taskId)

Один раз читает задачу: её состояние, ссылки на результат и сколько она стоила.

getTask(taskId: string, options?: { signal?: AbortSignal }): Promise<TaskRecord>
ПолеЗначение
taskId, modelКакая это задача и какой моделью она создана
statequeued или running, пока задача выполняется; succeeded, failed или expired, когда она завершена (canceled встречается только в старых записях)
output.assetsФайлы результата, описанные в следующей таблице. Только для succeeded
output.textТекстовый результат — у моделей, которые возвращают текст
errorCode, errorMessageПочему задача завершилась ошибкой. Решения принимайте по errorCode; формулировка сообщения может меняться
costПока settled равно false, это зарезервированная сумма; когда true, это окончательное списание
settledОкончательна ли сумма в cost
createdAt, completedAt, deadlineAtКогда задача создана, когда завершена и когда она истечёт
inputСохранённые входные данные. Удаляются, когда заканчивается срок хранения промптов
sourceTaskIdЗаполняется у задачи, созданной через retryTask, и указывает на исходную
contentStatepresent; expired (удалено по правилам хранения) или purged (вы удалили содержимое через purgeTask)
retentionКогда будут удалены файлы (outputsExpireAt) и промпт (promptsExpireAt)

Каждый элемент output.assets:

ПолеЗначение
urlВременная ссылка для скачивания. Скачивайте без API-ключа
expiresAtКогда эта ссылка перестанет работать, обычно примерно через 20 минут. Чтобы получить свежую ссылку, снова вызовите getTask
mimeТип файла, например image/png или video/mp4
width, height, durationSeconds, bytesСведения о размере, если они применимы
pendingtrue, пока файл ещё копируется в хранилище. Проверьте снова через несколько секунд
unavailabletrue, если файл удалён навсегда. Ожидание не поможет; сгенерируйте заново
keyВнутреннее имя файла; нужно только для createDownloadUrl

Если у SpicyAPI нет значения для поля, поле не включается в ответ, а не получает null, поэтому считайте все поля выше необязательными.

Стоит денег: нет. Ошибки: SpicyApiError с 404, если задачи с таким ID ваш ключ не видит.

const task = await client.getTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3"); // ID, который вы сохранили

if (task.state === "succeeded") {
  for (const asset of task.output?.assets ?? []) console.log(asset.mime, asset.url);
} else if (task.state === "queued" || task.state === "running") {
  console.log("Still working. On hold: $" + task.cost);
} else {
  console.log(`Finished as ${task.state}: ${task.errorCode}`);
}

waitForTask(taskId, options?)

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

waitForTask(taskId: string, options?: WaitForTaskOptions): Promise<TaskRecord>

Параметры: timeoutMs (по умолчанию 600000, максимум 86400000), intervalMs, onUpdate и signal — все они работают так, как описано в разделе run.

Метод возвращается, когда state становится succeeded, failed, expired или canceled, а для успешной задачи — когда ни один файл результата больше не находится в состоянии pending. Каждая проверка — это один запрос getTask, который расходует лимит запросов вашего аккаунта; поэтому интервал по умолчанию со временем увеличивается.

Стоит денег: нет. Ошибки: SpicyWaitTimeoutError, когда истёк timeoutMs. Она содержит taskId, timeoutMs и lastTask (последнее увиденное состояние). Задача не отменяется: можно просто подождать ещё. Также SpicyApiError (например, 404) и SpicyTransportError.

const task = await client.waitForTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3", {
  timeoutMs: 30 * 60_000,
  onUpdate: (update) => console.log(new Date().toISOString(), update.state),
});
console.log(task.state, task.output?.assets?.[0]?.url);

retryTask(taskId, options?)

Запускает задачу, завершившуюся ошибкой, ещё раз — как совершенно новую задачу с той же моделью и теми же входными данными. Исходная задача остаётся в точности такой, какой была, а новая ссылается на неё через sourceTaskId.

retryTask(taskId: string, options?: { idempotencyKey?: string; signal?: AbortSignal }): Promise<RetryTaskResult>

Метод работает с задачами в состояниях failed и expired (и со старыми canceled). Успешная или ещё выполняющаяся задача отклоняется с кодом 409. Цена, ваш баланс и права ключа проверяются заново, а новая задача сохраняет настройку хранения исходной.

Поскольку входные данные используются без изменений, повтор помогает, только если сбой был временным. Сначала прочитайте errorCode исходной задачи: rate_limited, upstream_unavailable, timeout и upstream_failed стоит один раз повторить после паузы; для generation_failed и invalid_asset нужен изменённый запрос, отправленный через createTask; content_rejected, invalid_request и unsupported_combination не следует повторять без изменений. См. Частые коды ошибок и Ошибки и повторы.

Метод возвращает taskId (новая задача), sourceTaskId (исходная), state, estimatedCost и deadlineAt.

Стоит денег: да. У новой задачи свой резерв, и при успехе за неё списываются деньги отдельно. Передайте idempotencyKey, чтобы при двойном запуске вашего кода повтора создавался только один повтор.

Ошибки: 409 (задачу нельзя повторить), а также ошибки баланса, лимитов и доступности, перечисленные в разделе createTask. Если в исходных входных данных был загруженный файл spicy://, этим файлом можно пользоваться только один день; загрузите его заново и используйте createTask.

retryTask не для сетевых ошибок

Если сам createTask завершился сетевой ошибкой, задачи для повтора у вас нет. Отправьте createTask ещё раз с тем же ключом идемпотентности. Используйте retryTask только для задачи, у которой вы прочитали state со значением failed или expired.

Полный пример — в рецепте Повтор задачи, завершившейся ошибкой.

listTasks(options?)

Выводит задачи, созданные этим API-ключом, начиная с самых новых, по одной странице за раз. Помогает найти работу после перезапуска или пропущенного вебхука.

listTasks(options?: ListTasksOptions): Promise<TaskList>

Свойство

Тип

Метод возвращает items, hasMore и, если страниц больше, nextCursor. У каждого элемента есть taskId, model, state, cost, settled, createdAt, deadlineAt и completedAt. Входные данные, результаты и ссылки не включаются; для нужной задачи вызовите getTask.

Стоит денег: нет. Ошибки: SpicyApiError (например, 400 при недопустимом диапазоне дат), SpicyTransportError.

let cursor;
do {
  const page = await client.listTasks({
    from: "2026-09-01", // на каждой странице оставляйте те же даты
    to: "2026-09-08",
    state: "failed",
    limit: 100,
    cursor,
  });
  for (const item of page.items) console.log(item.taskId, item.model, item.createdAt);
  cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);

getBalance()

Показывает, сколько денег на вашем аккаунте.

getBalance(options?: { signal?: AbortSignal }): Promise<Balance>
ПолеЗначение
availableДеньги, которыми можно оплачивать новые задачи. Значение может быть отрицательным, если ваш аккаунт пользуется одобренной кредитной линией
heldДеньги, зарезервированные под задачи, которые ещё выполняются
totalavailable плюс held
fundingНеобязательная разбивка: предоплаченные средства, промоначисления (с моделями, к которым они применимы, и сроком действия) и кредитная линия, если она есть. В более старых ответах этого поля может не быть

Стоит денег: нет. Проверять баланс перед каждой задачей не нужно: SpicyAPI проверяет его при создании задачи и отвечает 40201, если денег не хватает. Все поля разбивки описаны на странице Оплата.

const balance = await client.getBalance();
console.log(`Available: $${balance.available} (on hold: $${balance.held})`);

getUsage(options?)

Показывает, сколько потратил этот API-ключ, с группировкой по дням и по моделям.

getUsage(options?: { from?: string; to?: string; signal?: AbortSignal }): Promise<UsageReport>

from и to — даты в UTC (YYYY-MM-DD); from входит в диапазон, to — нет. Если не указать ни то, ни другое, вы получите последние семь дней, включая сегодняшний. Диапазон может охватывать не больше 92 дней.

ПолеЗначение
totalCallsЧисло задач в диапазоне
totalSpendТолько окончательные списания; деньги, которые ещё в резерве, не учитываются
daysПо одной записи на каждый день с вызовами: day, calls, succeeded, failed, spend
modelsПо одной записи на каждую модель: model, calls, succeeded, failed, spend

Стоит денег: нет. Этот отчёт тяжелее других операций чтения, поэтому у него есть дополнительный лимит — 30 запросов в минуту; используйте его для отчётов, а не в цикле. Ошибки: SpicyApiError (429 сверх этого лимита), SpicyTransportError.

const usage = await client.getUsage({ from: "2026-09-01", to: "2026-09-08" });
console.log(`${usage.totalCalls} tasks, $${usage.totalSpend} spent`);
for (const row of usage.models) console.log(row.model, row.calls, "$" + row.spend);

Загрузка файлов

Если модели нужны на входе изображение, видео или звук, в поле входных данных можно прямо указать публичную HTTPS-ссылку на файл. Файл с вашего компьютера сначала нужно загрузить. В ответ вы получите адрес spicy:// — его и укажите в поле входных данных. Загрузка бесплатна.

  • Изображения (JPEG, PNG, WebP, GIF): до 10 MiB.
  • Видео (MP4, WebM) и аудио (MP3, WAV): до 90 MiB и не длиннее 600 секунд.
  • Модель может задать в своей схеме более низкий предел.
  • Загруженным файлом можно пользоваться один день, и только в вашем аккаунте.

uploadFile(path, options?)

Загружает файл с диска.

uploadFile(path: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

Тип файла определяется по расширению: .jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mp3 или .wav. Для любого другого имени передайте contentType: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/webm, audio/mpeg или audio/wav.

Возвращаемое полеЗначение
uriАдрес spicy://, который нужно указать во входных данных задачи
expiresAtКогда адрес перестанет работать (через один день)
fileId, status, bytes, contentType, sha256Сведения о сохранённом файле; status равен ready
width, height, durationSecondsИзмеренные размеры или длительность, если они применимы

Стоит денег: нет. Ошибки: TypeError, если тип определить не удалось, — в его сообщении перечислены все распознаваемые расширения: contentType is required unless the file extension is one of: gif, jpeg, jpg, png, webp, mp4, webm, mp3, wav; ошибка Node.js ENOENT, если путь неверный; SpicyUploadError (со status), если хранилище не приняло байты или файл больше допустимого (413); SpicyApiError, если загрузку отклонил SpicyAPI; SpicyTimeoutError, если отправка длилась дольше uploadTimeoutMs.

const file = await client.uploadFile("./photo.jpg");
console.log(file.uri, "can be used until", file.expiresAt);

uploadBytes(data, options)

Загружает байты, которые уже есть у вас в памяти, например файл, который пользователь отправил на ваш сервер. Подойдёт Buffer из Node.js.

uploadBytes(data: Uint8Array, options: { contentType: string; signal?: AbortSignal }): Promise<UploadedFile>

contentType обязателен. Результат и ошибки такие же, как у uploadFile.

const response = await fetch("https://example.com/reference.png");
const bytes = new Uint8Array(await response.arrayBuffer());
const file = await client.uploadBytes(bytes, { contentType: "image/png" });

uploadBase64(value, options?)

Загружает файл, который есть у вас в виде текста Base64: либо data URI, например data:image/png;base64,iVBOR…, либо обычный Base64 вместе с contentType.

uploadBase64(value: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

Кодировка и размер сначала проверяются на вашей машине; некорректные или слишком большие данные выбрасывают TypeError, и ничего не загружается. Результат такой же, как у uploadFile.

createUploadUrl и commitUploadedFile

Три вспомогательных метода выше построены из этих двух вызовов нижнего уровня. Они нужны, только если байты отправляются не из вашего процесса Node.js, а откуда-то ещё.

createUploadUrl(contentType: string, bytes: number, options?: { signal?: AbortSignal }): Promise<UploadTicket>
commitUploadedFile(fileId: string, options?: { signal?: AbortSignal }): Promise<UploadedFile>
  1. createUploadUrl возвращает fileId, uploadUrl, method (PUT), headers, maxBytes и expiresAt.
  2. Отправьте файл запросом PUT на uploadUrl ровно с перечисленными headers и без заголовка Authorization. Заявленный размер должен совпадать с размером файла.
  3. commitUploadedFile(fileId) проверяет сохранённый файл и возвращает uri вида spicy://. Файлом, который загрузили, но не подтвердили через commit, пользоваться нельзя.

Весь процесс, включая загрузку из браузера, описан на странице Загрузка медиа и результатов.

createDownloadUrl(taskId, key?)

Создаёт свежую временную ссылку для скачивания одного файла результата. Нужен он редко: getTask, waitForTask, run и вебхуки уже содержат готовый url для каждого результата.

createDownloadUrl(taskId: string, key?: string, options?: { signal?: AbortSignal }): Promise<DownloadTicket>

Передайте key элемента результата, чтобы выбрать файл, или не указывайте его, чтобы получить первый. Метод возвращает key, url и expiresAt (около 20 минут).

Стоит денег: нет. Ошибки: SpicyApiError с 409, пока файл ещё копируется (подождите несколько секунд), или с 404, если задача не завершилась успешно или key ей не принадлежит.

purgeTask(taskId)

Безвозвратно удаляет у завершённой задачи сгенерированные медиа, данные результата (сохранённые данные о результате), промпт и прочий текст входных данных раньше, чем это сделал бы срок хранения. Используйте, когда результат нельзя хранить.

purgeTask(taskId: string, options?: { signal?: AbortSignal }): Promise<PurgeTaskResult>
Возвращаемое полеЗначение
contentStatepurged, когда содержимое удалено. present означает, что на этот раз ничего не удалено; попробуйте позже
purgedAtКогда содержимое было удалено впервые
contentRemovedByuser, если его удалили вы
billingRetainedВсегда true: запись о том, сколько стоила задача, сохраняется
mediaDeletionPendingtrue, пока сохранённые файлы ещё удаляются в фоне, обычно меньше минуты

Стоит денег: нет, и это не возврат денег. Запись в журнале операций, списанная сумма, модель, состояние, временные метки и ID запроса остаются в учёте. Удаление нельзя отменить. Повторный вызов для той же задачи безопасен. Ошибки: SpicyApiError с 400, если задача ещё в состоянии queued или running, и с 404, если её не существует. Принятую задачу отменить нельзя, API для отмены нет, поэтому дождитесь её завершения через waitForTask, а затем удалите содержимое. См. Хранение и удаление данных.

const result = await client.purgeTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3");
console.log(result.contentState, "billing kept:", result.billingRetained);

Функции для вебхуков

verifyWebhook и computeWebhookSignature — обычные функции, а не методы клиента. Они описаны вместе с рабочим примером в разделе Вебхуки.

Офлайн-помощники и типы

Они работают без подключения к сети и без ключа.

ЭкспортЧто он даёт
searchDocumentation(query, limit)Ищет по небольшому встроенному индексу этого сайта документации. Возвращает до limit записей (от 1 до 25, по умолчанию 10), у каждой есть slug, title, summary, keywords и url, где url имеет вид https://docs.spicyapi.ai/docs/<slug>. Если совпадений нет, возвращается пустой массив; при пустом query возвращаются первые записи
DOCUMENTATIONВесь этот встроенный индекс в виде массива, по одной записи на каждую страницу документации
readOpenApiContract()Контракт OpenAPI 3.1, входящий в эту версию SDK, в виде текста YAML
DEFAULT_API_BASE_URL, DEFAULT_SERVICE_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_UPLOAD_TIMEOUT_MS, DEFAULT_WAIT_TIMEOUT_MS, DEFAULT_WAIT_INTERVAL_MS, MAX_API_RESPONSE_BYTESЗначения по умолчанию, описанные на этой странице. Ответы API больше MAX_API_RESPONSE_BYTES (4 MiB) отклоняются

Экспортируются и типы TypeScript для каждого запроса и ответа, например import type { TaskRecord, ApiModel, Balance } from "@spicyapi/sdk".

Путь импортаЧто содержит
@spicyapi/sdkВсё, что описано на этой странице
@spicyapi/sdk/webhooksТолько verifyWebhook, computeWebhookSignature и SpicyWebhookVerificationError — лёгкий импорт для обработчика вебхуков
@spicyapi/sdk/openapiТолько исходные типы OpenAPI paths, components и operations

Вебхуки

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

Почему каждый вебхук нужно проверять

Адрес вашего вебхука — публичный URL: любой, кто его угадает, может прислать туда поддельное сообщение «задача выполнена, вот файл». Если ваш код поверит такому сообщению, он может отдать клиенту не тот файл или отметить заказ как выполненный.

Каждая настоящая доставка подписана секретным ключом подписи, который знаете только вы и SpicyAPI. verifyWebhook заново вычисляет подпись по точным байтам, которые вы получили, и сравнивает две подписи. Кроме того, функция проверяет время доставки, чтобы злоумышленник не смог перехватить настоящее сообщение и прислать его вам повторно через несколько часов. Действуйте по сообщению только после того, как оно прошло проверку.

Настройка

Получите ключ подписи

Ключ подписи создаётся автоматически при регистрации, но его значение никогда не показывается, поэтому нужно создать ключ, который можно скопировать. Откройте в консоли страницу Вебхуки и нажмите Создать (или Сменить, если на карточке написано, что ключ настроен). Новый ключ показывается только один раз: скопируйте его и сохраните на своём сервере как SPICY_WEBHOOK_SECRET. Если вы его потеряли, смените ключ ещё раз.

При смене ключа нет переходного периода: старый ключ сразу перестаёт подписывать, и даже доставки, которые ещё повторяются, подписываются новым. Сразу после смены обновите ключ во всех работающих обработчиках. Подробности — на странице Webhook.

Запустите обработчик на публичном HTTPS-адресе

SpicyAPI может отправлять запросы только на адрес, доступный из интернета и начинающийся с https://. Обычный http://, localhost и адреса частных сетей отклоняются. Во время разработки откройте доступ к локальному серверу через туннельный сервис, который даёт ему публичный HTTPS-адрес, или пользуйтесь опросом, пока не развернёте приложение. Все правила для адреса обратного вызова собраны на странице Webhook.

Отправляйте callBackUrl вместе с задачами

Добавьте callBackUrl: "https://your-app.example.com/hooks/spicy" в запрос, который передаёте в createTask. См. рецепт Результаты через вебхук.

Минимальный обработчик

Этот обработчик использует только встроенный модуль http из Node.js, так что ничего больше устанавливать не нужно. Сохраните его как receiver.mjs и запустите командой node receiver.mjs.

receiver.mjs
import { createServer } from "node:http";
import { SpicyWebhookVerificationError, verifyWebhook } from "@spicyapi/sdk/webhooks";

const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");

// ID доставок, которые вы уже обработали. В рабочей среде используйте таблицу базы данных,
// чтобы список пережил перезапуск.
const handled = new Set();

const server = createServer(async (request, response) => {
  if (request.method !== "POST" || request.url !== "/hooks/spicy") {
    response.writeHead(404).end();
    return;
  }
  if (request.headers["x-webhook-payload-version"] !== "2") {
    response.writeHead(400).end();
    return;
  }

  // 1. Читаем тело как исходные байты. Не разбирайте его до проверки.
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks);

  // 2. Проверяем подпись и временную метку.
  let event;
  try {
    event = verifyWebhook({
      rawBody,
      secret,
      timestamp: String(request.headers["x-webhook-timestamp"] ?? ""),
      signature: String(request.headers["x-webhook-signature"] ?? ""),
      payloadVersion: 2,
    });
  } catch (error) {
    const reason = error instanceof SpicyWebhookVerificationError ? error.reason : String(error);
    console.warn("Rejected a webhook:", reason);
    response.writeHead(401).end();
    return;
  }

  // 3. Отвечаем быстро. SpicyAPI ждёт не больше 15 секунд, а затем повторяет доставку.
  response.writeHead(204).end();

  // 4. Одна и та же доставка может прийти больше одного раза. Обрабатываем каждую один раз.
  if (!event.deliveryId || handled.has(event.deliveryId)) return;
  handled.add(event.deliveryId);

  if (!("data" in event.payload)) return; // в теле версии 2 задача лежит в data
  const task = event.payload.data; // те же поля, что возвращает getTask()
  console.log("Task", task.taskId, "is", task.state);
  if (task.state === "succeeded") {
    console.log("Result:", task.output?.assets?.[0]?.url);
  }
});

server.listen(8080, () => console.log("Listening on http://localhost:8080/hooks/spicy"));

Что в нём важно:

  • Исходные байты. Подпись вычисляется по точным байтам, которые отправил SpicyAPI. Если фреймворк сначала разберёт JSON, а вы потом превратите его обратно в текст, байты изменятся и каждая проверка будет проваливаться. В Express используйте для этого маршрута express.raw({ type: "application/json" }).
  • Отвечайте быстро. Любой ответ 2xx означает «получено». Если ответа нет в течение 15 секунд или пришёл любой другой статус, SpicyAPI несколько раз повторит доставку в течение следующих часов. Долгую работу выполняйте после ответа.
  • Дубликаты случаются. Из-за повторов и сетевых сбоев одно и то же сообщение может прийти дважды. event.deliveryId не меняется между повторами, поэтому запоминайте ID, которые уже обработали.
  • Успех определяется по state. Доставка о задаче, завершившейся ошибкой, — тоже корректная доставка. Читайте task.state и никогда не ориентируйтесь на code из оболочки ответа.
  • В рабочей среде сначала сохраните проверенное событие в базу данных и отвечайте 2xx только после успешного сохранения, а ID задач, которые создавало не ваше приложение, игнорируйте. На странице Webhook есть вариант с базой данных.

Проверьте обработчик до запуска в работу

computeWebhookSignature создаёт настоящую подпись, поэтому можно отправить самому себе подписанную тестовую доставку. Запустите receiver.mjs в одном терминале, задайте тот же SPICY_WEBHOOK_SECRET в другом и выполните там:

send-test.mjs
import { computeWebhookSignature } from "@spicyapi/sdk";

const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");

const body = JSON.stringify({
  code: 200,
  msg: "success",
  request_id: "whd_local_test_1",
  data: {
    taskId: "job_local_test",
    state: "succeeded",
    cost: "0.01",
    settled: true,
    output: { assets: [{ url: "https://example.com/result.png" }] },
  },
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = computeWebhookSignature("job_local_test", timestamp, body, secret);

const response = await fetch("http://localhost:8080/hooks/spicy", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Webhook-Timestamp": timestamp,
    "X-Webhook-Signature": signature,
    "X-Webhook-Payload-Version": "2",
  },
  body,
});
console.log("Your receiver answered", response.status); // ожидается 204

Запустите его дважды: обработчик выведет задачу один раз, потому что у второй доставки тот же request_id. Измените после подписи хотя бы один символ в body, и обработчик ответит 401.

verifyWebhook(options)

verifyWebhook(options: VerifyWebhookOptions): VerifiedWebhook

Свойство

Тип

Функция возвращает payload (разобранное тело; в версии 2 задача лежит в payload.data), payloadVersion, taskId, deliveryId (request_id версии 2, одинаковый при всех повторах) и timestamp. Функция синхронная: await не нужен.

Если проверка не пройдена, функция выбрасывает SpicyWebhookVerificationError, у которой reason принимает одно из значений invalid_signature, stale_timestamp, invalid_timestamp, invalid_json, invalid_payload, body_too_large или invalid_secret. Ответьте 401 и ничего не делайте по этому сообщению.

computeWebhookSignature(taskId, timestamp, rawBody, secret) возвращает подпись тела в Base64. Она нужна только для тестов вроде приведённого выше. Полный алгоритм подписи для других языков описан на странице Webhook.

Совет для TypeScript: payload типизирован как любой из двух форматов. Сузьте тип проверкой "data" in event.payload, как это делает обработчик, или передайте ожидаемый тип: verifyWebhook<components["schemas"]["TaskRecordEnvelope"]>(…) вместе с import type { components } from "@spicyapi/sdk".

Обработка ошибок

Что-то может пойти не так двумя разными способами, и обрабатывать их нужно по-разному:

  1. Не удался вызов. SDK выбрасывает ошибку: ключ оказался неверным, на балансе не хватило денег, оборвалась сеть. Перехватывайте её через try / catch.
  2. Не удалась задача. Вызов сработал, а генерация — нет. Ничего не выбрасывается: у возвращённой задачи state: "failed" и есть errorCode. При ошибке или истечении срока задачи резерв обычно освобождается полностью. Некоторые отказы по содержанию оплачиваются по принятой цене, только если это исключение прямо указано на странице модели. Окончательную сумму проверяйте по settled и cost.

Классы ошибок

КлассКогда вы его видитеПолезные поля
SpicyApiErrorSpicyAPI ответил ошибкой, например отклонил ключ или сообщил о нехватке балансаcode (бизнес-код, по которому нужно действовать, например 40201), status (HTTP-статус), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining
SpicyTransportErrorПригодный ответ не пришёл: сбой сети, проблема с DNS или ответ больше 4 MiBmessage, cause (исходная ошибка)
SpicyTimeoutErrorОдин запрос длился дольше timeoutMs (или uploadTimeoutMs). Это разновидность SpicyTransportErrortimeoutMs
SpicyWaitTimeoutErrorУ run или waitForTask закончилось время. Задача всё ещё выполняетсяtaskId, timeoutMs, lastTask
SpicyUploadErrorХранилище не приняло загруженный файл, или файл оказался слишком большимstatus
SpicyWebhookVerificationErrorВебхук не прошёл проверкуreason
TypeErrorСам вызов недопустим: ключ не задан, параметр вне допустимого диапазона, ID пустой. Ничего не было отправленоmessage

Решайте, что делать, по code, а не по message: сообщения предназначены для людей, их формулировка может меняться, и они выводятся на языке, выбранном в настройке языка ошибок вашего аккаунта. Когда обращаетесь в поддержку, всегда указывайте requestId: по нему мы найдём именно этот запрос.

handle-errors.mjs
import { randomUUID } from "node:crypto";
import {
  SpicyApiError,
  SpicyClient,
  SpicyTransportError,
  SpicyWaitTimeoutError,
} from "@spicyapi/sdk";

const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // настоящий ID из listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

const idempotencyKey = randomUUID(); // сохраните его: для повторной попытки нужен этот же ключ

try {
  const task = await client.run(
    { model: MODEL_ID, input: { prompt: "A paper lantern on a rainy street" } },
    { idempotencyKey, onAccepted: ({ taskId }) => console.log("Task ID:", taskId) },
  );
  if (task.state === "succeeded") console.log("Done:", task.output?.assets?.[0]?.url);
  else console.log(`Finished as ${task.state} (${task.errorCode}). Cost: ${task.cost}; settled: ${task.settled}.`);
} catch (error) {
  if (error instanceof SpicyWaitTimeoutError) {
    // Закончилось ваше ожидание, а не задача. Проверьте её позже по тому же ID.
    console.log(`Still running: ${error.taskId}. Call waitForTask() again later.`);
  } else if (error instanceof SpicyApiError) {
    console.log(`SpicyAPI said no: ${error.code} (HTTP ${error.status}) ${error.message}`);
    console.log(`Quote this if you contact support: ${error.requestId}`);
    if (error.code === 40201) console.log("Your balance is too low. Top up in the console.");
    if (error.code === 40202) console.log("A spend cap was reached. Raise it on the API keys page.");
    if (error.code === 429) console.log(`Too many requests. Wait ${error.retryAfterSeconds ?? 1} s.`);
  } else if (error instanceof SpicyTransportError) {
    // Сбой сети или локальный тайм-аут: задача могла быть создана, а могла и не быть.
    // Чтобы безопасно это выяснить, отправьте тот же запрос с ТЕМ ЖЕ idempotencyKey.
    console.log(`Network problem (${error.message}). Retry with key ${idempotencyKey}.`);
  } else {
    throw error; // TypeError здесь означает ошибку в самом вызове, например отсутствующий ключ
  }
}

Частые коды ошибок

error.codeЗначениеСтоит ли повторять?
400Некорректный запрос, обычно поле входных данных, которое модель не принимаетНет. Исправьте запрос
401Ключ отсутствует, имеет неверный формат, отозван или истёкНет. Проверьте ключ
40201Баланса не хватает на резервНет. Сначала пополните баланс
40202Достигнут лимит расходовНет. Повысьте лимит или дождитесь ежедневного или ежемесячного сброса
40301Этому ключу нельзя пользоваться этой модельюНет. Измените список моделей ключа
404Для этого ключа нет такой модели или задачиНет
409Конфликт с текущим состоянием, например ключ идемпотентности повторно использован для другой работыЗависит от ситуации; см. Ошибки и повторы
40901Срок расчёта стоимости истёк, или цена измениласьПолучите расчёт заново, затем подтвердите
429Слишком много запросовДа, через retryAfterSeconds. Операции чтения SDK уже повторяет за вас
500Что-то сломалось на стороне SpicyAPIДа, после паузы
50301Модель сейчас недоступнаДа, позже — или выберите другую модель

Когда завершается ошибкой задача, task.errorCode принимает одно из значений invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset или upstream_failed. retryTask отправляет исходные входные данные снова без изменений, поэтому следующий шаг выбирайте по коду:

task.errorCodeЧто делать
rate_limited, upstream_unavailable, timeout, upstream_failedПодождите, затем один раз вызовите retryTask. Если timeout повторяется, попробуйте через createTask меньшую длительность или более низкое разрешение
generation_failedИзмените промпт или изображение и отправьте новую задачу через createTask; с теми же входными данными ошибка обычно повторяется
invalid_assetЗагрузите файл заново и отправьте новую задачу через createTask с новым uri; повтор использовал бы старый файл
invalid_request, unsupported_combinationНе повторяйте без изменений. Поменяйте настройки, затем используйте createTask
content_rejectedНе повторяйте без изменений. Выбранная модель отклонила запрос по своей собственной политике; измените промпт или выберите другую модель

Полная таблица — на странице Ошибки и повторы.

Сохраняйте свои результаты

  • url результата работает около 20 минут. Чтобы получить свежий, снова вызовите getTask.
  • Сгенерированные файлы удаляются самое позднее примерно через 14 дней, а если так задано в настройках аккаунта или в retentionSeconds, то раньше. Загруженные входные файлы хранятся один день, сохранённые промпты — 30 дней.
  • Как только задача выполнена успешно, скачайте то, что хотите сохранить, в собственное хранилище. Не храните в базе данных саму временную ссылку url. Подробности: Хранение и удаление данных.

Готовые рецепты

В рецептах MODEL_ID_FROM_CATALOG используется как заполнитель. Прежде чем запускать рецепт, замените его значением model подходящего элемента из актуального каталога (listModels() или GET /api/v1/models), у которого и enabled, и available равны true. Если забудете, каждый рецепт остановится с понятным сообщением.

Видео из изображения: сначала загрузите локальную картинку

image-to-video.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

// Замените на ID модели image-to-video из актуального каталога:
// await client.listModels({ modality: "video", task: "image-to-video" })
const MODEL_ID = "MODEL_ID_FROM_CATALOG";
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// 1. Загружаем локальную картинку. Бесплатно. Адрес spicy:// действует один день.
const file = await client.uploadFile("./photo.jpg");
console.log("Uploaded:", file.uri);

// 2. Выводим схему модели: какое поле принимает изображение и какие поля обязательны.
const { inputSchema } = await client.getModel(MODEL_ID);
console.log(JSON.stringify(inputSchema, null, 2));

// 3. Создаём видео и ждём его. Видео часто занимает несколько минут.
const task = await client.run(
  {
    model: MODEL_ID,
    input: {
      prompt: "Slow push-in, light rain, neon reflections on wet stone",
      image_url: file.uri, // используйте имя поля, которое показывает схема выше
    },
  },
  {
    idempotencyKey: randomUUID(),
    timeoutMs: 30 * 60_000,
    onAccepted: ({ taskId }) => console.log("Task ID (save it):", taskId),
  },
);

console.log(task.state, task.output?.assets?.[0]?.url ?? task.errorCode);

Выведенная схема — главный источник сведений об именах полей и обязательных полях; видеомодель может также требовать длительность или разрешение, и цена часто от них зависит. Если ваше изображение уже доступно в интернете по публичному HTTPS-адресу, пропустите загрузку и укажите этот адрес прямо в поле изображения. Если хотите сначала увидеть цену, вызовите quoteTask с тем же запросом.

Пакет задач с ограниченной параллельностью

Если запустить сотни задач сразу, можно упереться в лимиты расходов и лимит запросов. Этот скрипт обрабатывает список промптов так, чтобы одновременно выполнялось не больше двух задач.

batch.mjs
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // настоящий ID из listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// Меняйте имя пакета каждый раз, когда меняете промпты.
const BATCH_NAME = "lanterns-2026-09-17";
const prompts = [
  "A paper lantern on a rainy street",
  "A paper lantern floating on a lake",
  "A paper lantern in a snowy forest",
  "A paper lantern above a night market",
];
const CONCURRENCY = 2; // сколько задач может выполняться одновременно

const results = prompts.map((prompt) => ({ prompt, outcome: "not started", detail: "" }));
let nextIndex = 0;

async function worker() {
  while (nextIndex < prompts.length) {
    const index = nextIndex++;
    try {
      const task = await client.run(
        { model: MODEL_ID, input: { prompt: prompts[index] } },
        {
          // То же имя пакета + та же позиция = тот же ключ. Если скрипт упадёт и вы
          // запустите его снова в течение 24 часов, вернутся уже созданные задачи,
          // и платить за них второй раз не придётся.
          idempotencyKey: `${BATCH_NAME}-${index}`,
          timeoutMs: 20 * 60_000,
        },
      );
      results[index].outcome = task.state;
      results[index].detail = task.output?.assets?.[0]?.url ?? task.errorCode ?? "";
    } catch (error) {
      results[index].outcome = "error";
      results[index].detail = error instanceof Error ? error.message : String(error);
      // Закончились деньги (40201) или превышен лимит расходов (40202): новые задачи не запускаем.
      if (error instanceof SpicyApiError && (error.code === 40201 || error.code === 40202)) {
        nextIndex = prompts.length;
      }
    }
  }
}

await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
console.table(results);
  • Почему ключи строятся из имени пакета. Ключ идемпотентности, привязанный к «этому пакету, элементу 3», означает, что при перезапуске после сбоя используются уже созданные задачи. Со случайным ключом при каждом запуске вы заплатили бы за то же изображение ещё раз.
  • Почему имя пакета нужно менять вместе с промптами. Повторное использование ключа для другого запроса отклоняется с кодом 409.
  • Как выбрать CONCURRENCY. Каждая выполняющаяся задача держит в резерве часть вашего баланса, а каждая проверка состояния расходует лимит запросов вашего аккаунта. Начните с малого и увеличьте значение, когда пакет будет проходить без проблем.

Результаты через вебхук вместо опроса

В веб-приложении или фоновом обработчике ждать результата внутри запроса расточительно. Создайте задачу с callBackUrl, сохраните ID задачи и дайте обработчику разобраться с результатом, когда он придёт.

webhook-create.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // настоящий ID из listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// В настоящем приложении сохраните этот ключ вместе с заказом клиента до вызова.
const idempotencyKey = randomUUID();

const accepted = await client.createTask(
  {
    model: MODEL_ID,
    input: { prompt: "A paper lantern on a rainy street" },
    callBackUrl: "https://your-app.example.com/hooks/spicy",
  },
  { idempotencyKey },
);

console.log(`Accepted ${accepted.taskId}; $${accepted.estimatedCost} is on hold.`);
// Сохраните accepted.taskId прямо сейчас. Когда задача завершится, SpicyAPI обратится к вашему обработчику.

Держите подстраховку на случай доставок, которые так и не пришли, например потому что ваш сервер был недоступен во время всех повторов: время от времени вызывайте getTask для сохранённых задач, у которых deadlineAt давно прошёл, а вебхука не было, или просматривайте страницы listTasks. История доставок вебхуков с кнопкой ручной повторной отправки есть на странице «Вебхуки» в консоли.

Повтор задачи, завершившейся ошибкой

retry.mjs
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const taskId = process.argv[2]; // запуск: node retry.mjs job_...
if (!taskId) throw new Error("Usage: node retry.mjs <taskId>");

// Повтор отправляет исходные входные данные без изменений, поэтому повторять стоит только временные сбои.
// При любом другом коде измените запрос и используйте createTask.
const WORTH_ONE_RETRY = new Set(["rate_limited", "upstream_unavailable", "timeout", "upstream_failed"]);

const source = await client.getTask(taskId);

if (
  (source.state !== "failed" && source.state !== "expired") ||
  !WORTH_ONE_RETRY.has(source.errorCode ?? "")
) {
  console.log(`Not retrying: the task is ${source.state} (${source.errorCode ?? "no error code"}).`);
} else {
  // Фиксированный ключ на попытку повтора: если запустить скрипт дважды, повтор будет один, а не два.
  const retry = await client.retryTask(source.taskId, {
    idempotencyKey: `retry-${source.taskId}-attempt-1`,
  });
  console.log(`New task ${retry.taskId}; $${retry.estimatedCost} is on hold.`);

  const finished = await client.waitForTask(retry.taskId, { timeoutMs: 20 * 60_000 });
  console.log("The retry finished as", finished.state);
}

Скрипт повторяет только сбои, которые в следующий раз могут закончиться иначе, — занятый или временно недоступный сервис, тайм-аут или неизвестная ошибка, — и только один раз, с фиксированным ключом идемпотентности. При invalid_asset загрузите файл заново и создайте новую задачу, потому что повтор использовал бы истёкшую загрузку. При generation_failed измените промпт или изображение и создайте новую задачу. При content_rejected, invalid_request и unsupported_combination меняйте запрос, а не повторяйте его. Если и единственный повтор завершится ошибкой, остановитесь и разберитесь с ошибкой; если повторяется timeout, попробуйте через createTask меньшую длительность или более низкое разрешение. См. Ошибки и повторы.

Устранение неполадок и частые вопросы

Исходный код

Пакет распространяется под лицензией MIT и разрабатывается открыто по адресу github.com/spicyapi-ai/spicy-sdk. Там же можно прочитать код любого метода с этой страницы, сообщить о поведении, которое расходится с описанным, или предложить изменение.

Что дальше

Каталог моделей и схемы

Как читать inputSchema модели и выбирать допустимые входные данные

Асинхронные задачи

Состояния задач, опрос и значение каждого поля

Вебхуки

Формат доставки, расписание повторов и проверка подписи на других языках

Медиа

Загрузки, ограничения Base64 и скачивание результатов

Ошибки

Все коды ошибок и какие из них стоит повторять

Оплата

Резервы, окончательное списание, лимиты расходов и ваш баланс

Идемпотентность

Безопасные повторы без двойной оплаты

Инструмент командной строки

Те же возможности из терминала, без написания кода

MCP-сервер

Пусть ИИ-ассистент для программирования работает со SpicyAPI за вас

Python SDK

Тот же процесс на Python, без зависимостей времени выполнения

Go SDK

Тот же процесс на Go, на одной стандартной библиотеке

PHP SDK

Тот же процесс на PHP, без зависимостей от библиотек

Java SDK

Тот же процесс на Java, с запечатанной иерархией ошибок

На этой странице

Что такое SDKТот ли это инструмент?Чего SDK не делаетСлова, которые встречаются на этой страницеЧто можно делатьПеред началомУстановите Node.js 22.13 или новееСоздайте папку проекта и установите SDKСоздайте API-ключСохраните ключ в переменной окруженияЗапустите первый файлВаш первый результатЧто делает каждый шагСправочник по APIСоздание клиентаАвтоматические повторыОстановка вызова через signalgetStatus()listModels(options?)getModel(modelId)quoteTask(request)createTask(request, options?)run(request, options?)getTask(taskId)waitForTask(taskId, options?)retryTask(taskId, options?)listTasks(options?)getBalance()getUsage(options?)Загрузка файловuploadFile(path, options?)uploadBytes(data, options)uploadBase64(value, options?)createUploadUrl и commitUploadedFilecreateDownloadUrl(taskId, key?)purgeTask(taskId)Функции для вебхуковОфлайн-помощники и типыВебхукиПочему каждый вебхук нужно проверятьНастройкаПолучите ключ подписиЗапустите обработчик на публичном HTTPS-адресеОтправляйте callBackUrl вместе с задачамиМинимальный обработчикПроверьте обработчик до запуска в работуverifyWebhook(options)Обработка ошибокКлассы ошибокЧастые коды ошибокСохраняйте свои результатыГотовые рецептыВидео из изображения: сначала загрузите локальную картинкуПакет задач с ограниченной параллельностьюРезультаты через вебхук вместо опросаПовтор задачи, завершившейся ошибкойУстранение неполадок и частые вопросыИсходный кодЧто дальше