Java SDK
Официальный SDK SpicyAPI для Java (ai.spicyapi spicyapi-java): установка, расчёт стоимости и первая генерация, загрузка локального файла, проверка обратных вызовов и исчерпывающий switch по запечатанной иерархии ошибок.
spicyapi-java — официальный SDK SpicyAPI для Java. Он прячет генерацию изображений, видео и звука за одним SpicyClient: подставляет ваш API-ключ, раскладывает конверт ответа по записям (record), дожидается задачи с ограниченными сверху паузами, загружает локальные файлы одним вызовом, проверяет подписи обратных вызовов и сводит любую неудачу к запечатанному типу исключения, так что switch по этой иерархии оказывается исчерпывающим уже на этапе компиляции.
Ему нужна Java 17 или новее и одна зависимость: Jackson (com.fasterxml.jackson.core:jackson-databind) версии 2.12 или новее — именно с этого выпуска записи стали отображаться на JSON, а здешние типы как раз записи. Jackson подключён обычной зависимостью времени компиляции — не optional и не затенённой (shaded), — поэтому побеждает та версия, которая уже есть в вашем проекте. На module path клиент называется ai.spicyapi, и это имя закреплено в манифесте, а не выведено из имени JAR-файла.
Тот ли это инструмент?
| Вы хотите… | Что использовать |
|---|---|
| Вызывать SpicyAPI из собственного кода на Java или другом языке JVM | Этот SDK |
| Вызывать его из Node.js или TypeScript | TypeScript SDK |
| Вызывать его из Python, Go или PHP | SDK для Python, Go или PHP |
| Генерировать из терминала, не написав ни строчки кода | CLI |
| Чтобы ИИ-ассистент вызывал SpicyAPI за вас | MCP-сервер и Agent Skill |
| Пользоваться чат-моделью или текстовой моделью | Проверенный клиент на Java для форматов OpenAI, Anthropic или Gemini — см. Текст и потоковые ответы |
Установка
Подключите зависимость из Maven Central:
<dependency>
<groupId>ai.spicyapi</groupId>
<artifactId>spicyapi-java</artifactId>
<version>0.1.0</version>
</dependency>Или в Gradle (Kotlin DSL):
implementation("ai.spicyapi:spicyapi-java:0.1.0")Положите ключ в переменную окружения, чтобы он никогда не попадал ни в ваш код, ни в систему контроля версий:
export SPICY_API_KEY="sk-spicy-..."Первый результат
Узнайте цену, примите решение, создайте задачу, дождитесь её и заберите результат. Расчёт стоимости бесплатен и ничего не резервирует. run — это createTask, за которым сразу идёт waitForTerminal.
SpicyClient client = SpicyClient.builder().apiKeyFromEnvironment().build(); // SPICY_API_KEY
CreateTaskRequest request = CreateTaskRequest.of(
"MODEL_ID_FROM_CATALOG",
Map.of("prompt", "a lantern in fog"));
TaskQuote quote = client.quote(request); // бесплатно; ничего не резервирует
System.out.println("this will cost at most " + quote.maxCharge());
TaskRecord task = client.run( // createTask + waitForTerminal
request.withQuote(quote), UUID.randomUUID().toString());
task.outputText().ifPresent(System.out::println); // часть эндпоинтов отвечает текстом, а не файлами
for (TaskAsset asset : task.assets()) {
asset.url().ifPresent(System.out::println);
}MODEL_ID_FROM_CATALOG — это заглушка. Настоящий идентификатор берите из каталога, доступного по ключу, — это client.listModels(), то есть GET /api/v1/models, — и никогда по памяти или со старой страницы: модели добавляются, приостанавливаются и снимаются с публикации. Входные данные собирайте по inputSchema самой модели: его возвращает listModels(ModelFilter.all().withIncludeSchema(true)), и каждая модель принимает свой набор полей.
Проверяйте outputText() прежде, чем обходить assets(). Расшифровка речи и похожие эндпоинты кладут весь результат в текстовое поле и не возвращают ни одного файла; клиент, который ищет только файлы, сообщит, что совершенно успешная задача ничего не произвела.
Суммы передаются десятичными строками в долларах США, а не числами с плавающей точкой, потому что двоичная плавающая точка не хранит большинство десятичных сумм точно. Ни кредитов, ни баллов в этом API нет нигде.
Начать со своего файла
Для «изображения в видео», замены персонажа и редактирования изображений ваш материал сначала должен оказаться на нашей стороне. uploadFile проводит все три шага за один вызов и возвращает URI вида spicy://. Именно этот URI и подставляется во входные данные. Публично доступный HTTPS-адрес можно подставить напрямую, загружать его никуда не нужно.
UploadedFile uploaded = client.uploadFile(Path.of("/path/to/reference.png"));
// uploaded.uri() -> "spicy://f/fil_..."
TaskRecord task = client.run(
CreateTaskRequest.of("MODEL_ID_FROM_CATALOG",
Map.of("image", uploaded.uri(), "prompt", "slow dolly in")),
UUID.randomUUID().toString());Все три шага доступны и по отдельности — на случай, когда байты приходят откуда-то ещё:
UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data); // уходит в объектное хранилище, а не в API
UploadedFile file = client.commitUploadedFile(ticket.fileId());Если переписывать средний шаг самостоятельно, две вещи ломаются незаметно. Заголовки из разрешения входят в то, что подписано в предподписанной ссылке, поэтому они передаются без изменений: уберите один, переименуйте один или услужливо добавьте свой — и хранилище ответит 403. И никакой ключ SpicyAPI к этому запросу не прикладывается: сама ссылка и есть разрешение, а ведёт она в хранилище, а не к нам.
Изображения принимаются до 10 MiB, аудио и видео — до 90 MiB. Файл сверх лимита клиент отклоняет локально, ещё до запроса разрешения. См. Медиа.
Хранить результат меньше обычного
Результаты и сохранённый текст запроса хранятся столько, сколько настроено для аккаунта. Отдельная задача может попросить меньше:
TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt(); // что применилось на самом делеСрок только сокращается: сервис берёт наименьшее из вашего значения, настройки аккаунта и максимума платформы, поэтому запросом нельзя выторговать окно длиннее, чем разрешает аккаунт. Duration.ZERO — вполне допустимое значение: результаты исчезают, как только задача достигнет конечного состояния. Слишком большое значение не отклоняется, а обрезается, — потому и применилось то, что лежит в retention() записи, а не то число, которое вы отправили. У retryTask собственного срока хранения нет: повтор наследует срок исходной задачи. См. Хранение и удаление данных.
Баланс, расход и история
Balance balance = client.getBalance(); // по всему аккаунту
UsageReport usage = client.getUsage( // только этот ключ
LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20)); // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));Баланс — это ориентир по бюджету, а не решение о допуске. Часть его может быть подарочными средствами, привязанными к определённым моделям: разбивку даёт balance.fundingSources(), а пустой modelSlugs у подарка означает любую модель, а не ни одной. Точный ответ для одного конкретного запроса даёт quote() — он проходит те же проверки допуска и ничего не резервирует.
Расход нужен для сверки, а не для отслеживания хода работ. Он считает только окончательно списанное, поэтому ещё не закончившийся период всегда выглядит заниженным; запоздавшее списание может изменить уже прошедший день; и у него собственный лимит запросов на весь аккаунт, ограничитель которого при сбое закрывается, так что опрос способен лишить отчётности все ключи аккаунта. Следите за отдельной задачей через waitForTerminal.
listTasks ограничен вызывающим ключом и возвращает только метаданные. Обходите страницы, двигая курсор и не меняя больше ничего, и перед обходом явно задавайте from и to: если их не задать, сервис пересчитывает окно по умолчанию (to равно завтрашнему дню по UTC) на каждый запрос, и обход, пересекающий полночь, задаёт два разных вопроса и склеивает ответы. Никто об этом не сообщит.
Обратные вызовы
Укажите в запросе callBackUrl — и запись о задаче в конечном состоянии придёт к вам сама. Проверьте её, прежде чем ей верить:
CallbackSignatureVerifier verifier =
new CallbackSignatureVerifier(System.getenv("SPICY_WEBHOOK_SECRET"));
byte[] body = request.getInputStream().readAllBytes(); // исходные байты, без повторной сериализации
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* та же запись, что возвращает getTask */ });Три способа здесь ошибиться, и этот класс не допускает ни одного: пересобрать тело запроса до проверки (readValue, а следом writeValueAsString меняют байты, и свёртка уже никогда не совпадёт), сравнивать подписи через equals (метод возвращается на первом же несовпавшем байте, а это измеримая разница во времени) и пропустить проверку свежести (верная подпись остаётся верной навсегда, так что перехваченную доставку можно воспроизвести заново).
Доставки повторяются. Отсеивайте дубликаты по delivery.deliveryId(). См. Вебхуки.
Ошибки
Любая неудача — это SpicyException, тип запечатанный, поэтому switch по иерархии получается исчерпывающим:
| Тип | Что означает |
|---|---|
SpicyApiException | сервис ответил, и ответ был отрицательным |
SpicyProtocolException | что-то ответило, но не нашим конвертом: прокси, страница перехвата в сети, неверный базовый адрес |
SpicyTransportException | пригодного ответа нет вовсе: DNS, TLS, оборванное соединение |
SpicyTimeoutException | истёк локальный лимит времени на запрос. Исход неизвестен, а не «ничего не произошло» |
SpicyWaitTimeoutException | опрос прекращён. Задача всё ещё выполняется и всё ещё будет оплачена; её ID лежит в исключении |
SpicyUploadException | объектное хранилище отклонило предподписанный PUT |
SpicyWebhookException | входящая доставка оказалась неподлинной, несвежей или неправильно оформленной |
Ветвитесь по businessCode() и никогда по тексту сообщения: сообщения — это проза, они переводятся и не входят в контракт. За HTTP 503 стоят три разных бизнес-кода, поэтому один статус не подскажет, что делать:
| Code | Значение | Что делать |
|---|---|---|
40003 | загруженные байты не совпадают с выданным разрешением | начните заново, с нового разрешения; повторный commit тут не поможет |
40004 | ни одна конфигурация не обслуживает такое сочетание параметров | измените параметр, названный в сообщении |
40201 | на балансе недостаточно средств | пополните баланс |
40901 | цена изменилась до того, как задача была создана | получите расчёт заново, сохранив тот же ключ идемпотентности |
503 | зависимость ненадолго недоступна | выдержите паузу по Retry-After |
50301 | сейчас нет ни рабочей конфигурации, ни цены | выдержите паузу или возьмите другую модель |
50302 | синхронная генерация завершилась ошибкой на стороне исполнителя, средства возвращены | повторите под новым ключом идемпотентности; старый воспроизведёт ту же неудачу |
remediation() возвращает ту же подсказку во время выполнения, а describe() укладывает статус, код, идентификатор запроса, сообщение и совет в одну строку для журнала. Обращаясь в поддержку, указывайте requestId(). Все коды перечислены на странице Ошибки и повторы.
Две вещи, которые сэкономят вам деньги
На одну отправку — один ключ идемпотентности. Используйте его при каждой повторной отправке, в том числе после таймаута. Потерянный ответ не доказывает, что задача не создана: новый ключ превращает неизвестный исход во вторую платную задачу. По этой же причине createTask повторяет запрос автоматически только тогда, когда вы передали ключ: без него повтор после таймаута означал бы вторую генерацию и второе списание. См. Идемпотентность.
Успешная задача оплачивается, даже если результат разочаровал. quote() ничего не резервирует и ничего не создаёт, так что, когда цена важна, сначала получите расчёт. createTask удерживает estimatedCost, и этот резерв — потолок: неизрасходованное вернётся, а сверх него потом ничего не спишут.
Чего этот пакет не делает
- Текстовые модели. Они говорят в форматах OpenAI, Anthropic и Gemini — проверенные клиенты на Java для этих форматов уже работают с
https://api.spicyapi.ai/v1по тому же ключу. См. Текст и потоковые ответы. - Клиентские приложения. Никогда не встраивайте этот ключ в приложение для Android, в настольную программу или во что угодно ещё, что пользователь может открыть. Вызывайте со своего сервера.
- Выбор модели и заполнение её полей. Пакет отдаёт вам каталог и схему каждой модели; что именно отправлять, решаете вы.
- Отмена задачи. Принятая задача выполняется до конца.
Исходный код
Артефакт распространяется под лицензией MIT и разрабатывается открыто по адресу github.com/spicyapi-ai/spicy-java. Команда mvn -B verify прогоняет модульные тесты против локальной заглушки, а вместе с ними — проверку расхождений контракта: вшитая в пакет копия сверяется с опубликованной по адресу https://docs.spicyapi.ai/openapi.yaml, после чего заново проверяется каждое перечисление, которое пакет держит в коде. Иначе контракт, в котором появилось новое значение, нигде не вызовет ошибку — клиент просто начнёт отклонять это значение у себя, молча.
Что дальше
Каталог моделей и схемы
Как читать inputSchema модели и выбирать допустимые входные данные
Асинхронные задачи
Состояния задач, опрос и значение каждого поля
Расчёт цены и совместимость
Точная цена запроса ещё до того, как вы за него заплатите
Вебхуки
Формат доставки, расписание повторов и проверка подписи
Ошибки
Все коды ошибок и какие из них стоит повторять
PHP SDK
Тот же процесс на PHP, без зависимостей от библиотек
PHP SDK
Официальный SDK SpicyAPI для PHP (spicyapi/spicyapi): установка через Composer, расчёт стоимости и первая генерация, загрузка локального файла, безопасная проверка вебхуков и разбор всех кодов ошибок.
Агенты и автоматизация
Какой официальный инструмент выбрать, и регламент рабочей эксплуатации для ИИ-агентов, фоновых обработчиков на бэкенде и повторяемых медиаконвейеров.

