spicyapi문서
API로 개발하기
본문

미디어

참조 미디어의 사전 서명 직접 업로드와 생성 결과의 단기 다운로드 링크를 안내합니다.

많은 모델은 사진·영상·오디오를 입력(참조)으로 받고 미디어 모델은 파일(결과물)을 반환합니다. 이 페이지는 양쪽 흐름을 설명합니다.

  • 참조 입력 — 공개 HTTPS URL, 요청에 직접 넣은 작은 이미지 또는 업로드 파일을 전달하세요. 업로드는 티켓 요청, 객체 저장소로 직접 PUT, 업로드 확정 순서입니다. 확정 응답의 spicy:// URI를 작업 입력에 넣습니다.
  • 결과 다운로드 — 결과와 함께 받은 서명된 output.assets[].url을 다운로드합니다. 모든 모델이 파일을 만들지는 않습니다. 텍스트 결과 엔드포인트는 output.text로 응답하고 assets가 없으므로 다운로드나 링크 만료도 없습니다. 출력 형식 참조.

업로드 티켓과 결과 다운로드는 /api/v1/common/, 업로드 확정은 /api/v1/files/{fileId}/commit을 사용합니다. 업로드는 무료이며 파일을 사용하는 작업에만 비용을 지불합니다.

기존 HTTPS 미디어

공개 접근 가능한 HTTPS 이미지·영상·오디오 URL을 모델이 지원하는 필드(예: image_url)로 직접 전달하세요. 플랫폼 업로드나 spicy:// 변환은 필요 없습니다. 실행 중 사용자 정의 인증 헤더 없이 접근 가능해야 합니다. 큰 로컬 파일은 아래 업로드 흐름을 사용합니다.

URL은 파일 자체를 반환해야 합니다. 클라우드 드라이브 공유 페이지, 사진 앨범이나 소셜 게시물 링크는 이미지가 아닌 웹 페이지를 반환하여 invalid_asset로 실패합니다. 이런 경우 파일을 다운로드한 뒤 업로드하세요.

Base64와 로컬 파일

작은 이미지는 스키마에 선언된 이미지 필드에 직접 넣을 수 있습니다(예: image_url: "data:image/png;base64,..."). MIME 유형을 포함하고 표준 Base64를 사용하세요. MIME 없는 Base64, URL 안전 인코딩, 공백, SVG와 인라인 오디오·영상은 이 경로에서 지원하지 않습니다.

  • JPEG, PNG, WebP, GIF: 이미지별 디코딩 후 최대 1 MiB, 한 변 8192픽셀, 이미지당 8,388,608픽셀.
  • 요청당 최대 16장, 총 16,777,216픽셀. 전체 JSON 본문은 Base64의 약 1/3 크기 증가까지 포함하여 2 MiB 이내여야 합니다.
  • 모델별 스키마·미디어 한도도 적용됩니다. 견적은 업로드나 금액 선점 없이 검증합니다. 생성 시 이미지를 계정에 귀속되는 업로드로 저장하며 다른 업로드처럼 하루 유지합니다. 작업 입력에는 Base64를 되돌려 주지 않고 파일 URI가 포함됩니다.

큰 파일은 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. 참조 직접 업로드

공개 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—

iPhone 카메라의 기본 형식인 .mov도 받으며 입력 과정에서 MP4로 변환하므로 모델은 항상 MP4를 받습니다. 웹 업로더와 업로드 API 모두 허용합니다. CLI, SDK, MCP 서버는 확장자로 유형을 추론하지만 아직 목록에 없으므로 contentType: video/quicktime을 명시하세요.

LoRA 가중치는 application/octet-stream으로 업로드합니다. 최대 2 GiB의 .safetensors 파일이며(웹 업로더는 최대 90 MiB), 티켓은 20분이 아니라 2시간 동안 유효하고, 커밋 시점에 완전한 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

파일 바이트를 uploadUrl로 PUT하세요. curl, Node.js, Python 등 브라우저가 아닌 클라이언트는 headers의 모든 헤더를 주어진 그대로 보내야 합니다. 브라우저는 아래 방식으로 Content-Length를 처리합니다.

헤더 하나가 빠져도 서명이 일치하지 않습니다

Content-Type과 Content-Length 모두 서명에 포함됩니다. 어느 쪽이든 바꾸면 객체 저장소가 거절합니다. 이 403은 플랫폼이 아닌 저장소에서 발생하므로 원인을 헷갈리기 쉽습니다.

브라우저는 Content-Length를 금지된 요청 헤더로 지정하여 코드가 fetch나 XMLHttpRequest에서 설정할 수 없습니다. 브라우저에서는 티켓 bytes와 크기가 정확히 같은 File 또는 Blob을 보내고 사용자 에이전트가 본문에서 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"와 spicy://로 시작하는 uri를 확인합니다.

{
  "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로 새 티켓을 요청하고 다시 업로드
400PUT 미완료·실패로 파일이 없음PUT의 200을 확인한 뒤 다시 확정
404계정에 해당 fileId가 없음자신의 티켓 fileId 사용
409티켓·파일 만료 또는 이전 확정에서 파일 거절1단계부터 다시 시작
409 / 429이미 검증 중이거나 검증 기능이 사용 중1초 후 다시 확정

PUT 다음에 반드시 확정해야 합니다

미확정 파일은 작업 생성에 사용할 수 없습니다. 티켓의 임시 저장 경로를 사용하지 말고 확정 성공 전에는 호환 key 필드도 사용하지 마세요.

입력에 spicy:// URI 사용

참조 필드에는 확정 응답의 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
  }
}

플랫폼은 실행 시 확정된 spicy:// URI를 해석합니다. 따라서 임시 다운로드 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은 10초당 1,000회의 별도 안전장치로 악용·통제되지 않는 객체 저장소 쓰기 승인을 보호합니다.

비용은 비대칭적입니다. 티켓 서명은 플랫폼의 로컬 계산뿐이지만 각 티켓은 이후 플랫폼을 거치지 않는 쓰기 승인입니다. 사람이 파일을 고르는 속도로는 도달하기 어렵고 참조 수십 개의 배치도 충분히 처리할 수 있습니다.

2. 결과 다운로드

준비된 결과는 폴링과 v2 콜백에 이미 output.assets[].url과 expiresAt을 포함합니다. 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일이며 기간을 줄이면 더 짧습니다. 보관과 폐기 참조.
상황표시대응
자체 저장소에 복사 중recordInfo에 pending: true. key 없는 download-url은 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/ 업로드와 같은 방식이지만 1일 만료가 없습니다.

  • 워크스페이스 안에서만 비공개. spicy://a/ 참조는 같은 워크스페이스의 키로 보낸 요청에서만 작동합니다. 개인 키는 개인 라이브러리에, 팀 워크스페이스에서 만든 키는 그 팀의 공유 라이브러리에 연결되며, 둘은 서로 이어지지 않습니다(아래 참고).
  • 삭제할 때까지 보관. 생성 결과는 약 14일, 업로드는 1일 후 정리되지만, 저장한 에셋은 직접 삭제할 때까지 남습니다. 개인 라이브러리는 계정을 닫을 때, 팀 라이브러리는 팀과 함께 삭제됩니다.
  • 파일 호스팅이 아닙니다. API는 spicy://a/<id> 참조와, 본인에게만 서명되어 최대 20분간 유효한 previewUrl만 제공합니다. 공개 링크나 영구 링크는 없으며 참조를 링크로 바꿀 수도 없습니다. 저장해 둘 값은 previewUrl이 아니라 uri입니다.

개인 라이브러리와 팀 라이브러리

어떤 라이브러리를 쓰는지는 키가 정합니다. 개인 워크스페이스에서 만든 키는 개인 라이브러리에, 팀 워크스페이스에서 만든 키(만든 멤버가 누구든)는 그 팀의 공유 라이브러리에 연결됩니다. 팀 키는 누구의 개인 라이브러리에도 닿지 않고, 개인 키는 팀 라이브러리에 닿지 않으며, 다른 쪽 라이브러리의 spicy://a/ 참조는 존재하지 않는 ID처럼 거부됩니다.

팀 키로 변경할 수 있는 범위는 그 키를 만든 멤버의 현재 역할을 따릅니다.

역할저장이름 변경·삭제목록·조회·사용
owner, admin가능모든 에셋가능
developer가능본인이 저장한 에셋만가능
billing, readonly불가불가가능

키를 만든 멤버가 팀을 떠나면 그 키는 403(data.reason은 asset_workspace_forbidden)을 받습니다. 에셋에는 저장한 사람이 표시되지 않으며, 내가 변경할 수 있는지만 알 수 있습니다.

파일 저장

POST /api/v1/assets는 파일을 라이브러리로 복사합니다. Idempotency-Key 헤더가 필수입니다. 세 가지 출처 중 하나를 고릅니다.

source필드설명
task_outputtaskId, 그리고 key 또는 index본인의 성공한 작업. index는 0부터 시작하며 recordInfo의 output.assets 순서와 같습니다.
uploadfileId확정된 업로드(fil_… 또는 spicy://f/fil_…).
urlurl80 또는 443 포트의 공개 http/https 주소(사용자 이름과 비밀번호 없음). 파일을 한 번 내려받아 파일 내용으로 형식을 판단하며, 이미지, 비디오, 오디오만 허용됩니다.

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"
  }
}

작업에서 사용하기

모델의 inputSchema에서 x-ui.widget이 upload 또는 multi-upload인 파일 필드에 data.uri를 넣습니다.

{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": {
    "prompt": "slow dolly in, rain on the window",
    "image_url": "spicy://a/ast_01k3m9c4w8r2n6t5v1x7y0z3ab",
    "duration_seconds": 5
  }
}

삭제된 참조나 이 키가 닿지 않는 라이브러리(다른 계정 또는 다른 워크스페이스)의 참조는 존재하지 않는 ID와 똑같이 작업 생성 시 거부됩니다.

목록, 이름 변경, 삭제

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에서만 쓸 수 있으며, 다른 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에서는 컬렉션이라고 부릅니다)는 에셋을 복사하지 않고 묶어 두는 기능이며, 에셋 하나를 여러 폴더에 넣을 수 있습니다. 폴더는 웹 앱의 Assets 페이지에서 만들고 관리합니다. API로는 폴더를 만들거나 수정할 수 없지만, GET /api/v1/assets의 collectionId로 폴더 하나의 에셋을 조회할 수 있습니다(그 폴더가 키가 접근하는 라이브러리에 없으면 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 MB200100 MB
Pro$100 이상1 GB400100 MB
Studio$250 이상2 GB800100 MB
Max$1,000 이상10 GB4,000100 MB

충전 내역이 없으면 새로 저장할 수 없지만, 이미 저장한 에셋은 계속 사용할 수 있습니다. 파일당 100 MB 상한은 작업 결과물과 링크로 가져오는 파일에 적용됩니다. 업로드로 들어오는 파일에는 업로드 자체의 한도(이미지 10 MiB, 오디오·비디오 90 MiB)가 그대로 적용됩니다.

요청 한도와 오류

  • 읽기(목록, 조회, usage)는 분당 60회, 저장은 분당 30회이며, 새로 만들 수 있는 에셋은 UTC 하루 200개입니다. 에셋을 삭제해도 그날의 한도는 돌아오지 않습니다. 한도에 도달하면 Retry-After 헤더와 함께 429가 반환됩니다.
  • 라이브러리가 가득 찼거나, 저장 용량이 아직 열리지 않았거나, 파일당 상한을 넘으면 invalid_request인 400이 반환됩니다. 메시지에는 현재 플랜과 다음 플랜을 열기 위해 필요한 추가 충전액이 나옵니다.
  • 키가 해당 라이브러리에 닿지 않으면 403(data.reason은 asset_workspace_forbidden), 팀 역할이 변경을 허용하지 않으면 403(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일 보관합니다. 플랫폼 상한이며 보관과 폐기에서 단축하거나 완료 즉시 폐기로 바꿀 수 있습니다. 사용은 확정 응답의 expiresAt까지만 가능합니다. 만료 후 URI는 이를 참조한 작업의 재시도에서도 사용할 수 없습니다. 이후 객체 저장소 수명 주기 삭제가 최대 24시간 지연될 수 있지만 URI 사용 기간이 늘어나는 것은 아닙니다. 같은 참조가 다시 필요하면 자체 사본을 보관하고 다시 업로드하세요.

이 페이지에서