Медиафайлы
Прямая загрузка референсных медиафайлов по подписанным URL и временные ссылки для скачивания результатов.
Многие модели принимают изображение, видео или аудиофрагмент как входные данные (референс), а медиамодель возвращает файл (результат). Эта страница описывает оба направления:
- Входные референсы — передайте публичный URL HTTPS, небольшое изображение непосредственно в запросе или загруженный файл. Для загрузки запросите разрешение, отправьте файл методом
PUTпрямо в объектное хранилище, затем подтвердите загрузку. Подтверждение возвращает URIspicy://, который нужно передать во входных данных задачи. - Скачивание результатов — используйте подписанный
output.assets[].urlиз результата. Не каждая модель создаёт файл: эндпоинт с текстовым результатом отвечает вoutput.textи вообще не содержитassets, поэтому скачивать нечего и истекать никакой ссылке не предстоит. См. Формат результата.
Разрешения на загрузку и скачивание результатов находятся в /api/v1/common/; подтверждение загрузки использует /api/v1/files/{fileId}/commit. Загрузка бесплатна; оплачивается только задача, которая использует файл.
Уже размещённые медиафайлы HTTPS
Передайте общедоступный URL HTTPS изображения, видео или аудио прямо в поддерживаемом моделью поле (например, image_url). Загружать файл на платформу или преобразовывать его в spicy:// не требуется. URL должен оставаться доступным без специальных заголовков авторизации во время выполнения задачи. Для крупных локальных файлов используйте описанную ниже загрузку.
URL должен возвращать сам файл. Ссылка на страницу общего доступа (облачный диск, фотоальбом или публикацию в социальной сети) возвращает веб-страницу вместо изображения, и задача завершается с invalid_asset. В таком случае скачайте файл и загрузите его на платформу.
Base64 и локальные файлы
Небольшие изображения можно передавать непосредственно в полях изображений, объявленных схемой, например image_url: "data:image/png;base64,...". Укажите MIME-тип и используйте стандартный Base64. Чистая строка Base64 без обёртки, URL-безопасное кодирование, пробельные символы, SVG и непосредственная передача аудио или видео этим способом не поддерживаются.
- JPEG, PNG, WebP и GIF: не более 1 MiB после декодирования на изображение, 8192 пикселей по каждой стороне и 8388608 пикселей на изображение.
- Не более 16 изображений и 16777216 пикселей суммарно на запрос. Всё JSON-тело должно помещаться в 2 MiB, включая увеличение размера из-за Base64 примерно на треть.
- Ограничения схемы и медиафайлов конкретной модели по-прежнему действуют. Расчёт цены проверяет данные без загрузки и резервирования средств. При создании задачи изображения сохраняются как загрузки, привязанные к аккаунту, и хранятся один день, как остальные загрузки. Входные данные задачи содержат URI файлов, а не копию байтов Base64.
Для более крупных файлов SDK выполняет получение разрешения, PUT и подтверждение:
import { SpicyClient } from '@spicyapi/sdk';
const client = new SpicyClient();
const file = await client.uploadFile('./reference.png');
// With existing Base64, use uploadBase64(dataUri),
// or uploadBase64(rawBase64, { contentType: 'image/png' }).
console.log(file.uri);uploadBase64 — вспомогательный метод загрузки файла, использующий тот же API загрузки и ограничения ниже. Лимит 1 MiB для небольших изображений непосредственно в запросе на него не распространяется; отдельные модели всё равно должны поддерживать выбранный тип медиафайла. Для уже общедоступных медиафайлов HTTPS метод загрузки не нужен.
1. Прямая загрузка референса
Загрузка локального референсного файла, ещё не размещённого по публичному URL HTTPS, состоит из четырёх шагов. Команды curl ниже нужно выполнять последовательно в одном окне терминала (macOS или Linux с установленным jq): каждый шаг сохраняет нужные следующему шагу данные в переменную оболочки. На Windows используйте SDK или пример Python в конце раздела.
Запросите разрешение на загрузку
POST /api/v1/common/upload-urlСвойство
Тип
Поля имени файла намеренно нет
Ключи объектов всегда генерируются сервером. Если бы путь выбирал вызывающий клиент, любой мог бы получить URL загрузки для чужого объекта и перезаписать его.
Ограничения загрузки
contentType | Расширение | Максимальный размер | Максимальная длительность |
|---|---|---|---|
image/jpeg | .jpg | 10 MiB | — |
image/png | .png | 10 MiB | — |
image/webp | .webp | 10 MiB | — |
image/gif | .gif | 10 MiB | — |
audio/wav | .wav | 90 MiB | 600 секунд |
audio/mpeg | .mp3 | 90 MiB | 600 секунд |
video/webm | .webm | 90 MiB | 600 секунд |
video/mp4 | .mp4 | 90 MiB | 600 секунд |
video/quicktime | .mov | 90 MiB | 600 секунд |
application/octet-stream | .safetensors (веса LoRA) | 2 GiB | — |
.mov (формат записи камеры iPhone по умолчанию) принимается и преобразуется в MP4 при загрузке, поэтому модель всегда получает MP4. Его принимают веб-загрузчик и API загрузки; CLI, SDK и MCP-сервер определяют тип по расширению и пока не включают его в список, поэтому там явно передавайте contentType: video/quicktime.
Веса LoRA загружаются как application/octet-stream: файл .safetensors до 2 GiB (веб-загрузчик принимает до 90 MiB). Тикет действует два часа вместо 20 минут, а commit проверяет, что файл — полный safetensors. Подставьте возвращённый URI в loras[].path; файл весов нельзя использовать ни в каком другом поле. Файл, для скачивания которого нужен вход, например из закрытого репозитория Hugging Face, можно импортировать по ссылке: см. Веса адаптеров.
У модели могут быть более строгие ограничения: соблюдайте x-ui.max_size_mb её схемы, а определяющим максимумом для этой загрузки считайте maxBytes разрешения.
FILE=reference.png # the file to upload
CONTENT_TYPE=image/png # must match the file type in the table above
BYTES=$(wc -c < "$FILE" | tr -d ' ')
TICKET=$(curl -s -X POST https://api.spicyapi.ai/api/v1/common/upload-url \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg type "$CONTENT_TYPE" --argjson bytes "$BYTES" '{contentType: $type, bytes: $bytes}')")
echo "$TICKET" | jq .
UPLOAD_URL=$(echo "$TICKET" | jq -r .data.uploadUrl)
FILE_ID=$(echo "$TICKET" | jq -r .data.fileId)Ответ должен выглядеть примерно так; UPLOAD_URL и FILE_ID теперь содержат его uploadUrl и fileId:
{
"code": 200,
"msg": "success",
"data": {
"fileId": "fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"key": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"uploadUrl": "https://<account-id>.r2.cloudflarestorage.com/spicy-production-inputs/tmp/…?X-Amz-Signature=…",
"method": "PUT",
"headers": {
"Content-Type": "image/png",
"Content-Length": "402118"
},
"expiresAt": "2026-08-28T09:32:11Z",
"maxBytes": 10485760
}
}До expiresAt остаётся около 20 минут. И PUT, и подтверждение должны завершиться до этого времени; позже подтверждение возвращает 409, и потребуется новое разрешение. Запрашивайте его непосредственно перед загрузкой, а не заранее.
Отправьте файл методом PUT
Отправьте байты методом PUT по uploadUrl. Небраузерные клиенты, например curl, Node.js и Python, должны передать все заголовки из headers в точности как получены. Браузерные клиенты должны обрабатывать Content-Length так, как описано ниже.
Без одного из заголовков подпись не совпадёт
В подпись входят и Content-Type, и Content-Length. Изменение хотя бы байта в любом из них приводит к отказу объектного хранилища: 403 приходит от слоя хранения, а не от нас, что может затруднить понимание ошибки.
Браузеры считают Content-Length запрещённым заголовком запроса, поэтому код приложения не может задать его ни через fetch, ни через XMLHttpRequest. В браузере отправляйте File или Blob, чей размер точно совпадает с bytes разрешения, и позвольте браузеру сформировать Content-Length из тела. Самостоятельно задавайте только остальные заголовки разрешения. Ответ всё равно перечисляет Content-Length для небраузерных клиентов и для проверки размера файла браузерным кодом перед отправкой.
Не передавайте заголовок Authorization в этом запросе. Учётными данными служит сам подписанный URL; дополнительный заголовок авторизации может вызвать отказ у некоторых реализаций хранилищ.
# Prints only the HTTP status. Expect 200.
curl -s -o /dev/null -w "%{http_code}\n" -X PUT "$UPLOAD_URL" \
-H "Content-Type: $CONTENT_TYPE" \
-H "Content-Length: $BYTES" \
--data-binary @"$FILE"const uploadHeaders = new Headers(ticket.data.headers);
uploadHeaders.delete('Content-Length'); // the browser derives it from File/Blob
if (file.size !== Number(ticket.data.headers['Content-Length'])) {
throw new Error('file size does not match the upload ticket');
}
await fetch(ticket.data.uploadUrl, {
method: ticket.data.method,
headers: uploadHeaders,
body: file, // File or Blob; do not turn it into text, Base64 or FormData
});Подтвердите загрузку
Успешный PUT означает лишь запись временного объекта. Продолжайте с fileId из первого шага:
curl -s -X POST "https://api.spicyapi.ai/api/v1/files/$FILE_ID/commit" \
-H "Authorization: Bearer $SPICY_API_KEY" | jq .Ожидайте "status": "ready" и uri, начинающийся с spicy://:
{
"code": 200,
"msg": "success",
"data": {
"fileId": "fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"status": "ready",
"bytes": 402118,
"contentType": "image/png",
"sha256": "a91f4c2e5b7d16809e3f2a4c8d0b61e7c4a890e5d2f71b3c609ae8427d15f0c6",
"uri": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"expiresAt": "2026-08-29T09:12:11Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}Подтверждение сравнивает сохранённые размер и медиатип с разрешением, проверяет сигнатуру медиафайла, вычисляет SHA-256 и копирует байты в приватный объект, который старое разрешение уже не может перезаписать. Для аудио и видео оно также может вернуть durationSeconds в виде десятичной строки и числовые width и height. Повторное успешное подтверждение идемпотентно.
| Ответ | Значение | Как исправить |
|---|---|---|
400, code: 40003 | Размер, тип или формат сохранённого файла не соответствует заявленному | Запросите новое разрешение с правильным contentType и точным bytes, затем загрузите снова |
400 | Файла ещё нет: PUT не завершился или не удался | Проверьте, что PUT вернул 200, затем повторите подтверждение |
404 | На этом аккаунте нет такого fileId | Используйте fileId из своего разрешения |
409 | Разрешение или загруженный файл просрочены либо предыдущее подтверждение отклонило этот файл | Начните снова с шага 1 |
409 / 429 | Проверка файла уже выполняется либо проверяющий сервис занят | Подождите секунду и повторите подтверждение |
После PUT обязательно подтвердите загрузку
Неподтверждённый файл нельзя использовать для создания задачи. Не используйте временный путь хранения из разрешения и не используйте совместимое поле key до успешного подтверждения.
Передайте URI spicy:// во входных данных
В качестве референсного поля используйте только data.uri из ответа подтверждения — не uploadUrl, не ключ временного объекта и не отдельный fileId.
Какое поле принимает файл, зависит от модели. В её inputSchema такое поле имеет x-ui.widget, равный upload (один файл) или multi-upload (список — поместите URI в массив). x-ui.accept перечисляет типы файлов, а x-ui.max_size_mb — размер, принимаемый моделью. См. Каталог моделей и схемы.
MODEL_ID_FROM_CATALOG и остальные входные поля ниже — структурные заполнители. Выберите точное значение model и соответствующие схеме входные данные из одной записи актуального авторизованного каталога. Тип задачи определяется самим model, поэтому input не содержит поля эндпоинта; он принимает только поля, перечисленные в inputSchema этой модели, а одно лишнее поле возвращает 400:
{
"model": "MODEL_ID_FROM_CATALOG",
"input": {
"prompt": "slow dolly in, rain on the window",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"duration_seconds": 5
}
}Платформа разрешает подтверждённый URI spicy:// во время выполнения задачи. Так загруженные файлы остаются пригодными для использования, пока задача стоит в очереди, без необходимости управлять временными URL скачивания. Используйте URI из подтверждения без изменений.
URI, который ещё не готов, просрочен или принадлежит другому аккаунту, отклоняется при создании с 400.
Полный пример
import { readFile } from 'node:fs/promises';
const BASE = 'https://api.spicyapi.ai/api/v1';
const AUTH = { Authorization: `Bearer ${process.env.SPICY_API_KEY}` };
async function uploadReference(path, contentType) {
const file = await readFile(path);
// 1. Get a ticket
const ticketRes = await fetch(`${BASE}/common/upload-url`, {
method: 'POST',
headers: { ...AUTH, 'Content-Type': 'application/json' },
body: JSON.stringify({ contentType, bytes: file.byteLength }),
});
const ticket = await ticketRes.json();
if (ticket.code !== 200) throw new Error(`${ticket.code} ${ticket.msg}`);
// 2. Upload directly. No Authorization here; pass the headers through as-is
const put = await fetch(ticket.data.uploadUrl, {
method: ticket.data.method,
headers: ticket.data.headers,
body: file,
});
if (!put.ok) throw new Error(`upload failed: ${put.status}`);
// 3. Commit so the server verifies and freezes the uploaded bytes
const commitRes = await fetch(
`${BASE}/files/${encodeURIComponent(ticket.data.fileId)}/commit`,
{ method: 'POST', headers: AUTH },
);
const committed = await commitRes.json();
if (committed.code !== 200) throw new Error(`${committed.code} ${committed.msg}`);
// 4. Task input uses only the spicy:// URI returned by commit
return committed.data.uri;
}
console.log(await uploadReference('./reference.png', 'image/png'));import os, requests
BASE = "https://api.spicyapi.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"}
def upload_reference(path: str, content_type: str) -> str:
with open(path, "rb") as f:
data = f.read()
# 1. Get a ticket
ticket = requests.post(
f"{BASE}/common/upload-url",
headers={**AUTH, "Content-Type": "application/json"},
json={"contentType": content_type, "bytes": len(data)},
).json()
if ticket["code"] != 200:
raise RuntimeError(f'{ticket["code"]} {ticket["msg"]}')
# 2. Upload directly. No Authorization; pass the headers through as-is
put = requests.put(ticket["data"]["uploadUrl"], headers=ticket["data"]["headers"], data=data)
put.raise_for_status()
# 3. Commit so the server verifies and freezes the uploaded bytes
committed = requests.post(
f'{BASE}/files/{ticket["data"]["fileId"]}/commit',
headers=AUTH,
).json()
if committed["code"] != 200:
raise RuntimeError(f'{committed["code"]} {committed["msg"]}')
# 4. Task input uses only the spicy:// URI returned by commit
return committed["data"]["uri"]
print(upload_reference("reference.png", "image/png"))Дополнительное ограничение частоты
Сверх общего лимита открытого API у upload-url есть собственный защитный порог 1000 запросов за 10 секунд против злоупотреблений и неконтролируемой выдачи разрешений на запись в объектное хранилище.
Затраты здесь несимметричны: подписание разрешения для нас — чисто локальное вычисление, но каждое разрешение позволяет запись, которая уже не проходит через нас. Человек не может выбирать файлы с такой скоростью, а пакетный скрипт, загружающий несколько десятков референсов, всё ещё легко укладывается в лимит.
2. Скачивание результатов
Готовые результаты уже содержат output.assets[].url и expiresAt при опросе и в обратных вызовах v2. Скачивайте по этому URL напрямую без API-ключа; описанный ниже download-url необязателен. Обычно ссылки действуют 20 минут и никогда не выходят за срок хранения задачи — около 14 дней максимум или меньше, если вы сократили срок.
Чтобы сохранить результат, запишите его в собственное хранилище, пока ссылка действительна. Например:
curl -L -o result.png "PASTE_THE_OUTPUT_URL_HERE"Не добавляйте заголовок Authorization к этому запросу. Если ссылка просрочена (хранилище отвечает 403), снова запросите задачу через recordInfo, чтобы получить новую.
Необязательное обновление ссылки для скачивания
POST /api/v1/common/download-urlСвойство
Тип
curl -X POST https://api.spicyapi.ai/api/v1/common/download-url \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png"
}'{
"code": 200,
"msg": "success",
"data": {
"key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png",
"url": "https://<account-id>.r2.cloudflarestorage.com/spicy-production-results/tasks/…?X-Amz-Signature=…",
"expiresAt": "2026-08-28T09:32:11Z"
}
}Если у задачи несколько файлов результата, вызывайте эндпоинт отдельно для каждого key. Как и recordInfo, он работает только с API-ключом, создавшим задачу.
20 минут; после выдачи ссылку нельзя отозвать
Подписанный URL — строка, сама подтверждающая доступ: объектное хранилище не будет снова проверять, кто её предъявляет. Поэтому:
- Не храните её в базе данных и не помещайте в долгоживущий кеш, который выдаёте браузерам. Подписывайте ссылку по мере необходимости — подписание выполняется локально и практически бесплатно.
- Скачивайте всё, что хотите сохранить. У нас файлы результатов хранятся не более примерно 14 дней, а при сокращении срока — меньше; см. Хранение и уничтожение данных.
Если ссылки ещё нет или уже нет
| Ситуация | Что вы видите | Что делать |
|---|---|---|
| Файл ещё копируется в наше хранилище | Файл имеет pending: true в recordInfo; download-url без key возвращает 409 (с явно указанным key — 404 до окончания копирования) | Подождите несколько секунд и запросите снова |
| Копирование окончательно не удалось | Файл имеет unavailable: true, и у него нет key | Ожидание не поможет — выполните генерацию заново |
| Задача ещё не завершилась успешно | 404, «This task has no downloadable output» | Дождитесь state: "succeeded" |
| Срок хранения истёк или вы уничтожили результат | HTTP 410 с code: 404, «This output has expired or was deleted» | Данные удалены окончательно — выполните генерацию заново |
Чужая задача и key, не принадлежащий задаче, оба возвращают 404. Мы не различаем несуществующие и чужие задачи, поскольку по этому различию можно было бы перебирать ID задач других пользователей.
Библиотека ассетов
Некоторые файлы вы используете снова и снова: портрет персонажа, референс стиля, фото товара, образец голоса. Библиотека ассетов хранит их, пока вы сами их не удалите, и выдаёт каждому постоянную ссылку spicy://a/<id>. Подставьте её в любое медиаполе входных данных любой задачи, как и загрузку spicy://f/, только без истечения через сутки.
- Приватно в пределах рабочего пространства. Ссылка
spicy://a/работает только в запросах с ключами того же рабочего пространства. Личный ключ открывает вашу личную библиотеку, ключ, созданный в командном пространстве, открывает общую библиотеку этой команды; они никогда не связаны (см. ниже). - Хранится, пока не будет удалено. Результаты генерации очищаются примерно через 14 дней, загрузки через 1 день; сохранённые ассеты остаются, пока вы не удалите их. Личная библиотека удаляется при закрытии аккаунта, командная вместе с командой.
- Это не файлообменник. API отдаёт ссылку
spicy://a/<id>иpreviewUrl, подписанный только для вас и действующий не дольше 20 минут. Публичной или постоянной ссылки нет, и из ссылки её не получить. Сохраняйтеuri, а неpreviewUrl.
Личная и командная библиотеки
Библиотеку определяет ключ. Ключ, созданный в вашем личном пространстве, открывает вашу личную библиотеку. Ключ, созданный в командном пространстве (любым участником), открывает общую библиотеку этой команды. Командный ключ никогда не открывает чью-либо личную библиотеку, личный ключ не открывает командную, а ссылка spicy://a/ из другой библиотеки отклоняется так, как будто её не существует.
Что командный ключ может менять, зависит от текущей роли участника, создавшего его:
| Роль | Сохранение | Переименование или удаление | Список, чтение и использование |
|---|---|---|---|
owner, admin | Да | Любой ассет | Да |
developer | Да | Только свои | Да |
billing, readonly | Нет | Нет | Да |
Если этот участник покинул команду, ключ получает 403 с data.reason равным asset_workspace_forbidden. В ассетах не указано, кто их сохранил; видно лишь, можно ли вам изменить конкретный.
Сохранение файла
POST /api/v1/assets копирует файл в вашу библиотеку. Заголовок Idempotency-Key обязателен. Выберите один из трёх источников:
source | Поля | Примечания |
|---|---|---|
task_output | taskId и key или index | Одна из ваших успешных задач. index отсчитывается с 0 в порядке output.assets в recordInfo. |
upload | fileId | Подтверждённая загрузка: fil_… или spicy://f/fil_…. |
url | url | Публичный адрес http или https на порту 80 или 443, без имени пользователя и пароля. Файл скачивается один раз, а тип определяется по самому файлу; принимаются только изображения, видео и аудио. |
name необязателен (до 80 символов; по умолчанию Saved asset). Повторное сохранение того же источника возвращает уже существующий ассет и не занимает дополнительного места. Размер, разрешение и длительность мы измеряем сами.
curl -X POST https://api.spicyapi.ai/api/v1/assets \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"source": "task_output",
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"index": 0,
"name": "Hero portrait"
}'{
"code": 200,
"msg": "success",
"data": {
"id": "ast_01k3m9c4w8r2n6t5v1x7y0z3ab",
"uri": "spicy://a/ast_01k3m9c4w8r2n6t5v1x7y0z3ab",
"name": "Hero portrait",
"kind": "image",
"contentType": "image/png",
"bytes": 1843201,
"width": 1536,
"height": 1024,
"source": "task_output",
"sourceId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"createdAt": "2026-10-11T08:20:11Z",
"updatedAt": "2026-10-11T08:20:11Z",
"previewUrl": "https://…",
"previewUrlExpiresAt": "2026-10-11T08:40:11Z"
}
}Использование в задаче
Подставьте data.uri в любое файловое поле, у которого в inputSchema модели x-ui.widget равен upload или multi-upload:
{
"model": "MODEL_ID_FROM_CATALOG",
"input": {
"prompt": "slow dolly in, rain on the window",
"image_url": "spicy://a/ast_01k3m9c4w8r2n6t5v1x7y0z3ab",
"duration_seconds": 5
}
}Удалённая ссылка или ссылка на библиотеку, которую этот ключ не открывает (другого аккаунта или другого пространства), отклоняется при создании задачи точно так же, как неизвестный идентификатор.
Список, переименование и удаление
GET /api/v1/assets возвращает ваши ассеты в поле items, а также nextCursor, которого нет на последней странице. Фильтры: kind (image, video или audio), source (через запятую: task_output,upload,url), q (поиск по имени без учёта регистра), favorite (true только для избранного, false для всего остального) и collectionId (одна папка, см. ниже). Порядок задаёт sort: recent (по умолчанию, от новых к старым), oldest, name (от A до Z без учёта регистра) или size (сначала самые большие). limit от 1 до 100, по умолчанию 100. Чтобы получить следующую страницу, передайте предыдущий nextCursor как cursor; курсор работает только с тем sort, из которого он получен, а курсор от другой сортировки отклоняется с 400.
curl "https://api.spicyapi.ai/api/v1/assets?kind=image&q=portrait&sort=name&limit=20" \
-H "Authorization: Bearer $SPICY_API_KEY"
curl -X PATCH https://api.spicyapi.ai/api/v1/assets/ast_01k3m9c4w8r2n6t5v1x7y0z3ab \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Hero portrait v2"}'
curl -X DELETE https://api.spicyapi.ai/api/v1/assets/ast_01k3m9c4w8r2n6t5v1x7y0z3ab \
-H "Authorization: Bearer $SPICY_API_KEY"GET /api/v1/assets/{id} возвращает один ассет с новым previewUrl. PATCH меняет имя, флаг favorite или и то и другое (отправляйте только то, что нужно изменить), uri остаётся прежним. После DELETE ассет сразу исчезает, а его uri перестаёт работать в новых запросах. Оба вызова безопасно повторять, Idempotency-Key не нужен. Несуществующий ассет и ассет другого аккаунта возвращают один и тот же 404.
Избранное, папки и модель-источник
У каждого ассета есть флаг favorite. Его ставит PATCH с телом {"favorite": true}. В командной библиотеке отметка принадлежит библиотеке, поэтому все видят одно и то же избранное. Папки (в API они называются collections) объединяют ассеты без копирования, и один ассет может лежать в нескольких папках. Папки создаются и редактируются на странице Assets веб-приложения; через API их нельзя ни создать, ни изменить, но параметр collectionId в GET /api/v1/assets выводит ассеты одной папки (404, если папки нет в библиотеке, до которой дотягивается ваш ключ).
У каждого ассета также есть model (публичное имя модели, которая его создала) и modelName (её отображаемое название). Для загрузок и ссылок оба поля пустые строки.
Персонажи
Персонаж — это имя и упорядоченный набор ассетов-изображений из той же библиотеки: до 100 изображений и необязательная заметка до 280 символов. Изображения — это сами ассеты, ничего не копируется. Если удалить персонажа, его изображения останутся, а удалённое изображение пропадает из всех персонажей. Персонажей создают и редактируют на странице Assets веб-приложения или в Studio. Отдельного места они не занимают: учитывается только ваш тариф хранилища.
Через API персонажей можно только читать:
GET /api/v1/assets/entitiesвозвращает список от новых к старым. СassetIdостаются только персонажи, в которых есть этот ассет;limitот 1 до 100 (по умолчанию 50), аcursorпродолжает с предыдущегоnextCursor.GET /api/v1/assets/entities/{id}возвращает одного персонажа. Несуществующий персонаж, персонаж другого аккаунта и персонаж из библиотеки, недоступной вашему ключу, дают один и тот же404.
У каждого персонажа есть id, kind (всегда character), name, note, cover (обложка в виде ассета или null, пока изображений нет), assets (изображения по порядку, у каждого uri и приватный previewUrl), count, createdAt и updatedAt. Библиотеку определяет ключ, как и для ассетов, а чтение персонажей делит с чтением ассетов лимит 60 запросов в минуту.
Чтобы использовать персонажа, передайте его изображения в том же порядке как ссылки spicy://a/ в поле референсных изображений модели, а в промпте ссылайтесь на них так, как описано на странице этой модели. Раньше персонажи назывались персонами и хранились в отдельной библиотеке, теперь они часть библиотеки ассетов. Прежние ссылки spicy://p/ продолжают работать, но только в вашей личной библиотеке.
Когда вы добавляете персонажу изображение, которое загрузили сами (а не результат генерации), вы подтверждаете, что у вас есть права на него, а если на нём реальный человек, то и его согласие.
Тарифы хранилища
Объём хранилища открывается автоматически по мере накопленных пополнений и только растёт; изменение таблицы никогда не удаляет уже сохранённые ассеты. Для вашего аккаунта решающим источником является GET /api/v1/assets/usage: ваш тариф, занятое место, лимиты, следующий тариф и сумма, которую осталось пополнить до него, а также сколько ассетов вы создали сегодня. В таблице указаны текущие значения по умолчанию.
Хранилище принадлежит владельцу: тариф определяют его накопленные пополнения, а личная библиотека владельца и все принадлежащие ему командные библиотеки делят один общий лимит. usage охватывает весь этот лимит и содержит объект scope (workspaceId, personal, role, canWrite, canTopUp), который показывает, какую библиотеку открывает ваш ключ и что в ней разрешено. Если canTopUp равен false (командный ключ создан не владельцем команды), lifetimeTopupUsd и topupToNextTierUsd не возвращаются, а при заполненной библиотеке вместо суммы предлагается обратиться к владельцу команды.
| Тариф | Накопленные пополнения | Хранилище | Количество | На файл |
|---|---|---|---|---|
| Free | нет | нет: сохранять новое нельзя | 0 | не применяется |
| Plus | от $10 | 500 МБ | 200 | 100 МБ |
| Pro | от $100 | 1 ГБ | 400 | 100 МБ |
| Studio | от $250 | 2 ГБ | 800 | 100 МБ |
| Max | от $1 000 | 10 ГБ | 4 000 | 100 МБ |
Без пополнений сохранять новое нельзя, но уже сохранённые ассеты остаются доступными. Лимит 100 МБ на файл действует для результатов задач и ссылок. Файлы, пришедшие через загрузку, по-прежнему ограничены лимитами самой загрузки (изображения 10 МиБ, аудио и видео 90 МиБ).
Лимиты запросов и ошибки
- Чтение (список, получение, usage): 60 запросов в минуту. Сохранение: 30 запросов в минуту и 200 новых ассетов за сутки по UTC. Удаление ассета не возвращает дневной лимит. При превышении лимита приходит
429с заголовкомRetry-After. - Если библиотека заполнена, хранилище ещё не открыто или файл больше лимита на файл, возвращается
400сinvalid_request. В сообщении указаны ваш текущий тариф и сумма, которую нужно пополнить, чтобы открыть следующий. 403сdata.reason, равнымasset_workspace_forbidden, если ключ не открывает эту библиотеку, илиasset_permission_denied, если ваша роль в команде не допускает изменение.503, когда хранилище платформы временно заполнено; повторите запрос позже.
Ошибка на этих эндпоинтах может содержать машиночитаемый data.reason: asset_library_locked, asset_library_full, asset_file_too_large, asset_daily_limit, asset_storage_full, asset_permission_denied, asset_workspace_forbidden или asset_entity_name_taken. code, HTTP-статус и errorCode не меняются, есть причина или нет, а сама причина встречается только на эндпоинтах библиотеки ассетов; OpenAI-совместимый и другие слои совместимости сохраняют свои форматы ошибок. Неизвестное значение обрабатывайте как отсутствие причины. См. Коды причин библиотеки ассетов.
В веб-приложении есть и переключатель автосохранения в библиотеку. Он действует только для генераций, запущенных в веб-приложении; генерации, запущенные через API, автоматически не сохраняются, поэтому для нужных вызовите POST /api/v1/assets.
Хранение загруженных файлов
URI загруженного референса хранится 1 день после успешного подтверждения. Это максимум платформы; его можно сократить или заменить уничтожением после завершения, как описано в разделе Хранение и уничтожение данных. Использовать URI можно только до expiresAt, возвращённого подтверждением. После истечения срока URI становится непригодным, в том числе для повтора задачи, которая на него ссылалась. Удаление по правилам жизненного цикла объектного хранилища затем может задержаться до 24 часов, но этот период очистки не продлевает возможность использования URI. Если референс потребуется снова, сохраните собственную копию и загрузите её заново.

