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 | Нет (и деньги не возвращаются) |
| Проверить, что вебхук действительно пришёл от SpicyAPI | verifyWebhook (обычная функция) | Нет |
| Искать по документации или читать контракт 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-ключ
- Войдите на spicyapi.ai. Аккаунта ещё нет? Зарегистрируйтесь на spicyapi.ai/register; если регистрация новых аккаунтов приостановлена, на этой странице можно записаться в лист ожидания.
- Откройте в консоли страницу Ключи API и нажмите Создать ключ. Обязательно указать только название.
- Сразу скопируйте ключ. Он начинается с
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, вставьте в него этот код и сохраните:
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.mjsSpicyAPI 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 она создаёт одну задачу на изображение, для которой резервируется рассчитанная сумма, и эта сумма списывается, если изображение создано успешно.
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 --yesModel: <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>Что делает каждый шаг
getStatus()спрашивает у эндпоинта проверки работоспособности SpicyAPI, работает ли сервис. При сетевых проблемах он никогда не выбрасывает исключение, а вместо этого сообщаетoperational: false.listModels()возвращает актуальный каталог моделей с ценами вашего аккаунта. Используйте только элементы, у которых иenabled, иavailableравныtrue.includeExamples: trueдобавляет примеры входных данных, которые сервер уже проверил: это самая надёжная отправная точка. Позже вы отправляете значение поляmodelэлемента, то есть ID модели.- Входные данные. Каждая модель принимает свои поля; поле, которого модель не знает, отклоняется с ошибкой
400. Если скопировать проверенный пример и поменять только промпт, этого не случится. Чтобы увидеть все поля, которые принимает модель, вызовитеgetModel()и прочитайте еёinputSchema. quoteTask()рассчитывает стоимость именно этого запроса. Он ничего не создаёт и ничего не резервирует. Суммы указываются в долларах США и передаются текстом, например"0.04", чтобы не накапливались ошибки округления.run()создаёт задачу и затем проверяет её, пока она не завершится. ПередатьquoteIdиexpectedCostозначает «принимай только по той цене, которую мне показали»: если цена изменилась, вызов завершится ошибкой40901, а не спишет другую сумму.idempotencyKeyделает повтор после сетевого сбоя безопасным; в настоящем приложении сохраняйте его до вызова, чтобы перезапущенная программа могла использовать его снова.onAcceptedпередаёт вам ID задачи в тот момент, когда задача появилась. Сохраните его, чтобы потом проверить задачу, даже если ваша программа остановится.- Состояние задачи.
run()возвращается обычным образом и для задач, завершившихся ошибкой. При ошибке или истечении срока задачи резерв обычно освобождается полностью. Некоторые отказы по содержанию оплачиваются по принятой цене, только если это исключение прямо указано на странице модели. Окончательную сумму проверяйте поsettledиcost. - Скачивание. Каждый результат приходит с временной ссылкой
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>| Возвращаемое поле | Значение |
|---|---|
operational | true, когда сервис отвечает. Ориентируйтесь именно на это поле |
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 }. Самые полезные поля каждого элемента:
| Поле | Значение |
|---|---|
model | ID модели. При создании задачи отправляйте ровно это значение |
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>Запрос:
Свойство
Тип
Параметры:
Свойство
Тип
| Возвращаемое поле | Значение |
|---|---|
taskId | ID новой задачи. Сразу сохраните его |
state | queued для новой задачи. Если вы повторили ключ идемпотентности — текущее состояние исходной задачи |
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 | Какая это задача и какой моделью она создана |
state | queued или 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, и указывает на исходную |
contentState | present; expired (удалено по правилам хранения) или purged (вы удалили содержимое через purgeTask) |
retention | Когда будут удалены файлы (outputsExpireAt) и промпт (promptsExpireAt) |
Каждый элемент output.assets:
| Поле | Значение |
|---|---|
url | Временная ссылка для скачивания. Скачивайте без API-ключа |
expiresAt | Когда эта ссылка перестанет работать, обычно примерно через 20 минут. Чтобы получить свежую ссылку, снова вызовите getTask |
mime | Тип файла, например image/png или video/mp4 |
width, height, durationSeconds, bytes | Сведения о размере, если они применимы |
pending | true, пока файл ещё копируется в хранилище. Проверьте снова через несколько секунд |
unavailable | true, если файл удалён навсегда. Ожидание не поможет; сгенерируйте заново |
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 | Деньги, зарезервированные под задачи, которые ещё выполняются |
total | available плюс 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>createUploadUrlвозвращаетfileId,uploadUrl,method(PUT),headers,maxBytesиexpiresAt.- Отправьте файл запросом
PUTнаuploadUrlровно с перечисленнымиheadersи без заголовкаAuthorization. Заявленный размер должен совпадать с размером файла. 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>| Возвращаемое поле | Значение |
|---|---|
contentState | purged, когда содержимое удалено. present означает, что на этот раз ничего не удалено; попробуйте позже |
purgedAt | Когда содержимое было удалено впервые |
contentRemovedBy | user, если его удалили вы |
billingRetained | Всегда true: запись о том, сколько стоила задача, сохраняется |
mediaDeletionPending | true, пока сохранённые файлы ещё удаляются в фоне, обычно меньше минуты |
Стоит денег: нет, и это не возврат денег. Запись в журнале операций, списанная сумма, модель, состояние, временные метки и 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.
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 в другом и выполните там:
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".
Обработка ошибок
Что-то может пойти не так двумя разными способами, и обрабатывать их нужно по-разному:
- Не удался вызов. SDK выбрасывает ошибку: ключ оказался неверным, на балансе не хватило денег, оборвалась сеть. Перехватывайте её через
try/catch. - Не удалась задача. Вызов сработал, а генерация — нет. Ничего не выбрасывается: у возвращённой задачи
state: "failed"и естьerrorCode. При ошибке или истечении срока задачи резерв обычно освобождается полностью. Некоторые отказы по содержанию оплачиваются по принятой цене, только если это исключение прямо указано на странице модели. Окончательную сумму проверяйте поsettledиcost.
Классы ошибок
| Класс | Когда вы его видите | Полезные поля |
|---|---|---|
SpicyApiError | SpicyAPI ответил ошибкой, например отклонил ключ или сообщил о нехватке баланса | code (бизнес-код, по которому нужно действовать, например 40201), status (HTTP-статус), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining |
SpicyTransportError | Пригодный ответ не пришёл: сбой сети, проблема с DNS или ответ больше 4 MiB | message, cause (исходная ошибка) |
SpicyTimeoutError | Один запрос длился дольше timeoutMs (или uploadTimeoutMs). Это разновидность SpicyTransportError | timeoutMs |
SpicyWaitTimeoutError | У run или waitForTask закончилось время. Задача всё ещё выполняется | taskId, timeoutMs, lastTask |
SpicyUploadError | Хранилище не приняло загруженный файл, или файл оказался слишком большим | status |
SpicyWebhookVerificationError | Вебхук не прошёл проверку | reason |
TypeError | Сам вызов недопустим: ключ не задан, параметр вне допустимого диапазона, ID пустой. Ничего не было отправлено | message |
Решайте, что делать, по code, а не по message: сообщения предназначены для людей, их формулировка может меняться, и они выводятся на языке, выбранном в настройке языка ошибок вашего аккаунта. Когда обращаетесь в поддержку, всегда указывайте requestId: по нему мы найдём именно этот запрос.
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. Если забудете, каждый рецепт остановится с понятным сообщением.
Видео из изображения: сначала загрузите локальную картинку
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 с тем же запросом.
Пакет задач с ограниченной параллельностью
Если запустить сотни задач сразу, можно упереться в лимиты расходов и лимит запросов. Этот скрипт обрабатывает список промптов так, чтобы одновременно выполнялось не больше двух задач.
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 задачи и дайте обработчику разобраться с результатом, когда он придёт.
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. История доставок вебхуков с кнопкой ручной повторной отправки есть на странице «Вебхуки» в консоли.
Повтор задачи, завершившейся ошибкой
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, с запечатанной иерархией ошибок
Интеграция в рабочей среде
Чек-лист перед запуском для интеграции, которая умеет восстанавливаться, — подходящая модель, защищённые ключи и баланс, подтверждённая стоимость, надёжный учёт задач, устойчивые обратные вызовы и тесты, которые нужно пройти до выхода в работу.
Python SDK
Официальный SDK SpicyAPI для Python (spicyapi): установка, расчёт стоимости и первая генерация, загрузка локального файла, проверка вебхуков и разбор всех кодов ошибок.

