AppleToken API
一把憑證,接上多個世代的影片生成模型與對話模型。額度、估價、計量與計費都在同一層完成——你不需要面對任何上游供應商的帳號、金鑰或帳單。
chat/completions,計費逐 token。
額度模型 →預扣、結算、退還——以及平台在背景幫你收尾的部分。
錯誤碼 →每個狀態碼該重試、該修參數,還是該找我們。
這個 API 是什麼
AppleToken 是一層生成服務的中介。你用我們發的憑證呼叫我們的端點,我們代為向後端算力供應商送件、把成品取回、依實際用量計費。目前提供兩類能力:影片生成(非同步任務)與對話(同步,可串流)。
- 基底網址
https://appletoken.app- 版本前綴
/v1(本文件涵蓋的全部端點)- 協定
- HTTPS,HTTP/1.1
- 認證
Authorization: Bearer at_live_…- 編碼
- 請求與回應一律 UTF-8 JSON(成品下載與對話串流除外)
- 金額單位
- 美元(USD)。所有價格、額度、帳務數字皆同。
你只會看到 AppleToken 的命名與識別碼。模型名稱、任務 id、成品網址都是本平台自有的——底層用哪一家、哪一代模型,是我們的實作細節,不會出現在任何回應裡,也不需要你關心。
三件事值得先知道
- 費用可以在送出前算準。 影片用
POST /v1/quote,不扣額度、不呼叫後端,直接回傳這組參數會花多少錢。對話沒有估價端點,但預扣公式是公開的,見對話的預扣與串流計費。 - 送件即預扣額度。 影片任務一旦送出就無法取消,額度會立刻凍結,等結算或退還;對話則以
max_tokens為上限先凍結,結束後用實際用量結算。細節見額度模型。 - 結算由平台負責,不是由你的輪詢負責。 平台有背景程序主動追蹤每一筆任務並完成結算,你查不查都不影響帳務正確性,凍結不會因為你沒查詢而卡住。你仍然應該輪詢——那是你知道任務結果、能夠取件的唯一方式。詳見結算、退還與對帳。
五分鐘上手
1. 取得憑證
向 AppleToken 索取一組 API 憑證,格式長這樣:
at_live_9f2c81ab_kJ3nR7xQwZ0pLm5TfYbHs2VdEcAu1NgX
憑證只會在建立與換發的當下顯示一次。沒有任何端點可以再把它讀出來——遺失就只能換發,舊的立即失效。
2. 看看有哪些模型
取得你這張憑證可以使用的模型與現行價格:
curl https://appletoken.app/v1/models \
-H "Authorization: Bearer $APPLETOKEN_KEY"
3. 先問價格
curl https://appletoken.app/v1/quote \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Seedance-2.0-fast",
"seconds": 5,
"resolution": "720p",
"aspect_ratio": "16:9"
}'
{
"model": "Seedance-2.0-fast",
"unit": "output_tokens",
"billable": { "output_tokens": 108900 },
"price": 0.539055,
"estimated": false,
"available": 20.0
}
4. 送出任務
curl https://appletoken.app/v1/videos/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Seedance-2.0-fast",
"prompt": "清晨的漁港,漁船緩緩進港,海面反射金色晨光",
"seconds": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"generate_audio": true
}'
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "processing",
"model": "Seedance-2.0-fast",
"created_at": 1756300000,
"price_reserved": 0.539055
}
5. 輪詢到完成
curl https://appletoken.app/v1/videos/job_1a2b3c4d5e6f7a8b9c0d \
-H "Authorization: Bearer $APPLETOKEN_KEY"
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "completed",
"model": "Seedance-2.0-fast",
"seconds": 5,
"resolution": "720p",
"created_at": 1756300000,
"completed_at": 1756300182,
"content": "/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content",
"price_charged": 0.539055
}
6. 取件
curl https://appletoken.app/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-o output.mp4
quote ──► generations ──► videos/{id} ──► videos/{id}/content
估價 送出+預扣額度 輪詢+取得結果 下載 mp4
不扣額度 回 job_… completed 才可取件 不影響額度
結算在背後獨立進行:平台的背景程序會追到每一筆任務的終局,
你的輪詢只是順手觸發同一段結算邏輯,兩者冪等。7. 想要對話,只要一個呼叫
對話是同步的,沒有任務 id、不需要輪詢:
curl https://appletoken.app/v1/chat/completions \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ByteDance-Seed-1.8",
"max_tokens": 512,
"messages": [{ "role": "user", "content": "用三句話說明什麼是向量資料庫" }]
}'
max_tokens 不可省略——平台以它為上限預扣額度。完整契約見 POST /v1/chat/completions。
認證
所有需要認證的端點都用同一種方式:HTTP 標頭帶 Bearer 憑證。沒有 OAuth、沒有簽章、沒有時間戳。
Authorization: Bearer at_live_9f2c81ab_kJ3nR7xQwZ0pLm5TfYbHs2VdEcAu1NgX
標頭是 Bearer 加空白再接憑證。Bearer 的大小寫不拘
(bearer、BEARER 都可以,這是 HTTP 規範對 auth-scheme 的要求),
中間的空白也允許多個。但憑證本身大小寫嚴格,且不能放在 query string 或 request body。
憑證結構
at_live_<id>_<secret>
│ └─ 機密部分。伺服器只保存不可逆摘要,我們自己也還原不出來。
└──────── 憑證 id,8 個十六進位字元。可公開,用於對帳與客服溝通。
保管建議
- 放在環境變數或密鑰管理服務,不要進版控、不要進前端、不要寫進設定檔。
- 永遠不要下發到瀏覽器或行動 App。憑證等同你的額度,請一律由你的後端持有並代呼叫。
- 一旦憑證出現在截圖、log、對話紀錄裡,就當作外洩處理,立即換發。
- 不同環境(正式/測試)與不同用途,建議申請不同憑證,方便分開看用量與停用。
憑證失效時的行為
| 情況 | 回應 | 說明 |
|---|---|---|
| 未帶或格式不符 | 401 unauthorized | 訊息為「憑證格式不正確」 |
| 憑證不存在,或機密部分錯誤 | 401 unauthorized | 兩者回完全相同的訊息「憑證無效」,以避免憑證列舉 |
| 憑證被停用 | 403 forbidden | 「這組憑證已被停用」 |
| 憑證已到期 | 403 forbidden | 與停用共用同一句訊息,刻意不區分 |
| 憑證已換發 | 401 unauthorized | 舊憑證在換發當下立即失效,沒有寬限期 |
| 模型不在憑證的允許清單 | 404 not_found | 「未知的模型 {model}」——與模型不存在時回同一種錯誤,避免拿來探測目錄。影片與對話同樣適用 |
| 額度不足 | 402 quota_exceeded | 這不是認證問題,憑證仍然有效 |
/v1/* 不使用 cookie,因此沒有 CSRF 相關要求;也不綁定來源 IP。
請求與回應慣例
請求
- 只支援
GET、HEAD、POST。其他方法(PUT/PATCH/DELETE)沒有對應路由,會得到404 not_found。 POST的 body 必須是合法 JSON 物件(最外層是{},不能是陣列或純量),並帶Content-Type: application/json。型別不對會回400「Content-Type 必須是 application/json」。- Body 上限 24 MB(因應內嵌 data URI 檔案而提高,見內嵌檔案自動轉存)。若要傳圖片,可用可公開存取的網址,或直接內嵌 data URI。
- 未列在文件中的欄位會被靜默忽略,不會報錯。詳見建立任務的白名單說明。
回應
- 成功時直接回資料物件,沒有
{"data": …}外層——列表型端點例外(/v1/models、/v1/jobs、/v1/usage會包在data陣列裡)。 - 時間欄位一律是 Unix 秒級時間戳(整數,UTC)。
- 金額為浮點數,單位美元。平台內部以 micro-dollar(百萬分之一美元)的整數運算,回應時轉成最多 6 位小數,因此不會有浮點尾差。
- 回應一律帶
Cache-Control: no-store,請不要快取任何回應。 - 兩個端點不回 JSON:
/v1/videos/{id}/content回原始位元組;/v1/chat/completions在stream: true時回text/event-stream。
錯誤
所有錯誤共用同一個外層結構:
{
"error": {
"code": "quota_exceeded",
"message": "額度不足",
"needed": 1.25,
"available": 0.42
}
}
code 是穩定的機器可讀識別,請用它來分支處理;message 是給人看的,措辭可能調整。部分錯誤會附加額外欄位(上例的 needed、available),詳見錯誤碼總表。
模型目錄
可用的模型與價格會隨時增減,所以不列在這份文件裡——
這裡只講不會變的協定規則。你需要知道兩件事:id 是你在請求裡寫的字串,
命名穩定、不會因為底層更換而變動;modality 決定它走哪一組端點——
video 走 /v1/videos/generations,
image 走 /v1/images/generations,
text 走 /v1/chat/completions,
互相呼叫會得到 400。
模型清單——網頁版,列出每個模型的參數限制與 完整費率階梯。要登入才看得到:費率是商業資訊, 只對已開通的帳號揭露,未登入點進去會被導到登入頁。還沒有帳號的話, 先註冊再回來看。
GET /v1/models——程式版,
整合時請從這裡讀取而不要寫死。兩者都直接從平台的 catalog 產生,
後台改完立刻反映,不會出現文件與實際不符。
若你的憑證設有模型白名單,GET /v1/models
只會回白名單內的模型。清單上有的就是你能用的,不會出現叫了才發現沒權限。
平台在整備上游對接期間,公開目錄可能不含任何模型。 我們不會列出還不能真正叫用的模型——你照著串接卻叫不動,那是我們的問題。 協定不受影響,這份文件描述的一切在目錄有內容後即刻適用。
兩種計價單位
每個模型的 pricing.unit 會告訴你它用哪一種:
| 單位 | 怎麼算 |
|---|---|
output_tokens |
影片依畫面尺寸與影格數換算成 token 量;文字直接用輸出的 token 數。再乘上單價。公式在此。 |
video_seconds |
秒數 × 單價。與解析度無關。 |
有多段費率的模型,pricing.from 是最低單價、pricing.to 是最高單價。
多段費率
有些模型的單價不是固定值,會依請求的條件切換——例如帶影片參考、或解析度拉到 1080p 以上。 費率由上往下比對,第一條命中的勝出。
哪些模型有幾段、各段的條件與單價,列在
模型目錄(需登入)——那一頁直接從平台的 catalog 讀,
永遠是當下生效的數字。這裡不重複,免得兩邊對不上。
未登入時請改用 GET /v1/models,憑證即可讀取。
不確定會落在哪一段時,先呼叫 /v1/quote——它回的
price 就是實際會凍結的金額,不必自己推算。
三種輸入模式
所有影片模型共用 POST /v1/videos/generations 這一個端點,實際模式由你給的欄位決定:
| 模式 | 關鍵欄位 | 用途 |
|---|---|---|
| 文生影 | 只給 prompt | 純文字描述生成。 |
| 圖生影 | image_url 或 frame_images | 以圖片作為起始畫面延伸出影片。 |
| 參考導引 | input_references | 提供多張圖、影片片段或音訊作為風格/內容/節奏參考。 |
frame_images 與 input_references 不可同時提供。兩者都需要時,請一律走 input_references,並在 prompt 裡描述期望的起始畫面——注意這不保證像素級一致。
參考素材的數量上限
| 模型 | 參考素材總數 | 組成上限 | 純音訊參考 |
|---|---|---|---|
Seedance-2.5 | 50 | 圖片 30、影片 10、音訊 10 | 支援 |
Seedance-2.0/-fast/-mini | 15 | 圖片 9、影片 3、音訊 3 | 不支援,至少要有一張圖片或一段影片 |
有宣告 constraints.maxReferences 的模型 | 以該欄位為準 | 總數以該欄位為準;上表兩組的組成上限仍然適用 | |
| 其他模型 | 沒有平台層的數量限制,但上游仍可能拒絕,建議先用 /v1/quote 確認 | ||
上表列出的模型,平台會在送出前檢查數量並回 400 validation_error,不會凍結額度、也不會產生任務。訊息會直接說明是哪一類超量,例如「最多接受 30 個圖片參考素材」。
沒有列在上表、也沒有宣告 constraints.maxReferences 的模型仍然不做預先檢查——那類請求會送出成功(狀態為 processing),錯誤在任務被追蹤到終局時才以 status: "failed" 出現。
計數看的是每個項目的 type。沒有填 type 的項目計入總數,但不計入任何一種組成上限——這類項目平台不替你推斷,能不能用由上游決定。
影片參考與長度
當 input_references 含 type: "video" 的項目時,成品長度由參考影片決定,你給的 seconds 不會改變輸出長度。
seconds仍然必填——平台用它估價與預扣額度。- 實際長度與最終費用以結算為準;最終金額就是
/v1/usage那一列的price。 - 若不確定參考影片長度,建議估高不估低,避免結算金額超過預扣造成餘額變負。
圖片素材的格式要求
- 必須是圖片檔的直連網址(結尾是實際圖片資源),不能是網頁網址。傳網頁網址會得到「格式不支援」類的失敗。
- 本機檔案無法被後端直接取得,可用 base64 data URI 內嵌——平台會自動轉存成網址再送出(見內嵌檔案自動轉存);或先上傳到你自己可公開存取的儲存空間。
- 建議事先轉成 JPEG 並把長邊縮到 2048 以內:可同時避開格式不支援,並讓內嵌的 data URI 遠低於 20 MB 的單檔上限。
包含可辨識真人臉孔的參考素材,不能直接給公開網址或內嵌 data URI——必須先上傳成資產,再用 asset:// 引用,做法見素材資產。直接送件會以任務失敗告終,且該次額度仍會走完預扣與退還流程。
名人與公眾人物的肖像一律不支援,上傳成資產也不能用。
參數限制總表
長度 seconds
- 必須是數字型別(JSON number)。傳字串
"5"會回400 validation_error「seconds 必須是數字」——這是明確的參數錯誤,修好再送即可。 - 必須大於 0,且落在該模型允許的範圍內。部分模型只接受列舉的長度(
constraints.allowedSeconds), 其餘走區間(constraints.minSeconds/constraints.maxSeconds)。模型未宣告上限時,平台仍會套用 60 秒的硬上限—— 沒有上界的請求會凍結一筆上游必然拒絕的金額。 - 整數為宜。長度只由這個參數決定——在
prompt裡寫「三十秒的影片」不會生效。
解析度 resolution 與尺寸 size
resolution接受"480p"、"720p"、"1080p"等形式(也接受不帶p的"720"),必須在該模型的支援清單內。size可直接指定像素,格式"1280x720"。同時提供時size優先決定計價用的畫面尺寸。- token 制的模型必須至少提供
resolution或size其中之一,否則無法精算用量,會回400。
size 一樣要通過解析度檢查
只給 size 而不給 resolution 時,平台會把 size 的短邊換算出來,再套用該模型的解析度清單檢查。例如對只支援 480p / 720p 的模型送 "1920x1080"(短邊 1080),會直接回 400「{model} 支援的解析度為 480p、720p」,不會送到後端、也不會產生任何費用。
兩個細節值得注意:一是條件式費率是看 resolution 而不是 size——
若某個模型的高費率條件寫的是解析度門檻,只給 size 會落在預設費率;
哪些模型有幾段費率請看 模型目錄(需登入)或 GET /v1/models 的 pricing。
二是短邊必須剛好等於清單裡的某個值(720p 清單接受短邊 720,不接受 700 或 768)。除非確有精確畫面需求,建議只用 resolution。
畫面比例 aspect_ratio
| 值 | 比值 | 720p 時的實際像素 |
|---|---|---|
16:9(預設) | 1.778 | 1280 × 720 |
9:16 | 0.5625 | 720 × 1280 |
1:1 | 1.0 | 720 × 720 |
4:3 | 1.333 | 960 × 720 |
3:4 | 0.75 | 720 × 960 |
21:9 | 2.333 | 1680 × 720 |
填入清單以外的值(例如 "2:1")不會報錯,系統會退回預設的 16:9 並以此計價。請只使用上表的值,並在送件前用 /v1/quote 確認算出來的價格符合預期。
音訊 generate_audio
- 布林值。僅對音訊欄位標示為 ✅ 的模型有效。
- 模型的
constraints.audio為false時不支援音訊;對它傳true不會報錯,但成品不會有聲音。 未宣告audio的模型代表平台沒有這項資訊,實際是否出聲以上游行為為準。 - 音訊不影響計價——費用只看畫面尺寸、影格數或秒數。
對話參數
| 欄位 | 限制 | 不符合時 |
|---|---|---|
messages | 非空陣列,每個元素的 role 與 content 都必須是字串 |
400「缺少 messages」或「messages[i].content 必須是字串」 |
max_tokens | 必填,必須是大於 0 的數字(小數會被無條件捨去成整數) | 400「max_tokens 必須是大於 0 的數字(平台以它為上限預扣額度,因此不可省略)」 |
temperature | 選填,必須是數字;平台不檢查範圍,原樣轉給後端 | 400「temperature 必須是數字」 |
stream | 選填,必須是布林值 true / false。平台只做真假值判斷,因此任何非空字串(包含 "false")都會被當成 true 而啟用串流 |
不報錯,但可能走到你沒預期的模式 |
GET /v1/models
取得模型目錄與現行價格。
不帶任何參數時,行為與這個端點原本的樣子完全相同——回傳同一份完整清單,既有整合不需要修改。以下三個查詢參數都是選填,讓你在目錄變大之後可以自行篩選。
若這張憑證設有模型白名單,這裡只會列出白名單內的模型——
你看到的清單就是你實際能呼叫的清單,不會出現叫了才發現沒權限的情況。
沒有設白名單的憑證會看到全部可用模型。下面的 modality、q 篩選是疊加在這層過濾之上,不會取代它——即使關鍵字或模態命中白名單以外的模型,一樣不會出現在結果裡。
Query 參數
| 參數 | 預設 | 範圍 | 說明 |
|---|---|---|---|
modality | 不篩選 | video / image / text / audio / embedding | 只回傳指定模態的模型。不在這個集合內回 400 validation_error,訊息會列出可用的值。 |
q | 不篩選 | 最長 100 字元 | 關鍵字比對,同時比對模型的 id、label、description 三個欄位,不分大小寫、子字串比對。超過長度上限回 400 validation_error。 |
limit | 500 | 1–500 | 回傳筆數上限。這裡的預設值是 500,不是 /v1/jobs、/v1/usage 的 100——模型目錄不是時序資料,「最近 100 筆」對它沒有意義,沿用同一個預設反而會讓既有整合在無聲無息中少拿到模型。超出範圍同樣回 400 validation_error,不會被夾到邊界值。若目錄未來成長超過 500 個模型,這個上限會需要調整。 |
三個參數可以自由組合,例如同時用 modality 縮小模態、q 比對關鍵字。
curl "https://appletoken.app/v1/models?modality=text&q=gpt" \
-H "Authorization: Bearer $APPLETOKEN_KEY"
回應
{
"data": [
{
"id": "Seedance-2.0-fast",
"label": "Video Fast",
"description": "生成較快,成本較低",
"modality": "video",
"constraints": {
"minSeconds": 4,
"maxSeconds": 15,
"resolutions": ["480p", "720p"],
"audio": true
},
"pricing": { "unit": "output_tokens", "from": 4.95, "to": 4.95 }
},
{
"id": "veo-3.1-generate-001",
"label": "Video Cinema",
"description": "電影感畫質,固定 4/6/8 秒",
"modality": "video",
"constraints": {
"allowedSeconds": [4, 6, 8],
"resolutions": ["720p", "1080p"],
"audio": true
},
"pricing": { "unit": "video_seconds", "from": 0.9, "to": 0.9 }
},
{
"id": "ByteDance-Seed-1.8",
"label": "Chat Pro",
"description": "長上下文與複雜推理,支援串流",
"modality": "text",
"constraints": {
"maxOutputTokens": 8192,
"defaultMaxOutputTokens": 2048
},
"pricing": {
"unit": "output_tokens", "from": 3, "to": 3,
"components": [
{ "unit": "input_tokens", "from": 0.375, "to": 0.375 },
{ "unit": "output_tokens", "from": 3, "to": 3 }
]
}
}
]
}
| 欄位 | 型別 | 說明 |
|---|---|---|
id | string | 模型識別,用於所有請求的 model 欄位。 |
label | string | 顯示名稱,適合直接放進你的 UI。 |
description | string | 一句話定位說明。 |
modality | string | "video"、"image"、"text"、"audio"、"embedding" 其中之一(沒有 "chat" 這個值——對話模型的 modality 是 "text")。決定這個模型走哪一組端點:"video" 走 /v1/videos/generations,"image" 走 /v1/images/generations,"text" 走 /v1/chat/completions。請用它來分類而不是靠 id 前綴猜。 |
constraints | object | 這個模型的參數限制。所有限制都包在這個物件裡,鍵名是 camelCase(不是攤平在頂層、也不是 snake_case)。物件一定存在,但可能是空的 {}——沒有宣告任何限制的模型就是空物件。未知的鍵請忽略。 |
constraints.minSeconds / constraints.maxSeconds | int | 長度區間。與 allowedSeconds 互斥,只會出現其中一組。僅影片模型有。 |
constraints.allowedSeconds | int[] | 只接受列舉值的模型才有。沒有這個欄位也沒有 maxSeconds 時,平台套用 60 秒硬上限。 |
constraints.resolutions | string[] | 允許的 resolution 值。僅影片模型有。 |
constraints.aspectRatios | string[] | 允許的 aspect_ratio 值。有宣告的模型才有。 |
constraints.audio | bool | 是否能產生音軌。僅影片模型有。 |
constraints.maxReferences | int | 可帶的參考素材數量上限。有宣告的模型才有。沒有這個欄位不代表沒有上限——部分模型的上限由平台內建,見參考素材的數量上限。 |
constraints.maxOutputTokens / constraints.defaultMaxOutputTokens | int | 輸出 token 的上限,以及你沒有指定 max_tokens 時預扣所依據的預設值。對話模型才有。 |
constraints.sizes | string[] | 允許的 size 值,含 2K 這類簡寫。僅圖片模型有。 |
constraints.minPixels / constraints.maxPixels | int | 兩個一起出現,代表這個模型除了 sizes 列舉的值之外,還接受任意 寬x高 像素字串,只要寬 × 高的總像素落在這個閉區間內。沒有這兩個欄位 = 只能用 sizes 裡的值。僅圖片模型有,見圖片端點。 |
pricing.unit | string | "output_tokens" 或 "video_seconds"。 |
pricing.from / to | float | 同一個計價單位(pricing.unit)之下的價格區間(USD):from 是最低單價、to 是最高單價。兩者相等表示單一費率,不等表示這個模型有多段費率;實際落在哪一段由請求的條件決定,用 /v1/quote 可以確認。它們不是「輸入單價/輸出單價」——輸入與輸出各自的單價請讀 pricing.components。 |
pricing.components | array | 有多個計價單位的模型(例如輸入與輸出各有單價)才有,每個元素是 { "unit", "from", "to" },各單位有自己的區間。單一計價單位的模型不會有這個欄位,請用「這個欄位存不存在」判斷。詳見對話的預扣與串流計費。 |
目錄未來可能新增模型、新增 modality,或補上欄位。請以 id 為準做對映,遇到未知欄位或未知 modality 忽略即可,不要用嚴格 schema 驗證擋掉整份回應。
內嵌檔案自動轉存
影片端點(POST /v1/videos/generations)的素材參數收的是網址,平台會把網址交給後端去下載——如果那個網址需要登入、是私有分享連結、或有防盜鏈,後端就抓不到,送件會失敗並回報「resource download failed」。若你沒有可公開存取的儲存空間,可以直接把檔案以 data URI 的形式放進素材參數——平台會在送件前自動把它存進暫存空間、換成一條可下載的網址再交給後端,呼叫方式完全不用改變。
這個轉存過程不呼叫任何模型,不扣額度。
| 做法 | 適合 |
|---|---|
| 直接內嵌 data URI | 沒有可公開存取的儲存空間時。 |
| 自備公開網址 | 你本來就有可匿名存取的儲存空間——大檔或重複使用的素材建議用這個,避免每次都傳一份 base64。 |
適用的參數:影片端點(POST /v1/videos/generations)的 image_url、input_references、frame_images——巢狀在物件或陣列裡的值也會被處理;以及圖片端點(POST /v1/images/generations)的 image,那裡只收圖片型別(image/png、image/jpeg、image/webp、image/gif)。對話端點(POST /v1/chat/completions)不支援這個行為。格式為 data:image/png;base64,<base64 內容>。
- 支援的型別:
image/png、image/jpeg、image/webp、image/gif、video/mp4、video/quicktime、video/webm、audio/mpeg、audio/wav、audio/mp4;每個檔案上限 20 MB,型別不支援或超過大小一律回400 validation_error。 - 內容必須真的是宣告的型別:平台會檢查檔案開頭的識別位元組來確認實際格式,宣告
image/png卻放進去的其實是 HTML、指令碼或其他格式,會被擋下並回400 validation_error。 - 一次請求最多內嵌 10 個檔案,超過會回
400 validation_error。 - 值不是 data URI 時完全不受影響——一般網址原樣送給後端,這個功能不會改變既有整合的行為。
base64 編碼會讓資料膨脹約 1.33 倍,而整個 request body 上限是 24 MB。檔案較大、或同一份素材要在多次請求裡重複使用時,建議改用你自己可公開存取的網址——避免每次請求都重新內嵌一份 base64。
POST /v1/quote
用一組參數換一個價格。不扣額度、不凍結、不送件、不呼叫後端,可以放心在使用者按下「生成」之前先呼叫,用來顯示預估費用。
這個端點的估價邏輯建立在送出前就算得出來的量上:影片是「畫面尺寸 × 影格數」或「秒數」,圖片是張數(按 token 計價的圖片模型則是每張的 token 上限)。兩者都報得出來。
對話模型沒有這種量——費用取決於實際產生多少 token,無法事先算準,因此對 chat-* 呼叫 /v1/quote 會回 400。對話改以 max_tokens 為上限預扣,公式完全公開,見對話的預扣與串流計費。
請求欄位
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
model | string | 必填 | 模型 id。 |
seconds | number | 必填 | 長度,必須 > 0 且為數字型別。 |
resolution | string | 視模型 | token 制模型需要它(或 size)才能算量。 |
size | string | 選填 | "1280x720"。提供時優先於 resolution 決定尺寸,但仍要通過解析度檢查。 |
aspect_ratio | string | 選填 | 預設 "16:9"。 |
input_references | array | 選填 | 只用來判斷是否含影片參考。部分模型的費率會因此跳到另一段(見各模型的 pricing),內容不會被送到後端。數量仍然會驗:超過上限的話估價就會回 400,不必等到送件,見參考素材的數量上限。 |
要報價圖片模型,請送與 POST /v1/images/generations 相同的欄位(model、prompt,以及 n/size/image 等),不要送 seconds。平台跑的是與送件完全相同的那一套驗證,所以參數不合法時這裡就會回 400,而且不扣額度、不凍結。
圖片模型的 prompt 是必填——這是與影片不同的一點,理由同上:驗證與送件同一套。
prompt 在這個端點不是必填——只想問價格可以不給,價格本身與內容無關。但它是兩條回應路徑的分岔點:影片模型且帶了一個非空白的 prompt,回應就會多一張報價單,可以拿去確認後送件(見送件前確認);沒帶就是單純的價格查詢。只有空白字元的 prompt 視同沒帶。
回應:兩條路徑
這個端點有兩條回應路徑,分岔只看一件事:有沒有帶 prompt。
| 路徑 | 什麼時候走這條 | 回應 |
|---|---|---|
| 純比價 | 沒帶 prompt,或模型不是影片模型 | 只有下面那幾個欄位,與這個端點原本的回應一個字都沒變。 |
| 報價單 | 影片模型且帶了非空白的 prompt | 純比價的欄位全部保留,再疊加一組報價單欄位(quote_id、resolved、materials 等)。 |
純比價那條路徑的回應形狀完全沒有變動。只帶模型與秒數問價錢的程式碼不需要修改,也不會突然多收到欄位——報價單欄位只在你自己帶了 prompt 時才出現。
路徑一:純比價
{
"model": "Seedance-2.0-fast",
"unit": "output_tokens",
"billable": { "output_tokens": 108900 },
"price": 0.539055,
"estimated": false,
"available": 20.0
}
| 欄位 | 說明 |
|---|---|
unit | 計價單位,"output_tokens" 或 "video_seconds"。 |
billable | 計費量。token 制為 {"output_tokens": N};秒制為 {"video_seconds": N}。 |
price | 本次預估費用(USD,6 位小數)。這也是送件時會被凍結的金額。 |
estimated | 這個價格是精算還是上限估計。false=精算,這個數字就是送件時會凍結、也是預期實收的金額——影片一律是這種(秒制與 token 制都是),按張計價的圖片模型也是。true=只給得出上限,實收會等於或低於它,最終金額以結算為準——目前只有按 token 計價的圖片模型會回這個值。 |
available | 呼叫當下帳戶的可用餘額。額度掛在帳戶上,名下所有憑證共用同一筆。這是即時快照,不代表送件當下仍然足夠。 |
圖片模型也走這條路徑(圖片沒有報價單,不會有 quote_id)。送出與送件相同的參數,回應形狀完全一樣:
{
"model": "imagen-4-fast",
"unit": "request",
"billable": { "request": 2 },
"price": 0.12,
"estimated": false,
"available": 20.0
}
estimated 說明這個數字的性質:按張計價的圖片模型 unit 是 "request"、estimated 為 false(精算,等於實收);按 token 計價的圖片模型 unit 是 "output_tokens"、estimated 為 true(上限估計,實收以結算為準)。
路徑二:報價單(帶了 prompt)
回應是路徑一的全部欄位,再加上下面這些。多出來的部分是讓你在送出前核對「實際會送給後端的到底是什麼」,確認無誤再憑 quote_id 送件——回 POST /v1/videos/generations 帶上 quote_id 與 confirm 就會真的送出,完整做法見送件前確認,兩個入口產生的是同一種報價單。
{
"model": "Seedance-2.0-fast",
"unit": "output_tokens",
"billable": { "output_tokens": 108900 },
"price": 0.539055,
"estimated": false,
"available": 20.0,
"quote_id": "q_a1b2c3d4e5f6a7b8c9d0",
"expires_at": "2026-09-02T10:30:00Z",
"payload_digest": "sha256:3f2a…",
"resolved": {
"model": "Seedance-2.0-fast", "prompt": "海邊的黃昏,鏡頭緩慢推近",
"seconds": 5, "resolution": "720p", "aspect_ratio": "16:9",
"image_url": "https://…/tenants/…/uploads/….jpg",
"async": true, "seed": null,
"seed_note": "未指定 seed,上游每次生成的結果都不同;要重現近似結果請指定 seed"
},
"materials": [
{ "field": "image_url", "index": 0, "upload_id": "…", "bytes": 545459,
"mime": "image/jpeg", "sha256": "…",
"url": "https://…/tenants/…/uploads/….jpg", "preview_url": "https://…" }
],
"estimate": { "output_tokens": 108900, "points": 0.539055,
"basis": "exact", "billable": { "output_tokens": 108900 },
"unit": "output_tokens" },
"balance": { "available": 20.0, "sufficient": true,
"note": "此報價不保留額度,確認送出時才凍結" }
}
| 欄位 | 說明(以下全部只在報價單路徑出現) |
|---|---|
quote_id | 報價單編號。送件時憑它確認,用法見送件前確認。 |
expires_at | 報價單的有效期限,ISO 8601 時間字串(例如 "2026-09-02T10:30:00Z")。過期後這張單子不能再拿來送件。 |
payload_digest | 最終送件內容的指紋。確認送出時平台會重新組一次請求並比對這個值,內容被改過就不放行。 |
resolved | 你送出的每一個參數,原樣回顯(包含 prompt)。只有兩處不是原樣:model 換成對外的模型名稱,以及 seed 沒帶時補成 null,讓「這次沒帶」看得出來。 |
resolved 的素材欄位 | image_url/frame_images/input_references 在這裡的值,是轉存之後、實際會送給後端的網址,不是你原本送來的 data URI。這正是要你核對的東西:你看到的網址,就是後端會去下載的那一條。 |
resolved.seed_note | 一句說明,告訴你這次有沒有帶 seed,以及那對結果的影響。沒帶 seed 時說明每次生成都會不同;有帶時說明上游會以它為隨機起點。 |
materials | 陣列,每個被轉存的素材一筆:field(來自哪個欄位)、index、upload_id、bytes、mime、sha256(內容指紋),以及 url 與 preview_url。這兩個網址目前是同一條,也與 resolved 裡對應欄位的值相同。沒有內嵌素材時是空陣列。 |
estimate | { points, basis, billable, unit };token 制模型另有 output_tokens。points 就是這筆的估算金額,與外層的 price 相同。 |
estimate.basis | "exact" 表示這是精算不是估計;"upper_bound" 表示只能給出上限,實收會等於或低於它。 |
balance | { available, sufficient, note }。note 是一句固定說明,講的就是下面這則警告。 |
balance.sufficient | 呼叫當下的餘額夠不夠付這一筆。只是快照,不是保留。 |
拿到 quote_id 不代表額度被保留下來了。balance.sufficient 是呼叫當下的快照,同帳戶的其他請求(或你自己接著送的別件)都可能把餘額用掉,因此它不保證你送件時仍然足夠。額度要到確認送出的當下才凍結。
這是刻意的:比價本來就會連打很多次,每次都凍結會把餘額卡光。
對秒制模型,估價通常就是實收。對 token 制模型,最終金額以後端回報的實際 token 量重算,可能略高或略低於估價——差額會在結算時反映。詳見結算、退還與對帳。
POST /v1/videos/generations
送出一個生成任務。這個呼叫會立即凍結額度,且送出後無法取消。
這條端點有兩種送法,用的是同一個網址、同一份請求欄位,差別只在回應。直接送出是預設:帶齊下表的參數一次送出,回應就是任務本身。先確認再送出則是這一次呼叫帶上 require_confirmation: true(或由帳戶層開啟「送件前確認」)——同樣的請求不會直接送出,平台先把「即將送出的內容」回給你一張確認單(列出即將送出的每一個參數與素材網址、預估點數),你核對無誤再回一個 confirm,才真的送出。像匯款——按下送出之後才跳確認,而不是要你先去別的地方領一張單子再回來。不帶 require_confirmation(或帶 false)、帳戶也沒開,就是直接送出。帳戶層的開關是下限:後台為你的帳戶開啟「送件前確認」之後,即使這一次帶 require_confirmation: false,一樣會回確認單。
require_confirmation: false 也繞不過
這個開關預設關閉,關閉時這條端點的行為完全不變。一旦由後台為你的帳戶開啟,它就凌駕請求裡的旗標:即使你明確帶 require_confirmation: false,這條端點也不會直接送件。開關是下限而不是預設值——開了之後,請求說不要不該把它關掉。
既有的自動化流程會在沒有任何徵兆的情況下停止送件——HTTP 狀態碼仍然是 200,但 object 從 "video" 變成 "video.confirmation",回應裡沒有 id 可以拿去輪詢。所以這個開關由我們為你的帳戶開啟,開啟前請先與我們確認,並確認你的程式碼已經會判斷 object 與 status。
請求欄位
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
quote_id | string | 二擇一 | 確認單編號,見送件前確認。帶了它就不能再帶下表其他任何欄位,否則回 400——確認階段不允許改動內容。不帶則走直接送件,下面的必填欄位照舊。 |
confirm | bool/string | 選填 | 只在帶 quote_id 時有意義。true(或 "yes")送出、false(或 "cancel")取消;不帶則把確認內容再回給你看一次,不送出。 |
model | string | 必填 | 模型 id,必須是 modality: "video" 的模型。 |
prompt | string | 必填 | 內容描述。不可為空字串。 |
seconds | number | 必填 | 長度。數字型別,須符合模型限制。 |
resolution | string | 視模型 | 解析度,須在模型的支援清單內。 |
aspect_ratio | string | 選填 | 沒帶時由後端決定預設比例,平台不會替你補上。但平台的計價在沒帶時一律以 "16:9" 估算,所以實際產出的比例與估價依據可能不同。 |
size | string | 選填 | "1280x720",決定尺寸時優先於 resolution。 |
generate_audio | bool | 選填 | 是否產生音軌。 |
negative_prompt | string | 選填 | 不希望出現的內容描述。 |
seed | number | 選填 | 亂數種子,用於重現近似結果。平台不驗證其值。 |
output_format | string | 選填 | 輸出格式偏好。 |
image_url | string | 選填 | 圖生影的起始圖片直連網址或 data URI。沒有公開儲存空間?可直接以 data URI 內嵌,平台會自動代管(見內嵌檔案自動轉存)。 |
frame_images | array | 選填 | 關鍵影格圖片,可為直連網址或 data URI。與 input_references 互斥。 |
input_references | array | 選填 | 多模態參考素材,項目形如 {"type":"image"|"video"|"audio","url":"…"},url 可為直連網址或 data URI。沒有公開儲存空間?可直接以 data URI 內嵌,平台會自動代管(見內嵌檔案自動轉存)。數量有上限:圖片、影片、音訊分開計算,超過會在送件當下回 400,見參考素材的數量上限。帶 {"type":"video"} 的參考素材時,seconds 只用來計價:你仍必須送一個大於 0 的 seconds(否則估不出價),但實際輸出長度由後端依參考影片決定,不是你送的那個數字。 |
post_process | object | 選填 | 音軌風格後處理,形如 {"audio_style":"phone","intensity":3}。任務完成後平台會另外產出一支加了聲音效果的成品,原片不受影響,兩支都可取件(見音軌風格後處理)。這是平台層參數,不會送到後端,也不影響生成內容。 audio_style 可用值:phone(電話)、walkie(對講機)、radio(廣播)、vinyl(老唱片)、muffled(隔壁房間)、hall(大廳迴音)、lofi_16k/lofi_24k(降取樣率)、mute(去音軌);intensity 為 1–5,預設 3(lofi_* 與 mute 忽略此值)。原片沒有音軌時( generate_audio: false)除 mute 外都無意義,後處理會以 no_audio_stream 失敗,但不影響原片的交付與計費。 |
只有上表的欄位會被送到後端。其中兩個常見欄位是被刻意移除的:
async— 影片一律以非同步模式送件,你無法改成同步等待。這是為了避免長影片把連線卡到逾時。callbackUrl/webhook— 基於安全考量一律丟棄。目前沒有回呼機制;請用輪詢取得結果(額度結算不需要你輪詢,見結算章節)。extra_body— 已不再支援。它曾經可以把任意欄位原樣直通後端,那等於在白名單上開一個洞,現在一併丟棄。你送了不會收到錯誤訊息,它只是完全沒有作用。
其他如 n、user、metadata 等欄位同樣會被忽略。stream 在這個端點也無效——串流只存在於對話端點。若你送出後發現某個參數毫無作用,請先確認它在上表之中。
一個例外:post_process 在上表之中,但它不會被送到後端——它描述的是成品產生「之後」平台要做的事,與生成內容無關。
回應:直接送出
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "processing",
"model": "Seedance-2.0-fast",
"created_at": 1756300000,
"price_reserved": 0.539055
}
| 欄位 | 說明 |
|---|---|
id | 任務識別,格式 job_ + 20 個十六進位字元。請保存它——這是之後查詢與取件的唯一憑據。 |
status | 送件成功時為 "processing"。 |
model | 你送出的模型 id。 |
price_reserved | 本次凍結的金額(USD)。 |
created_at | 建立時間(Unix 秒)。 |
回應中可能出現其他透傳欄位,但只有 id、status、model、price_reserved 是穩定契約,請勿依賴其餘欄位。
200 不代表會成功
部分參數組合的檢查發生在後端排程之後,因此送件會回成功、失敗卻延後到任務被追蹤到終局時才以 status: "failed" 出現。務必以查詢結果為準,不要在送件成功時就通知使用者完成。
回應:先確認再送出時的確認單
開啟送件前確認之後,你照常送出請求——欄位一個都不用改:
curl https://appletoken.app/v1/videos/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Seedance-2.0-fast",
"prompt": "海邊的黃昏,鏡頭緩慢推近",
"seconds": 5, "resolution": "720p",
"image_url": "data:image/jpeg;base64,…"
}'
回應不是任務,而是一張待確認的單子:
{
"object": "video.confirmation",
"status": "needs_confirmation",
"quote_id": "q_a1b2c3d4e5f6a7b8c9d0",
"expires_at": "2026-09-02T10:30:00Z",
"payload_digest": "sha256:3f2a…",
"resolved": {
"model": "Seedance-2.0-fast", "prompt": "海邊的黃昏,鏡頭緩慢推近",
"seconds": 5, "resolution": "720p", "aspect_ratio": "16:9",
"image_url": "https://…/tenants/…/uploads/….jpg",
"async": true, "seed": null,
"seed_note": "未指定 seed,上游每次生成的結果都不同;要重現近似結果請指定 seed"
},
"materials": [
{ "field": "image_url", "index": 0, "upload_id": "…", "bytes": 545459,
"mime": "image/jpeg", "sha256": "…",
"url": "https://…/tenants/…/uploads/….jpg", "preview_url": "https://…" }
],
"estimate": { "output_tokens": 108900, "points": 0.539055, "basis": "exact" },
"balance": { "available": 20.0, "sufficient": true,
"note": "此報價不保留額度,確認送出時才凍結" },
"next": {
"confirm": { "method": "POST", "url": "/v1/videos/generations",
"body": { "quote_id": "q_a1b2c3d4e5f6a7b8c9d0", "confirm": true } },
"cancel": { "method": "POST", "url": "/v1/videos/generations",
"body": { "quote_id": "q_a1b2c3d4e5f6a7b8c9d0", "confirm": false } }
},
"message": "這是即將送出的內容,目前尚未送出、尚未扣點。…"
}
後端完全沒有被呼叫,額度也沒有凍結。素材已經轉存到圖庫(所以 resolved 裡的網址是可以直接點開看的),但這是為了讓你核對,不是已經開始生成。
| 欄位 | 說明 |
|---|---|
status | "needs_confirmation"。看到這個值就代表還沒送出,在等你回覆。 |
quote_id | 這張確認單的編號。確認時要帶它。有效期 15 分鐘。 |
resolved | 你送出的每一個參數,原樣回顯——包含 prompt。素材欄位(image_url/frame_images/input_references)的值是轉存到圖庫之後、實際會送給後端的網址,不是你原本送來的 data URI。沒帶 seed 時會補一個 null 讓你看得到「這次沒帶」。async 是平台強制加的,照實顯示不藏起來。 |
materials | 內嵌素材逐件列出:第幾件、多大、什麼型別、sha256 指紋,以及轉存後的 url(可直接點開)。與 resolved 裡對應欄位的網址是同一條。 |
estimate | basis: "exact" 表示這是精算不是估計——影片的計費量在送出前就算得準。 |
payload_digest | 這張單子送出時,後端會收到的內容的指紋。 |
next | 下一步該打什麼,直接寫在回應裡,不必回頭翻文件。 |
balance.sufficient | 當下餘額夠不夠。這一步不凍結額度,所以是快照不是保留。 |
確認、取消,或再看一次
# 確認送出
-d '{"quote_id": "q_a1b2c3d4e5f6a7b8c9d0", "confirm": true}'
# 取消(要改內容就取消掉,重新送一次完整請求)
-d '{"quote_id": "q_a1b2c3d4e5f6a7b8c9d0", "confirm": false}'
# 只帶 quote_id:把確認內容再看一次,不會送出
-d '{"quote_id": "q_a1b2c3d4e5f6a7b8c9d0"}'
confirm 收布林值,也收人會直接打出來的字:"yes"/"y"/"confirm"/"ok" 都算確認,"no"/"cancel" 算取消。填了看不懂的值一律回 400——確認這件事上,猜你的意思是危險的。
確認成功後的回應與一般送件完全相同(多一個 quote_id 欄位),拿 id 去輪詢即可。取消則回 {"object":"video.confirmation","status":"cancelled"}。
送出時平台會重新組一次請求,跟你確認的那份內容比對指紋(payload_digest),不一致就拒絕送出(回 409)。所以確認畫面上的內容與後端實際收到的內容一定相同,不是靠承諾,是每次都真的比對過。
確認階段的規則
- 帶了
quote_id就不能再帶生成參數,否則回400。要改內容請先取消,再重新送出一次完整請求——確認階段允許改動的話,「確認」就沒有意義了。 - 確認單有效期 15 分鐘,過期回
400。 - 確認之前不凍結額度,確認送出的當下才凍結。
- 兩步必須用同一把憑證;別把憑證(即使是同一個帳戶)一律回
404。 - 同一張確認單重複確認只會產生一個任務,回應帶
"idempotent": true。逾時重試不會扣兩次錢。 - 送件失敗(後端拒收)時額度原路退回,確認單也會退回可用狀態,可以直接重試同一張。
- 素材不會重新上傳——送出去的就是你確認時看到的那幾個檔案。
- 已經送出的取消不了(回
409):後端沒有取消機制,送出去就收不回來。 - 重複取消不算錯誤,第二次的
cancelled會是false,照實告訴你這次沒有真的改變什麼。
同時開多張報價單
同一把憑證可以同時持有多張未確認的報價單,彼此完全獨立,不會互相覆蓋或取消。要對多個模型比價、或一次排好幾支影片再逐一確認,直接開多張就好。
- 每張報價單有自己的
quote_id,確認時各自帶各自的那一張——平台不會替你猜是哪一張。 - 有效期是各自的 15 分鐘,從各自建立的時間起算。
- 報價單不凍結額度,所以多張的總額可以超過你的餘額。超額不會在報價時被擋下來,而是在確認送出的當下才有人撞到
402。 - 撞到
402的那一張會退回可用狀態,加值後拿同一張quote_id直接重試即可;其他還沒確認的報價單不受影響。
做法見完整範例裡的「多張報價單 → 逐一確認送出」。
錯誤
直接送件時,驗證依下列順序進行,先觸發者先回:
- 缺
model→400 - 模型不在憑證的允許清單 →
404 - 長度或解析度不符模型限制、參考素材數量超過上限、缺必要計價參數 →
400(多個問題會用「;」串接在同一則訊息) - 估價金額超過可用餘額 →
402,附needed與available - 缺
prompt→400(刻意排在凍結之前,避免建立又立刻退還的無謂凍結) - 凍結額度(此後才會產生費用紀錄)
- 送往後端;任何階段失敗都會退還凍結,帳本記
release(submit_failed)
帶 quote_id 的確認呼叫走的是另一組檢查:
| 狀態 | 情況 |
|---|---|
400 | 帶 quote_id 時夾帶了生成參數;確認單已過期或已取消;confirm 的值看不懂 |
402 | 確認送出時凍結額度,估價金額超過可用餘額 → 402,附 needed 與 available |
404 | 確認單不存在,或不屬於這把憑證 |
409 | 確認後模型設定或費率有變動,內容已與確認當下不同;要取消一張已經送出的單子;或這張單子正在送出中 |
收到 402 時確認單會退回可用狀態:額度沒有被凍結,這張單子也沒有作廢。加值之後拿同一張 quote_id 再送一次確認即可,不必重新報價(只要還在 15 分鐘有效期內)。
沒有指定 seed 時,後端每次都用新的隨機起點,所以即使模型、秒數、素材完全相同,結果也不會一樣。要重現近似的結果,請自行帶上 seed。確認單的 resolved.seed 會明白告訴你這次有沒有帶。
另外提醒:-fast 版與標準版是不同的模型,秒數與參考圖數量也都會顯著改變成品。resolved 與 materials 就是用來讓這些差異在送出前一眼看得出來。
不想開啟強制確認、但某些請求想先看一眼的話,在 /v1/quote 帶上 prompt 與素材,回應同樣會帶一個 quote_id,接著走上面的確認步驟即可。兩個入口產生的是同一種東西。
GET /v1/videos/{job_id}
查詢任務狀態。若任務已經完成或失敗,這個呼叫會順手完成結算或退還——但結算不依賴你的查詢:平台的背景程序會做同一件事,兩者走同一段程式碼且彼此冪等,先到者生效。
回應
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "completed",
"model": "Seedance-2.0-fast",
"seconds": 5,
"resolution": "720p",
"created_at": 1756300000,
"completed_at": 1756300182,
"content": "/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content",
"outputs": [
{ "type": "original", "status": "completed",
"content": "/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content?output=original" },
{ "type": "processed", "id": "pp_9f8e7d6c5b4a3210fedc", "status": "completed",
"audio_style": "phone", "intensity": 3,
"content": "/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content?output=processed" }
],
"price_charged": 0.539055
}
status | 意義 | 額度影響 |
|---|---|---|
processing | 仍在生成中。 | 維持凍結。 |
completed | 已完成,可以取件。 | 凍結轉為實扣,回傳 price_charged。由本次呼叫或背景程序完成,先到者生效。 |
failed | 生成失敗。 | 凍結全額退還,price_charged 為 0.0。同樣不需要你觸發。 |
content只在completed時出現,是取件用的相對路徑(接在https://appletoken.app後面就是完整網址)。回應裡不會有成品的絕對網址——成品由平台代為轉送,原始網址不外流。若你的程式在等一個叫url的欄位,請改讀這個欄位,或直接呼叫GET /v1/videos/{job_id}/content。outputs只在有後處理時才會有第二筆。第一筆永遠是type: "original",指向與content相同的路徑;第二筆是type: "processed",帶自己的status。後處理失敗不會影響原片:原片照常交付、照常計費,第二筆的status會是failed並附error。content欄位本身的意義與行為完全沒有改變。成品的bytes、duration_seconds、sample_rate不在這裡,改由GET /v1/post-process/{id}提供。price_charged只在已結算後出現。尚在processing時,回應帶的是price_reserved。- 結算是冪等的——重複呼叫不會重複扣款,你的查詢與背景程序同時發生也不會。
- 查詢別人的任務一律回
404,而不是403;我們不透露任務是否存在。 - 查詢時若後端暫時不可用,平台會直接回傳目前已知的狀態,而不是讓你的查詢一起失敗。
首次等待 10 秒後開始查詢,之後每 5–10 秒一次,並設定總時限(建議 15 分鐘)。收到 429 時採指數退避。不要用小於 3 秒的間隔連續輪詢——平台自己也在背景輪詢後端,你查得再密也不會更快。
登入後台的「任務」頁會列出你的每一支任務,已完成的可以直接在頁面上預覽與下載;「用量」頁的每一筆也能展開詳情,回頭看當初送出的內容與拿回的結果。要確認「到底成功了沒」,這是最快的一條路,不必自己寫輪詢。
GET /v1/videos/{job_id}/content
下載成品。回應是 Content-Type: video/mp4 的原始位元組,不是 JSON、也不是轉址。
- 成品由平台代為取回後轉送,原始成品網址不會外流,也無法被第三方直接存取。
- 下載前平台會先向後端刷新這個任務的狀態。若任務此時已經完成,這次呼叫就可能是觸發結算的那一次——結算是冪等的,與背景程序同時發生也只會扣一次款。仍建議先呼叫
GET /v1/videos/{job_id}確認completed,再來取件。 - 任務不存在、不屬於你、或尚無可下載內容,一律回
404。 - 成品保留時間有限,請在完成後盡快下載並存到你自己的儲存空間。
- 帶
?output=processed可取回後處理片;不帶或帶?output=original都是原片(預設行為與過去完全相同)。後處理尚未完成或已失敗時回404。
curl -f https://appletoken.app/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-o output.mp4
# 取回後處理片(例如電話音效那一支)
curl -f "https://appletoken.app/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content?output=processed" \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-o output_phone.mp4
音軌風格後處理
對影片的音軌套用聲音效果,例如讓對白聽起來像從電話裡傳出來。影像不會被重新編碼,只有音軌被處理,所以畫質與原片完全一致。
兩種用法
- 生成時順帶:在
POST /v1/videos/generations帶post_process,任務完成後自動產出第二支成品。 - 對既有影片加工:用下面的獨立端點,來源可以是平台上已完成的任務、你上傳的素材,或內嵌的 data URI。
請求
{
"job_id": "job_1a2b3c4d5e6f7a8b9c0d",
"audio_style": "phone",
"intensity": 3
}
| 欄位 | |
|---|---|
job_id / asset_id / url | 來源三選一,放在 body 頂層(不是包在 source 裡):job_id(平台上已完成的任務)/asset_id(你上傳的素材)/url(內嵌的 data URI,只收 data:video/mp4;base64,…)。三個只能給一個。 |
audio_style | 九選一(見下方清單)。 |
intensity | 1–5 的整數,預設 3。實際套用時會收斂成三個段位:1–2 走輕、3–4 走中、5 走重;lofi_* 與 mute 忽略此值。字串(例如 "3")會被拒。 |
回應 202
{
"object": "video.post_process",
"id": "pp_9f8e7d6c5b4a3210fedc",
"status": "queued",
"audio_style": "phone",
"intensity": 3,
"created_at": 1756300000
}
GET /v1/post-process/{id} · GET /v1/post-process/{id}/content · GET /v1/post-process
可用的聲音風格
phone | 電話:窄頻、單聲道、略帶失真 |
walkie | 對講機:更窄的頻寬、壓縮強烈 |
radio | 老收音機/廣播 |
vinyl | 老唱片:溫暖、輕微抖動 |
muffled | 隔壁房間/門後:悶住並帶迴音 |
hall | 大廳迴音 |
lofi_16k | 降到 16 kHz 取樣再還原 |
lofi_24k | 降到 24 kHz 取樣再還原 |
mute | 移除音軌 |
限制與計費
- 來源影片上限 200 MB、10 分鐘,格式 mp4。
- 來源必須屬於你。引用別人的任務或素材一律回
404,我們不透露它是否存在。 - 處理是非同步的,通常數秒完成。用
GET /v1/post-process/{id}輪詢,或直接看任務的outputs。 - 目前不收費。帳務路徑仍然完整(會產生一筆金額
0.0的用量紀錄),日後若開始計費會事先公告。 - 成品保留在平台,可用
/content取回。
錯誤碼
no_audio_stream | 這支影片沒有可以處理的音軌(除 mute 外的效果都需要音軌) |
unsupported_input | 格式不支援,或參數不合法 |
too_long | 超過長度上限 |
ffmpeg_failed | 處理過程失敗,請聯繫我們 |
insufficient_space | 平台暫時無法配置空間,請稍後再試 |
postprocess_timeout | 長時間未完成,已停止等待(凍結全額退還) |
lambda_error | 後端處理失敗 |
lambda_bad_response | 後端回應無法解析 |
internal_error | 後端內部錯誤 |
postprocess_failed | 其他失敗。認不得的錯誤碼也會收斂成這一個,所以你的程式碼要有處理它的分支 |
錯誤只會回 code 與一句固定文案,不會帶內部細節(檔案路徑、堆疊、後端訊息一律不外流)。download_failed 與 upload_failed 是暫時性的,平台會自動重試,不會出現在最終狀態;重試到時限仍未成功會變成 postprocess_timeout。
POST /v1/images/generations
圖片生成。與影片不同,這是同步端點:一次呼叫就直接拿到圖片,沒有任務 id、沒有輪詢、沒有取件。所有圖片模型共用這一個端點,帶不帶輸入圖都一樣。
影片是非同步的:送件先拿到 job_id,再輪詢狀態、最後取件。圖片沒有這一段——回應本身就是成品。/v1/jobs 不會列出圖片,/v1/usage 裡圖片那一列的 job_id 是 null,回應裡的 id 也不能拿去查詢。
請求欄位
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
model | string | 必填 | 模型 id,必須是 modality: "image" 的模型。拿影片或對話模型呼叫會回 400「{model} 不是 image 模型」。 |
prompt | string | 必填 | 文字描述,不可為空白。 |
n | number | 視模型 | 要產生幾張,省略時為 1 張。部分模型不支援 n,一次固定回 1 張,送了會回 400;支援的模型各有自己的上限。 |
size | string | 視模型 | 輸出尺寸。可用值依模型而異——有的收 "1024x1024" 這類寬×高像素值,有的收 "1K"/"2K"/"4K" 這類簡寫,有的兩種都收,也有模型完全不接受 size。不在該模型可用值之內會回 400,錯誤訊息會把可用值列出來。 |
image | string|array | 選填 | 輸入圖(以圖生圖/改圖),單一值或陣列都收。每一項可以是可公開存取的網址,也可以直接內嵌 base64 data URI——平台會自動轉存後再送出,你不需要自備公開儲存空間(與影片素材同一套行為,見內嵌檔案自動轉存)。接受幾張依模型而異,也有模型完全不接受輸入圖。部分模型帶輸入圖時尺寸由輸入圖決定,此時不能同時指定 size。 |
response_format | string | 選填 | "url"(預設)或 "b64_json"。預設回平台代管的圖片網址;"b64_json" 把影像內容直接放進回應。兩個值所有圖片模型都收。 |
quality | string | 視模型 | 畫質檔位。只有部分模型有這個參數,其餘模型送了會回 400,訊息會列出可用值。 |
background | string | 視模型 | 背景處理方式。同上,只有部分模型支援。 |
negative_prompt | string | 視模型 | 不希望出現的內容。同上,只有部分模型支援。 |
seed | number | 視模型 | 隨機種子,整數。同上,只有部分模型支援。 |
其他未列在上表的欄位會被靜默忽略,不會報錯。
size 的可用值、n 的上限、可接受的輸入圖張數,以及有沒有 quality/background/negative_prompt/seed——每個模型都不一樣,而且會隨模型改版調整。寫死一張對照表只會過期,所以這裡不寫。
做法是:直接送,看錯誤訊息。超出範圍一律回 400 validation_error,訊息會指出是哪一個參數、以及那個模型可以接受什麼值(例如「size 只接受 …」「每次固定產生 1 張圖,不接受 n」)。一次送出多個不合法的參數時,訊息會把它們全部列出來,不會只講第一個。
想在真正送出前先試,可以拿同一組參數打 /v1/quote——它跑的是同一套驗證,不合法時回同樣的 400,而且完全不扣額度。
唯一的例外是下面這一段。Seedream 系列的 size 另外有一道「面積」門檻,而錯誤訊息只講得出下限、講不出上限——問不出來的東西沒有理由不寫,所以那張表列在這裡。
size:簡寫與面積區間
size 有兩種寫法,兩種都收:簡寫(1K/2K/3K/4K,各模型可用的簡寫不同),或 寬x高 像素字串(例如 "1024x1024")。簡寫走的是白名單,通過之後不再套面積檢查;送像素字串時才會走下表的面積判定,看的是寬 × 高的總像素,不是個別邊長、也不是比例。
| 模型 | 可用簡寫 | 面積下限 | 面積上限 |
|---|---|---|---|
| Seedream 4.0 | 1K、2K、4K | 921,600 | 16,777,216 |
| Seedream 4.5 | 2K、4K | 3,686,400 | 16,777,216 |
| Seedream 5.0-lite | 2K、3K、4K | 3,686,400 | 16,777,216 |
| Seedream 5.0-pro | 1K、2K | 921,600 | 4,624,220 |
品牌前綴不影響這張表:模型 id 去掉 ByteDance-/Dola-/NSFW- 之後對到哪一列,就套哪一列。例如 ByteDance-Seedream-4.0 與 NSFW-Seedream-4.0 共用第一列,Dola-Seedream-5.0-pro 與 NSFW-Dola-Seedream-5.0-pro 共用最後一列。
幾個算好的值可以直接對照:"1024x1024" 是 1,048,576、"1080x1920" 是 2,073,600,兩者對 4.0 與 5.0-pro 都可以,對 4.5 與 5.0-lite 都太小;"2560x1440" 剛好是 3,686,400,四個模型都收;"2048x2048" 是 4,194,304,仍在 5.0-pro 的上限之內;"4096x4096" 是 16,777,216,除了 5.0-pro 以外的三個收得下。兩端都是包含——剛好等於下限或上限都算通過。
這張表是現況快照,權威在 GET /v1/models:每個圖片模型的 constraints 直接給出 sizes、minPixels、maxPixels。要寫進程式請讀那三個欄位,不要把上面的數字寫死。
2026-09-07 的變更:先前 5.0-pro 沿用了 4.5 / 5.0-lite 的 3,686,400 下限,於是 1K/2K 級距的尺寸在平台這一層就被擋下(回 400,沒有送到後端、也沒有扣款)。實測後端對 5.0-pro 的 "1024x1024"、"768x1360"、"1080x1920" 都能正常生成,因此下限已放寬到 921,600;同時補上 4,624,220 的上限,那個數字是後端自己回報的、只在 5.0-pro 身上出現過。其餘三個模型這次一格都沒有變動。
同日的第二項變更:5.0-pro 的可用簡寫也一併更正了,從 2K/3K/4K 改成 1K/2K——上表最後一列已經是更正後的值。依據是直接對後端送簡寫字串的實測:"1K" 與 "2K" 都回 200,"3K" 與 "4K" 都回 400,訊息是 must be 'WIDTHxHEIGHT' or a supported size preset.。也就是說先前列出的三個簡寫裡有兩個從來就不成立,只是平台這一層放行了、由後端擋下。
面積門檻與簡寫清單是兩條各自獨立的檢查,分開看才不會誤判:送簡寫走的是 preset 查表,每個模型接受的 preset 不一樣,通過與否跟面積門檻完全無關;送 寬x高 像素字串走的才是上表的面積範圍檢查。還有一件事值得先知道:後端依 preset 實際產出的尺寸,不遵守本文件的像素對照——5.0-pro 送 "2K" 回來的是 2816×1584、送 "1K" 回來的是 1248×832,都不是把 K 數換算成正方形的結果。輸出尺寸要確定,請直接送像素字串。
錯誤訊息可以告訴你卡在哪一層:平台擋下時回的是下面那段繁體中文的 validation_error,它同一句話同時列出可用簡寫與面積下限,所以不會直接指出是簡寫還是像素字串出問題——拿你送出的值對照那兩項就知道了。若拿到的是英文的 must be 'WIDTHxHEIGHT' or a supported size preset.,那是後端回的,代表這個值通過了平台的檢查、被後端拒絕。
error.message 顯示給你的使用者
尺寸不合法時回 400,body 就是標準的錯誤結構:
{
"error": {
"code": "validation_error",
"message": "Dola-Seedream-5.0-pro 的 size 只接受 1K、2K,或寬×高像素值(總像素需 921,600 以上)"
}
}
message 已經寫成人話,並且列出了該模型接受的簡寫與面積下限。但它不會講出上限——上限請讀 /v1/models 的 constraints.maxPixels。
把這段訊息吞掉、只對使用者顯示「生成失敗」,使用者就只剩下試誤這一條路可走。請務必把 error.message 原文透出去。
請求範例
curl -X POST https://appletoken.app/v1/images/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "imagen-4-fast",
"prompt": "清晨的港口,薄霧,暖色調",
"n": 2,
"size": "1024x1024"
}'
回應
{
"id": "img_1a2b3c4d5e6f7a8b9c0d",
"created": 1774794546,
"model": "imagen-4-fast",
"data": [
{ "url": "https://…/tenants/…/images/….png" },
{ "url": "https://…/tenants/…/images/….png" }
],
"price_charged": 0.12
}
形狀與 OpenAI 的圖片端點相容,只多一個 price_charged。
| 欄位 | 說明 |
|---|---|
id | 這次生成的識別碼,img_ 開頭。它不是任務 id,沒有任何端點可以拿它查詢。 |
created | 生成時間,Unix 秒。 |
model | 你送出的模型 id(平台的命名,不是後端的)。 |
data | 陣列,一張圖一個元素。 |
data[].url | response_format 為 "url"(預設)時出現:平台代管的圖片網址,不是後端的網址。建議下載後存到你自己的儲存空間。 |
data[].b64_json | response_format 為 "b64_json" 時出現:base64 編碼的影像內容。與 url 只會出現其中一個。 |
data[].revised_prompt | 選擇性出現:這次生成有改寫過提示詞時才有,沒改寫就整個欄位不存在。 |
price_charged | 本次實收金額(USD)。凍結已在回應送出前結算完畢,你不需要再查一次。 |
按張計價的模型(/v1/models 的 pricing.unit 為 "request"):一張一個固定價格,與提示詞長度無關。結算看的是實際回來幾張,不是你請求的 n——要三張只回兩張,就只收兩張的錢。
按 token 計價的模型(pricing.unit 為 "output_tokens"):與對話同一套。送出前只能給上限估計,結算改用後端回報的實際用量重算,可能略高或略低於預扣。
兩種都可以先用 POST /v1/quote 問價,回應的 estimated 會告訴你那是精算(false)還是上限估計(true)。
這個端點的執行順序是固定的:模型與能力檢查 → 參數驗證 → 內嵌檔案轉存 → 凍結額度 → 生成 → 結算 → 落地成網址。
所有在本地就判定得出的失敗——尺寸、張數、輸入圖張數,以及 response_format: "url" 需要的儲存設定——全部排在扣款與檔案轉存之前。因此參數不合法時你拿到的是 400,額度一分都沒有被動到,也不會產生任何暫存檔案。
生成本身失敗(後端錯誤)時,凍結原路退回、不記帳,同樣不收費。
錯誤
| 狀態碼 | code | 典型情況 |
|---|---|---|
400 | validation_error | 缺 prompt;模型不是 image 模型;n 送給不支援的模型或超出上限;size 不在該模型的可用值;輸入圖張數超過上限、或該模型不收輸入圖;帶輸入圖時又指定了 size;內嵌檔案的型別不支援、或內容與宣告的型別不符。 |
401 / 403 | unauthorized / forbidden | 憑證無效、已停用或已到期。 |
402 | quota_exceeded | 預扣金額超過 available,附 needed 與 available。不會留下凍結。 |
404 | not_found | 模型不在憑證的允許清單,或這個模型不存在——兩者回同一種錯誤。 |
502 / 503 | upstream_error / service_unavailable | 後端不可用;或這個模型平台尚未開放圖片生成;或你要 response_format: "url" 但平台的圖片儲存尚未設定(此時可改用 "b64_json" 直接取得影像內容)。凍結全額退還。 |
素材資產
資產是你存在平台上、可以重複使用的參考素材。它存在的理由只有一個:含人臉的參考素材,後端不接受公開網址——必須先上傳成資產,再用 asset:// 引用。
可以用來做什麼
| 用途 | 你提供什麼 | 得到什麼 |
|---|---|---|
| 角色一致性 | 一張臉部照片(資產) | 同一個人出現在不同場景、不同背景的多支影片裡,臉不會換人。 |
| 動作參考 | 一段動作影片 + 一張角色圖 | 你的角色做出那段影片裡的動作。 |
| 換臉 | 臉部照片(資產) | 把指定的臉套進生成的影片。 |
這三件事都需要人臉,所以都必須走資產。不含人臉的參考素材(風景、物件、色調參考…)照舊用公開網址或內嵌 data URI 即可,不必上傳成資產。
不論是換臉還是動作參考,上傳成資產也不能用。這是後端的政策,我們無法在本地事先判斷,只能把它的拒絕訊息如實轉達。
介面與限制
| 項目 | 限制 |
|---|---|
| 支援格式 | 圖片 PNG/JPEG/WebP/GIF 影片 MP4/MOV/WebM 音訊 MP3/WAV/M4A |
| 單檔大小 | 最大 20 MB |
| 圖片尺寸 | 長寬都必須在 300–6000 像素之間。上傳當下就會擋下,不會白等一趟;錯誤訊息會直接告訴你這張圖是幾乘幾、該放大還是縮小。 |
| 重複上傳 | 同一個帳戶重複上傳同一份檔案,會拿回同一個 id,不會產生第二份,也不會再往上游傳一次。重試是安全的。(比對的是檔案內容的 sha256,與檔名無關。) |
| 內容驗證 | 檔案內容必須與宣稱的型別相符——副檔名對、內容不對的檔案會被擋下。 |
| 一次生成可引用幾個 | Seedance-2.0/-fast/-mini:共 15 個(圖片 9、影片 3、音訊 3),不支援只給音訊,至少要有一張圖或一段影片。Seedance-2.5:共 50 個(圖片 30、影片 10、音訊 10),支援只給音訊。 |
| 資產數量 | 沒有平台層的上限。列表一次最多回 100 筆。 |
| 保留期 | 資產長期保留,直到你自己刪除。與影片素材的 30 天暫存不同——那是送件當下的暫存,這是可重複使用的資產。 |
怎麼上傳
三步:上傳 → 等它變成可用 → 在送件時引用。
第一步:上傳
curl https://appletoken.app/v1/assets \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAA…",
"name": "主角臉部參考"
}'
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
source | string | 必填 | 內嵌檔案的 data URI,格式 data:<mime>;base64,<內容>——與影片送件的素材欄位同一種寫法,不必學新的。 |
name | string | 選填 | 方便你自己辨認,不影響生成結果。 |
type | string | 選填 | image/video/audio。不給就從檔案型別判斷,通常不需要指定。 |
回應是 202——已收下,但還不能用:
{
"id": "ast_a1b2c3d4e5f6a7b8c9d0",
"object": "asset",
"type": "image",
"name": "主角臉部參考",
"status": "processing",
"bytes": 545459,
"mime": "image/jpeg",
"sha256": "3f2a…",
"reference": "asset://ast_a1b2c3d4e5f6a7b8c9d0",
"created_at": 1788000000,
"activated_at": null
}
| 回應欄位 | 說明 |
|---|---|
id | 這份資產的編號。後續讀取、刪除都用它。 |
reference | 直接複製這個字串貼進 input_references 的 url 就可以了,不必自己組。 |
status | processing 處理中(還不能用於生成)/active 可用/failed 失敗。 |
sha256 | 檔案指紋。用來確認「這兩次送的是不是同一份檔案」。 |
activated_at | 變成可用的時間;還在處理中時是 null。 |
第二步:等它變成 active
curl https://appletoken.app/v1/assets/ast_a1b2c3d4e5f6a7b8c9d0 \
-H "Authorization: Bearer $APPLETOKEN_KEY"
後端是非同步處理的,一般在幾秒到十幾秒內完成。建議每 3 秒查一次,直到 status 變成 active。
後端對還在處理中的資產不會拒絕,而是生出一支沒有正確參考到素材的影片——那種失敗最難查,錢也照扣。所以我們直接擋在送件前,回 400 並告訴你它還在處理中。
第三步:在送件時引用
{
"model": "Seedance-2.0",
"prompt": "這個人在海邊散步,黃昏,鏡頭緩慢推近",
"seconds": 5,
"resolution": "720p",
"input_references": [
{ "type": "image", "url": "asset://ast_a1b2c3d4e5f6a7b8c9d0", "role": "reference_image" },
{ "type": "image", "url": "https://your-cdn.com/背景參考.jpg", "role": "reference_image" }
]
}
同一個請求裡可以混用:含人臉的用 asset://,其餘照舊給網址。role 依型別填 reference_image/reference_video/reference_audio。
帶影片參考時,seconds 必須是 -1(長度由輸入影片決定)——這條規則與資產無關,見影片送件。
列出與刪除
列出你的資產,最新的在前。查詢參數:limit(1–100,預設 50)、status(processing/active/failed)、type(image/video/audio)。回應是 {"object":"list","data":[…]}。
永久刪除,回 204。刪掉之後就不能再用於生成,已經送出的任務不受影響。
上傳前我們會算檔案指紋。同一份檔案重複上傳會直接回傳既有的資產(同一個 id),不會產生第二筆,也不會再送一次後端。所以重試是安全的。
列表只會回你自己的;讀取、刪除、以及送件時的 asset:// 引用,都會先確認那份資產屬於你。引用不屬於你的資產一律回 404——與「不存在」同一種回應,不會告訴你那個編號是否真的存在。
錯誤
| 狀態 | 情況 |
|---|---|
400 | 缺少 source;型別不支援;內容與宣稱的型別不符;檔案超過 20 MB;圖片尺寸不在 300–6000px;引用的資產還在 processing |
404 | 資產不存在,或不屬於你 |
503 | 素材資產功能尚未開通。這不是你的請求有問題,請聯絡我們 |
GET /v1/jobs
列出這張憑證送出的影片任務,最新的在前。適合用在你自己的後台列出近期送件,不必逐一保存 job_id。
Query 參數
| 參數 | 預設 | 範圍 | 說明 |
|---|---|---|---|
limit | 100 | 1–500 | 回傳筆數。超出範圍不會被夾到邊界值,而是回 400 validation_error;非整數或非數字同樣回 400。省略時才使用預設值。 |
curl "https://appletoken.app/v1/jobs?limit=20" \
-H "Authorization: Bearer $APPLETOKEN_KEY"
回應
{
"object": "list",
"data": [
{
"id": "job_2b3c4d5e6f7a8b9c0d1e",
"object": "video",
"model": "veo-3.1-generate-001",
"status": "processing",
"created_at": 1756300500,
"completed_at": null
},
{
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"object": "video",
"model": "Seedance-2.0-fast",
"status": "completed",
"created_at": 1756300000,
"completed_at": 1756300182,
"content": "/v1/videos/job_1a2b3c4d5e6f7a8b9c0d/content"
}
]
}
- 每一筆的形狀與
GET /v1/videos/{job_id}相同,status的意義也相同。 - 只會列出這張憑證送出的任務。同帳戶其他憑證的任務不會出現在這裡。
- 這個端點不會觸發結算,也不會即時向後端更新狀態。要拿到權威的狀態與
price_charged,請對單一任務呼叫GET /v1/videos/{job_id}。
POST /v1/chat/completions
對話生成。與影片不同,這是同步端點:沒有任務 id、沒有輪詢、沒有取件。一次呼叫要嘛拿到完整回應,要嘛拿到一段 SSE 串流。
quote_id
影片可以先報價、確認、再送件(見送件前確認)。對話這條端點不行——不是還沒做,是這件事對對話沒有意義。
差別在計費的本質。影片的計費量在送出前就算得準:尺寸與秒數決定 token 數,報價等於實收,所以「確認這個數字」是有內容的。對話則取決於模型實際生成多少 token,事前不可知,平台能給的只有一個上限。要你確認一個上限沒有意義——你確認了 8,000 tokens,實際可能只用掉 300。
所以對話走的是另一套:以 max_tokens 為上限預扣,生成結束後以實際用量結算,多預扣的退回。公式完全公開,見對話的預扣與串流計費。
還有一個現實理由:這條端點與 OpenAI 的 /chat/completions 相容,多一道必要的往返會讓現成的 SDK 直接不能用。
請求欄位
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
model | string | 必填 | 模型 id,必須是 modality: "text" 的模型(也就是對話模型;目錄裡沒有 "chat" 這個 modality)。對影片模型呼叫會回 400「{model} 不是對話模型」。 |
messages | array | 必填 | 對話內容,非空陣列。每個元素形如 {"role": "user", "content": "…"},兩個欄位都必須是字串。 |
max_tokens | number | 必填 | 輸出 token 上限,必須大於 0。平台以它為上限預扣額度,因此不能省略——理由見下方。 |
stream | bool | 選填 | 預設 false。設為 true 時改回 SSE 串流。必須傳布林值 true / false——平台只做真假值判斷,任何非空字串(包含 "false")都會被當成 true 而啟用串流。 |
temperature | number | 選填 | 取樣溫度。平台不檢查範圍,原樣轉給後端。 |
其他欄位(top_p、n、stop、tools、user 等)目前不被支援,會被靜默忽略。
max_tokens 一定要給
對話的用量到最後一刻才知道,但額度必須在呼叫後端之前就凍結——否則你可能在餘額只剩 0.01 美元時觸發一次十幾塊的長回應,帳算得出來卻收不到錢。max_tokens 是你對「這次最多產出多少」給出的承諾,平台用它算出最壞情況的價格並凍結那個金額。
這對你是實質約束,但代價很小:結算一律以實際用量重算,通常遠低於預扣,差額在請求結束的當下就還給你。要留意的只有一件事——max_tokens 設得太大會暫時佔住額度,讓同時進行的其他請求更容易撞到 402。請照實際需要設,不要一律填上限。
回應(非串流)
{
"object": "chat.completion",
"model": "ByteDance-Seed-1.8",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "向量資料庫把文字…" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 52, "completion_tokens": 180 },
"price_charged": 0.004284
}
| 欄位 | 說明 |
|---|---|
object | 固定為 "chat.completion"。 |
model | 你送出的模型 id(平台的命名,不是後端的)。 |
choices | 目前永遠只有一個元素(不支援 n)。message.role 固定為 "assistant"。 |
usage.prompt_tokens | 輸入 token 數。後端有回報就用實際值,否則用平台的估算值。 |
usage.completion_tokens | 輸出 token 數,同上。 |
price_charged | 本次實收金額(USD)。凍結已在回應送出前結算完畢,你不需要再查一次。 |
回應沒有 id 也沒有 created——對話不是可查詢的任務,沒有識別碼可以拿。要對帳請用 /v1/usage 裡對應那一列的 id(對話的列 job_id 為 null)。
回應(串流,stream: true)
回應標頭是 Content-Type: text/event-stream; charset=utf-8,狀態碼永遠是 200。內容為一連串 data: 事件,以空行分隔:
data: {"object":"chat.completion.chunk","model":"ByteDance-Seed-1.8","choices":[{"index":0,"delta":{"content":"向量"}}]}
data: {"object":"chat.completion.chunk","model":"ByteDance-Seed-1.8","choices":[{"index":0,"delta":{"content":"資料庫"}}]}
data: {"object":"chat.completion.chunk","model":"ByteDance-Seed-1.8","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":52,"completion_tokens":180},"price_charged":0.004284}
data: [DONE]
- 內容片段在
choices[0].delta.content。把它們依序串起來就是完整回應。 - 最後一則 chunk 帶
usage與price_charged,delta為空物件、finish_reason有值。這是刻意的:你不必再查一次就能對帳。 - 串流以
data: [DONE]結束。收到它就可以關閉連線。 - 沒有其他 SSE 欄位(
event:、id:、retry:),也沒有 keep-alive 註解行。
SSE 的標頭在第一個 chunk 之前就送出去了,之後再改狀態碼已經來不及。因此串流開始後的任何錯誤都以事件形式送出,形狀是這樣:
data: {"object":"error","error":{"code":"upstream_error","message":"生成服務回報錯誤"}}
data: [DONE]
你的串流讀取器必須檢查每一則事件的 object:"chat.completion.chunk" 是內容,"error" 是錯誤。只看 HTTP 狀態碼的客戶端會把失敗的請求當成成功、把半截回應當成完整回應。
錯誤事件之後一樣會送 data: [DONE]。此時額度已經結算完畢——已產生的部分照收,一個 token 都沒產出才全額退還。詳見對話的預扣與串流計費。
在串流開始「之前」發生的錯誤
驗證、額度檢查、模型解析都發生在第一個 chunk 之前,因此這些錯誤走正常的 HTTP 狀態碼與 JSON 錯誤格式:
| 狀態碼 | code | 典型情況 |
|---|---|---|
400 | validation_error | 缺 max_tokens、messages 為空、模型不是對話模型。 |
401 / 403 | unauthorized / forbidden | 憑證無效、已停用或已到期。 |
402 | quota_exceeded | 預扣金額超過 available,附 needed 與 available。不會留下凍結。 |
404 | not_found | 模型不在憑證的允許清單,或這個模型不存在——兩者回同一種錯誤。 |
502 / 503 | upstream_error / service_unavailable | 後端不可用,或這個模型不支援你要的模式(stream 與非串流分別檢查)。凍結全額退還。 |
GET /v1/me
查詢這張憑證的狀態,以及帳戶的額度。適合用在啟動自檢、餘額看板與低餘額告警。
額度是帳戶層級的:同一個帳戶的每一把憑證查到的餘額都相同,任何一把憑證的消費都會反映在這個數字上。
{
"id": "9f2c81ab",
"name": "production",
"status": "active",
"granted": 100.0,
"used": 12.482,
"reserved": 0.539055,
"available": 86.978945,
"allowed_models": null,
"calls": 37,
"created_at": 1756000000,
"last_used_at": 1756300000,
"expires_at": null,
"expired": false,
"prefix": "at_live_9f2c81ab_…",
"display": "at_live_9f2c81ab_••••••••"
}
| 欄位 | 說明 |
|---|---|
granted | 額度上限。由 AppleToken 設定,你無法自行調整。 |
used | 已結算的累計金額。 |
reserved | 目前凍結中的金額(已預扣但尚未結算的請求)。 |
available | granted − used − reserved。這是實際可用的餘額。 |
allowed_models | 模型白名單。null 表示不限制。 |
calls | 成功建立凍結的請求數——影片送件與對話請求都算,估價、查詢、取件、/v1/me 都不算。 |
last_used_at | 最後一次成功預扣的時間,同樣不因查詢而更新。 |
expires_at / expired | 到期時間與是否已過期。null 表示永不過期。 |
prefix / display | 憑證的遮罩形式,可安全顯示在介面或日誌中。 |
GET /v1/usage
取得這張憑證的帳務流水,最新的在前。這是你端對帳的權威來源。
用量以憑證為界(哪支鑰匙做了什麼),額度則是帳戶共用的。同帳戶其他憑證的消費不會出現在這裡,但會影響 /v1/me 的餘額。
Query 參數
| 參數 | 預設 | 範圍 | 說明 |
|---|---|---|---|
limit | 100 | 1–500 | 回傳筆數。超出範圍不會被夾到邊界值,而是回 400 validation_error;非整數或非數字同樣回 400。省略時才使用預設值。 |
回應
{
"object": "list",
"data": [
{
"id": "80421",
"job_id": null,
"model": "ByteDance-Seed-1.8",
"modality": "text",
"billable": { "input_tokens": 52, "output_tokens": 180 },
"price": 0.004284,
"status": "settled",
"created_at": 1756300400
},
{
"id": "80418",
"job_id": "job_1a2b3c4d5e6f7a8b9c0d",
"model": "Seedance-2.0-fast",
"modality": "video",
"billable": { "video_seconds": 5 },
"price": 0.539055,
"status": "settled",
"created_at": 1756300182
}
]
}
| 欄位 | 型別 | 說明 |
|---|---|---|
id | string | 這一列的識別碼,數字字串、單調遞增。它是唯一保證存在的識別碼——對話沒有 job_id,只能靠它。 |
job_id | string|null | 影片任務 id,與 POST /v1/videos/generations 回傳的 id 相同。對話與圖片沒有任務,這裡是 null。 |
model | string | 模型 id,與 /v1/models 的 id 是同一個字串。 |
modality | string | 這筆消費屬於哪一種模態(video / image / text / audio / embedding)。用它拆分不同類型的成本。 |
billable | object | 實際計費的用量:鍵是計價單位、值是數量,例如 {"video_seconds": 5} 或 {"input_tokens": 52, "output_tokens": 180}。出現哪些鍵依模型而定,請以實際存在的鍵為準。 |
price | float | 這一列的實收金額(USD)。 |
status | string | 目前一律是 "settled"。 |
created_at | int | 結算寫進帳本的時間,Unix 秒。排序依據就是它(最新的在前)。 |
每一列都是一筆已完成結算的消費。額度的預扣與退還不會在這裡留下任何一列。所以:
額度模型
每張憑證有三個彼此獨立的數字,恆等式永遠成立:
available = granted − used − reserved
│ │ │ └─ 已預扣、尚未結算的請求金額
│ │ └─────────── 已結算的實收累計
│ └────────────────────── 額度上限(由 AppleToken 設定)
└─────────────────────────────────── 你現在真正能用的錢為什麼要「預扣」
生成請求一旦送出就無法取消,成本在那一刻就已經產生。因此平台在請求離開之前先凍結預估金額,確保不會出現超支後才發現餘額不足的情況。這也代表:
- 凍結發生在呼叫後端之前。若後端拒絕或連線失敗,凍結會立即全額退還。
- 同時進行多個請求時,每個請求各自持有一筆凍結,
reserved是它們的總和。 available不足以支付預扣時,請求會直接被擋下並回402,不會產生任何費用。- 影片的預扣金額是估價(可以事先算準);對話的預扣金額是最壞情況(輸入 token +
max_tokens),結算時幾乎一定會退回大部分。
額度耗盡時
{
"error": {
"code": "quota_exceeded",
"message": "額度不足",
"needed": 1.25,
"available": 0.42,
"granted": 100.0,
"used": 98.33,
"reserved": 1.25
}
}
先看 used:如果它接近 granted,就是單純用完了,請加值。
再看 reserved:這是正在進行中的請求總和。平台會主動追蹤每一筆任務並在它結束時結算,所以 reserved 高只代表你同時跑了很多任務,或者某些任務還在生成——它會自己降下來,不需要你做任何事。若要壓低瞬時的 reserved,方向是限制併發數,以及把對話的 max_tokens 設得貼近實際需要。
reserved 最長會停留多久
任務最多追蹤 2 小時:上游暫時查不到結果(包含你自己綁定的上游金鑰額度不足這類情況)時,平台會持續重試到這個時限。超過 2 小時仍沒有結果,系統會自動判定為失敗(error.code 為 generation_timeout)並把凍結全額退還,不需要你做任何事。也因此,某筆 reserved 超過 2 小時仍未消失,才是值得回報的異常,請帶著憑證 id 前綴與大約時間聯繫我們。
用量與計價公式
token 制影片模型(output_tokens)
計費量由畫面尺寸與影格數決定,與內容複雜度無關,因此可以在送出前算得完全精確:
frames = 24 × seconds + 1 (影格率固定 24,不可調整)
tokens = round( 寬 × 高 ÷ 1024 × frames )
price = tokens ÷ 1,000,000 × 單價實例:Seedance-2.0-fast、5 秒、720p、16:9
寬 × 高 = 1280 × 720 = 921,600 px
每格 token = 921,600 ÷ 1024 = 900
frames = 24 × 5 + 1 = 121
tokens = 900 × 121 = 108,900
price = 108,900 ÷ 1,000,000 × 4.95 = 0.539055 USD畫面尺寸的決定順序是:
- 有
size→ 直接採用其中的寬高。 - 否則以
resolution的數字為短邊,再用aspect_ratio推出長邊。
/v1/quote 確認
上面的公式與平台實際使用的完全相同,你可以在客戶端預估費用。但 aspect_ratio 的靜默退回、模型費率分段等行為只有伺服器知道,正式報價請以 /v1/quote 為準。
秒制影片模型(video_seconds)
price = round(seconds) × 單價 (與解析度、比例都無關)例如 veo-3.1-generate-001 生成 8 秒:8 × 0.90 = 7.20 USD。
對話的預扣與串流計費
對話是唯一「用量到最後才知道」的能力,因此計費分成兩步:先按上限預扣,結束後按實際結算。兩步都用同一條公式。
公式
price = 輸入 tokens ÷ 1,000,000 × 輸入單價
+ 輸出 tokens ÷ 1,000,000 × 輸出單價
預扣:輸入 tokens = 平台估算的 prompt 長度
輸出 tokens = 你給的 max_tokens ← 最壞情況
結算:輸入 tokens = 實際 prompt_tokens
輸出 tokens = 實際 completion_tokens ← 通常遠低於 max_tokens實例:ByteDance-Seed-1.8,max_tokens: 512
單價(取自 GET /v1/models 的 pricing.components)
輸入 0.375 / 1M tokens
輸出 3.00 / 1M tokens
預扣(請求開始)
估算輸入 48 tokens × 0.375 ÷ 1,000,000 = 0.000018
上限輸出 512 tokens × 3.00 ÷ 1,000,000 = 0.001536
reserved = 0.001554 USD
結算(請求結束,實際只產生 180 tokens)
實際輸入 52 tokens × 0.375 ÷ 1,000,000 = 0.000020
實際輸出 180 tokens × 3.00 ÷ 1,000,000 = 0.000540
actual = 0.000560 USD
delta = actual − reserved = −0.000994(退回你的餘額)分段計價的模型,GET /v1/models 的
pricing 會多一個 components 陣列,各單位有自己的區間:
"pricing": {
"unit": "output_tokens", "from": 3, "to": 3,
"components": [
{ "unit": "input_tokens", "from": 0.375, "to": 0.375 },
{ "unit": "output_tokens", "from": 3, "to": 3 }
]
}
單一計價單位的模型不會有 components——
請用「這個欄位存不存在」判斷,不要靠模型名稱猜。
unit/from/to 描述的是主要單位(輸出優先),
分段計價時不能拿它當總價估算。
不是先加總再取位。這樣 components 裡各段的價格相加會剛好等於
price,明細對得起總額;若反過來,兩者可能差到 0.000001。
平台在呼叫後端之前還拿不到真正的 tokenizer 結果,因此用一個固定、可重算的近似式估算輸入長度(約每 4 個字元算 1 個 token,以 Unicode code point 計)。中文會被略微低估、英文會被略微高估,兩者都在同一個數量級內。這個估算值只影響預扣;結算一律採用後端回報的實際 prompt_tokens。
串流有三種結束方式,三種都會結算
這是串流計費最容易出錯的地方,所以平台把三條路徑收斂到同一個結算出口:
| 結束方式 | 怎麼計費 | 帳本 basis |
|---|---|---|
| 後端正常結束並回報用量 | 以後端回報的 prompt_tokens / completion_tokens 結算。 | actual |
| 後端正常結束但沒回報用量 | 以平台自行累計的輸出內容長度估算後結算,不是照預扣全收。 | stream |
| 你的客戶端中途斷線,或後端中途斷線 | 以已經產生的部分結算——不整筆退、也不照預扣全收。 | stream |
一個 token 都沒產出時(後端還沒開口就斷了、或你在第一個字之前就關閉連線),凍結全額退還,帳本記 release,reason 為 client_abort 或 upstream_failed。只要吐出了半句話,那半句話就要付錢。
你的程式中止讀取、使用者關掉分頁、負載平衡器切斷連線——這些都會讓平台停止轉發並關閉對後端的連線,但已經產生的 token 照樣計費。成本在後端產生它們的當下就發生了,斷線只是你選擇不再接收。
因此「按下停止就不用付錢」是錯的預期。若要控制成本,正確的作法是把 max_tokens 設小,而不是靠中途斷線。
串流的閒置逾時
串流沒有整體逾時——長回應本來就會跑很久。但相鄰兩個 chunk 之間有間隔上限,預設 60 秒。後端連著卻不吐字超過這個時間,平台會中止串流,並照「已產生的部分」結算。
這是刻意的:整體逾時會砍掉正常的長回應,而「連著卻不吐字」一定是出事了,必須有上限,否則連線與凍結會一起無限期懸著。你會在串流中看到一個 upstream_error 事件(訊息「生成服務逾時未回應」),接著是 data: [DONE]。
結算、退還與對帳
平台有背景程序主動追蹤每一筆已送出的任務,在它完成或失敗時完成結算或退還。你查不查、你的程式有沒有崩潰、有沒有重啟,都不影響帳務正確性——凍結不會因為沒人查詢而卡在 reserved 裡。
你呼叫 GET /v1/videos/{job_id} 時,若任務已有結果,也會順手走同一段結算邏輯。兩者彼此冪等:同時發生只有一次生效,不會重複扣款。
對話沒有這個問題——它是同步的,結算在回應(或串流的最後一則 chunk)送出之前就已經完成。
不輪詢不會讓你的額度卡住,但會讓你不知道任務結果——影片成品必須先確認 completed 才能取件,而成品保留時間有限。所以:
- 把送出的
job_id持久化,程式重啟後繼續查——理由是取件,不是額度。 - 查詢到
completed後盡快下載並存進你自己的儲存空間。 - 查詢到
failed時,退還早已完成(或即將完成),你要做的是決定要不要重送。
四種結局
| 結局 | 觸發 | 額度變化 | 在 /v1/usage 的表現 |
|---|---|---|---|
| 完成並結算 | 後端回報 completed(由背景程序或你的查詢先發現) |
reserved 減去凍結額,used 加上實際金額 | 出現一列,status 為 "settled" |
| 失敗退還 | 後端回報 failed(同上) |
reserved 減去凍結額,used 不變 | 不會出現——沒有扣款就沒有這一列 |
| 送件失敗退還 | 請求未能送達後端 | 同上,在送件當下立即發生 | 不會出現——沒有扣款就沒有這一列 |
| 對話結束 | 回應或串流結束的當下(含中途斷線) | 依串流的三種結束方式結算;零產出才退還 | 有結算才會出現一列;零產出全額退還時不會出現 |
實收金額怎麼決定
- token 制影片:以後端回報的實際 token 用量重算。若後端未回報用量,則以送件時的估價結算。
- 秒制影片:以實際成品秒數重算;沒有回報時採用送件時的
seconds。 - 對話:以實際輸入/輸出 token 重算;後端沒回報用量或中途斷線時,以平台累計的產出量結算。
- 重算的結果就是
/v1/usage那一列的price,計費用量在同一列的billable。它可能略高或略低於送件時的price_reserved——差額不會另外列成一筆。
當實際用量高於估價(最常見於帶影片參考、實際長度大於你填的 seconds 時),結算會以實際金額計算,此時 available 可能變成負數。這是刻意的——帳要真實,不能因為超支就少記。請在估價時保守取高值,並監控 /v1/me 的 available。
對帳建議
- 你端以
job_id為主鍵記錄每個影片任務,並保存送件時回傳的price_reserved;對話則記錄回應中的price_charged。 - 定期拉
GET /v1/usage。用created_at圈出你要對的時間區間,並記下這一批裡最大的id——下次只處理id比它大的列,就不會重複也不會漏。 - 有
job_id的列(影片)直接對回你自己的任務紀錄,核對price與你保存的price_reserved。有差額是正常的:實收以實際用量重算,可能略高或略低。 - 沒有
job_id的列(對話、圖片)以id為主鍵,用created_at與model對回你自己的請求紀錄,核對price與回應中的price_charged。 - 你送出了、卻在
/v1/usage找不到對應列的請求,只有兩種可能:還沒結算(仍在進行中)或已全額退還(送件失敗、生成失敗、零產出的串流)。兩者都不需要你做任何事——用GET /v1/videos/{job_id}查狀態就能分辨。只有某筆影片任務超過 2 小時仍停在processing,才是值得回報的異常(正常情況下最晚 2 小時內就會被系統自動判定為失敗並退還)。 - 核對
Σ price:帳戶下只有這一張憑證時,它應該等於/v1/me的used;有多張憑證時,各憑證的Σ price相加才等於used。
錯誤格式與狀態碼
{
"error": {
"code": "validation_error",
"message": "veo-3.1-generate-001 只接受 4/6/8 秒"
}
}
該怎麼分支
| 狀態碼 | 代表 | 你該做的事 |
|---|---|---|
400 | 請求本身有問題 | 修參數再送。重試沒有意義。 |
401 | 憑證無效 | 檢查標頭格式與憑證是否已換發。不要自動重試。 |
402 | 額度不足 | 停止送件、通知負責人加值。若 reserved 很高,那是進行中的請求,等它們結束即可。 |
403 | 憑證被停用或已過期 | 聯繫 AppleToken。不要自動重試。 |
404 | 任務不存在、不屬於你,模型不在憑證的允許清單,或路徑/方法不存在 | 確認 job_id 與使用的憑證是否為送件時的同一張;確認 model 出現在 /v1/models 的清單裡;確認方法是 GET 或 POST。 |
429 | 請求太頻繁 | 自行做指數退避後重試。先看 error.code 分辨來源:rate_limited 是平台的每分鐘限流(帶憑證 600 次/分,未帶憑證 20 次/分);upstream_error 則是上游的節流被原樣透傳。兩種都不含 retry_after,額度也都不會被扣。 |
500 | 平台內部錯誤 | 可重試一次;持續發生請回報並附上時間與 job_id。 |
502 / 503 / 504 | 生成服務暫時不可用 | 指數退避後重試。額度不會被扣掉(串流已產出的部分除外)。 |
當錯誤來自後端生成服務時,平台會沿用後端的狀態碼(可能是 400–599 任一值),但 code 一律是 upstream_error。請以 error.code 判斷責任歸屬:是你的參數問題,還是服務端的問題。
後端的 401 / 403 是例外:那代表平台自己的上游憑證出了問題,與你的憑證無關,因此會被轉成 503 service_unavailable,避免你誤以為要換自己的 key。
反過來也不要只看 error.code。請以 HTTP 狀態碼判斷這是不是請求端的錯誤:目前缺少必填欄位的請求(影片缺 prompt、對話缺 messages)會回 400,但 error.code 是 upstream_error 而不是 validation_error。凡是 4xx 都請當成要修參數,不要因為 code 看起來像服務端問題就重試。
stream: true 的請求一旦開始送出內容,狀態碼就固定是 200,之後的錯誤全部以 {"object":"error", …} 事件送達。請務必檢查每一則 SSE 事件的 object 欄位,詳見 對話端點。
錯誤碼總表
| code | 狀態碼 | 典型訊息 | 成因與處理 |
|---|---|---|---|
validation_error | 400 | request body 不是合法 JSON | 檢查 JSON 語法。 |
validation_error | 400 | Content-Type 必須是 application/json | 補上或修正 Content-Type 標頭。 |
validation_error | 400 | request body 過大 | 超過 24 MB。內嵌多個 data URI 檔案時尤其容易撞到,大檔案建議改用可公開存取的網址。 |
validation_error | 400 | request body 必須是 JSON 物件 | 最外層必須是 {}。 |
validation_error | 400 | 缺少 model | 補上 model。 |
validation_error | 400 | 缺少 prompt | 補上非空的 prompt。此檢查排在凍結之前,不會留下凍結。 |
validation_error | 400 | 未知的模型 {id} | 以 /v1/models 的 id 為準,不做模糊比對。 |
validation_error | 400 | seconds 必須是數字 | 傳了字串(如 "5")或非有限數值。表單取值最容易在這裡出錯。 |
validation_error | 400 | seconds 必須大於 0 | 調整 seconds。 |
validation_error | 400 | {id} 只接受 4/6/8 秒 | veo-3.1-generate-001 的固定長度限制。 |
validation_error | 400 | {id} 的長度需在 {min}–{max} 秒之間 | 調整 seconds。 |
validation_error | 400 | {id} 支援的解析度為 … | 改用清單內的解析度。只給 size 也會被檢查:短邊必須等於清單中的某個值。 |
validation_error | 400 | 需要 seconds 才能報價… | 即使帶影片參考也必須提供 seconds 供估價。 |
validation_error | 400 | 需要 resolution 或 size 才能精算 token 用量 | token 制模型至少要有其中之一。對 chat-* 呼叫 /v1/quote 也會撞到這則。 |
validation_error | 400 | 無法解析的 size:{值} / 無法解析的解析度:{值} | 格式必須是 "1280x720"、"720p" 或 "720"。 |
validation_error | 400 | 這組參數沒有對應的計價規則 | 參數組合超出模型的費率涵蓋範圍。實務上很少見,因為每個模型都有一條無條件的預設費率resolution(它的費率分段以解析度為條件)。 |
validation_error | 400 | 缺少 messages / messages[i].content 必須是字串 | 對話端點:messages 必須是非空陣列,元素的 role 與 content 都是字串。 |
validation_error | 400 | max_tokens 必須是大於 0 的數字(平台以它為上限預扣額度,因此不可省略) | 對話端點必填,見對話端點。 |
validation_error | 400 | temperature 必須是數字 | 型別錯誤,改送 JSON number。 |
validation_error | 400 | {id} 不是對話模型 | 對影片模型呼叫了 /v1/chat/completions。以 modality 分類。 |
unauthorized | 401 | 憑證格式不正確 | 檢查 Bearer 前綴與空白。 |
unauthorized | 401 | 憑證無效 | 憑證不存在、機密錯誤或已被換發/撤銷。 |
quota_exceeded | 402 | 額度不足 | 附 needed/available/granted/used/reserved。 |
forbidden | 403 | 這組憑證已被停用 | 停用或已過期,兩者訊息相同。 |
not_found | 404 | 未知的模型 {model} | 憑證設有模型白名單而這個模型不在其中。與模型不存在回同一種錯誤,避免拿來探測目錄。影片與對話同樣適用。 |
not_found | 404 | 找不到這個任務 | 任務不存在,或屬於別張憑證。 |
not_found | 404 | 這個任務沒有可下載的內容 | 尚未完成,或成品已失效。 |
not_found | 404 | not found | 路徑或 HTTP 方法不存在(例如對 /v1/quote 用 PUT)。只有帶著有效憑證時才看得到這個回應:未認證的請求一律先回 401,不論路徑存不存在——否則 401 與 404 的差異會變成一個可以用來枚舉端點的訊號。 |
rate_limited | 429 | 請求太頻繁,請稍後再試 | 平台自己的限流,時間窗為 1 分鐘:帶 Authorization 標頭時每分鐘 600 次(以憑證分別計數),未帶憑證時每分鐘 20 次(以來源 IP 計數);/healthz 不受限。回應不含 retry_after,請用自己的指數退避。額度未被扣除。平台保留調整配額的權利,請不要把這些數字寫死進整合裡(見常見問題)。 |
upstream_error | 429 | (沿用後端的訊息,已清洗) | 上游服務對這次請求做了節流,平台原樣透傳狀態碼。這與上一列的 rate_limited 是兩種不同來源,請以 error.code 分辨。回應不含 retry_after,請用自己的指數退避。額度未被扣除。 |
upstream_error | 沿用後端 | 生成服務回報錯誤 | 後端拒絕了這次請求。訊息已清洗,不含任何上游識別。狀態碼不固定,請以 code 分支。 |
upstream_error | 沿用後端(通常 400) | 素材網址無法被下載。請確認它可以匿名開啟(不需登入、沒有防盜連、未過期);若你沒有可公開的儲存空間,可以直接把檔案以 data URI 內嵌在素材欄位裡(image_url / input_references / frame_images),平台會自動代管。 | 後端抓不到你給的素材網址時的改寫訊息,兩條出路:改善網址的可存取性,或改用內嵌 data URI。 |
upstream_error | 502 | 暫時無法連線到生成服務,請稍後再試 | 連線或逾時。退避重試,額度未被扣除。 |
upstream_error | 504 | 生成服務逾時未回應 | 串流專屬:相鄰兩個 chunk 的間隔超過上限(預設 60 秒)。已產生的部分照樣結算。 |
service_unavailable | 503 | 這個模型目前沒有可用的費率 | 該模型暫時沒有生效中的費率卡。請改用其他模型並回報。 |
service_unavailable | 503 | 這個模型目前無法使用 | 對應的後端暫時未接上。請改用其他模型或稍後再試。 |
service_unavailable | 503 | 這個模型不支援串流 / 不支援非串流呼叫 | 換一種 stream 設定,或換模型。 |
service_unavailable | 503 | 平台尚未設定上游憑證,請聯絡管理者 | 平台端設定問題,請直接聯繫我們。 |
service_unavailable | 503 | 平台上游憑證異常,請聯絡管理者 | 指的是你的帳戶所綁定的上游憑證,不是你的 at_live_ 憑證——換發後者不會有幫助。請直接聯絡我們。 |
internal_error | 500 | 發生未預期的錯誤 | 平台端的問題,請回報時間與 job_id。常見的欄位型別錯誤(例如 seconds 傳成字串)在 v2 已改為 400 validation_error,若你仍收到 500,那不是你的參數造成的。 |
完整範例
影片:估價 → 送件 → 取件
以下三個範例做同一件事:估價 → 送件 → 輪詢到結束 → 下載成品。輪詢是為了知道結果並取件;額度的結算由平台在背景完成,不靠這段程式碼。
#!/usr/bin/env python3
"""AppleToken 最小客戶端 — 只用標準庫。"""
import json, os, time, urllib.request, urllib.error
BASE = "https://appletoken.app/v1"
KEY = os.environ["APPLETOKEN_KEY"]
def call(path, payload=None, raw=False):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + path, data=data,
method="POST" if data else "GET")
req.add_header("Authorization", "Bearer " + KEY)
if data:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=120) as r:
return r.read() if raw else json.loads(r.read())
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", "replace")
try:
err = json.loads(body)["error"]
except Exception:
err = {"code": "http_%d" % e.code, "message": body[:200]}
raise RuntimeError("%s: %s" % (err.get("code"), err.get("message")))
def generate(prompt, model="Seedance-2.0-fast", seconds=5, resolution="720p", **extra):
params = {"model": model, "seconds": seconds, "resolution": resolution, **extra}
quote = call("/quote", params)
print("預估費用 %.6f USD,餘額 %.6f" % (quote["price"], quote["available"]))
job = call("/videos/generations", {**params, "prompt": prompt})
job_id = job["id"]
print("已送出 %s,凍結 %.6f" % (job_id, job["price_reserved"]))
# 輪詢是為了拿到結果與取件;額度另有平台的背景結算保底
deadline = time.time() + 900
time.sleep(10)
while time.time() < deadline:
st = call("/videos/" + job_id)
if st["status"] == "completed":
print("完成,實收 %.6f USD" % st.get("price_charged", 0))
return job_id
if st["status"] == "failed":
raise RuntimeError("生成失敗,額度已全額退還:" + job_id)
time.sleep(8)
raise TimeoutError("等待逾時,請保留 job id 稍後續查:" + job_id)
if __name__ == "__main__":
jid = generate("清晨的漁港,漁船緩緩進港", seconds=5, aspect_ratio="16:9")
with open("output.mp4", "wb") as f:
f.write(call("/videos/%s/content" % jid, raw=True))
print("已存成 output.mp4")
// AppleToken 最小客戶端 — Node 18+ 內建 fetch,無需任何套件。
import { writeFile } from 'node:fs/promises'
const BASE = 'https://appletoken.app/v1'
const KEY = process.env.APPLETOKEN_KEY
async function call(path, payload, raw = false) {
const res = await fetch(BASE + path, {
method: payload ? 'POST' : 'GET',
headers: {
Authorization: `Bearer ${KEY}`,
...(payload ? { 'Content-Type': 'application/json' } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
})
if (!res.ok) {
const { error } = await res.json().catch(() => ({ error: {} }))
throw new Error(`${error.code ?? res.status}: ${error.message ?? res.statusText}`)
}
return raw ? Buffer.from(await res.arrayBuffer()) : res.json()
}
const sleep = s => new Promise(r => setTimeout(r, s * 1000))
async function generate(prompt, opts = {}) {
const params = { model: 'Seedance-2.0-fast', seconds: 5, resolution: '720p', ...opts }
const quote = await call('/quote', params)
console.log(`預估費用 ${quote.price} USD,餘額 ${quote.available}`)
const job = await call('/videos/generations', { ...params, prompt })
console.log(`已送出 ${job.id},凍結 ${job.price_reserved}`)
// 輪詢是為了拿到結果與取件;額度另有平台的背景結算保底
const deadline = Date.now() + 900_000
await sleep(10)
while (Date.now() < deadline) {
const st = await call(`/videos/${job.id}`)
if (st.status === 'completed') {
console.log(`完成,實收 ${st.price_charged} USD`)
return job.id
}
if (st.status === 'failed') throw new Error(`生成失敗,額度已退還:${job.id}`)
await sleep(8)
}
throw new Error(`等待逾時,請保留 job id 稍後續查:${job.id}`)
}
const id = await generate('清晨的漁港,漁船緩緩進港', { aspect_ratio: '16:9' })
await writeFile('output.mp4', await call(`/videos/${id}/content`, undefined, true))
console.log('已存成 output.mp4')
# 1) 估價
curl -s https://appletoken.app/v1/quote \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"Seedance-2.0-fast","seconds":5,"resolution":"720p"}'
# 2) 送件,取出 job id
JOB=$(curl -s https://appletoken.app/v1/videos/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"Seedance-2.0-fast","prompt":"清晨的漁港,漁船緩緩進港",
"seconds":5,"resolution":"720p","generate_audio":true}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
echo "job: $JOB"
# 3) 輪詢到終局狀態(為了知道結果,不是為了釋放額度)
while true; do
ST=$(curl -s "https://appletoken.app/v1/videos/$JOB" \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["status"])')
echo "status: $ST"
[ "$ST" = "completed" ] && break
[ "$ST" = "failed" ] && { echo "生成失敗,額度已退還"; exit 1; }
sleep 8
done
# 4) 取件
curl -f "https://appletoken.app/v1/videos/$JOB/content" \
-H "Authorization: Bearer $APPLETOKEN_KEY" -o output.mp4
影片:多張報價單 → 逐一確認送出
同時對幾個模型詢價,再逐一確認送出。重點有三個:每張報價單各自保存自己的 quote_id、確認時各自帶自己的那一張,以及額度到確認的當下才凍結——所以 402 只會出現在確認這一步,而且撞到之後那張報價單仍然可以用。
#!/usr/bin/env python3
"""同時開多張報價單 → 逐一確認送出 — 只用標準庫。"""
import json, os, urllib.request, urllib.error
BASE = "https://appletoken.app/v1"
KEY = os.environ["APPLETOKEN_KEY"]
def call(path, payload=None):
"""回傳 (狀態碼, 回應內容);錯誤不丟例外,因為 402 要當成正常分支處理。"""
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + path, data=data,
method="POST" if data else "GET")
req.add_header("Authorization", "Bearer " + KEY)
if data:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=120) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", "replace")
try:
return e.code, json.loads(body)
except Exception:
return e.code, {"error": {"code": "http_%d" % e.code, "message": body[:200]}}
PROMPT = "清晨的漁港,漁船緩緩進港"
CANDIDATES = [
{"model": "Seedance-2.0-fast", "seconds": 5, "resolution": "720p"},
{"model": "Seedance-2.0", "seconds": 5, "resolution": "1080p"},
]
# 1) 一次開多張報價單。彼此獨立,不會互相覆蓋;這一步不凍結任何額度。
slips = []
for params in CANDIDATES:
status, body = call("/quote", dict(params, prompt=PROMPT))
if status != 200:
print("報價失敗 %s:%s" % (params["model"], body["error"]["message"]))
continue
slips.append({"model": params["model"],
"quote_id": body["quote_id"], # 各自保存,確認時要帶對的那一張
"price": body["price"],
"expires_at": body["expires_at"]}) # 各自 15 分鐘
print("%s 報價 %.6f USD,quote_id=%s,有效到 %s"
% (params["model"], body["price"], body["quote_id"], body["expires_at"]))
# 2) 逐一確認送出。額度到這一刻才凍結,所以 402 只會在這裡出現。
jobs, retryable = [], []
for s in slips:
status, body = call("/videos/generations",
{"quote_id": s["quote_id"], "confirm": True})
if status == 200:
jobs.append(body["id"])
print("已送出 %s → %s(凍結 %.6f)"
% (s["model"], body["id"], body["price_reserved"]))
continue
err = body["error"]
if status == 402:
# 餘額不足:額度沒被凍結,這張報價單退回可用狀態。
# 加值後拿同一個 quote_id 再確認一次即可,不必重新報價。
retryable.append(s)
print("%s 餘額不足(需要 %s,可用 %s);quote_id 仍可用:%s"
% (s["model"], err.get("needed"), err.get("available"), s["quote_id"]))
elif status == 400:
# 逾期或已取消:這張不能再用了,要重新報價
print("%s 這張報價單不能用了:%s" % (s["model"], err["message"]))
else:
# 上游拒收等其他失敗:凍結已原路退回,報價單也退回可用狀態,可重試同一張
retryable.append(s)
print("%s 送出失敗(%s):%s;稍後可重試同一張"
% (s["model"], err["code"], err["message"]))
print("已送出 %d 支,還有 %d 張報價單可以重試" % (len(jobs), len(retryable)))
# 之後對每個 job id 輪詢 GET /v1/videos/{id},做法與上一個範例相同
// 同時開多張報價單 → 逐一確認送出。Node 18+ 內建 fetch,無需任何套件。
const BASE = 'https://appletoken.app/v1'
const KEY = process.env.APPLETOKEN_KEY
// 回傳 { status, body };錯誤不丟例外,因為 402 要當成正常分支處理。
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? 'POST' : 'GET',
headers: {
Authorization: `Bearer ${KEY}`,
...(payload ? { 'Content-Type': 'application/json' } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
})
return { status: res.status, body: await res.json() }
}
const PROMPT = '清晨的漁港,漁船緩緩進港'
const CANDIDATES = [
{ model: 'Seedance-2.0-fast', seconds: 5, resolution: '720p' },
{ model: 'Seedance-2.0', seconds: 5, resolution: '1080p' },
]
// 1) 一次開多張報價單。彼此獨立,不會互相覆蓋;這一步不凍結任何額度。
const slips = []
for (const params of CANDIDATES) {
const { status, body } = await call('/quote', { ...params, prompt: PROMPT })
if (status !== 200) {
console.log(`報價失敗 ${params.model}:${body.error.message}`)
continue
}
// 各自保存自己的 quote_id 與到期時間,確認時要帶對的那一張
slips.push({ model: params.model, quoteId: body.quote_id,
price: body.price, expiresAt: body.expires_at })
console.log(`${params.model} 報價 ${body.price} USD,quote_id=${body.quote_id},有效到 ${body.expires_at}`)
}
// 2) 逐一確認送出。額度到這一刻才凍結,所以 402 只會在這裡出現。
const jobs = []
const retryable = []
for (const s of slips) {
const { status, body } = await call('/videos/generations',
{ quote_id: s.quoteId, confirm: true })
if (status === 200) {
jobs.push(body.id)
console.log(`已送出 ${s.model} → ${body.id}(凍結 ${body.price_reserved})`)
continue
}
const err = body.error
if (status === 402) {
// 餘額不足:額度沒被凍結,這張報價單退回可用狀態。
// 加值後拿同一個 quote_id 再確認一次即可,不必重新報價。
retryable.push(s)
console.log(`${s.model} 餘額不足(需要 ${err.price},可用 ${err.available});quote_id 仍可用:${s.quoteId}`)
} else if (status === 400) {
// 逾期或已取消:這張不能再用了,要重新報價
console.log(`${s.model} 這張報價單不能用了:${err.message}`)
} else {
// 上游拒收等其他失敗:凍結已原路退回,報價單也退回可用狀態,可重試同一張
retryable.push(s)
console.log(`${s.model} 送出失敗(${err.code}):${err.message};稍後可重試同一張`)
}
}
console.log(`已送出 ${jobs.length} 支,還有 ${retryable.length} 張報價單可以重試`)
// 之後對每個 job id 輪詢 GET /v1/videos/{id},做法與上一個範例相同
對話:串流讀取
重點只有兩個:每則事件都要檢查 object(錯誤不走狀態碼),以及從最後一則 chunk 取出 price_charged。
#!/usr/bin/env python3
"""AppleToken 對話串流 — 只用標準庫。"""
import json, os, urllib.request
BASE = "https://appletoken.app/v1"
KEY = os.environ["APPLETOKEN_KEY"]
def chat_stream(messages, model="ByteDance-Seed-1.8", max_tokens=512):
body = json.dumps({
"model": model, "messages": messages,
"max_tokens": max_tokens, "stream": True,
}).encode()
req = urllib.request.Request(BASE + "/chat/completions", data=body, method="POST")
req.add_header("Authorization", "Bearer " + KEY)
req.add_header("Content-Type", "application/json")
charged = None
with urllib.request.urlopen(req, timeout=300) as r:
for raw in r: # SSE 一行一行讀
line = raw.decode("utf-8").strip()
if not line.startswith("data: "):
continue
payload = line[6:]
if payload == "[DONE]":
break
ev = json.loads(payload)
# 一定要檢查 object:串流開始後錯誤不走 HTTP 狀態碼
if ev.get("object") == "error":
raise RuntimeError("%s: %s" % (ev["error"]["code"], ev["error"]["message"]))
delta = ev["choices"][0]["delta"].get("content")
if delta:
print(delta, end="", flush=True)
if "price_charged" in ev:
charged = ev["price_charged"]
print()
return charged
if __name__ == "__main__":
cost = chat_stream([{"role": "user", "content": "用三句話說明什麼是向量資料庫"}])
print("實收 %.6f USD" % cost)
// AppleToken 對話串流 — Node 18+ 內建 fetch,無需任何套件。
const BASE = 'https://appletoken.app/v1'
const KEY = process.env.APPLETOKEN_KEY
async function chatStream(messages, opts = {}) {
const res = await fetch(`${BASE}/chat/completions`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'ByteDance-Seed-1.8', max_tokens: 512, ...opts, messages, stream: true }),
})
// 串流「開始之前」的錯誤仍然是正常的 HTTP 狀態碼加 JSON
if (!res.ok) {
const { error } = await res.json().catch(() => ({ error: {} }))
throw new Error(`${error.code ?? res.status}: ${error.message ?? res.statusText}`)
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let charged = null
for (;;) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// SSE 事件可能被切在任何位置,只處理已經看到換行的完整行
let nl
while ((nl = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, nl).trim()
buffer = buffer.slice(nl + 1)
if (!line.startsWith('data: ')) continue
const payload = line.slice(6)
if (payload === '[DONE]') return charged
const ev = JSON.parse(payload)
// 一定要檢查 object:串流開始後錯誤不走 HTTP 狀態碼
if (ev.object === 'error') throw new Error(`${ev.error.code}: ${ev.error.message}`)
const delta = ev.choices[0].delta.content
if (delta) process.stdout.write(delta)
if (ev.price_charged !== undefined) charged = ev.price_charged
}
}
return charged
}
const cost = await chatStream([{ role: 'user', content: '用三句話說明什麼是向量資料庫' }])
console.log(`\n實收 ${cost} USD`)
# 串流:-N 關掉緩衝,才看得到逐段輸出
curl -N https://appletoken.app/v1/chat/completions \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"ByteDance-Seed-1.8","max_tokens":512,"stream":true,
"messages":[{"role":"user","content":"用三句話說明什麼是向量資料庫"}]}'
# 非串流:一次拿完整回應,price_charged 就在裡面
curl -s https://appletoken.app/v1/chat/completions \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"ByteDance-Seed-1.8","max_tokens":256,
"messages":[{"role":"user","content":"用一句話說明什麼是向量資料庫"}]}'
上線檢查清單
正確性
- ☐ 憑證存在環境變數或密鑰服務,不在版控、不在前端。
- ☐ 每個送出的
job_id都持久化,程式重啟後仍會被查詢完並取件(成品保留時間有限)。 - ☐ 輪詢會走到
completed或failed才停,逾時的任務會被排入稍後續查。 - ☐ 送件前呼叫
/v1/quote,把預估費用顯示給使用者或記入你的成本帳。 - ☐
seconds確定是數字而不是字串(表單取值最容易在這裡出錯)。 - ☐
aspect_ratio只使用文件列出的六個值。 - ☐ 以
/v1/models的modality分類模型,不要靠 id 前綴猜。
對話
- ☐ 每個對話請求都帶
max_tokens,而且貼近實際需要——它決定預扣金額。 - ☐ 串流讀取器檢查每則事件的
object,遇到"error"當成失敗處理。 - ☐ 從最後一則 chunk(或非串流回應)取出
price_charged記入成本帳。 - ☐ 明白中途斷線仍會為已產生的內容付費,不要用「使用者按停止」當成本控制手段。
- ☐ HTTP 客戶端沒有在串流上套整體逾時(長回應是正常的)。
韌性
- ☐
429、502、503、504走指數退避重試;400、401、403一律不重試。 - ☐ HTTP 逾時設定足夠寬鬆(非串流建議 120 秒),但輪詢間隔不小於 3 秒。
- ☐ 送件的重試有冪等保護——重送等於再付一次錢,請確認上一次真的沒成立。
- ☐ 下載成品後立刻存入你自己的儲存空間,不要依賴平台長期保存。
成本控制
- ☐ 定期呼叫
/v1/me,在available低於門檻時告警。 - ☐ 限制併發數:
reserved是所有進行中請求的凍結總和,衝太高會讓後續請求撞到402。 - ☐ 以
/v1/usage的id為主鍵做每日對帳(有job_id的列再對回你的任務紀錄),並用modality拆分不同類型的成本。 - ☐ 正式與測試環境使用不同憑證,方便分開看用量、獨立停用。
常見問題
可以取消已送出的任務嗎?
不行。成本在送出當下就已產生,因此沒有取消端點。請在送件前用 /v1/quote 確認參數與費用。對話串流可以中止讀取,但已產生的部分照樣計費。
有 webhook 或回呼通知嗎?
目前沒有,請求中的 callbackUrl 類欄位會被丟棄,請使用輪詢。但要澄清一個常見誤解:輪詢不是結算的前提。平台有背景程序負責結算與退還,你不查詢也不會有額度卡住。輪詢的目的是知道結果、把成品抓下來。
生成失敗要付錢嗎?
影片任務失敗時凍結全額退還,used 不受影響;退還由平台自動完成,不需要你輪詢到 failed。
對話則要看有沒有產出:一個 token 都沒產出才全額退還;只要吐出了內容(即使只有半句話、即使後來斷線),那部分照實計費。
為什麼對話一定要給 max_tokens?
因為額度必須在呼叫後端之前凍結,而對話的用量到最後一刻才知道。max_tokens 是「這次最多產出多少」的上限,平台用它算出最壞情況的價格先凍結起來。結算一律以實際用量重算,差額當場退回,所以你付的永遠是實際用量。詳見對話的預扣與串流計費。
串流到一半我把連線關掉,會被收多少?
收「已經產生的部分」。不會整筆退還,也不會照 max_tokens 的預扣全收。差額在斷線的當下就結算完畢。要少花錢,正確作法是把 max_tokens 設小。
對話可以先估價嗎?
/v1/quote 只支援影片模型。對話的費用取決於實際產生多少 token,事前無法算準——但預扣公式是公開的,你可以自己算出「這次最多會花多少」,那正是平台凍結的金額。
可以同時送多個任務嗎?
可以,只要 available 足以覆蓋所有請求的預扣總和。每個請求各持有一筆獨立凍結。建議自行限制併發數,並對 429 做退避。
為什麼 calls 的數字比我的呼叫次數少?
calls 只計算成功建立凍結的請求——影片送件與對話請求都算,估價、查詢、下載、/v1/me 都不算。
底層用的是哪一家的模型?
這是 AppleToken 的實作細節,不對外揭露,也不需要你關心。我們保證的是文件所列的介面、能力與價格;底層供應商更換時,你的整合不需要任何修改。
價格會變嗎?
可能會。請從 /v1/models 讀取價格而非寫死,並以 /v1/quote 作為每次影片交易的報價依據。價格調整會事先通知,且既有的請求一律以送出當下生效的費率結算。
有速率限制嗎?
有。平台以 1 分鐘為時間窗做限流:帶 Authorization 標頭的請求是每分鐘 600 次,以憑證分別計數;未帶憑證的請求是每分鐘 20 次,以來源 IP 計數。/healthz 不受限。超過時回 429,error.code 是 rate_limited,訊息是「請求太頻繁,請稍後再試」,不會帶 retry_after。平台保留調整配額的權利,請不要把這些數字寫死進你的整合裡,該做的指數退避照樣要做。
429 有兩種來源,請用 error.code 分辨:rate_limited 是上面說的平台限流;upstream_error 則是上游服務的節流被原樣透傳。兩者的回應都不會帶 retry_after,額度也都不會被扣——請用自己的指數退避重試。
要留意的是整合寫錯造成的失控迴圈——那種情況下額度會在幾秒內燒光,而通常要等到帳單才會發現。請自行限制併發與重試次數。/v1/* 的每一個端點都需要憑證,GET /v1/models 也不例外。
額度不足不會回 429,而是回 402 quota_exceeded,並附上這次需要的金額與當下的可用餘額。那不是節流,重試也不會變好——請先加值,或降低同時進行的請求數。
版本紀錄
每一版改了什麼、哪些整合需要跟著調整,都列在版本歷程。那一頁由後台直接維護,發版當天就會更新,不必等這份文件重新發布;目前的 API 版本與文件版本也標示在那裡。
回報問題時請附上時間(含時區)、job_id、憑證 id 前綴(例如 at_live_9f2c81ab_…)與收到的 error.code。對話請求沒有 job_id,請改附 /v1/usage 裡對應那一列的 id 與 created_at。請勿在任何訊息中貼出完整憑證。