spicyapiДокументация
Разработка с API
Основное содержимое

Медиафайлы

Прямая загрузка референсных медиафайлов по подписанным URL и временные ссылки для скачивания результатов.

Многие модели принимают изображение, видео или аудиофрагмент как входные данные (референс), а медиамодель возвращает файл (результат). Эта страница описывает оба направления:

  • Входные референсы — передайте публичный URL HTTPS, небольшое изображение непосредственно в запросе или загруженный файл. Для загрузки запросите разрешение, отправьте файл методом PUT прямо в объектное хранилище, затем подтвердите загрузку. Подтверждение возвращает URI spicy://, который нужно передать во входных данных задачи.
  • Скачивание результатов — используйте подписанный 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.jpg10 MiB—
image/png.png10 MiB—
image/webp.webp10 MiB—
image/gif.gif10 MiB—
audio/wav.wav90 MiB600 секунд
audio/mpeg.mp390 MiB600 секунд
video/webm.webm90 MiB600 секунд
video/mp4.mp490 MiB600 секунд
video/quicktime.mov90 MiB600 секунд
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 разрешения.

curl
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; дополнительный заголовок авторизации может вызвать отказ у некоторых реализаций хранилищ.

curl
# 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"
Browser
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
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.

Полный пример

JavaScript
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'));
Python
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
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-ключом, создавшим задачу.

Подписанный 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_outputtaskId и key или indexОдна из ваших успешных задач. index отсчитывается с 0 в порядке output.assets в recordInfo.
uploadfileIdПодтверждённая загрузка: fil_… или spicy://f/fil_….
urlurlПубличный адрес http или https на порту 80 или 443, без имени пользователя и пароля. Файл скачивается один раз, а тип определяется по самому файлу; принимаются только изображения, видео и аудио.

name необязателен (до 80 символов; по умолчанию Saved asset). Повторное сохранение того же источника возвращает уже существующий ассет и не занимает дополнительного места. Размер, разрешение и длительность мы измеряем сами.

curl
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
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от $10500 МБ200100 МБ
Proот $1001 ГБ400100 МБ
Studioот $2502 ГБ800100 МБ
Maxот $1 00010 ГБ4 000100 МБ

Без пополнений сохранять новое нельзя, но уже сохранённые ассеты остаются доступными. Лимит 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. Если референс потребуется снова, сохраните собственную копию и загрузите её заново.

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