Быстрый старт
Получите ключ, создайте задачу и заберите результат. Весь процесс от начала до конца.
SpicyAPI предоставляет генерацию изображений и видео через единый протокол аутентификации, задач и результатов, а текстовые модели — в привычных форматах OpenAI, Anthropic и Gemini. При смене модели сохраняйте тот же порядок вызовов, а input формируйте по её inputSchema — списку принимаемых полей, их типов и допустимых значений.
Руководство использует нативный API задач по адресу https://api.spicyapi.ai/api/v1. Эндпоинты совместимости для текста и видео используют https://api.spicyapi.ai/v1. Они принимают один и тот же API-ключ, но форматы запросов и ответов различаются.
Следуйте этой странице, чтобы выбрать модель и получить результат. Для точной цены до оплаты см. Расчёт цены и совместимость. Для чата — Текст и потоковые ответы. Перед выпуском интеграции пройдите Интеграция для рабочей среды; при ошибке на одном из шагов используйте Устранение неполадок. Незнакомые слова объясняются в глоссарии.
Хотите увидеть результат до написания кода?
Откройте любую модель на spicyapi.ai/explore и используйте вкладку Playground; вкладка API показывает тот же запрос в виде команды для копирования. Для работы из терминала без написания кода используйте CLI или поручите это AI-ассистенту через MCP-сервер.
Официальные инструменты
Прямые вызовы HTTP API полностью поддерживаются; именно их показывают дальнейшие шаги страницы. Если вы не хотите писать HTTP-вызовы самостоятельно, доступны четыре npm-пакета с лицензией MIT. Каждый устанавливается отдельно и имеет своё руководство:
@spicyapi/sdk
TypeScript SDK: типы OpenAPI, адаптивное ожидание, загрузка файлов и проверка вебхуков
@spicyapi/cli
Команда spicyapi: каталог, расчёт цены, создание и отслеживание задач из терминала
@spicyapi/mcp
MCP-сервер, дающий агенту-разработчику каталог, расчёты цены и задачи с подтверждением каждого платного шага
@spicyapi/skill
Переносимый Agent Skill: порядок работы и ограничения расходов, устанавливаемые в вашего агента
Ещё четыре официальных SDK доступны для языков вне Node.js. Все реализуют тот же процесс и имеют лицензию MIT. Сейчас только модуль Go устанавливается из репозитория обычной командой; остальные три страницы показывают способ установки до публикации пакетов:
Python SDK
spicyapi для Python 3.11+, без зависимостей во время выполнения
Go SDK
github.com/spicyapi-ai/spicy-go для Go 1.23+, только стандартная библиотека
PHP SDK
spicyapi/spicyapi для PHP 8.1+, без библиотечных зависимостей
Java SDK
ai.spicyapi:spicyapi-java для Java 17+, с закрытой иерархией типов ошибок
Работаете в ComfyUI? Узлы ComfyUI представляют каждую модель изображений, видео и аудио узлом на холсте и показывают её цену до запуска.
Перед началом
Нужны четыре вещи. Если пропустить одну, ошибка возникнет позже и не обязательно скажет, чего именно не хватает.
- Аккаунт с подтверждённой электронной почтой. Зарегистрируйтесь на spicyapi.ai и нажмите ссылку в письме подтверждения. До этого каждый вызов с вашим ключом возвращает
403. - Баланс. Генерации оплачиваются из баланса в долларах США (баллов нет); новый аккаунт начинает с нуля. Пополните в разделе Консоль → Оплата. При недостатке средств создание задачи завершается кодом API
40201. Раздел Настройка аккаунта объясняет регистрацию, пополнение и создание ключа. - Терминал. macOS: приложение Terminal. Linux: любая оболочка. Windows: PowerShell. Если команды отличаются, шаг содержит вкладку Windows PowerShell. При желании команды macOS / Linux можно без изменений запускать в WSL или Git Bash на Windows.
- Несколько центов. Шаг 3 создаёт настоящую задачу изображения, оплачиваемую при успехе. Шаг 2 показывает цену каждой модели; необязательная команда на том же шаге бесплатно сообщает точную сумму заранее.
Сначала ознакомьтесь: модели и цены без аккаунта
Для просмотра моделей и цен не нужны ни ключ, ни аккаунт. Публичный каталог сайта читается без авторизации; в этой команде ничего заменять не нужно:
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"Формат ответа тот же, что описан внизу страницы. В data.items каждая запись содержит:
model— точное значение, которое позже нужно передать какmodel(slugсодержит то же значение);callable— может ли модель выполняться прямо сейчас. Выбирайте записи сtrue:statusописывает запланированное рабочее состояние, и модельliveвсё равно может временно быть недоступна для вызова;modalityиtasks— какой вид результата создаётся;price— самая дешёвая категория:amountв USD заunit(per_image,per_second,per_requestилиper_1k_tokens), а такжеtiers, число категорий. Для текстовых моделей читайтеprice.perMillionTokens: там отдельные цены входных, кешированных входных и выходных токенов за миллион. Самоamount— только выходной тариф.
Добавьте ID модели, чтобы получить полную запись одной модели, включая inputSchema, paramNotes и готовые examples:
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"MODEL_ID_FROM_CATALOG — заполнитель, используемый в документации, а не настоящая модель. Всегда копируйте реальный ID из актуального каталога.
locale по умолчанию равен en; передайте ja, ko, de, fr, es, pt-BR, ru, it или pl для переведённых названий, описаний и заметок к параметрам. Любое другое значение возвращает 400. strict=1 исключает записи без опубликованного текста на этом языке вместо возврата английской версии; обычно он не нужен. Эта группа эндпоинтов ограничена по адресу источника, а не аккаунту, и имеет отдельный лимит — см. Ограничения частоты запросов. Она также кешируется, поэтому изменение цены может появиться здесь не сразу.
Если tiers больше 1, указанная price — самая дешёвая категория, а не цена ваших настроек. price.variant показывает настройки самой дешёвой категории (например, 720p или generate_audio=false;resolution=480p при сочетании нескольких полей). Поле inputSchema["x-pricing"].variantFields записи одной модели, если есть, перечисляет все поля выбора категории. Её pricingNote — общее напоминание о приоритете актуального расчёта цены, а не список полей. Для точной суммы запроса используйте POST /api/v1/jobs/quote, когда получите ключ.
Публичный каталог — для ознакомления, авторизованный — для вызовов
Публичный каталог отвечает на вопрос о том, что предлагается и по какой опубликованной цене. Он не отвечает, что может вызвать мой ключ и сколько заплатит мой аккаунт: оба условия зависят от аккаунта. Получив ключ, перейдите на GET /api/v1/models в шаге 2 и считайте его единственным источником доступных ID. Запомненный ID, снимок каталога при запуске или маркетинговая страница его не заменяют.
Пять шагов
Получите ключ
API-ключ — пароль, который код передаёт с каждым запросом. Он определяет аккаунт и используется для оплаты.
- Откройте Консоль → API-ключи и нажмите Создать ключ.
- Задайте имя, например
my-first-test. Оставьте настройки Дополнительно как есть. - Скопируйте ключ, начинающийся с
sk-spicy-, или нажмите Сохранить в файл. Он показывается только один раз; при потере нужно создать новый. - В терминале для следующих шагов сохраните его в переменной окружения — именованном значении, передаваемом терминалом каждой запускаемой команде. Тогда ключ не нужно вводить в саму команду:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"Что должно появиться: sk-spicy-. Значение сохраняется до закрытия окна терминала, поэтому все дальнейшие шаги выполняйте в том же окне.
Если кнопки Создать ключ нет, ваша роль в текущей команде не позволяет создавать ключи. Обратитесь к владельцу или используйте Создать свою команду на той же странице. У новых ключей нет лимита расходов, пока вы его не зададите; дневной лимит в консоли ограничивает последствия неверной настройки или утечки. См. Аутентификация.
Выберите доступную для вызова модель
Авторизованный каталог — единственный источник доступных ID моделей, текущих схем и примеров. Используйте два вызова: лёгкий список сообщает, что можно вызвать, затем одна запись даёт схему выбранной модели. Чтение каталога бесплатно.
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
-H "Authorization: Bearer $SPICY_API_KEY"Ответ длинный. Нужная часть выглядит так (сокращённо):
{
"code": 200,
"msg": "success",
"data": {
"total": 1,
"items": [
{
"model": "MODEL_ID_FROM_CATALOG",
"displayName": "…",
"modality": "image",
"tasks": ["text-to-image"],
"enabled": true,
"available": true,
"startingPrice": { "variant": "", "unit": "per_image", "price": "0.008", "currency": "USD" }
}
]
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}Выберите запись, где enabled и available одновременно равны true; запомните цену, скопируйте model и используйте его на следующих шагах. При установленном jq команда curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' выводит только доступные для вызова ID.
# Replace MODEL_ID_FROM_CATALOG with the model value you copied
export SPICY_MODEL="MODEL_ID_FROM_CATALOG"curl "https://api.spicyapi.ai/api/v1/models/${SPICY_MODEL}" \
-H "Authorization: Bearer $SPICY_API_KEY"Косые черты ID проходят в пути без изменений; кодируйте их через процентное представление только если HTTP-клиент требует один сегмент для параметра пути. inputSchema записи определяет input: required перечисляет обязательные поля, а properties описывает каждое. examples содержит входные данные, уже проверенные сервером. Как читать остальные поля, включая pricing, — в разделе Каталог моделей и схемы.
Запрашивайте схемы только там, где нужны. Добавление includeSchema=1&includeExamples=1 к вызову списка возвращает полную JSON Schema каждой включённой модели одним ответом, значительно увеличивая его размер. Чтение каталога использует лимит чтения аккаунта независимо от создания задач. Не перечитывайте каталог перед каждой задачей; см. Кеширование каталога.
Если подходящих записей нет, остановитесь до создания платной задачи. Снимок каталога при запуске, маркетинговая страница или запомненный ID не заменяют пустой актуальный каталог. Как выбрать модель программно — в разделе Выбор модели из кода.
Необязательно: сначала узнайте точную цену. Расчёт бесплатен, ничего не создаёт и не резервирует. data.estimatedCost — максимальная стоимость этого конкретного запроса в USD:
curl -X POST https://api.spicyapi.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"Создайте задачу
Задача — генерация в фоне: ID вы получаете сразу, результат — позже. Этот шаг стоит денег.
Каждая новая задача получает новый ключ идемпотентности: случайный ID, который вы прикрепляете, чтобы повторная отправка того же запроса, например после сетевой ошибки, не создала две задачи. Создайте его отдельной командой, затем отправьте задачу:
SPICY_IDEMPOTENCY_KEY="$(uuidgen)"
# Linux without uuidgen: SPICY_IDEMPOTENCY_KEY="$(cat /proc/sys/kernel/random/uuid)"curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"Если 3b завершился тайм-аутом или соединение оборвалось, повторите 3b, не выполняя 3a: тот же ключ вернёт уже запущенную задачу, не заставляя платить дважды. Для другого изображения сначала выполните 3a. Ответ 409 означает повторное использование ключа с другим промптом или моделью. Подробнее — Идемпотентность.
Здесь отправляется одно поле. Если схема из шага 2 помечает другие поля обязательными, добавьте их: допустимый input определяет собственная схема каждой модели, а одно неизвестное поле — это 400.
Соединение не остаётся открытым. ID задачи возвращается сразу:
HTTP/1.1 202 Accepted{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.008",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}На macOS / Linux скопируйте taskId ответа и сохраните его (PowerShell уже сделал это):
export SPICY_TASK_ID="paste-the-taskId-here"202 означает принятие, а не завершение
Новая задача имеет state: "queued"; безопасный повтор с тем же Idempotency-Key возвращает исходную задачу в её текущем состоянии. HTTP 202 означает, что запрос принят, а не что генерация завершена. При первом принятии средства резервируются, а не списываются: estimatedCost откладывается из баланса. Резерв также ограничивает оплату задачи сверху: остаток освобождается, а позднее ничего сверх него не списывается. Неудавшиеся задачи обычно освобождают резерв; определённые отказы по содержанию, раскрытые на странице модели, оплачиваются по принятому расчёту. Проверяйте settled и cost. deadlineAt — последнее время завершения задачи, после которого она становится expired. Успешный код API code в общем формате остаётся 200. Принятую задачу отменить нельзя.
Получите результат
Если рабочий запрос содержит callBackUrl, мы обращаемся к вашему серверу, когда задача достигает конечного состояния. Обратный вызов (вебхук) — адрес HTTPS вашего сервера, вызываемый после завершения задачи, чтобы вам не приходилось постоянно проверять. Минимальный запрос выше не содержит его и может выполняться без публичного эндпоинта. Подпись обратного вызова обязательно проверяйте — см. Вебхуки.
Без эндпоинта обратного вызова запросите задачу:
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY"Используйте создавший задачу API-ключ: другой получит 404. Смотрите data.state:
data.state | Значение | Что делать |
|---|---|---|
queued или running | Генерация продолжается. Средства в резерве, не потрачены | Подождите несколько секунд и повторите ту же команду. Изображения обычно занимают секунды, видео — минуты. deadlineAt задаёт верхнюю границу |
succeeded | Готово | Перейдите к следующему шагу |
failed или expired | Задача не удалась. Резерв обычно освобождается; некоторые явно раскрытые на странице модели отказы по содержанию оплачиваются. Проверяйте settled и cost | Прочитайте data.errorCode (стабильный) и data.errorMessage (для человека), исправьте входные данные и начните с 3a |
Общий code равен 200, даже если задача не удалась: code показывает успех чтения задачи. Собственный результат задачи всегда в data.state. Ответы на разных этапах выглядят так (сокращённо):
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "MODEL_ID_FROM_CATALOG",
"state": "running",
"cost": "0.008",
"settled": false,
"createdAt": "2026-08-28T09:12:04Z",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w4"
}Пока settled равно false, cost показывает резерв, а не итоговое списание.
Скачайте файл результата
Возьмите data.output.assets[0].url успешного результата и скачайте файл напрямую. Не прикрепляйте API-ключ. Ссылка действует около 20 минут (expiresAt); если срок истёк, повторите шаг 4 — каждое чтение выдаёт новую ссылку. Сгенерированные файлы удаляются не позднее 14 дней, поэтому сохраняйте собственные копии нужных файлов.
# Paste data.output.assets[0].url. Keep the double quotes: the URL contains & characters.
RESULT_URL="paste-the-url-here"
curl -L "$RESULT_URL" -o result.png # use result.mp4 when mime is video/mp4При установленном jq можно обойтись без копирования и вставки:
RESULT_URL="$(curl -s "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY" | jq -r '.data.output.assets[0].url')"Что должно появиться: файл result.png в текущей папке терминала. Проверьте списание в разделе Консоль → Логи или в cost, когда settled равно true.
Текстовые модели: один запрос, ответ в том же ответе HTTP
Текстовые модели отвечают в том же HTTP-ответе: без опроса и скачивания. Заголовок ответа X-Spicy-Task-Id указывает задачу с оплатой, если её потребуется проверить позже. В шаге 2 запросите модели с modality=text и сохраните одну в SPICY_MODEL, затем:
curl https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one sentence.\"}]}"Ответ находится в choices[0].message.content. Вызов оплачивается по использованным токенам. Эндпоинт использует формат ответа OpenAI, а не общий {code, msg, data}. См. Текст и потоковые ответы.
Выбор модели из кода
Пять шагов выше предполагают участие человека. Рабочая интеграция выбирает модель самостоятельно непосредственно перед отправкой: модель, доступная на прошлой неделе, сегодня может быть приостановлена, а цены зависят от аккаунта. Запросите схему и проверенный сервером пример одним вызовом, затем проверьте enabled и available:
CATALOG_JSON="$(curl --fail-with-body \
"https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1" \
-H "Authorization: Bearer $SPICY_API_KEY")"
MODEL_ID="$(printf '%s' "$CATALOG_JSON" | jq -er \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].model')"
MODEL_INPUT="$(printf '%s' "$CATALOG_JSON" | jq -cer \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].examples[0].input')"
printf 'Selected %s\n' "$MODEL_ID"SPICY_IDEMPOTENCY_KEY="$(uuidgen)" # new task, new key; reuse it only to resend this same request
TASK_PAYLOAD="$(jq -cn \
--arg model "$MODEL_ID" \
--argjson input "$MODEL_INPUT" \
'{model: $model, input: $input}')"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "$TASK_PAYLOAD"Если выбор ничего не дал, остановитесь, не переходя на запомненный ID. Три программы ниже выполняют тот же выбор без jq.
Полный процесс
Все три программы ниже читают каталог, создают задачу, опрашивают её с увеличением интервала и выводят URL скачивания. В рабочей среде используйте вебхуки: опрос — запасной вариант, а не основной план.
Запуск программ создаёт настоящую платную задачу
Каждая программа выбирает первую доступную модель генерации изображения из текста в вашем каталоге (не самую дешёвую) и отправляет один из проверенных примеров. Сначала проверьте её цену или измените выбор на конкретную нужную модель.
| Язык | Сохранить как | Сначала установить | Запуск |
|---|---|---|---|
| Node.js 20 или новее | generate.mjs | ничего | node generate.mjs |
| Python 3.9 или новее | generate.py | pip install requests | python3 generate.py (Windows: py generate.py) |
| Go 1.21 или новее | main.go в пустой папке | go mod init example | go run . |
Все три читают ключ из SPICY_API_KEY (шаг 1) и выводят URL результата после завершения задачи. Они ждут до десяти минут, что подходит для моделей изображений; для видео ждите до deadlineAt задачи.
const BASE = 'https://api.spicyapi.ai/api/v1';
const KEY = process.env.SPICY_API_KEY;
const headers = {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
};
async function call(path, init) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: { ...headers, ...init?.headers },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
// Branch on `code`, not on res.ok — the business code says far more
if (body.code !== 200) {
throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
}
return body.data;
}
async function generate() {
const catalog = await call(
'/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1',
);
const selected = catalog.items.find(
(item) => item.enabled && item.available && item.examples?.length,
);
if (!selected) throw new Error('No callable text-to-image model with a validated example');
const idempotencyKey = crypto.randomUUID();
const { taskId } = await call('/jobs/createTask', {
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey },
body: JSON.stringify({
model: selected.model,
input: selected.examples[0].input,
}),
});
// Start at 2s, multiply by 1.5, cap at 15s. The first check is almost
// certainly still `queued`, so a shorter start just wastes a round trip.
const deadline = Date.now() + 10 * 60_000;
let wait = 2000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, wait));
wait = Math.min(wait * 1.5, 15000);
const task = await call(`/jobs/recordInfo?taskId=${taskId}`);
if (task.state === 'succeeded') {
const asset = task.output?.assets?.find((item) => item.url);
if (asset) return asset.url;
if (task.output?.assets?.some((item) => item.pending)) continue;
throw new Error('Result is unavailable');
}
// Anything other than queued / running is terminal. This also handles
// historical canceled records and future terminal states safely.
if (task.state !== 'queued' && task.state !== 'running') {
throw new Error(`${task.state}: ${task.errorMessage ?? 'no detail'}`);
}
}
throw new Error(`task ${taskId} timed out`);
}
generate().then(console.log);import os, time, uuid, requests
BASE = "https://api.spicyapi.ai/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
"Content-Type": "application/json",
}
def call(method: str, path: str, headers=None, **kwargs) -> dict:
res = requests.request(
method, f"{BASE}{path}", headers={**HEADERS, **(headers or {})}, timeout=30, **kwargs
)
body = res.json()
# Branch on `code`, not on status_code
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
return body["data"]
def generate() -> str:
catalog = call(
"GET",
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
)
selected = next(
(
item
for item in catalog["items"]
if item["enabled"] and item["available"] and item.get("examples")
),
None,
)
if selected is None:
raise RuntimeError("No callable text-to-image model with a validated example")
idempotency_key = str(uuid.uuid4())
task_id = call("POST", "/jobs/createTask", headers={"Idempotency-Key": idempotency_key}, json={
"model": selected["model"],
"input": selected["examples"][0]["input"],
})["taskId"]
# Start at 2s, multiply by 1.5, cap at 15s
deadline = time.monotonic() + 600
wait = 2.0
while time.monotonic() < deadline:
time.sleep(wait)
wait = min(wait * 1.5, 15.0)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
if task["state"] == "succeeded":
assets = task.get("output", {}).get("assets", [])
ready = next((asset for asset in assets if asset.get("url")), None)
if ready:
return ready["url"]
if any(asset.get("pending") for asset in assets):
continue
raise RuntimeError("Result is unavailable")
# Anything other than queued / running is terminal.
if task["state"] not in {"queued", "running"}:
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
raise TimeoutError(f"task {task_id} timed out")
print(generate())package main
import (
"bytes"
"crypto/rand"
"encoding/json"
"encoding/hex"
"fmt"
"math"
"net/http"
"os"
"time"
)
const base = "https://api.spicyapi.ai/api/v1"
// envelope is the shape every endpoint returns. Data stays raw so each
// call site can decode it into whatever it actually expects.
type envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
RequestID string `json:"request_id"`
}
var client = &http.Client{Timeout: 30 * time.Second}
func newIdempotencyKey() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return "job-" + hex.EncodeToString(b)
}
func call(method, path string, body any, idempotencyKey string, out any) error {
var rdr *bytes.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(raw)
} else {
rdr = bytes.NewReader(nil)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SPICY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
// Branch on Code, not on res.StatusCode
if env.Code != 200 {
return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
}
return json.Unmarshal(env.Data, out)
}
func generate() (string, error) {
var catalog struct {
Items []struct {
Model string `json:"model"`
Enabled bool `json:"enabled"`
Available bool `json:"available"`
Examples []struct {
Input map[string]any `json:"input"`
} `json:"examples"`
} `json:"items"`
}
if err := call(http.MethodGet,
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
nil, "", &catalog); err != nil {
return "", err
}
var modelID string
var modelInput map[string]any
for _, item := range catalog.Items {
if item.Enabled && item.Available && len(item.Examples) > 0 {
modelID, modelInput = item.Model, item.Examples[0].Input
break
}
}
if modelID == "" {
return "", fmt.Errorf("no callable text-to-image model with a validated example")
}
var created struct {
TaskID string `json:"taskId"`
}
err := call(http.MethodPost, "/jobs/createTask", map[string]any{
"model": modelID,
"input": modelInput,
}, newIdempotencyKey(), &created)
if err != nil {
return "", err
}
// Start at 2s, multiply by 1.5, cap at 15s
wait := 2 * time.Second
deadline := time.Now().Add(10 * time.Minute)
for time.Now().Before(deadline) {
time.Sleep(wait)
wait = time.Duration(math.Min(float64(wait)*1.5, float64(15*time.Second)))
var task struct {
State string `json:"state"`
ErrorMsg string `json:"errorMessage"`
Output struct { Assets []struct { URL string `json:"url"`; Pending bool `json:"pending"` } `json:"assets"` } `json:"output"`
}
if err := call(http.MethodGet, "/jobs/recordInfo?taskId="+created.TaskID, nil, "", &task); err != nil {
return "", err
}
if task.State == "succeeded" {
pending := false
for _, asset := range task.Output.Assets {
if asset.URL != "" { return asset.URL, nil }
pending = pending || asset.Pending
}
if pending { continue }
return "", fmt.Errorf("result is unavailable")
}
// Terminal means "not one of the two in-flight states" — do not
// enumerate the terminal states, that breaks when a new one appears
if task.State != "queued" && task.State != "running" {
return "", fmt.Errorf("%s: %s", task.State, task.ErrorMsg)
}
}
return "", fmt.Errorf("task %s timed out", created.TaskID)
}
func main() {
url, err := generate()
if err != nil {
panic(err)
}
fmt.Println(url)
}Общий формат ответа
JSON-ответы нативного API задач используют этот формат; совместимые /v1 и SSE — свои форматы протоколов:
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }code— код API, а не HTTP-статус.200означает успех. HTTP-статус также корректен, поэтому можно обрабатывать любой из них, но выберите один. Конкретные коды различаются (недостаточный баланс —code: 40201с HTTP402), и смешение рано или поздно приводит к пропуску случая.- При ошибке
dataотсутствует, а не равенnull. request_idесть и при успехе, и при ошибке; он также возвращается в заголовке ответаX-Request-Id. Указывайте его при сообщении о проблеме.
Дальше
Аутентификация
Формат ключа, ограничения отдельных ключей и действия при утечке
Каталог моделей и схемы
Актуальный каталог, цены и построение таблицы параметров из inputSchema
Асинхронные задачи
Состояния, получение результатов и увеличение интервала опроса
Вебхуки
Формат данных, проверка подписи и повторные доставки
Медиафайлы
Прямая загрузка референсов и скачивание результатов
Идемпотентность
Безопасные повторы без двойной генерации или оплаты
Ошибки
Все коды API и случаи, когда повторы бесполезны
Подготовка аккаунта
От «аккаунта нет» до работающего API-ключа примерно за пятнадцать минут. Регистрация, подтверждение почты, пополнение баланса, создание ключа и бесплатный тестовый вызов — с объяснением каждого экрана.
Аутентификация
Формат API-ключа, ограничения отдельных ключей, доступ к задачам и действия при утечке ключа.

