spicyapi文件
主要內容

非同步任務模型

createTask 與 recordInfo 的完整契約、六種狀態的含義、以及回呼與輪詢該怎麼選。

生成是非同步的:提交與取結果是兩次獨立的呼叫

影像幾秒,影片常常幾分鐘。做成同步介面的話,你的程式要掛在連線上等,中途任何一次網路抖動都會讓你既拿不到結果、又不知道該不該重試——而重試一次同步生成請求,你不知道上一次是不是其實已經在跑了。

拆開之後,任務 ID 成為冪等的錨點:連線斷了重連,拿著同一個任務 ID 接著查就行。

一、建任務

POST /api/v1/jobs/createTask

請求標頭

標頭必填說明
AuthorizationBearer sk-spicy-…
Content-Typeapplication/json
Idempotency-Key強烈建議帶。見冪等鍵

請求本文

欄位

型別

建任務、串流請求與報價都會先驗證 callBackUrl:只接受使用 80443 埠、可公開存取的 http/https 網址,拒絕 URL 中的使用者名稱或密碼、內部網路 IP 以及無法解析的主機名稱。網址無效時回傳 HTTP 400,信封內為業務 code: 400msg: "Invalid callback URL",並附帶 request_id;不會回傳提交的 URL、主機名稱、IP 或 DNS 明細。實際建立連線時還會再次檢查目標,詳見回呼

回應

HTTP/1.1 202 Accepted

HTTP 狀態是 202 Accepted;回應本文仍使用統一信封,因此成功時業務欄位 code200

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "state": "queued",
    "estimatedCost": "0.008"
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}
欄位說明
taskId任務 ID,job_ 字首。後續全靠它
state新建任務為 queued;同鍵冪等重放為原任務的目前狀態
estimatedCost受理時凍結的金額,形如 "0.008"(美元,字串),也是本任務最終收費上限。少用釋放差額;之後不會補扣超過凍結額的金額

金額是字串,但裡面沒有貨幣符號

estimatedCostcost、餘額三項都是形如 "0.008"十進位字串:沒有 $、沒有千分位,可以直接 parseFloat / float() 解析。

用字串而不是 JSON 數字,是因為 JSON 的數字在多數語言裡會解析成 float64,而 0.008 在二進位浮點裡不精確——價格一旦進過一次浮點,就再也說不清「到底扣了多少」。要精確對帳請用十進位型別(decimal.DecimalBigDecimal)解析,不要用 float

貨幣恆為美元,見計費

範例

下文的 MODEL_ID_FROM_CATALOG 是明確的結構預留位置。請從同一條即時目錄記錄中取得 model 與符合 Schema 的輸入,一起替換預留位置和示意用的 input;不要原樣傳送這個預留位置。

curl
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8814-render-1" \
  -d '{
    "model": "MODEL_ID_FROM_CATALOG",
    "input": {
      "prompt": "slow dolly in, rain on the window",
      "image_url": "https://example.com/reference.png",
      "duration_seconds": 5,
      "resolution": "768p"
    },
    "callBackUrl": "https://your-app.example.com/hooks/spicy"
  }'
JavaScript
const res = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
    'Content-Type': 'application/json',
    // 每個「邏輯請求」一個鍵,重試時重複使用同一個
    'Idempotency-Key': 'order-8814-render-1',
  },
  body: JSON.stringify({
    model: 'MODEL_ID_FROM_CATALOG',
    input: { prompt: 'a folded paper lantern, hard side light' },
    callBackUrl: 'https://your-app.example.com/hooks/spicy',
  }),
});

const body = await res.json();
if (res.status !== 202 || body.code !== 200) throw new Error(`${res.status} ${body.code} ${body.msg}`);
console.log(body.data.taskId);
Python
import os, requests

res = requests.post(
    "https://api.spicyapi.ai/api/v1/jobs/createTask",
    headers={
        "Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
        "Content-Type": "application/json",
        # 每個「邏輯請求」一個鍵,重試時重複使用同一個
        "Idempotency-Key": "order-8814-render-1",
    },
    json={
        "model": "MODEL_ID_FROM_CATALOG",
        "input": {"prompt": "a folded paper lantern, hard side light"},
        "callBackUrl": "https://your-app.example.com/hooks/spicy",
    },
)

body = res.json()
if res.status_code != 202 or body["code"] != 200:
    raise RuntimeError(f'{res.status_code} {body["code"]} {body["msg"]}')
print(body["data"]["taskId"])

202 的 state 不一定總是 queued

首次受理會建立 queued 任務。如果同一 API Key 用同一 Idempotency-Key 和相同請求重放,這個 202 會回傳原任務的目前狀態,它可能已經是 running 或終態,但不會再建立任務或再次凍結金額。

二、狀態機

queued ──→ running ──┬──→ succeeded
                     ├──→ failed
                     └──→ expired

任務一經受理不可撤銷,queuedrunning 都沒有取消操作。介面仍能讀取舊系統留下的 canceled 記錄,但新任務不會由使用者或營運操作進入該狀態。

狀態含義是否終態計費
queued已受理並凍結預估金額,等待 worker 領取已凍結,尚未收費
running模型正在生成,等待結果已凍結,尚未收費
succeeded生成成功按實際用量結算,不超過凍結額
failed生成失敗全額退回
canceled僅相容歷史記錄;不再提供取消入口全額退回
expired超出該模型的最大執行時長仍未出結果全額退回

失敗不計費是承諾,不是盡力而為

succeeded 外的所有終態都會全額釋放凍結金額。即使我們的程式在中途崩潰,補救掃描也會在下一輪把錢釋放回可用餘額。你不需要為此做任何事,也不需要來找我們對帳。

判終態的正確寫法是 state 不在 {queued, running},而不是逐個列舉終態——這樣既能相容舊的 canceled 記錄,也不會在將來新增終態時無限輪詢。

三、查詢任務

GET /api/v1/jobs/recordInfo?taskId=job_…

只有一個查詢參數 taskId,必填。任務不屬於本帳號時回傳 404(與「不存在」回傳同一個回應,避免靠差異列舉別人的任務 ID)。

回應欄位

欄位型別說明
taskIdstring任務 ID
modelstring模型識別碼
statestring見上表
inputobject已驗證的參數;內嵌圖片會正規化為平台檔案 URI。保留期過後不再回傳
outputobject生成結果的描述。只在 succeeded 時有意義
errorCodestring失敗原因的機器可讀識別碼。僅失敗時出現
errorMessagestring失敗原因的說明,預設英文,可改用其他語言(見錯誤說明的語言)。僅失敗時出現
coststring已結算則是實收金額,未結算則是凍結金額;最終收費不會超過凍結額
settledboolean計費是否已終結。為 falsecost 仍可能變動
createdAtstringRFC 3339 時間
deadlineAtstring伺服器端在受理時儲存的任務執行截止時間;歷史回呼可能沒有此欄位
completedAtstringRFC 3339 時間。僅終態時出現

四種不同的期限

  • SDK timeoutMs 可以設定,控制本機提交與等待的總時長;停止等待不會取消遠端任務。儲存 onAccepted 回傳的任務 ID,隨後繼續查詢原任務。
  • deadlineAt 是伺服器端的實際執行期限,冪等重放不延長,明確重試所建立的新任務有自己的期限。不要把它當成本機倒數計時的退款依據,仍需檢查任務終態與 settled
  • output.assets[].expiresAt 是臨時結果連結到期時間,通常20分鐘。重新查詢可續簽,並不建立新任務。
  • 生成媒體在完成後保留14天,平台上傳的輸入保留1天;換新連結不會延長素材本身的留存期。

簡單指令碼可以呼叫 client.run(input, { idempotencyKey, onAccepted, timeoutMs }),由 SDK 完成一次提交和退避輪詢;預設從2秒退避到最多10秒。後端整合優先使用 Webhook,收到完整結果並驗證簽章後即可直接使用,無需再查詢一次。

直接使用結果網址

就緒的輸出檔案包含 urlexpiresAtkey 和媒體後設資料。直接 GET output.assets[].url,不攜帶 API Key;URL 到期可重新輪詢。原 /common/download-url 保留為選用介面。連結通常20分鐘有效且不超過14天結果留存期。

succeeded
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "succeeded",
    "input": { "prompt": "a folded paper lantern, hard side light" },
    "output": {
      "assets": [
        {
          "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png",
          "url": "https://example.r2.cloudflarestorage.com/results/result.mp4?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
          "expiresAt": "2026-08-28T09:32:11Z",
          "mime": "image/png",
          "width": 1024,
          "height": 1024,
          "bytes": 1483920
        }
      ]
    },
    "cost": "0.008",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "completedAt": "2026-08-28T09:12:11Z"
  }
}
failed
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "failed",
    "errorCode": "upstream_failed",
    "errorMessage": "Generation failed; the charge has been refunded",
    "cost": "0",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "completedAt": "2026-08-28T09:12:39Z"
  }
}

查詢成功 ≠ 任務成功

上面兩個回應的 code 都是 200——查詢本身成功了。任務失敗體現在 data.state 裡。業務碼說的是「這次傳遞怎麼樣」,state 說的是「那個任務怎麼樣」,別混著判斷。

回呼推過去的請求本文與這裡逐字同形,同樣是 code: 200 的信封 + 同一組 data 欄位。所以這一節的解析程式碼可以原封不動地重複使用在回呼端點上,兩個出口只需要一個解析器

output 的形狀

影像與影片模型的輸出檔案在 output.assets[],文字模型的輸出在 output.text

欄位型別說明
keystring物件鍵,取得下載網址時用它。pending 為真時為空
urlstring可直接 GET 的已簽章 URL,無需攜帶 API Key;pending 或 unavailable 時省略
expiresAtstring本次 URL 的 RFC 3339 有效期,可在留存期內重新輪詢重新整理
mimestring內容型別,例如 image/pngvideo/mp4
width · heightinteger畫素尺寸。音訊與文字結果不含此欄位
durationSecondsnumber時長(秒)。影片與音訊結果才有
bytesinteger位元組數
pendingboolean見下
unavailableboolean見下

取不到的欄位不會出現(不是 null 也不是 0),解析時請當作選用欄位處理。

還有兩個狀態旗標要處理:

  • pending: true —— 輸出檔案還在轉存進我們的儲存空間,key 暫時為空。等幾秒再取。此時呼叫 download-url 會拿到 409
  • unavailable: true —— 資產已超過留存期,或多次轉存失敗後無法取回。再等沒有意義,重新生成即可。

四、回呼優先,輪詢備援

回呼輪詢
延遲生成完成的一刻取決於你的輪詢間隔
請求數每個任務 1 次(我們發起)每個任務 N 次(你發起)
佔不佔你的速率限制額度不佔,見速率限制
要不要對外公開的入口不要

正式環境請用回呼。 一個跑三分鐘的影片任務,每秒輪詢一次就是 180 次幾乎沒有資訊增量的請求。目前開放 API 的 1000 次 / 10 秒高位保險絲不會限制正常付費生成,但緊密輪詢會製造無意義流量,並在用戶端失控時與新任務提交爭用同一個桶。

輪詢只在這兩種情況下才是合理選擇:本機除錯,以及你的服務沒有對外公開的入口。

輪詢要退避

recordInfocreateTask 共用同一個速率限制桶。輪詢打得越兇,你能提交的新任務就越少——最壞情況是輪詢把額度吃光,導致新任務提交回傳 429

建議的退避形態:

  • 起步 2–3 秒。再短沒有意義,影像模型最快也要幾秒。
  • 每次乘 1.5,封頂 10–15 秒。
  • 設一個總逾時,超過之後停止輪詢並按失敗處理——不要寫出一個能無限輪詢下去的迴圈。
JavaScript
async function waitForTask(taskId, { timeoutMs = 10 * 60 * 1000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  let wait = 2000;

  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, wait));
    wait = Math.min(wait * 1.5, 15000);

    const res = await fetch(
      `https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${taskId}`,
      { headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` } },
    );
    const body = await res.json();

    // 觸發速率限制時就依 Retry-After 等待,不要把它當成任務失敗
    if (body.code === 429) {
      wait = Number(res.headers.get('retry-after') ?? 1) * 1000;
      continue;
    }
    if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);

    // 判終態:不在這兩個中間態裡就是終態,不要逐個列舉終態
    const { state } = body.data;
    if (state !== 'queued' && state !== 'running') return body.data;
  }
  throw new Error('等待任務逾時');
}
Python
import os, time, requests

BASE = "https://api.spicyapi.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"}


def wait_for_task(task_id: str, timeout: float = 600.0) -> dict:
    deadline = time.monotonic() + timeout
    wait = 2.0

    while time.monotonic() < deadline:
        time.sleep(wait)
        wait = min(wait * 1.5, 15.0)

        res = requests.get(f"{BASE}/jobs/recordInfo", headers=AUTH, params={"taskId": task_id})
        body = res.json()

        # 觸發速率限制時就依 Retry-After 等待,不要把它當成任務失敗
        if body["code"] == 429:
            wait = float(res.headers.get("retry-after", 1))
            continue
        if body["code"] != 200:
            raise RuntimeError(f'{body["code"]} {body["msg"]}')

        # 判終態:不在這兩個中間態裡就是終態
        if body["data"]["state"] not in ("queued", "running"):
            return body["data"]

    raise TimeoutError("等待任務逾時")

五、查餘額

GET /api/v1/chat/credit
curl
curl https://api.spicyapi.ai/api/v1/chat/credit \
  -H "Authorization: Bearer $SPICY_API_KEY"
{
  "code": 200,
  "msg": "success",
  "data": {
    "available": "128.42",
    "held": "0.36",
    "total": "128.78"
  }
}

available 能用於新任務,held 是在途任務凍結的部分,total 是兩者之和。計費口徑見計費

任務歷史與恢復

GET /api/v1/jobs 分頁列出目前使用的 API Key 所建立的可見任務,可用於應用程式重啟或漏收回呼後的追查。其他 Key 的任務與已隱藏記錄不會出現。列表只有任務 ID、模型、狀態、費用、結算狀態和時間;不回傳提示詞、輸入、輸出或媒體連結。找到目標任務後,用 recordInfo 取得結果。

支援 state、精確目錄 model 或已宣告別名,以及 UTC 日期 from / toYYYY-MM-DD,左閉右開)。預設查詢截至 UTC 明天的七天,最長 92 天;limit 預設 20,最大 100。

curl --fail-with-body \
  'https://api.spicyapi.ai/api/v1/jobs?from=2026-09-01&to=2026-09-07&state=failed&limit=20' \
  -H "Authorization: Bearer $SPICY_API_KEY"

讀取 data.items;當 data.hasMore 為 true,把 data.nextCursor 作為下一頁的 cursor,並保持全部篩選條件和明確的日期範圍不變。列表按 createdAt、任務 ID 由新到舊排列;分頁讀取的是即時狀態,並非凍結快照。不要自行構造游標。查詢歷史不會新建或重試任務。

SDK client.listTasks(...)、CLI spicyapi tasks list 和唯讀 MCP 工具 spicyapi_tasks_list 提供相同能力。cost 保持 USD 十進位字串;settled 為 false 時,它仍是凍結估算額,不能當成最終收費。

建任務的回應遺失時,應先攜帶原 Idempotency-Key 重試完全相同的請求。歷史頁暫時沒有記錄,不代表可以換新冪等鍵重複建立付費任務。

本頁目錄