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

Ошибки

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

При ошибке вызова ответ сообщает две вещи: число (code), определяющее тип проблемы, и предложение (msg), предназначенное для человека. Эта страница перечисляет все числа, их смысл и возможность повторной попытки. Если нужен быстрый способ устранить конкретный симптом, начните с Устранение неполадок.

Общий формат

JSON-ответы нативного API задач используют этот формат: одинаковые четыре поля вокруг каждого ответа. Совместимые ответы /v1 и SSE разбирайте по правилам их протоколов:

{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }
  • code — код API, а не HTTP-статус. 200 означает успех.
  • При ошибке data отсутствует (не равен null): вы получаете code, msg и request_id. Единственное исключение: часть ошибок библиотеки ассетов добавляет data.reason.
  • msg предназначен для человека: формулировка и язык могут меняться (см. ниже). Не используйте его для выбора ветви обработки; используйте code.
  • request_id также возвращается в заголовке X-Request-Id. Указывайте его при сообщении о проблеме.

HTTP-статус тоже корректен. Можно использовать любой из двух, но только один: конкретные коды различаются (недостаточный баланс — code: 40201 с HTTP 402), а смешение рано или поздно приводит к пропуску случая.

Рекомендуем обрабатывать code. 404 сообщает лишь об отсутствии объекта, а 40201 — о недостаточном доступном балансе.

Не каждый ответ — JSON

Если ответ не разбирается как JSON, например это текст или HTML от нашей пограничной сети, поля code нет. Обрабатывайте HTTP-статус, записывайте начало тела и не повторяйте 403 вслепую: региональную блокировку повторы не устранят.

Язык сообщений об ошибках

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

  • msg в общем формате ответа;
  • errorMessage неудавшейся задачи в recordInfo и обратных вызовах вебхуков (с одним исключением ниже);
  • error.message в объектах ошибок совместимых эндпоинтов OpenAI, Anthropic и Google Gemini (/v1beta).

Поддерживаемые коды языков: en, ja, ko, de, fr, es, pt-BR, ru, it, pl.

Исключение: если сама модель сообщила конкретную причину ошибки, например нечитаемое референсное изображение или ограничение авторских прав для аудио, errorMessage передаёт эту причину на английском. Внутренние сведения — названия сервисов, хосты, URL и ID запросов — удаляются, но вы видите реальную причину. Причины, упоминающие состояние аккаунта или оплаты, никогда не передаются напрямую; вместо них вы получаете наше сообщение. Content-Language по-прежнему описывает msg, а для обработки всегда используется errorCode.

Выбор языка

Применяется первый подходящий вариант:

  1. Заголовок запроса Accept-Language — только для этого запроса. Используется стандартный приоритет HTTP: выбирается распознанный язык с наибольшим q. Региональные варианты сводятся к языку: de-DE считается de, а pt-PT — pt-BR. Китайский не предлагается, поэтому его теги не совпадают ни с одним языком и проверяется следующий кандидат. * означает отсутствие предпочтения.
  2. Настройка аккаунта в разделе Настройки → Язык ошибок API консоли. Она применяется ко всем запросам с API-ключами аккаунта, у которых нет распознанного Accept-Language. Большинство серверных SDK и curl по умолчанию не передают этот заголовок, поэтому на настройку опирается большинство интеграций. У обратных вызовов вебхуков нет заголовка исходного запроса, поэтому они всегда следуют настройке аккаунта.
  3. Иначе — английский.

Ответы с ошибками содержат заголовок Content-Language с фактическим языком сообщения.

Меняется только текст для человека

code, errorCode, совместимые с OpenAI type и code, совместимые с Gemini code и status, а также все имена полей одинаковы для любого языка. Обрабатывайте коды, не разбирая формулировку; записывайте code и request_id в логи и уведомления.

Пример

Та же ошибка 40201 (недостаточный баланс) по умолчанию возвращается на английском:

Default
HTTP/1.1 402 Payment Required
Content-Language: en

{ "code": 40201, "msg": "Insufficient balance", "request_id": "req_…" }

Добавьте один заголовок Accept-Language: ja; остальное — тот же вызов создания задачи из быстрого старта:

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" \
  -H "Accept-Language: ja" \
  --data "$TASK_PAYLOAD"

Теперь msg на японском; code по-прежнему 40201:

Accept-Language: ja
HTTP/1.1 402 Payment Required
Content-Language: ja

{ "code": 40201, "msg": "残高が不足しています", "request_id": "req_…" }

Общие коды

codeHTTPЗначениеПовторять?
200200Успех—
400400Неверный формат запроса или параметрыНет. Исправьте и отправьте снова
40003400Размер, тип или содержимое загруженного файла не совпадает с заявленным при запросе URL загрузкиНет. Запросите новый URL с правильными contentType и размером, загрузите снова и подтвердите
40004400Запрос корректен, но сейчас модель не может выполнить эту комбинацию настроекНет. Измените настройку из msg (например, разрешение или соотношение сторон) либо выберите другую модель. См. ниже
401401Ошибка аутентификации; msg указывает один из трёх вариантовНет. Исправьте заголовок или ключ
403403Нет разрешения. При вызове с действительным ключом ограничен аккаунт — см. ниже. Неподтверждённая почта вместо этого возвращает 40310Нет
404404Объект не существует или не принадлежит вамНет
409409Конфликт с текущим состояниемИногда — см. ниже
40901409Расчёт цены просрочен, запрос или цена изменилисьРассчитайте и подтвердите снова; восстанавливайте принятые запросы с исходным ключом идемпотентности
413413Тело запроса превышает допустимый сервером размерНет. Уменьшите тело или загрузите медиафайлы через процесс загрузки
429429Слишком много запросовОбычно да. Выдержите паузу по Retry-After. Один вариант 429 не связан со скоростью — см. Ограничения частоты запросов
500500Сбой на нашей сторонеДа. Выдержите паузу и повторите
503503Вспомогательный сервис (расчёт цены, сохранение изображений из запроса, загрузка медиафайлов) временно недоступенДа. Дождитесь Retry-After, если есть, и повторите; для createTask — с тем же Idempotency-Key. Если ошибка сохраняется без Retry-After, следуйте msg (например, используйте публичный URL HTTPS вместо изображения в запросе)

503 и 50301 имеют общий HTTP-статус, но разные коды API: 503 относится к вспомогательному сервису, 50301 — к самой модели.

Как различать три ошибки 401

Три совершенно разные ошибки возвращают 401 при аутентификации и требуют разных исправлений. Во всех случаях code остаётся 401; различие показывает msg.

msg (английский)Что произошлоЧто изменить
Missing credentials: send your API key in the Authorization header as "Bearer sk-spicy-..." (на совместимых /v1 и /v1beta: …as "Authorization: Bearer sk-spicy-...", x-api-key, or x-goog-api-key)Запрос пришёл вообще без учётных данныхКлиент теряет заголовок. Проверьте перехватчик, прокси и загрузку переменной окружения в выполняющий запрос процесс
API key format is not recognized: keys begin with sk-spicy-Учётные данные пришли, но это не наш ключВы вставили значение сессии консоли или ключ другого сервиса. Ключи SpicyAPI начинаются с sk-spicy-; echo "${SPICY_API_KEY:0:9}" должен вывести именно этот префикс
Credentials are invalid or expiredКлюч имеет наш формат, но не существует, отозван или просроченПроверьте на странице API-ключей; если отозван или просрочен, создайте замену. Повторы никогда не превратят ошибку в 200

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

Здесь строки показаны на английском; msg следует выбору языка выше, а формулировка не является контрактом. Обрабатывайте code, а сообщение читайте, показывайте или записывайте в лог — не сравнивайте его с фиксированной строкой.

Аккаунт не может вызывать API

В обоих случаях ключ действителен; вызовы пока недоступны самому аккаунту.

40310 означает, что адрес электронной почты не подтверждён. Сообщение: Verify your account email to run models and call the API…:

  1. Откройте консоль. Баннер вверху сообщает о неподтверждённой почте и позволяет отправить письмо снова.
  2. Нажмите ссылку в письме подтверждения (проверьте спам).
  3. Повторите тот же вызов. Новый ключ не нужен.

Пока API заблокирован, консоль продолжает работать, и история остаётся доступной.

Обычный 403 с This account cannot call the API right now означает ограничение аккаунта. Обратитесь в поддержку с request_id.

Баланс и лимиты

codeHTTPЗначениеЧто делать
40201402Баланс не покрывает оценочную стоимостьПополните в разделе Оплата консоли — см. Настройка аккаунта. Проверьте GET /api/v1/chat/credit
40202402Достигнут лимит расходовПрочитайте msg, чтобы определить его. Лимит ключа (This key has reached its …): повысьте на странице API-ключей или дождитесь обновления. Лимит команды (Workspace … budget has been reached): попросите владельца, администратора или управляющего бюджетом повысить его на странице команды. Лимит платформы (Platform quota for today is exhausted…): его изменить нельзя; повторите после 00:00 UTC. Дневные лимиты обновляются в 00:00 UTC, месячные — 1-го числа в 00:00 UTC, общий за всё время не сбрасывается

Повторять оба случая бессмысленно: повторный запрос не меняет баланс или лимиты. См. Лимиты расходов.

Разрешения

codeHTTPЗначениеЧто делать
40301403Ключу не разрешена эта модельРасширьте список разрешённых моделей ключа в консоли
40302403Адрес источника отсутствует в списке разрешённых IP-адресов ключаДобавьте его или уберите IP-ограничение ключа
40303403Сервис не предлагается в этом регионеОбойти ограничение нельзя. См. Где мы работаем
40310403Почта аккаунта ещё не подтверждена, поэтому запуск моделей недоступенОткройте ссылку из письма регистрации или отправьте новое письмо из консоли. После подтверждения ключ заработает без изменений с вашей стороны

Ни один из этих случаев не исправляется повтором.

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

Ошибки на эндпоинтах библиотеки ассетов (/api/v1/assets…) могут добавлять в обычную оболочку одно машиночитаемое поле data.reason:

{ "code": 400, "msg": "…", "data": { "reason": "asset_library_locked" }, "request_id": "req_…" }

code, HTTP-статус, msg и errorCode задачи одинаковы с причиной и без неё, поэтому существующая обработка продолжает работать. Причина появляется только на эндпоинтах библиотеки ассетов, но не на других нативных эндпоинтах, а OpenAI-совместимый и другие слои совместимости сохраняют свои форматы ошибок. Список может расти: неизвестное значение обрабатывайте как отсутствие причины.

data.reasonHTTPЗначениеЧто делать
asset_library_locked400Библиотека ассетов открывается после первого пополненияПополните баланс. Уже сохранённые ассеты остаются доступными
asset_library_full400Лимит хранилища (по числу или объёму) исчерпанУдалите ассеты или перейдите на тариф выше; следующий показывает GET /api/v1/assets/usage
asset_file_too_large400Файл превышает лимит тарифа на один файлИспользуйте файл меньше или перейдите на тариф выше
asset_daily_limit429Достигнут суточный лимит новых ассетовПодождите до 00:00 UTC и соблюдайте Retry-After
asset_storage_full503Хранилище всей платформы заполненоПовторите запрос позже
asset_permission_denied403Ваша роль в команде не допускает это изменениеОбратитесь к владельцу или администратору команды либо используйте ключ, создатель которого имеет подходящую роль
asset_workspace_forbidden403Этот ключ не открывает эту библиотеку, например потому, что участник, создавший командный ключ, покинул командуСоздайте новый ключ в нужном рабочем пространстве
asset_entity_name_taken409В библиотеке уже есть персонаж с таким именем (без учёта регистра). Персонажей именуют на странице Assets веб-приложения, поэтому API, который пока только читает, этого не возвращаетВыберите другое имя

Доступность моделей

codeHTTPЗначениеПовторять?
50301503Модель сейчас недоступнаДа. Повторите позже или выберите другую модель
50302503Текстовый запрос (потоковый или обычный) завершился ошибкой модели до выдачи ответа; резерв возвращёнДа. Отправьте снова с новым Idempotency-Key
40004400Модель работает, но не с этими настройкамиНет. Измените настройку из msg

50301 означает, что модель не могла принять задачу в момент отправки: она не предлагается, не имеет активной цены или доступных сейчас ресурсов генерации. Временная нехватка ресурсов может разрешиться при ограниченном числе повторов. Недоступность модели или отсутствие цены повторные запросы не исправят. Обновите каталог и получите подтверждение пользователя до смены модели.

40004 и 50301 намеренно различаются. 50301 означает, что модель сейчас вообще не принимает задачи; ожидание может помочь. 40004 означает, что модель работает, но не обслуживает это сочетание настроек. Поле msg называет настройку в скобках, например This model cannot serve this parameter combination right now (…). Ожидание не поможет; изменение значения — поможет. Ни один из случаев не создаёт задачу и не резервирует деньги.

Ошибка генерации после принятия появляется как state: failed

Ошибка генерации после получения ID от createTask появляется в recordInfo как state: "failed" с errorCode / errorMessage. Резерв обычно освобождается полностью, кроме определённых отказов по содержанию, явно указанных как платные на странице модели. Ситуация, соответствующая 40004, но обнаруженная после принятия, появляется как errorCode: "unsupported_combination". Не добавляйте синхронные коды API, которых нет на этой странице.

Коды ошибки задачи

Ошибка после принятия задачи приходит в errorCode на recordInfo. Набор закрытый: внутренняя диагностика сюда не попадает, поэтому любое значение вне списка обрабатывайте как upstream_failed.

errorCodeЗначениеПовторять?
invalid_requestПараметры не принятыНет. Исправьте и отправьте снова
unsupported_combinationСочетание сейчас не поддерживаетсяНет. Выберите другие настройки
content_rejectedВыбранная модель отклонила контент. SpicyAPI сам не проверяет запросы — см. Ответственность за контент и использованиеНет. Измените промпт или выберите другую модель
rate_limitedСервис генерации занятДа. Выдержите паузу и повторите
upstream_unavailableГенерация временно недоступнаДа. Повторите вскоре
invalid_assetРеференсный файл больше недоступенНе через повтор задачи. retry использует исходные входные данные, поэтому загрузите файл заново и создайте новую задачу с новым URI
generation_failedГенерация началась, но ничего не создалаВозможно. Те же входные данные обычно дают ту же ошибку
timeoutЗадача не завершилась до крайнего срокаВозможно. Попробуйте меньшую длительность или разрешение
upstream_failedВсе прочие случаиДа. Выдержите паузу и повторите

Неудавшиеся или просроченные задачи обычно освобождают весь резерв. Некоторые отказы по содержанию оплачиваются по принятому расчёту только если страница модели явно раскрывает это исключение. Проверяйте settled и cost для итогового списания. Поле errorMessage предназначено для человека: оно объясняет причину текстом, может передавать причину самой модели на английском (см. выше), а формулировка меняется. Обрабатывайте только errorCode. «Повтор» здесь означает отправку новой задачи, которая снова оплачивается при успехе.

Какие ошибки стоят денег

Состояние задачи и состояние соединения — разные факты

Явно отклонённое создание, просроченные задачи и исторические отменённые записи не оплачиваются за генерацию. Неудавшиеся задачи обычно не оплачиваются, кроме определённых отказов по содержанию, явно раскрытых как платные на странице модели. Тайм-аут или отключение клиента — не ошибка задачи. Принятые задачи нельзя отменить: восстановите исходную задачу и проверьте её конечное состояние и расчёт. Отключённый текстовый поток может оплачиваться, если корректные данные об использовании уже записаны.

СитуацияОплачивается?
API явно отклоняет createTask, не создавая задачу (например, 40201, 40202, 40004 или 50301)Нет. Создание и резерв используют одну транзакцию; ошибка шлюза или отсутствие ответа не доказывают отказ
Отправка завершилась тайм-аутом, вкладка закрылась или соединение оборвалосьПока неизвестно. Восстановите факт принятия исходным ключом идемпотентности; не считайте это бесплатным повтором
createTask возвращает 202, а позже задача — failed / expiredОбычно не оплачивается. Некоторые результаты failed / content_rejected оплачиваются по принятому расчёту при явном указании на странице модели. Прочитайте settled и cost
Историческая задача — canceledНет. Это состояние совместимости, а не доступное сейчас действие
createTask возвращает 202, задача — succeededДа, по фактическому использованию, не выше принятого резерва. Остаток освобождается; позднее ничего сверх резерва не списывается
Любая ошибка recordInfo, chat/credit или common/*Нет. Эти эндпоинты никогда не оплачиваются
Ограничение частоты (429) или дедупликация ключом идемпотентностиНет. Первый запрос не был принят; второй возвращает исходную задачу, оплаченную один раз

Расчёт или освобождение завершаются автоматически, в том числе после прерывания процесса. Обычные ошибки освобождают весь резерв; определённые отказы по содержанию, явно раскрытые на странице модели, оплачиваются по принятому расчёту. Проверьте расчёт задачи и баланс, чтобы подтвердить результат — см. Оплата.

Когда имеет смысл повторять 409

СлучайПовторять?
Редкий конфликт одновременных createTask с тем же ключом и неизменным теломДа. Подождите несколько сотен миллисекунд и повторите с тем же ключом. См. Идемпотентность
Ключ уже использован с другим телом или API-ключомНет. Остановитесь и исправьте принадлежность ключа; повторы не меняют конфликт
Изображения из запроса или подтверждение загрузки ещё обрабатываются (может быть Retry-After)Да. Дождитесь Retry-After или пары секунд и отправьте снова с тем же Idempotency-Key
Цены или конфигурация аккаунта изменились во время запросаДа. Получите новый расчёт, если используете расчёты цены, затем отправьте снова
Файл результата ещё копируетсяДа. Подождите несколько секунд и снова запросите ссылку скачивания
jobs/retry для незавершённой задачи или задачи с незавершённым расчётомДа, позже. Дождитесь конечного состояния исходной задачи и settled, равного true, затем повторите
jobs/retry для успешной задачи или задачи с уже удалёнными по сроку входными даннымиНет. Создайте новую задачу через createTask
URL загрузки или загруженный файл просроченыНет. Загрузите файл снова

Реализация повторов

Не повторяйте вслепую

400, 401, 40004, 40201 и подобные ошибки входных данных или авторизации дают тот же ответ при любом числе попыток. Каждый вариант с пометкой «Нет» выше — бесполезная работа, которая расходует ваш лимит запросов.

Экспоненциальное увеличение паузы со случайной добавкой не даёт пакету неудавшихся запросов одновременно повториться:

JavaScript
// 409 is operation-specific and must be handled at the call site.
const RETRYABLE = new Set([429, 500, 503, 50301]);

async function callWithRetry(path, init, { attempts = 4 } = {}) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(`https://api.spicyapi.ai/api/v1${path}`, init);

    // A page from the edge network is not our JSON envelope
    if (!res.headers.get('content-type')?.includes('json')) {
      const text = await res.text();
      throw new Error(`HTTP ${res.status}, not JSON: ${text.slice(0, 200)}`);
    }
    const body = await res.json();

    if (body.code === 200) return body.data;
    if (!RETRYABLE.has(body.code)) {
      throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
    }

    // When the server says how long to wait, trust it
    const retryAfter = Number(res.headers.get('retry-after'));
    const backoff = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : 2 ** i * 1000 + Math.random() * 500;

    await new Promise((r) => setTimeout(r, backoff));
  }
  throw new Error('out of retries');
}
Python
import random, time, requests

# 409 is operation-specific and must be handled at the call site.
RETRYABLE = {429, 500, 503, 50301}
BASE = "https://api.spicyapi.ai/api/v1"


def call_with_retry(method: str, path: str, attempts: int = 4, **kwargs) -> dict:
    for i in range(attempts):
        res = requests.request(method, f"{BASE}{path}", **kwargs)

        # A page from the edge network is not our JSON envelope
        try:
            body = res.json()
        except ValueError:
            raise RuntimeError(f"HTTP {res.status_code}, not JSON: {res.text[:200]}")

        if body["code"] == 200:
            return body["data"]
        if body["code"] not in RETRYABLE:
            raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')

        # When the server says how long to wait, trust it
        retry_after = res.headers.get("retry-after")
        backoff = float(retry_after) if retry_after else 2**i + random.random() * 0.5
        time.sleep(backoff)

    raise RuntimeError("out of retries")
Go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"strconv"
	"time"
)

// retryable lists the codes worth another attempt. Anything else returns
// the same answer however many times you ask — and each ask eats your own
// rate limit.
// Whether the generation itself failed is read from recordInfo's state.
var retryable = map[int]bool{429: true, 500: true, 503: true, 50301: true}

type envelope struct {
	Code      int             `json:"code"`
	Msg       string          `json:"msg"`
	Data      json.RawMessage `json:"data"`
	RequestID string          `json:"request_id"`
}

func callWithRetry(newReq func() (*http.Request, error), attempts int, out any) error {
	for i := 0; i < attempts; i++ {
		req, err := newReq()
		if err != nil {
			return err
		}
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			// Transport-level failure: the request may well have arrived.
			// On createTask, send the same Idempotency-Key or this retry
			// turns into a second generation and a second charge.
			time.Sleep(backoff(i, ""))
			continue
		}

		var env envelope
		decErr := json.NewDecoder(res.Body).Decode(&env)
		io.Copy(io.Discard, res.Body)
		res.Body.Close()
		if decErr != nil {
			// Not our JSON envelope, for example a page from the edge network.
			return fmt.Errorf("HTTP %d, response is not JSON: %w", res.StatusCode, decErr)
		}

		if env.Code == 200 {
			return json.Unmarshal(env.Data, out)
		}
		if !retryable[env.Code] {
			return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
		}
		// When the server says how long to wait, trust it
		time.Sleep(backoff(i, res.Header.Get("Retry-After")))
	}
	return fmt.Errorf("out of retries")
}

// backoff trusts Retry-After when present, otherwise backs off
// exponentially with jitter so a batch of failures does not come back
// as one thundering herd.
func backoff(attempt int, retryAfter string) time.Duration {
	if s, err := strconv.Atoi(retryAfter); err == nil && s > 0 {
		return time.Duration(s) * time.Second
	}
	base := time.Duration(1<<attempt) * time.Second
	return base + time.Duration(rand.Intn(500))*time.Millisecond
}

При повторе createTask всегда передавайте тот же Idempotency-Key, иначе успешный запрос с потерянным ответом превратится в двойную генерацию и двойную оплату.

Диагностика

Если ошибка непонятна, проверьте по порядку:

  1. request_id — передайте его нам, чтобы найти точный запрос.
  2. code — таблицы выше.
  3. Логи консоли — вставьте ID задачи или request_id в поиск на странице Логи. Откроется задача: статус, стоимость, задержка, отправивший ключ, код ошибки и причина. Параметры и результаты показаны только пока хранятся, а в команде — только ролям с правом чтения содержимого.

Поле msg для 500 всегда общее: «сервис временно недоступен», без внутренних деталей. В ответах никогда не появляются внутренняя структура, имена компонентов или стеки вызовов. Их можно исследовать только через request_id.

Для связи откройте обращение в разделе Поддержка или напишите на support@spicyapi.ai. Приложите request_id или ID задачи, время (UTC) и code. Никогда не отправляйте API-ключ.

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