Media
Bezpośrednie przesyłanie mediów referencyjnych przez podpisane adresy oraz krótkotrwałe linki do pobierania plików wynikowych.
Wiele modeli przyjmuje obraz, wideo lub klip dźwiękowy jako dane wejściowe (materiał referencyjny), a model multimedialny zwraca plik (plik wynikowy). Ta strona opisuje oba kierunki:
- Dane referencyjne — podaj publiczny adres HTTPS, mały obraz zapisany bezpośrednio w żądaniu lub przesłany plik. Aby przesłać plik, zamów bilet przesyłania, wyślij plik przez
PUTbezpośrednio do magazynu obiektowego, a następnie go zatwierdź; zatwierdzenie zwraca URIspicy://, które umieszczasz w danych wejściowych zadania. - Pobieranie plików wynikowych — pobierz podpisany
output.assets[].urlzwrócony z wynikiem. Nie każdy model tworzy plik: endpoint zwracający tekst odpowiada woutput.texti nie ma żadnychassets, więc nie ma nic do pobrania ani linku, który mógłby wygasnąć. Zobacz Struktura wyniku.
Bilety przesyłania i pobieranie wyników znajdują się w /api/v1/common/; zatwierdzanie przesłanych plików używa /api/v1/files/{fileId}/commit. Przesyłanie jest bezpłatne; płacisz tylko za zadanie, które wykorzystuje plik.
Istniejące media pod adresem HTTPS
Podaj publicznie dostępny adres HTTPS obrazu, wideo lub audio bezpośrednio w polu obsługiwanym przez model (na przykład image_url). Nie musisz przesyłać pliku na platformę ani konwertować go na spicy://. Adres musi pozostawać dostępny bez własnych nagłówków autoryzacji przez cały czas wykonywania zadania. Większe pliki lokalne przesyła się zgodnie z procesem opisanym poniżej.
Adres musi zwracać sam plik. Link do strony udostępniania (dysku w chmurze, albumu zdjęć, posta społecznościowego) zwraca stronę internetową zamiast obrazu, a zadanie kończy się błędem invalid_asset. W takim przypadku pobierz plik i prześlij go zamiast używać linku.
Base64 i pliki lokalne
Małe obrazy można przekazywać bezpośrednio w polach obrazu zadeklarowanych w schemacie, na przykład image_url: "data:image/png;base64,...". Uwzględnij typ MIME i używaj standardowego Base64. Ta ścieżka nie obsługuje samego Base64 bez prefiksu, kodowania URL-safe, białych znaków, SVG ani audio lub wideo osadzonego w żądaniu.
- JPEG, PNG, WebP i GIF: najwyżej 1 MiB po zdekodowaniu na obraz, 8192 piksele na bok i 8 388 608 pikseli na obraz.
- Najwyżej 16 obrazów i łącznie 16 777 216 pikseli na żądanie. Cała treść JSON musi mieścić się w 2 MiB, wliczając około jedną trzecią dodatkowego rozmiaru wynikającą z Base64.
- Nadal obowiązują schemat i limity mediów konkretnego modelu. Wycena waliduje dane bez przesyłania plików i rezerwowania środków. Tworzenie zadania zapisuje obrazy jako przesłane pliki przypisane do konta, dostępne przez jeden dzień jak każdy inny przesłany plik; dane zadania zawierają URI plików zamiast powtarzać bajty Base64.
Dla większych plików SDK obsługuje bilet, PUT i zatwierdzenie:
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 to funkcja pomocnicza przesyłania plików, korzystająca z tego samego API przesyłania i opisanych niżej limitów. Limit 1 MiB dla małych obrazów osadzonych w żądaniu nie dotyczy tej funkcji; konkretny model nadal musi obsługiwać wybrany typ mediów. Istniejące publiczne media HTTPS nie wymagają funkcji przesyłania.
1. Bezpośrednie przesyłanie materiału referencyjnego
Dla lokalnego pliku referencyjnego, który nie jest już hostowany pod publicznym adresem HTTPS, przesyłanie obejmuje cztery kroki. Poniższe polecenia curl należy wykonywać kolejno w tym samym oknie terminala (macOS lub Linux, z zainstalowanym jq): każdy krok zapisuje w zmiennej powłoki dane potrzebne w następnym. W Windows użyj SDK lub przykładu Python na końcu tej sekcji.
Zamów bilet
POST /api/v1/common/upload-urlWłaściwość
Typ
Brak pola nazwy pliku jest celowy
Klucze obiektów zawsze generuje serwer. Gdyby wywołujący mógł wybrać ścieżkę, każdy mógłby uzyskać adres przesyłania wskazujący cudzy obiekt i go nadpisać.
Limity przesyłania
contentType | Rozszerzenie | Największy plik | Najdłuższy czas trwania |
|---|---|---|---|
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 sekund |
audio/mpeg | .mp3 | 90 MiB | 600 sekund |
video/webm | .webm | 90 MiB | 600 sekund |
video/mp4 | .mp4 | 90 MiB | 600 sekund |
video/quicktime | .mov | 90 MiB | 600 sekund |
application/octet-stream | .safetensors (wagi LoRA) | 2 GiB | — |
.mov (domyślny format nagrań aparatu iPhone) jest akceptowany i konwertowany na MP4 podczas przyjmowania pliku, więc model zawsze otrzymuje MP4. Obsługują go internetowy formularz przesyłania i API przesyłania; CLI, SDK i serwer MCP rozpoznają typ na podstawie rozszerzenia i jeszcze go nie wymieniają, więc przekaż tam jawnie contentType: video/quicktime.
Wagi LoRA przesyła się jako application/octet-stream: plik .safetensors do 2 GiB (internetowy formularz przyjmuje do 90 MiB). Bilet jest ważny dwie godziny zamiast 20 minut, a commit sprawdza, czy plik jest kompletnym safetensors. Wstaw zwrócone URI do loras[].path; pliku wag nie można użyć w żadnym innym polu. Plik, który do pobrania wymaga logowania, na przykład z prywatnego repozytorium Hugging Face, można zaimportować z jego linku: zobacz Wagi adapterów.
Model może przyjmować mniej: przestrzegaj x-ui.max_size_mb jego schematu, a maxBytes biletu traktuj jako wiążącą górną granicę tego przesłania.
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)Powinna pojawić się odpowiedź podobna do poniższej, a UPLOAD_URL i FILE_ID zawierają teraz jej uploadUrl i 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 przypada za około 20 minut. Zarówno PUT, jak i zatwierdzenie muszą zakończyć się przed tym terminem; później zatwierdzenie zwraca 409 i potrzebny jest nowy bilet. Zamawiaj bilet tuż przed przesyłaniem, nie z wyprzedzeniem.
Wyślij plik przez PUT
Wyślij bajty przez PUT do uploadUrl. Klienty poza przeglądarką, takie jak curl, Node.js i Python, powinny przesłać każdy nagłówek z headers dokładnie w podanej postaci. Klienty przeglądarkowe muszą obsłużyć Content-Length zgodnie z opisem poniżej.
Pominięcie jednego nagłówka unieważnia podpis
Zarówno Content-Type, jak i Content-Length są podpisane. Zmiana choćby bajtu w jednym z nich powoduje odrzucenie przesłania przez magazyn obiektowy — z kodem 403 z warstwy magazynu, a nie od nas, co może wydawać się niezrozumiałe.
Przeglądarki uznają Content-Length za zabroniony nagłówek żądania, więc kod aplikacji nie może ustawić go przez fetch ani XMLHttpRequest. W przeglądarce wyślij File lub Blob o rozmiarze dokładnie zgodnym z bytes biletu i pozwól przeglądarce wygenerować Content-Length z treści; samodzielnie ustaw tylko pozostałe nagłówki biletu. Odpowiedź nadal wymienia Content-Length dla klientów poza przeglądarką oraz po to, aby kod przeglądarki mógł sprawdzić rozmiar pliku przed wysłaniem.
Nie wysyłaj nagłówka Authorization w tym żądaniu. Podpisany adres jest poświadczeniem; dodatkowy nagłówek autoryzacji powoduje odrzucenie przez niektóre implementacje magazynu.
# 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
});Zatwierdź przesłanie
Udany PUT oznacza tylko zapisanie obiektu tymczasowego. Kontynuuj z fileId z pierwszego kroku:
curl -s -X POST "https://api.spicyapi.ai/api/v1/files/$FILE_ID/commit" \
-H "Authorization: Bearer $SPICY_API_KEY" | jq .Oczekuj "status": "ready" i uri zaczynającego się od 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"
}Zatwierdzenie porównuje zapisany rozmiar i typ mediów z biletem, sprawdza sygnaturę pliku multimedialnego, oblicza SHA-256 i kopiuje bajty do prywatnego obiektu, którego stary bilet nie może nadpisać. Dla audio i wideo może też zwrócić durationSeconds jako ciąg dziesiętny oraz liczbowe width i height. Powtórzenie udanego zatwierdzenia jest idempotentne.
| Odpowiedź | Znaczenie | Rozwiązanie |
|---|---|---|
400, code: 40003 | Rozmiar, typ lub format zapisanego pliku nie odpowiada deklaracji | Zamów nowy bilet z poprawnym contentType i dokładnym bytes, a następnie prześlij ponownie |
400 | Plik jeszcze nie dotarł: PUT nie zakończył się lub nie powiódł | Sprawdź, czy PUT zwrócił 200, a potem ponownie zatwierdź |
404 | Na tym koncie nie ma takiego fileId | Użyj fileId z własnego biletu |
409 | Bilet lub przesłany plik wygasł albo wcześniejsze zatwierdzenie odrzuciło ten plik | Zacznij ponownie od kroku 1 |
409 / 429 | Weryfikacja tego pliku już trwa albo weryfikator jest zajęty | Poczekaj sekundę i zatwierdź ponownie |
Po PUT musi nastąpić zatwierdzenie
Niezatwierdzonego pliku nie można użyć do utworzenia zadania. Nie używaj tymczasowej ścieżki magazynu z biletu ani pola zgodności key, dopóki zatwierdzenie się nie powiedzie.
Umieść URI spicy:// w danych wejściowych
Jako pole referencyjne stosuj wyłącznie data.uri z odpowiedzi zatwierdzenia — nie uploadUrl, tymczasowy klucz obiektu ani samo fileId.
Pole przyjmujące plik zależy od modelu. W inputSchema modelu takie pole ma x-ui.widget ustawione na upload (jeden plik) lub multi-upload (lista — umieść URI w tablicy). x-ui.accept wymienia typy plików, a x-ui.max_size_mb rozmiar przyjmowany przez model. Zobacz Katalog modeli i schematy.
MODEL_ID_FROM_CATALOG i pozostałe poniższe pola wejściowe są strukturalnymi symbolami zastępczymi; wybierz dokładne model i dane zgodne ze schematem z tej samej pozycji uwierzytelnionego katalogu na żywo. Zadanie określa sam model, więc input nie zawiera pola endpointu; przyjmuje tylko pola wymienione w inputSchema tego modelu, a jedno dodatkowe pole zwraca 400:
{
"model": "MODEL_ID_FROM_CATALOG",
"input": {
"prompt": "slow dolly in, rain on the window",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"duration_seconds": 5
}
}Platforma rozwiązuje zatwierdzone URI spicy:// podczas wykonywania zadania. Dzięki temu przesłane pliki pozostają użyteczne, gdy zadanie czeka w kolejce, bez konieczności zarządzania tymczasowymi adresami pobierania. Używaj URI zwróconego przez zatwierdzenie bez zmian.
URI, które nie jest gotowe, wygasło lub należy do innego konta, jest odrzucane przy tworzeniu zadania z 400.
Pełny przykład
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"))Dodatkowy limit żądań
Oprócz ogólnego limitu otwartego API upload-url ma własne ogólne zabezpieczenie 1 000 żądań na 10 sekund przed nadużyciami lub niekontrolowanym wydawaniem autoryzacji zapisu w magazynie obiektowym.
Koszt jest tu asymetryczny: podpisanie biletu to dla nas wyłącznie lokalne obliczenie, lecz każdy bilet jest autoryzacją zapisu, która nie przechodzi już przez nas. Człowiek wybierający pliki nie osiągnie takiego tempa, a skrypt przesyłający partię kilkudziesięciu materiałów referencyjnych nadal mieści się w limicie z dużym zapasem.
2. Pobieranie wyników
Gotowe wyniki zawierają już output.assets[].url i expiresAt w odpowiedziach na odpytywanie oraz w callbackach v2. Pobieraj ten adres bezpośrednio bez klucza API; opisany niżej download-url jest opcjonalny. Linki zwykle są ważne przez 20 minut i nigdy nie dłużej niż okres przechowywania zadania — ograniczony do około 14 dni, a krótszy, jeśli tak go ustawisz.
Aby zachować wynik, zapisz go we własnym magazynie, gdy link jest jeszcze ważny. Na przykład:
curl -L -o result.png "PASTE_THE_OUTPUT_URL_HERE"Nie dodawaj do tego żądania nagłówka Authorization. Jeśli link wygasł (magazyn odpowiada 403), odpytaj zadanie ponownie przez recordInfo, aby uzyskać nowy.
Opcjonalne odświeżenie linku pobierania
POST /api/v1/common/download-urlWłaściwość
Typ
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"
}
}Dla zadania z kilkoma plikami wynikowymi wywołaj endpoint raz dla każdego key. Podobnie jak recordInfo działa on tylko z kluczem API, który utworzył zadanie.
20 minut ważności, bez możliwości unieważnienia po wydaniu
Podpisany adres URL to ciąg znaków, który sam potwierdza uprawnienie: magazyn obiektowy nie zapyta ponownie, kto go posiada. Dlatego:
- Nie zapisuj go w bazie danych ani nie umieszczaj w długotrwałej pamięci podręcznej udostępnianej przeglądarkom. Podpisuj adres wtedy, gdy jest potrzebny — podpis to lokalne obliczenie i praktycznie nic nie kosztuje.
- Pobierz wszystko, co chcesz zachować. Pliki wynikowe są po naszej stronie dostępne najwyżej około 14 dni, a krócej, jeśli skrócisz ten czas; zobacz Przechowywanie i niszczenie danych.
Gdy linku jeszcze nie ma — albo już nie ma
| Sytuacja | Co widzisz | Co zrobić |
|---|---|---|
| Kopiowanie do naszego magazynu nadal trwa | Zasób pokazuje pending: true w recordInfo; download-url bez key zwraca 409 (z jawnym key zwraca 404 do ukończenia kopii) | Poczekaj kilka sekund i odpytaj ponownie |
| Kopiowanie ostatecznie się nie powiodło | Zasób ma unavailable: true i nie ma key | Czekanie nie pomoże — wygeneruj ponownie |
| Zadanie nie zakończyło się jeszcze powodzeniem | 404, „This task has no downloadable output” | Poczekaj na state: "succeeded" |
| Upłynął okres przechowywania lub treść została zniszczona | HTTP 410 z code: 404, „This output has expired or was deleted” | Wynik bezpowrotnie zniknął — wygeneruj ponownie |
Zadanie, które nie należy do ciebie, oraz key, który nie należy do zadania, zwracają 404. Nie rozróżniamy „nie istnieje” od „należy do kogoś innego”, ponieważ tę różnicę można wykorzystać do wyliczania cudzych identyfikatorów zadań.
Biblioteka zasobów
Niektórych plików używasz w kółko: portretu postaci, referencji stylu, zdjęcia produktu, próbki głosu. Biblioteka zasobów przechowuje je, dopóki ich nie usuniesz, i nadaje każdemu stałe odwołanie spicy://a/<id>. Wstaw to odwołanie do dowolnego pola multimedialnego w danych wejściowych dowolnego zadania, tak jak przesłany plik spicy://f/, tylko bez wygasania po dobie.
- Prywatna w obrębie przestrzeni roboczej. Odwołanie
spicy://a/działa tylko w żądaniach wykonanych kluczami z tej samej przestrzeni roboczej. Klucz osobisty sięga do Twojej biblioteki osobistej, a klucz utworzony w przestrzeni zespołowej do wspólnej biblioteki tego zespołu; te dwie nigdy się nie łączą (zob. niżej). - Przechowywana do usunięcia. Wyniki generowania są czyszczone po około 14 dniach, a przesłane pliki po 1 dniu; zapisane zasoby zostają, dopóki ich nie usuniesz. Biblioteka osobista znika wraz z zamknięciem konta, a biblioteka zespołu wraz z zespołem.
- To nie hosting plików. API zwraca odwołanie
spicy://a/<id>orazpreviewUrlpodpisany tylko dla Ciebie, ważny najwyżej 20 minut. Nie ma publicznego ani stałego linku i nie da się go z odwołania wytworzyć. Zapisujuri, nigdypreviewUrl.
Biblioteka osobista i zespołowa
O bibliotece decyduje klucz. Klucz utworzony w Twojej przestrzeni osobistej sięga do Twojej biblioteki osobistej. Klucz utworzony w przestrzeni zespołowej, przez dowolnego członka, sięga do wspólnej biblioteki tego zespołu. Klucz zespołowy nigdy nie sięga do osobistej biblioteki niczyjej, klucz osobisty nigdy do biblioteki zespołu, a odwołanie spicy://a/ z drugiej biblioteki jest odrzucane tak, jakby nie istniało.
To, co klucz zespołowy może zmieniać, zależy od aktualnej roli członka, który go utworzył:
| Rola | Zapis | Zmiana nazwy lub usunięcie | Lista, odczyt i użycie |
|---|---|---|---|
owner, admin | Tak | Dowolny zasób | Tak |
developer | Tak | Tylko własne zapisy | Tak |
billing, readonly | Nie | Nie | Tak |
Jeśli ten członek opuścił zespół, klucz dostaje 403 z data.reason równym asset_workspace_forbidden. Zasoby nie pokazują, kto je zapisał; widać tylko, czy możesz zmienić dany zasób.
Zapisywanie pliku
POST /api/v1/assets kopiuje plik do Twojej biblioteki. Nagłówek Idempotency-Key jest wymagany. Wybierz jedno z trzech źródeł:
source | Pola | Uwagi |
|---|---|---|
task_output | taskId oraz key lub index | Jedno z Twoich udanych zadań. index liczy się od 0, w kolejności output.assets w recordInfo. |
upload | fileId | Zatwierdzone przesłanie: fil_… lub spicy://f/fil_…. |
url | url | Publiczny adres http lub https na porcie 80 albo 443, bez nazwy użytkownika i hasła. Plik jest pobierany raz, a jego typ odczytywany z samego pliku; przyjmowane są tylko obrazy, wideo i audio. |
name jest opcjonalne (do 80 znaków; domyślnie Saved asset). Ponowny zapis tego samego źródła zwraca zasób, który już masz, i nie zajmuje więcej miejsca. Rozmiar, wymiary i czas trwania mierzymy sami.
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"
}
}Użycie w zadaniu
Wstaw data.uri w dowolne pole pliku, które w inputSchema modelu ma x-ui.widget równe upload lub 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
}
}Usunięte odwołanie albo odwołanie do biblioteki, do której ten klucz nie sięga (innego konta lub drugiej przestrzeni roboczej), jest odrzucane przy tworzeniu zadania dokładnie tak jak nieznany identyfikator.
Lista, zmiana nazwy i usuwanie
GET /api/v1/assets zwraca Twoje zasoby w polu items oraz nextCursor, którego nie ma na ostatniej stronie. Filtruj przez kind (image, video lub audio), source (rozdzielone przecinkami: task_output,upload,url), q (wyszukiwanie w nazwie, bez rozróżniania wielkości liter), favorite (true tylko dla ulubionych, false dla całej reszty) i collectionId (jeden folder, patrz niżej). Kolejność ustawiasz parametrem sort: recent (domyślnie, od najnowszego), oldest, name (od A do Z, bez rozróżniania wielkości liter) albo size (od największego). limit mieści się w zakresie 1–100, domyślnie 100. Aby pobrać kolejną stronę, przekaż poprzedni nextCursor jako cursor; kursor działa tylko z tym sort, z którego pochodzi, a kursor z innego sortowania jest odrzucany z kodem 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} zwraca jeden zasób ze świeżym previewUrl. PATCH zmienia nazwę, flagę favorite albo obie (wyślij tylko to, co chcesz zmienić); uri pozostaje bez zmian. Po DELETE zasób znika od razu, a jego uri przestaje działać w nowych żądaniach. Oba wywołania można bezpiecznie powtarzać i nie wymagają Idempotency-Key. Nieznany zasób i zasób innego konta zwracają ten sam 404.
Ulubione, foldery i model, który wygenerował zasób
Każdy zasób ma flagę favorite. Ustawisz ją przez PATCH z treścią {"favorite": true}. W bibliotece zespołu oznaczenie należy do biblioteki, więc wszyscy widzą te same ulubione. Foldery (w API nazywane kolekcjami) grupują zasoby bez ich kopiowania, a jeden zasób może leżeć w kilku folderach. Foldery tworzysz i zarządzasz nimi na stronie Assets w aplikacji webowej; API nie pozwala ich tworzyć ani edytować, ale parametr collectionId w GET /api/v1/assets wylistuje zasoby jednego folderu (404, gdy folderu nie ma w bibliotece, do której sięga Twój klucz).
Każdy zasób zawiera też model, czyli publiczną nazwę modelu, który go wygenerował, oraz modelName, czyli jego nazwę wyświetlaną. Dla plików przesłanych i linków oba pola są pustymi ciągami.
Postacie
Postać to nazwa i uporządkowany zestaw zasobów obrazkowych z tej samej biblioteki: do 100 obrazów i opcjonalna notatka do 280 znaków. Obrazy to same zasoby, nic nie jest kopiowane. Usunięcie postaci zostawia jej obrazy, a usunięcie obrazu wyjmuje go ze wszystkich postaci. Postacie tworzysz i edytujesz na stronie Assets w aplikacji webowej albo w Studio. Nie zajmują własnego miejsca: liczy się tylko Twój plan biblioteki.
Przez API można postacie tylko odczytywać:
GET /api/v1/assets/entitieszwraca listę od najnowszej. ParametrassetIdzostawia tylko postacie zawierające dany zasób,limitmieści się w zakresie 1–100 (domyślnie 50), acursorkontynuuje od poprzedniegonextCursor.GET /api/v1/assets/entities/{id}zwraca jedną postać. Nieznana postać, postać innego konta i postać z biblioteki, do której klucz nie sięga, dają ten sam404.
Każda postać ma id, kind (zawsze character), name, note, cover (obraz okładki jako zasób albo null, dopóki nie ma obrazów), assets (obrazy po kolei, każdy z uri i prywatnym previewUrl), count, createdAt i updatedAt. Bibliotekę wybiera klucz, tak jak przy zasobach, a odczyt postaci dzieli z odczytem zasobów limit 60 żądań na minutę.
Aby użyć postaci, wstaw jej obrazy w tej kolejności jako odwołania spicy://a/ do pola obrazów referencyjnych modelu i wskazuj je w prompcie tak, jak opisuje strona tego modelu. Postacie nazywały się kiedyś personami i leżały w osobnej bibliotece; teraz są częścią biblioteki zasobów. Dotychczasowe odwołania spicy://p/ nadal działają, ale tylko w Twojej bibliotece osobistej.
Gdy dodajesz do postaci obraz, który sam przesłałeś (a nie wynik generowania), potwierdzasz, że masz do niego prawa, a jeśli widać na nim prawdziwą osobę, także jej zgodę.
Plany przechowywania
Miejsce odblokowuje się automatycznie wraz ze skumulowanymi doładowaniami i może tylko rosnąć; zmiana tabeli nigdy nie usuwa zasobów, które już zapisałeś. Źródłem prawdy dla Twojego konta jest GET /api/v1/assets/usage: Twój plan, ile zajmujesz, limity, następny plan i ile jeszcze doładować, by go osiągnąć, oraz ile zasobów utworzyłeś dziś. Tabela pokazuje aktualne wartości domyślne.
Miejsce należy do właściciela: o planie decydują jego skumulowane doładowania, a jego biblioteka osobista i wszystkie biblioteki zespołów, które posiada, dzielą jeden limit. usage obejmuje cały ten limit i zawiera obiekt scope (workspaceId, personal, role, canWrite, canTopUp), który mówi, do której biblioteki sięga Twój klucz i co może w niej robić. Gdy canTopUp ma wartość false (klucz zespołowy utworzony nie przez właściciela zespołu), pola lifetimeTopupUsd i topupToNextTierUsd są pomijane, a przy pełnej bibliotece zamiast kwoty dostajesz prośbę o kontakt z właścicielem zespołu.
| Plan | Skumulowane doładowania | Miejsce | Liczba | Na plik |
|---|---|---|---|---|
| Free | brak | brak: nie można zapisać niczego nowego | 0 | nie dotyczy |
| Plus | od $10 | 500 MB | 200 | 100 MB |
| Pro | od $100 | 1 GB | 400 | 100 MB |
| Studio | od $250 | 2 GB | 800 | 100 MB |
| Max | od $1000 | 10 GB | 4000 | 100 MB |
Bez żadnego doładowania nie zapiszesz niczego nowego, ale zasoby już zapisane nadal można wykorzystywać. Limit 100 MB na plik dotyczy wyników zadań i linków. Plik dodany przez przesłanie nadal podlega limitom samego przesyłania (obrazy 10 MiB, audio i wideo 90 MiB).
Limity żądań i błędy
- Odczyt (lista, pobranie, usage): 60 żądań na minutę. Zapis: 30 żądań na minutę i 200 nowych zasobów na dobę UTC. Usunięcie zasobu nie zwraca dziennego limitu. Po osiągnięciu limitu dostajesz
429z nagłówkiemRetry-After. - Pełna biblioteka, jeszcze nieodblokowane miejsce albo plik przekraczający limit na plik zwracają
400zinvalid_request. Komunikat podaje Twój obecny plan i ile musisz doładować, by odblokować następny. 403zdata.reasonrównymasset_workspace_forbidden, gdy klucz nie sięga do biblioteki, alboasset_permission_denied, gdy Twoja rola w zespole nie pozwala na zmianę.503, gdy miejsce na platformie jest chwilowo wyczerpane; spróbuj ponownie później.
Błąd na tych endpointach może zawierać odczytywalne maszynowo data.reason: asset_library_locked, asset_library_full, asset_file_too_large, asset_daily_limit, asset_storage_full, asset_permission_denied lub asset_workspace_forbidden. code, status HTTP i errorCode nie zmieniają się z powodem ani bez niego, a powód pojawia się tylko na endpointach biblioteki zasobów; warstwa zgodna z OpenAI i inne warstwy zgodności zachowują własne kształty błędów. Nieznaną wartość traktuj jak brak powodu. Zob. Kody powodów biblioteki zasobów.
Aplikacja webowa ma też przełącznik automatycznego zapisu do biblioteki. Dotyczy tylko generowań uruchomionych w aplikacji webowej; generowania uruchomione przez API nigdy nie są zapisywane automatycznie, więc dla tych, które chcesz zachować, wywołaj POST /api/v1/assets.
Przechowywanie przesłanych plików
URI przesłanego materiału referencyjnego jest przechowywane przez 1 dzień od udanego zatwierdzenia — to limit platformy, który można skrócić lub zastąpić usunięciem po zakończeniu w Przechowywaniu i niszczeniu danych — i można go używać tylko do expiresAt zwróconego przez to zatwierdzenie. Po wygaśnięciu URI przestaje być użyteczne, również przy ponowieniu zadania, które do niego odwoływało. Usunięcie w ramach cyklu życia magazynu obiektowego może nastąpić nawet 24 godziny później, ale ten czas na porządkowanie nie przedłuża okresu użyteczności URI. Jeśli znów potrzebujesz tego samego materiału referencyjnego, zachowaj własną kopię i prześlij ją ponownie.

