AppleToken API
一把憑證,接上多個世代的影片生成模型與對話模型。額度、估價、計量與計費都在同一層完成——你不需要面對任何上游供應商的帳號、金鑰或帳單。
/v2,模型名稱必須指定上游
自 API v2.0 起,請求裡的 model 不再接受裸的模型名稱(例如 qwen3.8-flash-next),必須改用模型 ID(mdl_…)、slug(sailflow-qwen3-8-flash-next)或上游/模型(sailflow/qwen3.8-flash-next)三者之一。同一個名稱由兩家上游供應,就是兩個獨立的模型,各有自己的售價與指標。舊寫法會收到 400 model_ref_required。
這份文件寫的是 /v2。/v1 照舊可用:行為與原本相同(只收裸名稱、固定使用 Siraya),既有整合不改任何一行也能繼續跑;要用多上游與新的模型寫法,才需要換到 /v2。
chat/completions,計費逐 token。
額度模型 →預扣、結算、退還——以及平台在背景幫你收尾的部分。
錯誤碼 →每個狀態碼該重試、該修參數,還是該找我們。
這個 API 是什麼
AppleToken 是一層生成服務的中介。你用我們發的憑證呼叫我們的端點,我們代為向後端算力供應商送件、把成品取回、依實際用量計費。目前提供兩類能力:影片生成(非同步任務)與對話(同步,可串流)。
- 基底網址
https://appletoken.app- 版本前綴
/v2(本文件涵蓋的全部端點)- 協定
- HTTPS,HTTP/1.1
- 認證
Authorization: Bearer at_live_…- 編碼
- 請求與回應一律 UTF-8 JSON(成品下載與對話串流除外)
- 金額單位
- 美元(USD)。所有價格、額度、帳務數字皆同。
一個模型=一家上游供應的一個模型,而且上游是公開的。請求裡的 model 寫「上游/模型」(或 AppleToken 發的模型 ID/slug),GET /v2/models 會回每個模型的 provider 與 upstream_model,各家的價格與速度你可以自己比。仍然由 AppleToken 自有、不會外露的是:任務 id、成品網址、上游的進價、上游的任務/素材 id 與金鑰——你拿到的一律是本平台的識別碼與網址,也只跟 AppleToken 結帳。
三件事值得先知道
- 費用可以在送出前算準。 影片用
POST /v2/quote,不扣額度、不呼叫後端,直接回傳這組參數會花多少錢。對話沒有估價端點,但預扣公式是公開的,見對話的預扣與串流計費。 - 送件即預扣額度。 影片任務一旦送出就無法取消,額度會立刻凍結,等結算或退還;對話則以
max_tokens為上限先凍結,結束後用實際用量結算。細節見額度模型。 - 結算由平台負責,不是由你的輪詢負責。 平台有背景程序主動追蹤每一筆任務並完成結算,你查不查都不影響帳務正確性,凍結不會因為你沒查詢而卡住。你仍然應該輪詢——那是你知道任務結果、能夠取件的唯一方式。詳見結算、退還與對帳。
五分鐘上手
1. 取得憑證
登入 主控台 → API 憑證,按「建立」就能自己產生一組,不需要另外申請。格式長這樣:
at_live_9f2c81ab_kJ3nR7xQwZ0pLm5TfYbHs2VdEcAu1NgX
憑證只會在建立與換發的當下顯示一次。沒有任何端點可以再把它讀出來——遺失就只能換發,舊的立即失效。
把它設成環境變數,下面每個範例都讀 APPLETOKEN_KEY:
export APPLETOKEN_KEY="at_live_…"
2. 看看有哪些模型
取得你這張憑證可以使用的模型與現行價格。這個請求不扣額度,餘額是 0 也能打——拿來確認憑證有效、連得上:
curl https://appletoken.app/v2/models \
-H "Authorization: Bearer $APPLETOKEN_KEY"
同一個請求的 Python 與 Node.js 寫法:
import os, requests
r = requests.get("https://appletoken.app/v2/models",
headers={"Authorization": "Bearer " + os.environ["APPLETOKEN_KEY"]})
print(r.status_code, len(r.json()["data"]))
const r = await fetch('https://appletoken.app/v2/models', {
headers: { Authorization: `Bearer ${process.env.APPLETOKEN_KEY}` },
})
console.log(r.status, (await r.json()).data.length)
3. 先問價格
curl https://appletoken.app/v2/quote \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "siraya/Seedance-2.0-fast",
"seconds": 5,
"resolution": "720p",
"aspect_ratio": "16:9"
}'
{
"model": "siraya/Seedance-2.0-fast",
"unit": "output_tokens",
"billable": { "output_tokens": 108900 },
"price": 0.539055,
"estimated": false,
"available": 20.0
}
4. 送出任務
curl https://appletoken.app/v2/videos/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "siraya/Seedance-2.0-fast",
"prompt": "清晨的漁港,漁船緩緩進港,海面反射金色晨光",
"seconds": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"generate_audio": true
}'
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "processing",
"model": "siraya/Seedance-2.0-fast",
"created_at": 1756300000,
"price_reserved": 0.539055
}
5. 輪詢到完成
curl https://appletoken.app/v2/videos/job_1a2b3c4d5e6f7a8b9c0d \
-H "Authorization: Bearer $APPLETOKEN_KEY"
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "completed",
"model": "siraya/Seedance-2.0-fast",
"seconds": 5,
"resolution": "720p",
"created_at": 1756300000,
"completed_at": 1756300182,
"content": "/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content",
"price_charged": 0.539055
}
6. 取件
curl https://appletoken.app/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-o output.mp4
quote ──► generations ──► videos/{id} ──► videos/{id}/content
估價 送出+預扣額度 輪詢+取得結果 下載 mp4
不扣額度 回 job_… completed 才可取件 不影響額度
結算在背後獨立進行:平台的背景程序會追到每一筆任務的終局,
你的輪詢只是順手觸發同一段結算邏輯,兩者冪等。7. 想要對話,只要一個呼叫
對話是同步的,沒有任務 id、不需要輪詢。它也是最便宜的第一次付費呼叫(一次約幾分錢),需要帳戶有餘額:主控台右上角「儲值」(最低 $10),或在同一個對話框輸入優惠碼。
curl https://appletoken.app/v2/chat/completions \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "siraya/ByteDance-Seed-1.8",
"max_tokens": 512,
"messages": [{ "role": "user", "content": "用三句話說明什麼是向量資料庫" }]
}'
同一個請求的 Python 與 Node.js 寫法:
import os, requests
r = requests.post("https://appletoken.app/v2/chat/completions",
headers={"Authorization": "Bearer " + os.environ["APPLETOKEN_KEY"]},
json={"model": "siraya/ByteDance-Seed-1.8", "max_tokens": 512,
"messages": [{"role": "user", "content": "用三句話說明什麼是向量資料庫"}]})
print(r.json()["choices"][0]["message"]["content"])
const r = await fetch('https://appletoken.app/v2/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.APPLETOKEN_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'siraya/ByteDance-Seed-1.8', max_tokens: 512,
messages: [{ role: 'user', content: '用三句話說明什麼是向量資料庫' }],
}),
})
console.log((await r.json()).choices[0].message.content)
max_tokens 和 max_completion_tokens 都是選填;建議明確設定輸出預算,方便控制預扣。未設定時,平台使用模型目錄的預設或最大輸出量估算。完整契約見 POST /v2/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 | 與停用共用同一句訊息,刻意不區分 |
| 來源 IP 不在這把憑證的 IP 白名單內 | 403 ip_not_allowed | 「這組憑證設定了 IP 白名單,來源 IP {ip} 不在名單內」——訊息會寫出平台看到的來源 IP,見下方 IP 白名單 |
| 憑證已換發 | 401 unauthorized | 舊憑證在換發當下立即失效,沒有寬限期 |
| 模型不在憑證的允許清單 | 404 not_found | 「未知的模型 {model}」——與模型不存在時回同一種錯誤,避免拿來探測目錄。影片與對話同樣適用 |
| 額度不足 | 402 quota_exceeded | 這不是認證問題,憑證仍然有效 |
/v2/* 不使用 cookie,因此沒有 CSRF 相關要求。憑證預設不綁定來源 IP,要綁定請設定下方的 IP 白名單。
IP 白名單(選用)
在前台「API 憑證」頁,每把憑證下方都可以設定「IP 白名單」。沒有設定的憑證可以從任何 IP 呼叫;設定後,只有名單內的來源 IP 能使用這把憑證,所有 API 端點都適用。
- 可以填單一 IP 或 CIDR 網段,IPv4、IPv6 都支援,一把憑證最多 20 筆,儲存後立即生效。
- 名單以外的 IP 呼叫時回
403 ip_not_allowed,訊息會寫出平台看到的來源 IP。這個檢查排在「停用/到期」之後,已停用的憑證仍然回403 forbidden。 - 來源 IP 是平台在網路邊緣判定的連線位址,不採用你自己帶的
X-Forwarded-For。程式在 NAT、Proxy 或 VPN 後面時,要填的是對外出口的 IP。 - 不確定要填什麼:先用這把憑證從你的伺服器呼叫一次,前台會在「最後使用」底下顯示平台看到的來源 IP,白名單表單也有按鈕可以直接帶入。前台顯示的「這個瀏覽器的 IP」是你開網頁的網路,不一定是伺服器的 IP。
- 程式跑在 IP 會變動的環境(Vercel、AWS Lambda 等 serverless 平台,或家用網路)時不建議設定,除非你為它設定了固定的對外出口 IP(例如 NAT Gateway)。
- 設錯不會被鎖在外面:前台是網頁登入,不受白名單影響,回到同一頁清空即可。
請求與回應慣例
請求
- 只支援
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": …}外層——列表型端點例外(/v2/models、/v2/jobs、/v2/usage會包在data陣列裡)。 - 時間欄位一律是 Unix 秒級時間戳(整數,UTC)。
- 金額為浮點數,單位美元。平台內部以 micro-dollar(百萬分之一美元)的整數運算,回應時轉成最多 6 位小數,因此不會有浮點尾差。
- 回應一律帶
Cache-Control: no-store,請不要快取任何回應。 - 兩個端點不回 JSON:
/v2/videos/{id}/content回原始位元組;/v2/chat/completions在stream: true時回text/event-stream。
錯誤
所有錯誤共用同一個外層結構:
{
"error": {
"code": "quota_exceeded",
"message": "額度不足",
"needed": 1.25,
"available": 0.42
}
}
code 是穩定的機器可讀識別,請用它來分支處理;message 是給人看的,措辭可能調整。部分錯誤會附加額外欄位(上例的 needed、available),詳見錯誤碼總表。
遷移指南:從 /v1 換到 /v2(v2.0)
一個模型=一家上游供應的一個模型。同一個名稱如果由兩家上游供應,在 AppleToken 裡就是兩個獨立的模型:各有自己的模型 ID、slug、售價,以及速度/穩定度/評分四個指標。所以請求裡的 model 必須明確指出你要的是哪一個——只寫名稱已經不夠。
① 把 base_url 從 https://appletoken.app/v1 換成 https://appletoken.app/v2(每一個端點都有,路徑的其餘部分不變)。② 把程式裡每一個 model 的值,從裸名稱改成下表三種新寫法之一。建議存模型 ID。其他參數與回應形狀都沒有換。
不換也可以:/v1 維持原本的行為,但不會再加新功能;多上游、模型 ID/slug、請求評價只在 /v2。
舊寫法與新寫法
| 寫法 | 例子 | 說明 |
|---|---|---|
| 舊:裸名稱 | qwen3.8-flash-next | 不再可用。分不出是哪一家,回 400 model_ref_required,並附上可用的候選。 |
| 模型 ID | mdl_7k2m9q4x1c(示意) | mdl_ 加 10 碼。由 AppleToken 發放,永久不變、永不重用。建議整合端存這個。 |
| slug | sailflow-qwen3-8-flash-next | 小寫英數字與連字號。由 AppleToken 發放,建立後永久不變。適合寫在人要讀的設定檔裡。 |
| 上游/模型 | sailflow/qwen3.8-flash-next | 斜線前是上游 id,斜線後是模型名。建立後永久不變,也就是回應標頭 X-AppleToken-Route 的值。 |
三種新寫法完全等價,指向同一個模型,而且在 AppleToken 內都是全域唯一的。格式互斥(ID 以 mdl_ 開頭、上游/模型含 /、slug 兩者都沒有),不會解析出兩種結果。ID 與 slug 是 AppleToken 自己的識別碼,不是上游的模型名;上游改名不會影響它們。下面三個請求打到的是同一個模型(mdl_7k2m9q4x1c 是示意用的 ID,實際的值請從 GET /v2/models 取得):
{ "model": "mdl_7k2m9q4x1c", … }
{ "model": "sailflow-qwen3-8-flash-next", … }
{ "model": "sailflow/qwen3.8-flash-next", … }
從 GET /v2/models 取得
GET /v2/models 的一列就是一個模型,id、slug、ref 都在上面,可以直接貼進 model。family 是原本的裸名稱,只用來把同名、不同家的模型排在一起比價,不能拿來呼叫。
# 列出所有模型的 id、slug、ref 與起價
curl -s https://appletoken.app/v2/models -H "Authorization: Bearer $APPLETOKEN_KEY" | jq -r '.data[] | [.id, .slug, .ref, .pricing.from] | @tsv'
# 原本叫 qwen3.8-flash-next 的模型有哪幾家(family 只能拿來找,不能拿來呼叫)
curl -s https://appletoken.app/v2/models -H "Authorization: Bearer $APPLETOKEN_KEY" | jq -r '.data[] | select(.family == "qwen3.8-flash-next") | "\(.id)\t\(.ref)\t\(.pricing.from)"'
遷移步驟
- 把 base_url 換成
https://appletoken.app/v2。同一把憑證、同一個帳戶與餘額在兩個版本都能用,可以一個端點一個端點慢慢換。 - 用上面的指令,把程式裡每個裸名稱對應到你要用的那一家。同名的兩家價格與速度可能不同,請依
pricing與四個指標挑選。 - 在設定或資料庫裡存模型 ID(
mdl_…)。三種寫法都不會變,但 ID 最短、最不容易被誤改。 - 把請求的
model換成新寫法。在/v2送裸名稱會直接回400 model_ref_required,照回應裡的candidates改即可。 - 如果你有帶
"fallback": true,把它拿掉(見下方)。 - 對帳程式改用
model_id歸戶(見下方)。 - 讀
GET /v2/models的程式改讀新形狀:default_route與routes[]已移除,現在一列就是一個模型。
回應裡的 model
- 生成與報價(
/v2/chat/completions含串流、/v2/images/generations、/v2/videos/generations、/v2/quote):回應 body 的model照你送的原字串回——送 ID 回 ID、送 slug 回 slug。實際用到的模型 ID 另外放在回應標頭X-AppleToken-Model-Id;X-AppleToken-Route照舊是「上游/模型」。 - 紀錄與查詢(
GET /v2/usage、GET /v2/jobs、GET /v2/videos/{job_id}):每一列的model一律是「上游/模型」,並多一個model_id。不論當初用哪種寫法送,這裡都是同一個值,對帳請以model_id歸戶。v2.0 之前的舊紀錄若對不到模型,model_id是null,model保留舊名稱。
fallback 已不支援
以前 "fallback": true 的意思是「這家失敗時,改走同一個模型的另一家」。現在不同家就是不同的模型——換家等於換模型,平台不會替你做這個決定。帶 "fallback": true 會回 400 fallback_unsupported(false 或不帶照常)。需要備援的話,請在你的程式裡自己改送另一個模型的 ID。
{
"error": {
"code": "fallback_unsupported",
"message": "fallback 已不支援:換一家上游就是換一個模型,請自行指定要用的模型",
"param": "fallback",
"docs_url": "https://appletoken.app/docs#model-migration"
}
}
錯誤回應
送裸名稱會收到 400,candidates 列出可以改用的模型,docs_url 指向這一節:
{
"error": {
"code": "model_ref_required",
"message": "模型名稱必須指定上游:'qwen3.8-flash-next' 已不再接受,請改用下列其中一個(或它的 id/slug),見遷移指南",
"param": "model",
"candidates": [
{ "id": "mdl_7k2m9q4x1c", "slug": "sailflow-qwen3-8-flash-next", "ref": "sailflow/qwen3.8-flash-next" }
],
"docs_url": "https://appletoken.app/docs#model-migration"
}
}
candidates只列出這把憑證能用的模型。裸名稱若不在你的憑證白名單內,回的是404 not_found「未知的模型」,與不存在的模型相同。- 不存在的 ID、slug 或「上游/模型」一律回
404 not_found。 - 錯誤訊息(
message)目前一律以繁體中文回傳;請以code分支,不要解析訊息文字。
/v1 照舊可用
/v1 維持原本的行為,既有整合不需要改任何一行:
model只收原本的裸名稱,固定由 Siraya 供應;「上游/模型」、模型 ID、slug 在/v1一律當作不存在的名稱(400「未知的模型」)。- 回應形狀與原本相同:
GET /v1/models沒有provider、routes這類上游欄位,回應也不帶X-AppleToken-Route標頭。 - 同一把憑證在兩個版本都能用,餘額、用量與任務是同一本帳:在
/v2產生的紀錄,/v1的列表照樣列出(以/v1的形狀)。 /v1不會再加新功能。目前沒有排定停止日期;若要停止,會提前在版本歷程公告。
/v1 的完整說明保留在舊版(v1)文件。
模型目錄
可用的模型與價格會隨時增減,所以不列在這份文件裡——
這裡只講不會變的協定規則。你需要知道兩件事:一個模型=一家上游供應的一個模型,
請求裡的 model 要寫它的模型 ID、slug 或「上游/模型」(見遷移指南),
三者都由 AppleToken 發放、永久不變;modality 決定它走哪一組端點——
video 走 /v2/videos/generations,
image 走 /v2/images/generations,
text 走 /v2/chat/completions,
互相呼叫會得到 400。
不論模型由哪一家上游供應,端點、參數、回應欄位與錯誤碼都相同,唯一的差別是 model 要寫明上游。各模型能接受的秒數、解析度、畫面比例、參考素材數量與音訊,只寫在該模型在 GET /v2/models 的 constraints 裡——請以它為準,不要依上游名稱推斷。平台依 constraints 在送件前檢查,細節見參數限制總表。
每個影片模型另有 checked:平台送件前會依該模型 constraints 檢查的參數。不在 checked 裡的參數平台不先檢查,不合規的值要等任務終局才會以失敗出現。
模型清單——網頁版,列出每個模型的參數限制與 完整費率階梯。要登入才看得到:費率是商業資訊, 只對已開通的帳號揭露,未登入點進去會被導到登入頁。還沒有帳號的話, 先註冊再回來看。
GET /v2/models——程式版,
整合時請從這裡讀取而不要寫死。兩者都直接從平台的 catalog 產生,
後台改完立刻反映,不會出現文件與實際不符。
若你的憑證設有模型白名單,GET /v2/models
只會回白名單內的模型。清單上有的就是你能用的,不會出現叫了才發現沒權限。
平台在整備上游對接期間,公開目錄可能不含任何模型。 我們不會列出還不能真正叫用的模型——你照著串接卻叫不動,那是我們的問題。 協定不受影響,這份文件描述的一切在目錄有內容後即刻適用。
三種計價單位
每個模型的 pricing.unit 會告訴你它用哪一種:
| 單位 | 怎麼算 |
|---|---|
output_tokens |
影片依畫面尺寸與影格數換算成 token 量;文字直接用輸出的 token 數。再乘上單價。公式在此。 |
video_seconds |
秒數 × 單價。與解析度無關。 |
request |
按張計價(圖片模型):一張一個固定價格,與提示詞長度無關;結算看實際回來幾張,不是請求的 n。見圖片端點。 |
有多段費率的模型,pricing.from 是最低單價、pricing.to 是最高單價。
多段費率
有些模型的單價不是固定值,會依請求的條件切換——例如帶影片參考、或解析度拉到 1080p 以上。 費率由上往下比對,第一條命中的勝出。
哪些模型有幾段、各段的條件與單價,列在
模型目錄(需登入)——那一頁直接從平台的 catalog 讀,
永遠是當下生效的數字。這裡不重複,免得兩邊對不上。
未登入時請改用 GET /v2/models,憑證即可讀取。
不確定會落在哪一段時,先呼叫 /v2/quote——它回的
price 就是實際會凍結的金額,不必自己推算。
三種輸入模式
所有影片模型共用 POST /v2/videos/generations 這一個端點,實際模式由你給的欄位決定:
| 模式 | 關鍵欄位 | 用途 |
|---|---|---|
| 文生影 | 只給 prompt | 純文字描述生成。 |
| 圖生影 | image_url 或 frame_images | 以圖片作為起始畫面延伸出影片。 |
| 參考導引 | input_references | 提供多張圖、影片片段或音訊作為風格/內容/節奏參考。 |
frame_images 與 input_references 不可同時提供。兩者都需要時,請一律走 input_references,並在 prompt 裡描述期望的起始畫面——注意這不保證像素級一致。
部分模型要求 image_url/frame_images 的圖片比例與輸出畫面相同(容差 10%;輸出畫面由 resolution+aspect_ratio 或 size 決定),也可能限制首尾幀合計的檔案大小。不符合時平台在送件前回 400 validation_error,訊息會寫出實際尺寸與原因;不會凍結額度。請先把圖片裁成輸出比例:直接送出的話,畫面會被拉伸變形。
參考素材的數量上限
每個模型能帶多少參考素材,寫在該模型 GET /v2/models 的 constraints 裡,請以它為準:
| 欄位 | 意義 |
|---|---|
constraints.maxReferences | 各類型加總的上限 |
constraints.references.image/.video/.audio | 各類型各自的上限 |
constraints.references.audioOnly | false=不能只給音訊,至少要有一張圖片或一段影片 |
"constraints": {
"maxReferences": 15,
"references": {"image": 9, "video": 3, "audio": 3, "audioOnly": false}
}
有宣告上限的模型(checked 含 input_references),平台會在送出前檢查數量並回 400 validation_error(param: "input_references"、reason: "too_many"),不會凍結額度、也不會產生任務。
沒有宣告的模型不做預先檢查——那類請求會送出成功(狀態為 processing),錯誤在任務被追蹤到終局時才以 status: "failed" 出現。
計數看的是每個項目的 type。沒有填 type 的項目計入總數,但不計入任何一種類型的上限——這類項目平台不替你推斷。
影片參考素材的規格與拒收訊息
參考影片由生成服務在上傳當下判定,不合規會被拒收。平台不會預先擋件;被拒收時,平台會拿上傳時讀到的檔頭規格對照下表,在錯誤訊息裡指出是哪一條。宣告了參考影片規格的模型,數值見 GET /v2/models 的 constraints.referenceVideo(只揭露,平台不預先檢查)。
| 項目 | 後端要求 | details.reasons 代碼 |
|---|---|---|
| 幀率 | 24–60 fps | video_fps_out_of_range |
| 總像素數(寬×高) | 409,600(640×640)~8,295,044(3326×2494);4K(3840×2160)在範圍內 | video_pixels_out_of_range |
| 長寬比(寬÷高) | 0.4–2.5 | video_aspect_out_of_range |
| 單邊長度 | 300–6000 px | video_side_out_of_range |
| 長度 | 至少 2 秒;上限依模型,由生成服務在生成時判定 | video_too_short |
| 格式 | mp4、mov;webm 可上傳但平台讀不出規格、無法解釋拒收原因 | — |
拒收時的回應維持 502 upstream_error(既有的錯誤處理不必改),差別在 message 會寫明原因與實際規格,並多一個 details 欄位供程式分流:
{"error": {"code": "upstream_error",
"message": "上游拒收這份影片素材:幀率 18 fps 不在 24–60 fps 之間(請在剪輯軟體匯出時改成 24、25 或 30 fps,解析度不必改)。(這支影片是 1920x1080、18 fps、7.8 秒)",
"details": {"reasons": ["video_fps_out_of_range"],
"video": {"width": 1920, "height": 1080, "fps": 18, "durationSeconds": 7.78}}}}
剪輯軟體匯出成 18 或 20 fps 的檔案看起來完全正常(1080p、mp4),但後端一律拒收。匯出時把幀率設為 24、25 或 30 fps 即可,解析度不必改。2026-09-14 之前這種檔案只會拿到「upstream service error」,看起來像服務故障;現在訊息會直接說是幀率。
規格全部合格卻仍被拒、或檔案讀不出規格時,message 退回通用版(列出該檢查的項目),且沒有 details。
影片參考與長度:reference_mode
當 input_references 含 type: "video" 的項目時,可以用 reference_mode 告訴後端這支參考影片要怎麼用。它決定你給的 seconds 會不會成為輸出長度:
reference_mode | 用途 | seconds | 輸出長度 |
|---|---|---|---|
| 不帶(預設) | 由後端依 prompt 自行判斷 | 必填,照你給的值送出 | 後端判成延伸/參考生影片時等於 seconds;判成編輯時任務會失敗(見下方) |
"auto" | 明確宣告「由後端依 prompt 判斷」 | 同「不帶」 | 同「不帶」 |
"edit" | 編輯:替換、增刪、修改原片內容 | 平台一律改送 -1,你給的值只用來估價與預扣 | 等於參考影片長度 |
"extend" | 延伸:接在原片之前或之後繼續發展 | 整數,就是輸出長度;範圍見 constraints.modes.video_extend,沒宣告時 4–30 | 等於 seconds;成品只有新增的部分,不含原片 |
"reference" | 參考生影片:只借用角色、風格、運鏡,拍新的畫面 | 整數,就是輸出長度;範圍見 constraints.modes.video_reference,沒宣告時 4–30 | 等於 seconds |
reference_mode只接受上表四個字串。沒有影片參考卻帶了reference_mode,或extend/reference的seconds不是範圍內的整數,都會回400 validation_error(後者帶param: "seconds"、mode)——不會被靜默忽略,也不會凍結額度。prompt的寫法要和宣告的模式一致。延伸就寫「接在最後一幀之後繼續……」,編輯就寫「把 X 換成 Y」,參考生影片描述要拍的新畫面、避開「替換、保持不變、延長、繼續」這類字眼。兩者不一致時任務會以失敗告終。- 沒指定模式(或
"auto")時,平台照你給的seconds送出,不會改成-1。生成服務若依prompt把任務判成編輯——編輯的長度只能由原片決定——帶了具體秒數的任務會失敗:送件照常成功,之後查詢時status變成failed,凍結全額退還。平台不會自動改送或重試;要編輯原片,請明確帶reference_mode: "edit"。同理,部分模型在延伸或編輯時比例只能跟隨原片:這類模型會在constraints.modes.<模式>.aspectRatio宣告"inherit",明確宣告該模式時帶了其他比例會直接回400(reason: "mode_conflict",不帶或填"adaptive"即可);沒宣告模式時平台無法預知,任務可能以失敗告終。失敗時error.upstream_message會附上生成服務的原文,請依原文調整參數,見查詢任務。 - 延伸的成品只有新增的部分。需要「原片+延伸」的完整成片時,請自行把兩段接起來。
- 估價(
/v2/quote)接受同一個欄位,報價單的resolved會原樣回顯reference_mode。 - 實際長度與最終費用以結算為準;最終金額就是
/v2/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 秒的硬上限—— 沒有上界的請求會凍結一筆必然被拒的金額。這時checked不含seconds:60 秒以內的值平台都會送出,模型實際能不能生成,要等任務結果。 - 整數為宜。長度只由這個參數決定——在
prompt裡寫「三十秒的影片」不會生效。
解析度 resolution 與尺寸 size
resolution接受"480p"、"720p"、"1080p"等形式,必須在該模型的支援清單內。有清單的模型請照清單的寫法送("720p";"720"會被擋下)。- HappyHorse 系列(
happyhorse-…)的後端只收大寫的"720P"、"1080P"(1.1 版另有"480P";1.0 版沒有 480P)。平台會自動把resolution轉成大寫再送出,所以送"720p"或"720P"都可以;其他模型的resolution原樣送出,不做轉換。 size可直接指定像素,格式"1280x720"。同時提供時size優先決定計價用的畫面尺寸。- token 制的模型必須至少提供
resolution或size其中之一,否則無法精算用量,會回400。
size 時的檢查
平台不會把 size 的短邊換算成解析度再檢查(舊版文件寫的「短邊必須等於清單裡的值」並沒有實作)。模型宣告了 constraints.sizes 時,size 必須是清單裡的值,否則在送件時回 400 validation_error,不會產生費用。其他模型的 size 原樣送出,由生成服務判定。
條件式費率是看 resolution 而不是 size——
若某個模型的高費率條件寫的是解析度門檻,只給 size 會落在標準費率;
哪些模型有幾段費率請看 模型目錄(需登入)或 GET /v2/models 的 pricing。除非確有精確畫面需求,建議只用 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 計算,實際送出時依模型處理——可能原值送出、在生成時才失敗,也可能改用 16:9。請只使用上表的值或 "adaptive"(由輸入素材決定比例,部分模型與模式適用),並在送件前用 /v2/quote 確認價格。模型宣告了 constraints.aspectRatios 時,比例必須在該清單內,否則回 400 validation_error。
音訊 generate_audio
- 布林值。僅對音訊欄位標示為 ✅ 的模型有效。
- 模型的
constraints.audio為false時不支援音訊;對它傳true不會報錯,但成品不會有聲音。 未宣告audio的模型,平台不保證成品有沒有音軌。 - 音訊不影響計價——費用只看畫面尺寸、影格數或秒數。
對話參數
| 欄位 | 限制 | 不符合時 |
|---|---|---|
messages | 非空陣列,每個元素的 role 與 content 都必須是字串 |
400「缺少 messages」或「messages[i].content 必須是字串」 |
max_tokens / max_completion_tokens | 選填;提供時請使用大於 0 的數字(小數會被無條件捨去成整數);同時提供時後者優先。未提供時依模型目錄的上限估算 | 400:所選上限無效,或模型目錄也沒有可用上限 |
temperature | 選填,必須是數字;平台不檢查範圍,原樣轉給後端 | 400「temperature 必須是數字」 |
stream | 選填,必須是布林值 true / false。平台只做真假值判斷,因此任何非空字串(包含 "false")都會被當成 true 而啟用串流 |
不報錯,但可能走到你沒預期的模式 |
GET /v2/models
取得模型目錄與現行價格。
一列就是一個模型(一家上游供應的一個模型)。同一個名稱由兩家上游供應,會出現兩列,各有自己的 id、slug、ref、售價與四個指標。每一列的 id、slug、ref 三個值都能直接放進請求的 model,見遷移指南。不帶任何參數時回傳完整清單;以下三個查詢參數都是選填。
若這張憑證設有模型白名單,這裡只會列出白名單內的模型——
你看到的清單就是你實際能呼叫的清單,不會出現叫了才發現沒權限的情況。
沒有設白名單的憑證會看到全部可用模型。下面的 modality、q 篩選是疊加在這層過濾之上,不會取代它——即使關鍵字或模態命中白名單以外的模型,一樣不會出現在結果裡。
Query 參數
| 參數 | 預設 | 範圍 | 說明 |
|---|---|---|---|
modality | 不篩選 | video / image / text / audio / embedding | 只回傳指定模態的模型。不在這個集合內回 400 validation_error,訊息會列出可用的值。 |
q | 不篩選 | 最長 100 字元 | 關鍵字比對,同時比對模型的 id、slug、ref、name、family、label、description 與上游(provider 的 id 與顯示名),不分大小寫、子字串比對。超過長度上限回 400 validation_error。 |
limit | 500 | 1–500 | 回傳筆數上限。這裡的預設值是 500,不是 /v2/jobs、/v2/usage 的 100——模型目錄不是時序資料,「最近 100 筆」對它沒有意義,沿用同一個預設反而會讓既有整合在無聲無息中少拿到模型。超出範圍同樣回 400 validation_error,不會被夾到邊界值。若目錄未來成長超過 500 個模型,這個上限會需要調整。 |
三個參數可以自由組合,例如同時用 modality 縮小模態、q 比對關鍵字。
curl "https://appletoken.app/v2/models?modality=text&q=gpt" \
-H "Authorization: Bearer $APPLETOKEN_KEY"
回應
{
"object": "list",
"data": [
{
"id": "mdl_3h8r5t0v6w",
"slug": "siraya-seedance-2-0-fast",
"ref": "siraya/Seedance-2.0-fast",
"provider": { "id": "siraya", "label": "Siraya" },
"name": "Seedance-2.0-fast",
"upstream_model": "SIRAYA-Seedance-2.0-fast",
"family": "Seedance-2.0-fast",
"label": "Video Fast · SIRAYA",
"description": "生成較快,成本較低",
"modality": "video",
"constraints": {
"minSeconds": 4,
"maxSeconds": 15,
"resolutions": ["480p", "720p"],
"audio": true
},
"pricing": { "unit": "output_tokens", "from": 4.95, "to": 4.95 },
"status": "active",
"speed_p50_ms": 38100, "speed_basis": "submit_to_complete", "speed_response_p50_ms": null,
"fail_rate": 0.008,
"rating": { "avg": 4.4, "count": 31 },
"sample": { "speed_n": 180, "speed_response_n": 0, "fail_n": 180, "window_days": 7 }
},
{
"id": "mdl_8f2j6n0r4v",
"slug": "sailflow-minimax-h3",
"ref": "sailflow/minimax-h3",
"provider": { "id": "sailflow", "label": "Sailflow" },
"name": "minimax-h3",
"upstream_model": "sailflow/minimax-h3",
"family": "minimax-h3",
"label": "MiniMax-H3",
"description": "含音訊。秒數 4–15;720p 實際輸出 1280×704。",
"modality": "video",
"constraints": {
"minSeconds": 4,
"maxSeconds": 15,
"resolutions": ["576p", "720p", "1024p"],
"aspectRatios": ["16:9", "9:16", "1:1"],
"audio": true
},
"pricing": { "unit": "video_seconds", "from": 0.01, "to": 0.025 },
"status": "active",
"speed_p50_ms": null, "speed_basis": "submit_to_complete", "speed_response_p50_ms": null,
"fail_rate": null,
"rating": null,
"sample": { "speed_n": 12, "speed_response_n": 0, "fail_n": 12, "window_days": 7 },
"reason": { "speed": "insufficient_samples", "stability": "insufficient_samples", "rating": "no_data" }
},
{
"id": "mdl_9p4d2n7e5f",
"slug": "siraya-bytedance-seed-1-8",
"ref": "siraya/ByteDance-Seed-1.8",
"provider": { "id": "siraya", "label": "Siraya" },
"name": "ByteDance-Seed-1.8",
"upstream_model": "ByteDance-Seed-1.8",
"family": "ByteDance-Seed-1.8",
"label": "Chat Pro · SIRAYA",
"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 }
]
},
"status": "active",
"speed_p50_ms": 820, "speed_basis": "first_token", "speed_response_p50_ms": 2950,
"fail_rate": 0.002,
"rating": { "avg": 4.6, "count": 88 },
"sample": { "speed_n": 1240, "speed_response_n": 310, "fail_n": 1550, "window_days": 7 }
}
]
}
| 欄位 | 型別 | 說明 |
|---|---|---|
id | string | 模型 ID,mdl_ 加 10 碼。由 AppleToken 發放,永久不變、永不重用。可直接放進請求的 model,建議整合端存這個。 |
slug | string | 由 AppleToken 發放的可讀識別碼(小寫英數字與連字號),建立後永久不變。可直接放進 model。 |
ref | string | 「上游/模型」,例如 siraya/Seedance-2.0-fast。可直接放進 model,也是回應標頭 X-AppleToken-Route 的值。 |
provider | object | { id, label }:上游 id 與顯示名。 |
name | string | ref 斜線後面那一段。單獨拿來呼叫就是裸名稱,會回 400。 |
upstream_model | string | 這家上游對這個模型的模型名,僅供參考,不能拿來呼叫。 |
family | string | 系列,也就是 v2.0 之前的裸名稱。同名、不同家的模型 family 相同,只用來分組與比價,不能拿來呼叫(拿它呼叫就是裸名稱,會回 400 model_ref_required)。 |
label | string | 顯示名稱,適合直接放進你的 UI。 |
description | string | null | 一句話定位說明。可能是 null(還沒有說明的模型),請不要假設一定是字串。 |
modality | string | "video"、"image"、"text"、"audio"、"embedding" 其中之一(沒有 "chat" 這個值——對話模型的 modality 是 "text")。決定這個模型走哪一組端點:"video" 走 /v2/videos/generations,"image" 走 /v2/images/generations,"text" 走 /v2/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.references | object | 參考素材的分類型上限:image/video/audio(int)與 audioOnly(bool)。見參考素材的數量上限。有宣告的模型才有。 |
constraints.combos | object | 解析度 → 允許的比例,例如 {"1024p": ["1:1"]}。只有部分組合合法的模型才有。揭露用;不合的組合在送件時回 400。 |
constraints.frameImages | object | max:frame_images 的張數上限。有宣告的模型才有。揭露用;超量在送件時回 400。 |
constraints.referenceVideo | object | 參考影片的規格(例如 minSeconds、fps)。只揭露,平台不預先檢查。 |
constraints.modes | object | 依模式而定的限制。鍵是平台的模式名稱:text、first_frame、first_last_frame、references、video_edit、video_extend、video_reference、video_auto;值可以含上面的秒數、解析度、比例欄位,覆寫頂層。aspectRatio: "inherit"=這個模式下比例跟隨輸入影片、不能指定;seconds: "inherit"=輸出長度由輸入影片決定。模式由請求欄位判定:有 reference_mode 照它(影片參考);有影片參考沒宣告是 video_auto(只套頂層);有 input_references 沒影片是 references;兩張 frame_images 是 first_last_frame;image_url 或一張首幀是 first_frame;其餘 text。 |
checked | string[] | 影片模型才有,與 constraints 同層(新增欄位):平台在估價與送件前,會依這個模型的 constraints 先檢查的參數(seconds、resolution、aspect_ratio、input_references)。不在清單裡的參數平台不先檢查(部分仍可能在送件時被拒,例如 constraints.sizes)。與模型無關的平台通則(秒數為整數且不超過 60、reference_mode 的範圍)一律適用,不列在這裡。 |
constraints.maxOutputTokens / constraints.defaultMaxOutputTokens | int | 輸出 token 的上限,以及你沒有指定 max_tokens 時預扣所依據的預設值。對話模型才有。 |
constraints.sizes | string[] | 允許的 size 值,含 2K 這類簡寫。圖片模型有;影片模型有宣告時才有,此時 size(寬x高)必須在清單內。 |
constraints.minPixels / constraints.maxPixels | int | 兩個一起出現,代表這個模型除了 sizes 列舉的值之外,還接受任意 寬x高 像素字串,只要寬 × 高的總像素落在這個閉區間內。沒有這兩個欄位 = 只能用 sizes 裡的值。僅圖片模型有,見圖片端點。 |
constraints.acceptsN | bool | 接不接受 n。false 代表一次固定產生 1 張,連 "n": 1 都會回 400 validation_error——這個模型沒有 n 這個參數,不要送。僅圖片模型有。 |
constraints.maxN | int | 一次最多產生幾張(n 的上限)。acceptsN 為 false 的模型是 1。僅圖片模型有。 |
constraints.maxInputImages | int | 輸入圖(參考圖/圖生圖的 image)張數上限。0 = 不接受輸入圖。僅圖片模型有。 |
constraints.responseFormats | string[] | 可以指定的 response_format,目前恆為 ["url", "b64_json"],與上游支援哪幾種無關。僅圖片模型有。 |
constraints.qualities | string[] | quality 的可用值。沒有這個欄位 = 這個模型不接受 quality。僅圖片模型有。 |
constraints.background | bool | 接不接受 background 參數。false 的模型送了會回 400 validation_error,不會被靜默忽略。僅圖片模型有。 |
constraints.negativePrompt | bool | 接不接受 negative_prompt 參數。false 的模型送了會回 400 validation_error。僅圖片模型有。 |
constraints.seed | bool | 接不接受 seed 參數(整數)。false 的模型送了會回 400 validation_error。僅圖片模型有。 |
constraints.watermark | bool | 接不接受 watermark 參數。不送時的預設是「有浮水印」;true 的模型才能用 "watermark": false 關掉,false 的模型送了會回 400 validation_error。僅圖片模型有。 |
pricing.unit | string | "output_tokens"、"video_seconds" 或 "request"(按張計價的圖片模型:一張一個固定價格,結算看實際回來幾張,見圖片端點)。 |
pricing.from / to | float | 同一個計價單位(pricing.unit)之下的價格區間(USD):from 是最低單價、to 是最高單價。兩者相等表示單一費率,不等表示這個模型有多段費率;實際落在哪一段由請求的條件決定,用 /v2/quote 可以確認。它們不是「輸入單價/輸出單價」——輸入與輸出各自的單價請讀 pricing.components。 |
pricing.components | array | 有多個計價單位的模型(例如輸入與輸出各有單價)才有,每個元素是 { "unit", "from", "to" },各單位有自己的區間。單一計價單位的模型不會有這個欄位,請用「這個欄位存不存在」判斷。詳見對話的預扣與串流計費。 |
status | string | active 或 paused(暫停中,指定它會回 503,reason: "route_paused")。 |
speed_p50_ms、speed_basis | int|null / string | 過去 7 天成功請求的延遲中位數(毫秒)。基準依模態:文字=first_token(只算串流請求的首個 chunk)、圖片=response(完整回應)、影片=submit_to_complete(送件到完成,含上游排隊)。這是觀測值不是 SLA。 |
speed_response_p50_ms | int|null | 只有文字模型有值:過去 7 天非串流請求的完整回應延遲中位數(毫秒)。圖片、影片恆為 null(它們的完整回應就是 speed_p50_ms)。 |
fail_rate | float|null | 過去 7 天的失敗率=上游失敗 ÷(成功+上游失敗)。只算上游的問題(連線錯、5xx、429、逾時、上游判 failed);你的 400/404、內容審核、額度不足不算進去。 |
rating | object|null | { avg, count }:過去 30 天透過 請求評價 打的分數。avg 是每個帳戶算一票的平均(先算每個帳戶自己的平均,再對這些平均取平均),所以單一帳戶評再多次也只佔一票;count 是總評分則數。 |
sample | object | { speed_n, speed_response_n, fail_n, window_days }:速度(兩種基準各自)與穩定的樣本數。 |
reason | object | 指標為 null 時說明原因:insufficient_samples(樣本未達門檻)或 no_data(沒有任何資料)。有值的指標不會出現在這裡;文字模型多一個 speed_response 鍵。門檻:速度兩種基準各自需 30 筆、失敗率需 30 筆(7 天內);評分需 20 則且至少 3 個不同帳戶(30 天內)。 |
目錄未來可能新增模型、新增 modality,或補上欄位。請以 id 為準做對映,遇到未知欄位或未知 modality 忽略即可,不要用嚴格 schema 驗證擋掉整份回應。
選擇上游
選上游就是選模型。直接用那一列的 id、slug 或 ref 呼叫,就會固定由那一家供應,不會換家。這條規則對 對話、圖片、影片、報價一體適用。怎麼從舊寫法改過來,見遷移指南。
例外:auto/(v2.9)。把 model 寫成 auto/<模型名稱>,由平台在提供這個模型的上游中替你選一家,失敗時自動換家。目前只支援對話,見平台代選。
default_route、routes[]:已移除。現在一列就是一個模型,沒有「預設走哪一家」這件事;原本routes[]裡每一項的售價與四個指標,現在就在每一列模型上。- 裸
model(例如Seedance-2.0-fast)走預設上游:已移除,回400 model_ref_required。 "fallback": true:已不支援,回400 fallback_unsupported。換家就是換模型,請自行指定。?include=routes:不再有作用。
回應裡怎麼知道用了哪個模型
成功的生成與報價回應都帶兩個標頭(串流是 SSE 的 200 標頭):X-AppleToken-Route: siraya/Seedance-2.0-fast(上游/模型)與 X-AppleToken-Model-Id: mdl_3h8r5t0v6w。回應 body 的 model 照你送的原字串回。/v2/usage、/v2/jobs、/v2/videos/{job_id} 的 model 一律是「上游/模型」,並帶 model_id。
錯誤
| 狀態 | error.code | 情形 |
|---|---|---|
400 | model_ref_required | 送了裸名稱。回應附 candidates(可改用的 id/slug/ref)與 docs_url,見遷移指南。 |
400 | fallback_unsupported | 帶了 "fallback": true。 |
404 | not_found | ID、slug 或「上游/模型」不存在,或不在憑證白名單內——與「未知的模型」回應完全相同。 |
503 | service_unavailable | reason: "route_paused"(這個模型暫停中)、"circuit_open"(最近連續失敗,稍後再試)、"pricing"(這個模型目前無法報價)。 |
400 | validation_error | fallback 不是布林;或請求引用的素材(asset://)只在另一家上游有效。 |
音軌後處理跑在平台自己的服務上,不經過上游,不適用上游選擇。
內嵌檔案自動轉存
影片端點(POST /v2/videos/generations)的素材參數收的是網址,平台會把網址交給後端去下載——如果那個網址需要登入、是私有分享連結、或有防盜鏈,後端就抓不到,送件會失敗並回報「resource download failed」。若你沒有可公開存取的儲存空間,可以直接把檔案以 data URI 的形式放進素材參數——平台會在送件前自動把它存進暫存空間、換成一條可下載的網址再交給後端,呼叫方式完全不用改變。
這個轉存過程不呼叫任何模型,不扣額度。
| 做法 | 適合 |
|---|---|
| 直接內嵌 data URI | 沒有可公開存取的儲存空間時。 |
| 自備公開網址 | 你本來就有可匿名存取的儲存空間——大檔或重複使用的素材建議用這個,避免每次都傳一份 base64。 |
適用的參數:影片端點(POST /v2/videos/generations)的 image_url、input_references、frame_images——巢狀在物件或陣列裡的值也會被處理;以及圖片端點(POST /v2/images/generations)的 image,那裡只收圖片型別(image/png、image/jpeg、image/webp、image/gif)。對話端點(POST /v2/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。圖片端點不套這個數字——那裡的上限是「該模型接受幾張輸入圖」(查/v2/models的constraints.maxInputImages,各模型 0~14 不等),與是不是內嵌無關。 - 值不是 data URI 時完全不受影響——一般網址原樣送給後端,這個功能不會改變既有整合的行為。而且走網址就沒有平台的大小限制:20 MB/24 MB 這兩道只約束「內嵌 base64」這條路,位元組不經過平台時不受它們約束(後端自己抓得動多大,由後端決定)。
base64 編碼會讓資料膨脹約 1.33 倍,而整個 request body 上限是 24 MB。檔案較大、或同一份素材要在多次請求裡重複使用時,建議改用你自己可公開存取的網址——避免每次請求都重新內嵌一份 base64。
POST /v2/quote
用一組參數換一個價格。不扣額度、不凍結、不送件、不呼叫後端,可以放心在使用者按下「生成」之前先呼叫,用來顯示預估費用。
這個端點的估價邏輯建立在送出前就算得出來的量上:影片是「畫面尺寸 × 影格數」或「秒數」,圖片是張數(按 token 計價的圖片模型則是每張的 token 上限)。兩者都報得出來。
對話模型沒有這種量——費用取決於實際產生多少 token,無法事先算準,因此對 chat-* 呼叫 /v2/quote 會回 400。對話改以 max_tokens 為上限預扣,公式完全公開,見對話的預扣與串流計費。
請求欄位
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
model | string | 必填 | 模型:模型 ID(mdl_…)、slug 或「上游/模型」三擇一,見遷移指南。裸名稱回 400 model_ref_required。 |
seconds | number | 必填 | 輸出影片的長度,單位是秒。必須是 JSON number 的整數;範圍規則與影片送件的 seconds 相同(模型沒宣告時為 1–60),範圍外回 400。帶影片參考時: reference_mode 為 "edit" 時長度由參考影片決定,這個值只用來估價;其餘("extend"/"reference"/"auto"/沒指定)照這個值估價並送出。沒指定或 "auto" 時,後端若把任務判成編輯,任務會失敗並附上後端原文。見影片參考與長度。 |
resolution | string | 視模型 | token 制模型需要它(或 size)才能算量。 |
size | string | 選填 | "1280x720"。提供時優先於 resolution 決定尺寸,但仍要通過解析度檢查。 |
aspect_ratio | string | 選填 | 預設 "16:9"。 |
input_references | array | 選填 | 只用來判斷是否含影片參考。部分模型的費率會因此跳到另一段(見各模型的 pricing),內容不會被送到後端。數量仍然會驗:超過上限的話估價就會回 400,不必等到送件,見參考素材的數量上限。 |
要報價圖片模型,請送與 POST /v2/images/generations 相同的欄位(model、prompt,以及 n/size/image 等),不要送 seconds。平台跑的是與送件完全相同的那一套驗證,所以參數不合法時這裡就會回 400,而且不扣額度、不凍結。
圖片模型的 prompt 是必填——這是與影片不同的一點,理由同上:驗證與送件同一套。
prompt 在這個端點不是必填——只想問價格可以不給,價格本身與內容無關。但它是兩條回應路徑的分岔點:影片模型且帶了一個非空白的 prompt,回應就會多一張報價單,可以拿去確認後送件(見送件前確認);沒帶就是單純的價格查詢。只有空白字元的 prompt 視同沒帶。
回應:兩條路徑
這個端點有兩條回應路徑,分岔只看一件事:有沒有帶 prompt。
| 路徑 | 什麼時候走這條 | 回應 |
|---|---|---|
| 純比價 | 沒帶 prompt,或模型不是影片模型 | 只有下面那幾個欄位,與這個端點原本的回應一個字都沒變。 |
| 報價單 | 影片模型且帶了非空白的 prompt | 純比價的欄位全部保留,再疊加一組報價單欄位(quote_id、resolved、materials 等)。 |
純比價那條路徑的回應形狀完全沒有變動。只帶模型與秒數問價錢的程式碼不需要修改,也不會突然多收到欄位——報價單欄位只在你自己帶了 prompt 時才出現。
路徑一:純比價
{
"model": "siraya/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": "siraya/ByteDance-Seedream-4.5",
"unit": "request",
"billable": { "request": 2 },
"price": 0.08,
"estimated": false,
"available": 20.0
}
estimated 說明這個數字的性質:按張計價的圖片模型 unit 是 "request"、estimated 為 false(精算,等於實收);按 token 計價的圖片模型 unit 是 "output_tokens"、estimated 為 true(上限估計,實收以結算為準)。
路徑二:報價單(帶了 prompt)
回應是路徑一的全部欄位,再加上下面這些。多出來的部分是讓你在送出前核對「實際會送給後端的到底是什麼」,確認無誤再憑 quote_id 送件——回 POST /v2/videos/generations 帶上 quote_id 與 confirm 就會真的送出,完整做法見送件前確認,兩個入口產生的是同一種報價單。
{
"model": "siraya/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": "siraya/Seedance-2.0-fast", "model_id": "mdl_3h8r5t0v6w", "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 /v2/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(mdl_…)、slug 或「上游/模型」三擇一,見遷移指南。裸名稱回 400 model_ref_required。必須是 modality: "video" 的模型。 |
prompt | string | 必填 | 內容描述。不可為空字串。 |
seconds | number | 必填 | 輸出影片的長度,單位是秒。必須是 JSON number 的整數(字串 "5" 會回 400)。範圍:模型在 /v2/models 的 constraints 宣告了 allowedSeconds 或 minSeconds/maxSeconds 時以宣告為準;沒宣告時平台接受 1–60 的整數。不是整數或超出範圍,送件當下就回 400 validation_error,不會預扣額度。這個範圍是平台的防呆上限,各模型實際支援的長度由後端決定(例如 Seedance-2.5 為 4–30 秒);後端不接受時任務失敗、預扣退還,不收費。帶影片參考時: reference_mode 為 "edit" 時長度由參考影片決定,這個值只用來估價與預扣(平台改送 -1);"extend"/"reference" 照這個值輸出(4–30 的整數);"auto" 或沒指定時也照這個值送出,但後端若把任務判成編輯,任務會失敗並附上後端原文。見影片參考與長度。 |
resolution | string | 視模型 | 解析度,須在模型的支援清單內。 |
aspect_ratio | string | 選填 | 沒帶時由後端決定預設比例,平台不會替你補上。但平台的計價在沒帶時一律以 "16:9" 估算,所以實際產出的比例與估價依據可能不同。 |
size | string | 選填 | "1280x720",決定尺寸時優先於 resolution。 |
generate_audio | bool | 選填 | 是否產生音軌。 |
negative_prompt | string | 選填 | 不希望出現的內容描述。 |
seed | number | 選填 | 亂數種子,用於重現近似結果。平台不驗證其值。 |
output_format | string | 選填 | 輸出格式偏好。 |
watermark | bool | 選填 | 浮水印開關,目前只對 HappyHorse 系列有作用。HappyHorse 的後端預設會加浮水印;送 false 可關閉、送 true 明確開啟。不帶時行為與過去相同(由後端預設,也就是有浮水印)。必須是 JSON 布林值,字串 "false" 不會被轉送;其他影片模型送了會被丟棄。 |
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 內嵌,平台會自動代管(見內嵌檔案自動轉存)。帶 {"type":"video"} 的參考素材時,seconds 是否成為輸出長度取決於 reference_mode,見下一列與影片參考與長度。 |
reference_mode | string | 選填 | 只能與影片參考(input_references 含 {"type":"video"})一起用:"edit"/"extend"/"reference"/"auto"。extend 與 reference 會照你的 seconds(4–30 的整數)產出;edit 的長度由參考影片決定(平台改送 -1);不帶或 auto 時照你的 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(camelCase)/webhook— 一律丟棄,不會轉給後端。平台自己的回呼請改用callback_url,見回呼(Webhook);輪詢仍是正式的取件方式。extra_body— 已不再支援。它曾經可以把任意欄位原樣直通後端,那等於在白名單上開一個洞,現在一併丟棄。你送了不會收到錯誤訊息,它只是完全沒有作用。
其他如 n、user、metadata 等欄位同樣會被忽略。stream 在這個端點也無效——串流只存在於對話端點。若你送出後發現某個參數毫無作用,請先確認它在上表之中。
一個例外:post_process 在上表之中,但它不會被送到後端——它描述的是成品產生「之後」平台要做的事,與生成內容無關。
回應:直接送出
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "processing",
"model": "siraya/Seedance-2.0-fast",
"model_id": "mdl_3h8r5t0v6w",
"created_at": 1756300000,
"price_reserved": 0.539055
}
| 欄位 | 說明 |
|---|---|
id | 任務識別,格式 job_ + 20 個十六進位字元。請保存它——這是之後查詢與取件的唯一憑據。 |
status | 送件成功時為 "processing"。 |
model | 照你送出的原字串回(ID、slug 或「上游/模型」)。實際用到的模型 ID 在回應標頭 X-AppleToken-Model-Id。 |
price_reserved | 本次凍結的金額(USD)。 |
created_at | 建立時間(Unix 秒)。 |
回應中可能出現其他透傳欄位,但只有 id、status、model、price_reserved 是穩定契約,請勿依賴其餘欄位。
200 不代表會成功
部分參數組合的檢查發生在後端排程之後,因此送件會回成功、失敗卻延後到任務被追蹤到終局時才以 status: "failed" 出現。務必以查詢結果為準,不要在送件成功時就通知使用者完成。
回應:先確認再送出時的確認單
開啟送件前確認之後,你照常送出請求——欄位一個都不用改:
curl https://appletoken.app/v2/videos/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "siraya/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": "siraya/Seedance-2.0-fast", "model_id": "mdl_3h8r5t0v6w", "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": "/v2/videos/generations",
"body": { "quote_id": "q_a1b2c3d4e5f6a7b8c9d0", "confirm": true } },
"cancel": { "method": "POST", "url": "/v2/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 就是用來讓這些差異在送出前一眼看得出來。
不想開啟強制確認、但某些請求想先看一眼的話,在 /v2/quote 帶上 prompt 與素材,回應同樣會帶一個 quote_id,接著走上面的確認步驟即可。兩個入口產生的是同一種東西。
GET /v2/videos/{job_id}
查詢任務狀態。若任務已經完成或失敗,這個呼叫會順手完成結算或退還——但結算不依賴你的查詢:平台的背景程序會做同一件事,兩者走同一段程式碼且彼此冪等,先到者生效。
回應
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "completed",
"model": "siraya/Seedance-2.0-fast",
"model_id": "mdl_3h8r5t0v6w",
"seconds": 5,
"resolution": "720p",
"created_at": 1756300000,
"completed_at": 1756300182,
"content": "/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content",
"outputs": [
{ "type": "original", "status": "completed",
"content": "/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content?output=original" },
{ "type": "processed", "id": "pp_9f8e7d6c5b4a3210fedc", "status": "completed",
"audio_style": "phone", "intensity": 3,
"content": "/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content?output=processed" }
],
"price_charged": 0.539055
}
失敗時的回應長這樣——注意 HTTP 狀態碼仍然是 200,任務失敗不是 HTTP 錯誤,請以 status 欄位判斷,不要只看狀態碼:
{
"object": "video",
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"status": "failed",
"model": "siraya/Seedance-2.0-fast",
"model_id": "mdl_3h8r5t0v6w",
"created_at": 1756300000,
"completed_at": 1756300182,
"price_reserved": 1.096198,
"error": {
"code": "generation_failed",
"message": "The request failed because the output audio may be related to copyright restrictions. Request id: 021700000000000000000000000000000000ffffc0a800000000",
"upstream_message": "The request failed because the output audio may be related to copyright restrictions. Request id: 021700000000000000000000000000000000ffffc0a800000000"
}
}
status | 意義 | 額度影響 |
|---|---|---|
processing | 仍在生成中。 | 維持凍結。 |
completed | 已完成,可以取件。 | 凍結轉為實扣,回傳 price_charged。由本次呼叫或背景程序完成,先到者生效。 |
failed | 生成失敗。回應會帶 error 說明失敗原因。 | 凍結全額退還。回應不會有 price_charged 這個欄位(不是 0.0),帶的是 price_reserved,也就是當初凍結、現已退還的金額。同樣不需要你觸發。 |
content只在completed時出現,是取件用的相對路徑(接在https://appletoken.app後面就是完整網址)。回應裡不會有成品的絕對網址——成品由平台代為轉送,原始網址不外流。若你的程式在等一個叫url的欄位,請改讀這個欄位,或直接呼叫GET /v2/videos/{job_id}/content。outputs只在有後處理時才會有第二筆。第一筆永遠是type: "original",指向與content相同的路徑;第二筆是type: "processed",帶自己的status。後處理失敗不會影響原片:原片照常交付、照常計費,第二筆的status會是failed並附error。content欄位本身的意義與行為完全沒有改變。成品的bytes、duration_seconds、sample_rate不在這裡,改由GET /v2/post-process/{id}提供。price_charged只在已結算後出現。尚在processing時,回應帶的是price_reserved。error只在failed時出現,形狀是{"code": "...", "message": "..."},失敗原因來自後端時另外帶upstream_message(與選擇性的upstream_code)。code是穩定的機器可讀識別,請用它來分支;message是失敗原因,多數情況是後端回的原文(例如內容審查未通過、參數不被接受、你的後端金鑰額度不足)。請把message顯示給你的使用者——把它吞掉、只顯示「生成失敗」,使用者就只剩下試誤這一條路可走,而多數失敗原因其實是他自己改個參數或補個額度就能解決的。error.upstream_message是後端回的原文(其中的供應商名稱已替換成upstream),結尾的Request id: …完整保留——向我們查詢某支任務時請一併提供,我們用它向後端對帳。error.upstream_code只在後端有給錯誤碼時出現。這兩個欄位從 2026-09-24 起失敗的任務才有;平台自己判定的失敗(例如generation_timeout)沒有這兩個欄位。- 結算是冪等的——重複呼叫不會重複扣款,你的查詢與背景程序同時發生也不會。
- 查詢別人的任務一律回
404,而不是403;我們不透露任務是否存在。 - 查詢時若後端暫時不可用,平台會直接回傳目前已知的狀態,而不是讓你的查詢一起失敗。
首次等待 10 秒後開始查詢,之後每 5–10 秒一次,並設定總時限(建議 15 分鐘)。收到 429 時採指數退避。不要用小於 3 秒的間隔連續輪詢——平台自己也在背景輪詢後端,你查得再密也不會更快。
登入後台的「任務」頁會列出你的每一支任務,已完成的可以直接在頁面上預覽與下載;「用量」頁的每一筆也能展開詳情,回頭看當初送出的內容與拿回的結果。要確認「到底成功了沒」,這是最快的一條路,不必自己寫輪詢。
error.reason(/v2)
error.code 維持 generation_failed,與以前完全相同。平台認得失敗原因時,/v2 另外多一個 error.reason(只新增欄位,認不出來就沒有這個欄位):
reason | 代表 | 你該做的事 |
|---|---|---|
content_rejected | 內容審核未通過(輸入素材或生成結果,例如音軌被判定為敏感內容) | 換提示詞、素材或設定再送。原樣重送通常會得到同樣結果。 |
invalid_parameters | 送件後才被判定參數不合法(例如模式與比例、長度的組合不被接受) | 依 upstream_message 調整參數。 |
凍結一樣全額退還。舊版 API(v1 文件)不帶 reason,回應不變。回呼(webhook)的 data.error 用同一套:code 不變,認得時多一個 reason。
GET /v2/videos/{job_id}/content
下載成品。回應是 Content-Type: video/mp4 的原始位元組,不是 JSON、也不是轉址。
- 成品由平台代為取回後轉送,原始成品網址不會外流,也無法被第三方直接存取。
- 下載前平台會先向後端刷新這個任務的狀態。若任務此時已經完成,這次呼叫就可能是觸發結算的那一次——結算是冪等的,與背景程序同時發生也只會扣一次款。仍建議先呼叫
GET /v2/videos/{job_id}確認completed,再來取件。 - 任務不存在、不屬於你、或尚無可下載內容,一律回
404。 - 影片成品自任務完成(
completed)起由平台保存 30 天,期限內可以重複下載。超過 30 天回410 content_expired,成品無法再取得,只能重新生成;重試不會成功。少數情況平台沒能存到成品、後端也已經沒有時,同樣回410 content_expired。需要長期保存請自行下載備份。 - 帶
?output=processed可取回後處理片;不帶或帶?output=original都是原片(預設行為與過去完全相同)。後處理尚未完成或已失敗時回404。
curl -f https://appletoken.app/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-o output.mp4
# 取回後處理片(例如電話音效那一支)
curl -f "https://appletoken.app/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content?output=processed" \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-o output_phone.mp4
音軌風格後處理
對影片的音軌套用聲音效果,例如讓對白聽起來像從電話裡傳出來。影像不會被重新編碼,只有音軌被處理,所以畫質與原片完全一致。
兩種用法
- 生成時順帶:在
POST /v2/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 /v2/post-process/{id} · GET /v2/post-process/{id}/content · GET /v2/post-process
可用的聲音風格
phone | 電話:窄頻、單聲道、略帶失真 |
walkie | 對講機:更窄的頻寬、壓縮強烈 |
radio | 老收音機/廣播 |
vinyl | 老唱片:溫暖、輕微抖動 |
muffled | 隔壁房間/門後:悶住並帶迴音 |
hall | 大廳迴音 |
lofi_16k | 降到 16 kHz 取樣再還原 |
lofi_24k | 降到 24 kHz 取樣再還原 |
mute | 移除音軌 |
限制與計費
- 來源影片上限 200 MB、10 分鐘,格式 mp4。
- 來源必須屬於你。引用別人的任務或素材一律回
404,我們不透露它是否存在。 - 處理是非同步的,通常數秒完成。用
GET /v2/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 /v2/images/generations
圖片生成。與影片不同,這是同步端點:一次呼叫就直接拿到圖片,沒有任務 id、沒有輪詢、沒有取件。所有圖片模型共用這一個端點,帶不帶輸入圖都一樣。
影片是非同步的:送件先拿到 job_id,再輪詢狀態、最後取件。圖片沒有這一段——回應本身就是成品。/v2/jobs 不會列出圖片,/v2/usage 裡圖片那一列的 job_id 就是回應的 id(img_…),但那個 id 不能拿去 /v2/videos/{id} 查詢——圖片沒有可輪詢的任務。
請求欄位
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
model | string | 必填 | 模型:模型 ID(mdl_…)、slug 或「上游/模型」三擇一,見遷移指南。裸名稱回 400 model_ref_required。必須是 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" 把影像內容直接放進回應。兩個值所有圖片模型都收。這條網址 24 小時後失效——它是有簽章、有到期時間的暫時連結,不是永久網址。存進資料庫當長期圖片連結會在隔天集體失效,請在取得後盡快下載並存到你自己的儲存空間。 |
quality | string | 視模型 | 畫質檔位。只有部分模型有這個參數,其餘模型送了會回 400,訊息會列出可用值。 |
background | string | 視模型 | 背景處理方式。同上,只有部分模型支援。 |
negative_prompt | string | 視模型 | 不希望出現的內容。同上,只有部分模型支援。 |
seed | number | 視模型 | 隨機種子,整數。同上,只有部分模型支援。 |
watermark | boolean | 視模型 | 浮水印開關。本平台預設為 false,也就是不加浮水印——要在圖片右下角加上「AI generated」標記,請明確送 true。目前只有 Seedream 系列支援,其餘模型送了會回 400。必須是 JSON 布林值,字串 "false" 會被擋下。 |
其他未列在上表的欄位會被靜默忽略,不會報錯。
size 的可用值、n 的上限、可接受的輸入圖張數,以及有沒有 quality/background/negative_prompt/seed——每個模型都不一樣,而且會隨模型改版調整。寫死一張對照表只會過期,所以這裡不寫。
做法是:直接送,看錯誤訊息。超出範圍一律回 400 validation_error,訊息會指出是哪一個參數、以及那個模型可以接受什麼值(例如「size 只接受 …」「每次固定產生 1 張圖,不接受 n」)。一次送出多個不合法的參數時,訊息會把它們全部列出來,不會只講第一個。
想在真正送出前先試,可以拿同一組參數打 /v2/quote——它跑的是同一套驗證,不合法時回同樣的 400,而且完全不扣額度。
唯一的例外是下面這一段。Seedream 系列的 size 另外有一道「面積」門檻,而錯誤訊息只講得出下限、講不出上限——問不出來的東西沒有理由不寫,所以那張表列在這裡。
Seedream 系列的模型原廠會在圖片右下角加上一個「AI generated」的圓角框標記,而本平台預設把它關掉——不帶 watermark 欄位時,你拿到的圖沒有這個標記。
要加上標記就送 "watermark": true。這是逐次請求的設定,沒有帳號層級的開關。
哪些模型支援,可以從 /v2/models 的 constraints.watermark 查到。不支援的模型本來就不會有這個標記,送 watermark 會回 400。
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 /v2/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 到 4,624,220 之間)"
}
}
message 已經寫成人話,並且列出了該模型接受的簡寫與面積的上下限。2026-09-08 之前這句話只講下限:於是送出的值超過上限時,訊息反而把人導向「是不是太小了」——例如對 5.0-pro 送 "3840x2160"(8,294,400)會看到「總像素需 921,600 以上」。現在上下限會出現在同一句裡。要寫進程式請讀 /v2/models 的 constraints.minPixels / constraints.maxPixels,不要去解析訊息文字——訊息的措辭會再變,那兩個欄位不會。
把這段訊息吞掉、只對使用者顯示「生成失敗」,使用者就只剩下試誤這一條路可走。請務必把 error.message 原文透出去。
後端對 5.0-pro(上表最後一列的兩個 id,含 NSFW 變體)在 2,610,000 總像素設了一道價格階梯,平台自 2026-09-08 起照它計價:
| 總像素(寬 × 高) | 單價 |
|---|---|
| ≤ 2,610,000 | $0.045 / 張 |
| > 2,610,000 | $0.09 / 張 |
門檻是包含式——總像素剛好等於 2,610,000 走便宜的那一段。其餘三個 Seedream 模型(4.0、4.5、5.0-lite,及各自的 NSFW 變體)不分段,一張一個固定價格,不受這道階梯影響。
這道階梯與上表的面積範圍是兩回事:面積範圍決定「收不收這個 size」,這道階梯決定「收多少錢」,兩者的數字沒有任何關係。2,610,000 不會出現在 constraints 裡;它反映在 /v2/models 的 pricing.from 與 pricing.to 上——5.0-pro 這兩個值不相等(0.045 與 0.09),正是「這個模型有多段費率」的意思。
寬x高 才享分段價:送簡寫一律以較貴的那一段計價
分段是用你送出的 size 字串算出來的,不是用後端實際產出的尺寸算的。
- 送
"1024x1024"這類寬x高像素字串:平台算得出總像素,套用上表的分段。 - 送
"1K"/"2K"這類簡寫:平台算不出總像素——簡寫實際對應的尺寸由後端決定,而且不保證等於本文件的像素換算——因此一律落到 > 2,610,000 那一段。完全不送size時也一樣。
所以同一張圖用兩種寫法送,價格可能不同。舉一個實際的例子:對 Dola-Seedream-5.0-pro 送 "1024x1024"(1,048,576 總像素)收 $0.045;同一個模型送 "1K" 收 $0.09——即使後端這次實際產出的是 1248×832,也就是 1,038,336 總像素,本來屬於便宜的那一段。
這是刻意保留的行為,不是缺陷:計價只認你送出的值,不會事後依後端實際產出的尺寸重算。要享分段價,請送 寬x高 像素字串。
怎麼確認這次走到哪一段:送件前拿同一組參數打 POST /v2/quote,回應的 price 就是那一段的價格(不扣額度);送件後看回應的 price_charged,或 /v2/usage 這一列的 price。平台帳本內部另外記了一個 rate_snapshot.context.image_resolution,內容是當次計價採用的總像素數,送簡寫(或不送 size)時為 null,null 就是「沒有套到分段」的判別依據;這個欄位屬於平台內部資料,不會出現在 /v2/usage 的回應裡,對帳有疑問時請向平台索取。
請求範例
curl -X POST https://appletoken.app/v2/images/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "siraya/ByteDance-Seedream-4.5",
"prompt": "清晨的港口,薄霧,暖色調",
"n": 2,
"size": "1024x1024"
}'
回應
{
"id": "img_1a2b3c4d5e6f7a8b9c0d",
"created": 1774794546,
"model": "siraya/ByteDance-Seedream-4.5",
"data": [
{ "url": "https://…/tenants/…/images/….png" },
{ "url": "https://…/tenants/…/images/….png" }
],
"price_charged": 0.08
}
形狀與 OpenAI 的圖片端點相容,只多一個 price_charged。
| 欄位 | 說明 |
|---|---|
id | 這次生成的識別碼,img_ 開頭。它不是任務 id,沒有任何端點可以拿它查詢。 |
created | 生成時間,Unix 秒。 |
model | 照你送出的原字串回(ID、slug 或「上游/模型」)。實際用到的模型 ID 在回應標頭 X-AppleToken-Model-Id。 |
data | 陣列,一張圖一個元素。 |
data[].url | response_format 為 "url"(預設)時出現:平台代管的圖片網址,不是後端的網址。建議下載後存到你自己的儲存空間。 |
data[].b64_json | response_format 為 "b64_json" 時出現:base64 編碼的影像內容。與 url 只會出現其中一個。 |
data[].revised_prompt | 選擇性出現:這次生成有改寫過提示詞時才有,沒改寫就整個欄位不存在。 |
price_charged | 本次實收金額(USD)。凍結已在回應送出前結算完畢,你不需要再查一次。 |
按張計價的模型(/v2/models 的 pricing.unit 為 "request"):一張一個固定價格,與提示詞長度無關。結算看的是實際回來幾張,不是你請求的 n——要三張只回兩張,就只收兩張的錢。
按 token 計價的模型(pricing.unit 為 "output_tokens"):與對話同一套。送出前只能給上限估計,結算改用後端回報的實際用量重算,可能略高或略低於預扣。
兩種都可以先用 POST /v2/quote 問價,回應的 estimated 會告訴你那是精算(false)還是上限估計(true)。
這個端點的執行順序是固定的:模型與能力檢查 → 參數驗證 → 內嵌檔案轉存 → 凍結額度 → 生成 → 結算 → 落地成網址。
所有在本地就判定得出的失敗——尺寸、張數、輸入圖張數,以及 response_format: "url" 需要的儲存設定——全部排在扣款與檔案轉存之前。因此參數不合法時你拿到的是 400,額度一分都沒有被動到,也不會產生任何暫存檔案。
生成本身失敗(後端錯誤)時,凍結原路退回、不記帳,同樣不收費。
錯誤
| 狀態碼 | code | 典型情況 |
|---|---|---|
400 | validation_error | 缺 prompt;模型不是 image 模型;n 送給不支援的模型或超出上限;size 不在該模型的可用值;輸入圖張數超過上限、或該模型不收輸入圖;帶輸入圖時又指定了 size;內嵌檔案的型別不支援、或內容與宣告的型別不符。 |
401 / 403 | unauthorized / forbidden | 憑證無效、已停用或已到期。 |
403 | ip_not_allowed | 這把憑證設定了 IP 白名單,來源 IP 不在名單內。 |
402 | quota_exceeded | 預扣金額超過 available,附 needed 與 available。不會留下凍結。 |
403 | membership_required | 兩個 API 版本都會回(舊版自 v2.16 起):這個模型標示 VIP,開放給 VIP 會員使用(目前是名稱以 NSFW 或 uncensored 開頭的系列)。/v2 附 model、required_plan、upgrade_url;舊版只有 code、message。不會留下凍結。 |
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 像素之間。上傳當下就會擋下,不會白等一趟;錯誤訊息會直接告訴你這張圖是幾乘幾、該放大還是縮小。 |
| 影片規格 | 後端要求幀率 24–60 fps、總像素 640×640~3326×2494、長寬比 0.4–2.5、單邊 300–6000 px、長度 ≥2 秒。平台不預先擋件,但被拒收時錯誤訊息會指出是哪一條,見影片參考素材的規格與拒收訊息。 |
| 重複上傳 | 同一個帳戶重複上傳同一份檔案,會拿回同一個 id,不會產生第二份,也不會再往上游傳一次。重試是安全的。(比對的是檔案內容的 sha256,與檔名無關。) |
| 內容驗證 | 檔案內容必須與宣稱的型別相符——副檔名對、內容不對的檔案會被擋下。 |
| 一次生成可引用幾個 | Seedance-2.0/-fast/-mini:共 15 個(圖片 9、影片 3、音訊 3),不支援只給音訊,至少要有一張圖或一段影片。Seedance-2.5:共 50 個(圖片 30、影片 10、音訊 10),支援只給音訊。 |
| 資產數量 | 沒有平台層的上限。列表一次最多回 100 筆。 |
| 保留期 | 資產長期保留,直到你自己刪除。與影片素材的 30 天暫存不同——那是送件當下的暫存,這是可重複使用的資產。 |
怎麼上傳
三步:上傳 → 等它變成可用 → 在送件時引用。
第一步:上傳
curl https://appletoken.app/v2/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/v2/assets/ast_a1b2c3d4e5f6a7b8c9d0 \
-H "Authorization: Bearer $APPLETOKEN_KEY"
後端是非同步處理的,一般在幾秒到十幾秒內完成。建議每 3 秒查一次,直到 status 變成 active。
後端對還在處理中的資產不會拒絕,而是生出一支沒有正確參考到素材的影片——那種失敗最難查,錢也照扣。所以我們直接擋在送件前,回 400 並告訴你它還在處理中。
第三步:在送件時引用
{
"model": "siraya/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 怎麼填、輸出多長,取決於 reference_mode,見影片參考與長度——這條規則與資產無關。
列出與刪除
列出你的資產,最新的在前。查詢參數: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 | 資產不存在,或不屬於你 |
502 | upstream_error:後端拒收這份素材。影片會在 message 寫明是幀率/像素數/長寬比/長度哪一條不合,並帶 details.reasons,見影片參考素材的規格與拒收訊息 |
503 | 素材資產功能尚未開通。這不是你的請求有問題,請聯絡我們 |
GET /v2/jobs
列出這張憑證送出的影片任務,最新的在前。適合用在你自己的後台列出近期送件,不必逐一保存 job_id。
Query 參數
| 參數 | 預設 | 範圍 | 說明 |
|---|---|---|---|
limit | 100 | 1–500 | 回傳筆數。超出範圍不會被夾到邊界值,而是回 400 validation_error;非整數或非數字同樣回 400。省略時才使用預設值。 |
curl "https://appletoken.app/v2/jobs?limit=20" \
-H "Authorization: Bearer $APPLETOKEN_KEY"
回應
{
"object": "list",
"data": [
{
"id": "job_2b3c4d5e6f7a8b9c0d1e",
"object": "video",
"model": "siraya/veo-3.1-generate-001",
"model_id": "mdl_2b6c8g1j4s",
"status": "processing",
"created_at": 1756300500,
"completed_at": null
},
{
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"object": "video",
"model": "siraya/Seedance-2.0-fast",
"model_id": "mdl_3h8r5t0v6w",
"status": "completed",
"created_at": 1756300000,
"completed_at": 1756300182,
"content": "/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content"
}
]
}
model一律是「上游/模型」,並帶model_id;與你送件時用哪種寫法無關。見遷移指南。- 每一筆的形狀與
GET /v2/videos/{job_id}相同,status的意義也相同。 - 只會列出這張憑證送出的任務。同帳戶其他憑證的任務不會出現在這裡。
- 這個端點不會觸發結算,也不會即時向後端更新狀態。要拿到權威的狀態與
price_charged,請對單一任務呼叫GET /v2/videos/{job_id}。
POST /v2/chat/completions
對話生成。與影片不同,這是同步端點:沒有任務 id、沒有輪詢、沒有取件。一次呼叫要嘛拿到完整回應,要嘛拿到一段 SSE 串流。
quote_id
影片可以先報價、確認、再送件(見送件前確認)。對話這條端點不行——不是還沒做,是這件事對對話沒有意義。
差別在計費的本質。影片的計費量在送出前就算得準:尺寸與秒數決定 token 數,報價等於實收,所以「確認這個數字」是有內容的。對話則取決於模型實際生成多少 token,事前不可知,平台能給的只有一個上限。要你確認一個上限沒有意義——你確認了 8,000 tokens,實際可能只用掉 300。
所以對話走的是另一套:以 max_tokens 為上限預扣,生成結束後以實際用量結算,多預扣的退回。公式完全公開,見對話的預扣與串流計費。
還有一個現實理由:這條端點與 OpenAI 的 /chat/completions 相容,多一道必要的往返會讓現成的 SDK 直接不能用。
請求欄位
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
model | string | 必填 | 模型:模型 ID(mdl_…)、slug 或「上游/模型」三擇一,見遷移指南。裸名稱回 400 model_ref_required。必須是 modality: "text" 的模型(也就是對話模型;目錄裡沒有 "chat" 這個 modality)。對影片模型呼叫會回 400「{model} 不是對話模型」。 |
messages | array | 必填 | 對話內容,非空陣列。每個元素形如 {"role": "user", "content": "…"},兩個欄位都必須是字串。 |
max_tokens | number | 選填 | 輸出預算;選填。提供時請使用大於 0 的數字;與 max_completion_tokens 同時提供時後者優先。 |
max_completion_tokens | number | 選填 | 與 max_tokens 相同用途;平台將它正規化為 max_tokens 轉送,並用它計算預扣。兩者同時提供時此欄優先。 |
reasoning_effort | string | 選填 | 推理強度設定,字串;各模型接受的值及效果不同,平台原樣轉送。 |
reasoning | object | 選填 | 推理設定物件;格式及支援情況依模型而異,平台原樣轉送。 |
thinking | object | 選填 | 思考設定物件;格式及支援情況依模型而異,平台原樣轉送。 |
stream | bool | 選填 | 預設 false。設為 true 時改回 SSE 串流。必須傳布林值 true / false——平台只做真假值判斷,任何非空字串(包含 "false")都會被當成 true 而啟用串流。 |
temperature | number | 選填 | 取樣溫度。平台不檢查範圍,原樣轉給後端。 |
其他白名單欄位包括 top_p、n、stop、tools、user 等;支援情況依模型而異,平台不保證它們都會生效。
下列參數只對指定模型(上游 siraya)原樣轉送;送到其他模型會被靜默忽略,與過去相同,不會報錯。值不經平台檢查,效果由模型決定,用量以回應的 usage 為準。
| 參數 | 模型 |
|---|---|
functions | gpt-4.1、gpt-4.1-mini、gpt-5.4、gpt-5.4-mini、gpt-5.4-nano、gpt-5.6-luna |
logit_bias | gpt-4.1、gpt-4.1-mini |
prediction | gpt-4.1、gpt-4.1-mini |
repetition_penalty | ByteDance-Seed-SC、NSFW-Seed-SC |
推理 token 可能佔用輸出預算,讓可見回答很短,甚至成功回應中沒有可見文字。這些推理控制僅在模型支援時有效,也不能保證固定用量或成本;模型若不遵守上限,實際用量仍可能超過預扣估算。請以回應中的實際用量與費用為準。
{"model":"YOUR_MODEL_ID","messages":[{"role":"user","content":"Hello"}],"max_completion_tokens":512,"reasoning_effort":"low"}max_tokens 要給多少推理模型會先「想」再回答。推理用掉的 token 算在 max_tokens 裡,也按 completion 計費(usage.completion_tokens_details.reasoning_tokens 是其中推理的部分)。預算不夠時,推理還沒想完就被截斷:finish_reason 會是 "length",content 可能是 null 或空字串,但這筆仍然成功並計費。
實測(2026-09-25,sailflow/qwen3.8-flash-next):一句話的簡單回答用了 27–29 個推理 token,寫一首俳句並解釋用了 275 個。建議推理模型的 max_tokens 至少 512,一般任務 1024 以上,再依實際 usage 調整。推理內容若上游有提供,會放在 message.reasoning_content(串流是 delta.reasoning_content)。
max_tokens 的模型2026-10-14 實測(上游 siraya,每筆都送 max_tokens: 24;Dola-Seed-2.0-mini、Dola-Seed-2.0-lite、NSFW-Seed-2.0-mini 是 2026-10-19 實測,mini 送 max_tokens: 8、其餘送 24):下列模型回傳的 completion_tokens 超過上限,多出來的主要是推理 token。上游不保證這些模型遵守 max_tokens,實際輸出可能超過你設的上限。
計費照實際用量(回應的 usage),不以 max_tokens 為上限;預扣仍以你送的 max_tokens 計算,所以結算金額可能高於預扣。表中是實測到的最大值,不是保證的上限。
| 模型 | 送出的上限 | 實測最多的 completion_tokens | 其中推理 |
|---|---|---|---|
Dola-Seed-2.0-mini | 8 | 1453 | 1452 |
Dola-Seed-2.1-turbo | 24 | 1185 | 1166 |
NSFW-Seed-2.0-Code | 24 | 576 | 553 |
NSFW-Dola-Seed-2.1-turbo | 24 | 545 | 522 |
Dola-Seed-2.0-lite | 24 | 430 | 406 |
ByteDance-Seed-1.8 | 24 | 191 | 172 |
NSFW-Seed-2.0-mini | 24 | 186 | 162 |
Dola-Seed-2.0-pro | 24 | 173 | 153 |
grok-4-20-reasoning | 24 | 165 | 164 |
NSFW-Seed-2.0-pro | 24 | 128 | 108 |
kimi-k2.7-code | 24 | 55 | 54 |
grok-4.5 | 24 | 46 | 33 |
deepseek-v4-pro | 24 | 33 | 32 |
kimi-k3 | 24 | 27 | 24 |
平台在呼叫後端前,會依你指定的輸出預算估算並預扣額度。兩個欄位都未提供時,改用模型目錄的預設或最大輸出量;若按輸出 token 計價的模型也沒有可用上限,請求可能回 400。預扣是估算,模型實際用量可能超過它。
結束後依實際用量結算並退回多預扣的額度。設定過大的預算會暫時佔用額度,讓並行請求更容易遇到 402;請按需要設定。
回應(非串流)
{
"object": "chat.completion",
"model": "siraya/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、slug 或「上游/模型」)。實際用到的模型 ID 在回應標頭 X-AppleToken-Model-Id。 |
choices | 上游回幾個就原樣回幾個,每個帶自己的 index。送了 n 會原樣轉給上游,實際回幾個由模型決定。預扣按輸出上限 × n 計算(輸入只算一次),結算照上游回報的 usage,n 份輸出都計費。message.role 固定為 "assistant"。 |
choices[].message.reasoning_content | 推理內容(只有推理模型且上游有提供時才會出現)。各家上游的原始欄位名不同,平台一律改成這個名稱;串流時在 delta.reasoning_content。 |
choices[].finish_reason | "stop":正常結束。"length":用完 max_tokens 而截斷,推理模型可能全部用在推理、content 是空的,見推理模型的 max_tokens。 |
usage.prompt_tokens | 輸入 token 數。後端有回報就用實際值,否則用平台的估算值。 |
usage.completion_tokens | 輸出 token 數,同上。 |
usage.prompt_tokens_details / usage.completion_tokens_details | token 細項,例如輸入命中快取的 cached_tokens、輸出中推理的 reasoning_tokens。平台只轉出前者的 cached_tokens、audio_tokens、text_tokens、cache_write_tokens 與後者的 reasoning_tokens、audio_tokens、accepted_prediction_tokens、rejected_prediction_tokens。 |
service_tier / system_fingerprint | 上游回報的服務層級與後端組態指紋。 |
prompt_filter_results / choices[].content_filter_results | 上游的內容審核結果:前者針對輸入(每則提示一筆),後者針對每個 choice 的輸出。 |
choices[].message.refusal | 模型拒答時的說明文字,沒有拒答時是 null。 |
price_charged | 本次實收金額(USD)。凍結已在回應送出前結算完畢,你不需要再查一次。 |
usage.*_details、service_tier、system_fingerprint、prompt_filter_results、content_filter_results、refusal 是上游帶回、平台照轉的欄位:有沒有、長什麼樣由模型決定,平台不保證一定出現,程式請容許它們缺席。
回應有 id(chatcmpl_…,平台自發、與上游無關)與 created(Unix 秒),但對話不是可查詢的任務——這個 id 不能拿去 /v2/videos/{id}。要對帳請用 /v2/usage 裡對應那一列(它的 job_id 就是這個 id)。
回應(串流,stream: true)
回應標頭是 Content-Type: text/event-stream; charset=utf-8,狀態碼永遠是 200。內容為一連串 data: 事件,以空行分隔:
data: {"object":"chat.completion.chunk","model":"siraya/ByteDance-Seed-1.8","choices":[{"index":0,"delta":{"content":"向量"}}]}
data: {"object":"chat.completion.chunk","model":"siraya/ByteDance-Seed-1.8","choices":[{"index":0,"delta":{"content":"資料庫"}}]}
data: {"object":"chat.completion.chunk","model":"siraya/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 | 模型沒有可用的輸出上限、messages 為空、模型不是對話模型。 |
401 / 403 | unauthorized / forbidden | 憑證無效、已停用或已到期。 |
403 | ip_not_allowed | 這把憑證設定了 IP 白名單,來源 IP 不在名單內。 |
402 | quota_exceeded | 預扣金額超過 available,附 needed 與 available。不會留下凍結。 |
403 | membership_required | 兩個 API 版本都會回(舊版自 v2.16 起):這個模型標示 VIP,開放給 VIP 會員使用(目前是名稱以 NSFW 或 uncensored 開頭的系列)。/v2 附 model、required_plan、upgrade_url;舊版只有 code、message。不會留下凍結。 |
404 | not_found | 模型不在憑證的允許清單,或這個模型不存在——兩者回同一種錯誤。 |
502 / 503 | upstream_error / service_unavailable | 後端不可用,或這個模型不支援你要的模式(stream 與非串流分別檢查)。凍結全額退還。 |
平台代選:auto/(v2.9)
用法
把 model 寫成 auto/<模型名稱>,例如 auto/qwen3.8-flash-next。模型名稱是 GET /v2/models 裡 ref 斜線後面那一段(同一個模型由幾家上游供應,這段就相同)。目前只支援 POST /v2/chat/completions(含串流)。
{"model":"auto/qwen3.8-flash-next","messages":[{"role":"user","content":"Hello"}],"max_tokens":256}
選擇規則
- 只從目前可用的上游中選:暫停中、最近連續失敗、這次請求無法報價或不支援串流的上游會先排除。
- 售價不高於上游進價的路才會被選。
- 依這次請求的售價由低到高;售價相同時依上游進價,再依上架先後。
自動換家
只有在還沒送出任何內容給你之前才會換家:連線失敗、5xx、429、逾時。非串流在拿到完整回應之前、串流在第一個 chunk 之前。串流一旦開始就不會換家,之後的錯誤照串流錯誤事件處理,不會重複扣款。
回應
model:照你送的原字串回(auto/qwen3.8-flash-next)。route:只有auto/的回應才有,形如{"provider": "Sailflow", "model": "sailflow/qwen3.8-flash-next"},是這次實際使用的上游與模型。串流放在最後一則 chunk(與usage、price_charged同一塊)。usage:不論走哪一家,一律是prompt_tokens、completion_tokens、total_tokens;有快取或推理用量時另帶prompt_tokens_details.cached_tokens、completion_tokens_details.reasoning_tokens。- 帳單與
/v2/usage記的是實際使用的上游與模型;標頭X-AppleToken-Route也是。
錯誤
| 狀態 | error.code | 情形 |
|---|---|---|
404 | not_found | 模型名稱不存在,或不在憑證白名單內。 |
503 | service_unavailable | reason: "no_route_available":目前沒有符合選擇規則的上游。 |
400 | auto_unsupported | 在影片、圖片或報價端點使用 auto/。 |
400 | fallback_unsupported | 帶了 "fallback": true(auto/ 不需要這個參數)。 |
GET /v2/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 | 成功建立凍結的請求數——影片送件與對話請求都算,估價、查詢、取件、/v2/me 都不算。 |
last_used_at | 最後一次成功預扣的時間,同樣不因查詢而更新。 |
expires_at / expired | 到期時間與是否已過期。null 表示永不過期。 |
prefix / display | 憑證的遮罩形式,可安全顯示在介面或日誌中。 |
GET /v2/usage
取得這張憑證的帳務流水,最新的在前。這是你端對帳的權威來源。
用量以憑證為界(哪支鑰匙做了什麼),額度則是帳戶共用的。同帳戶其他憑證的消費不會出現在這裡,但會影響 /v2/me 的餘額。
Query 參數
| 參數 | 預設 | 範圍 | 說明 |
|---|---|---|---|
limit | 100 | 1–500 | 回傳筆數。超出範圍不會被夾到邊界值,而是回 400 validation_error;非整數或非數字同樣回 400。省略時才使用預設值。 |
回應
{
"object": "list",
"data": [
{
"id": "80421",
"job_id": "chatcmpl_5e1f0a9c7b3d2e4f6a8b",
"model": "siraya/ByteDance-Seed-1.8",
"model_id": "mdl_9p4d2n7e5f",
"modality": "text",
"billable": { "input_tokens": 52, "output_tokens": 180 },
"price": 0.004284,
"status": "settled",
"created_at": 1756300400
},
{
"id": "80418",
"job_id": "job_1a2b3c4d5e6f7a8b9c0d",
"model": "siraya/Seedance-2.0-fast",
"model_id": "mdl_3h8r5t0v6w",
"modality": "video",
"billable": { "video_seconds": 5 },
"price": 0.539055,
"status": "settled",
"created_at": 1756300182
}
]
}
| 欄位 | 型別 | 說明 |
|---|---|---|
id | string | 這一列的識別碼,數字字串、單調遞增。對帳與增量拉取以它為準。 |
job_id | string|null | 這筆請求的 id,與生成回應的 id 相同:影片是 job_…(POST /v2/videos/generations)、對話是 chatcmpl_…、圖片是 img_…。只有來源不是任務的獨立音軌後處理才是 null。它就是 評價 用的 request_id。 |
model | string | 「上游/模型」,例如 siraya/Seedance-2.0-fast,與 /v2/models 的 ref 相同。不論當初用哪種寫法送,這裡都是同一個值。v2.0 之前的舊紀錄若對不到模型,保留舊名稱。 |
model_id | string|null | 模型 ID(mdl_…),對帳請以它歸戶。v2.0 之前的舊紀錄若對不到模型則為 null。(?include=route 仍然接受,但 model 已含上游,不再需要。) |
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 秒。排序依據就是它(最新的在前)。 |
每一列都是一筆已完成結算的消費。額度的預扣與退還不會在這裡留下任何一列。所以:
請求評價
對你自己的一筆請求打 1–5 分。分數會累積成平台對各模型「效果」的評比,讓所有整合者都受益;評分不影響計費,也不會改變任何既有請求的結果。
request_id 是什麼
就是生成回應裡的 id:對話的 chatcmpl_…、圖片的 img_…、影片的 job_…。它同時是 /v2/usage 每一列的 job_id,三種模態走同一條路。
請求
{
"request_id": "chatcmpl_5e1f0a9c7b3d2e4f6a8b",
"score": 4,
"comment": "回答準確,但比上次慢",
"tags": ["accurate", "slow"]
}
| 欄位 | 必填 | 說明 |
|---|---|---|
request_id | 是 | 要評的請求,見上方。格式不合法回 400。 |
score | 是 | 1–5 的 JSON 整數。"5"(字串)、4.5、0、6 一律回 400 validation_error,不會被夾到邊界值。 |
comment | 否 | 最多 500 個字。只儲存、不會在任何介面渲染;省略或 null 都可以。 |
tags | 否 | 最多 10 個,每個符合 ^[a-z0-9_-]{1,32}$(小寫英數、底線、連字號)。省略等同空陣列。 |
回應 200
{
"object": "rating",
"request_id": "chatcmpl_5e1f0a9c7b3d2e4f6a8b",
"score": 4,
"comment": "回答準確,但比上次慢",
"tags": ["accurate", "slow"],
"created_at": 1758460000,
"updated_at": 1758460000
}
新建與覆寫都回 200、同一形狀。GET /v2/ratings/{request_id} 回同一形狀;沒評過的請求回 404。時間戳是 Unix 秒。
規則
- 一個請求一票。同一個
request_id再送一次就是整筆覆寫——score、comment、tags全部換成新值(沒帶的欄位視為清空),created_at不變、updated_at前進。覆寫不會增加樣本數。 - 只能評本帳戶的請求;同帳戶的任何一把憑證都可以評,也可以覆寫彼此的評分。
- 請求結算後 30 天內才能評分(以
/v2/usage那一列的created_at起算)。逾期回409 conflict,已有的評分照樣讀得到。 - 只有已結算的請求才評得到:還在生成中的影片、以及失敗而全額退還的請求,都沒有
/v2/usage那一列,會回404;影片完成後再評即可。 - 影片若同時做了音軌後處理,評分綁的是生成本身,不是後處理。
錯誤
| 狀態 | error.code | 情形 |
|---|---|---|
400 | validation_error | score 不是 1–5 的整數、comment 超過 500 字、tags 不合格式、request_id 缺少或格式不合法。 |
404 | not_found | 找不到這筆請求、它尚未結算、或它不屬於你的帳戶——三種情形回應完全相同,無法用來探測別人的請求是否存在。GET 對沒評過的請求也回這個。 |
401 | unauthorized | 沒有帶憑證、或憑證無效/已停用/已到期。評分一律要用 API 憑證;登入前台的瀏覽器工作階段(cookie)不算,公開的評比頁也沒有任何評分入口。 |
409 | conflict | 這筆請求已超過 30 天,不再接受評價。 |
評分按模型(一家上游的一個模型)聚合,聚合結果經 GET /v2/models 每一列的 rating 公開(每個帳戶算一票,需 20 則且至少 3 個帳戶);這條端點的回應本身不含上游資訊。
額度模型
每張憑證有三個彼此獨立的數字,恆等式永遠成立:
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 小時:上游暫時查不到結果(包含平台的上游額度暫時不足這類情況;你不需要、也不能綁定上游金鑰,用哪一家上游由 model 的「上游/模型」決定)時,平台會持續重試到這個時限。超過 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推出長邊。
/v2/quote 確認
上面的公式與平台實際使用的完全相同,你可以在客戶端預估費用。但 aspect_ratio 的靜默退回、模型費率分段等行為只有伺服器知道,正式報價請以 /v2/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 /v2/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 /v2/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 /v2/videos/{job_id} 時,若任務已有結果,也會順手走同一段結算邏輯。兩者彼此冪等:同時發生只有一次生效,不會重複扣款。
對話沒有這個問題——它是同步的,結算在回應(或串流的最後一則 chunk)送出之前就已經完成。
不輪詢不會讓你的額度卡住,但會讓你不知道任務結果——影片成品必須先確認 completed 才能取件,而成品保留時間有限。所以:
- 把送出的
job_id持久化,程式重啟後繼續查——理由是取件,不是額度。 - 查詢到
completed後盡快下載並存進你自己的儲存空間。 - 查詢到
failed時,退還早已完成(或即將完成),你要做的是決定要不要重送。
四種結局
| 結局 | 觸發 | 額度變化 | 在 /v2/usage 的表現 |
|---|---|---|---|
| 完成並結算 | 後端回報 completed(由背景程序或你的查詢先發現) |
reserved 減去凍結額,used 加上實際金額 | 出現一列,status 為 "settled" |
| 失敗退還 | 後端回報 failed(同上) |
reserved 減去凍結額,used 不變 | 不會出現——沒有扣款就沒有這一列 |
| 送件失敗退還 | 請求未能送達後端 | 同上,在送件當下立即發生 | 不會出現——沒有扣款就沒有這一列 |
| 對話結束 | 回應或串流結束的當下(含中途斷線) | 依串流的三種結束方式結算;零產出才退還 | 有結算才會出現一列;零產出全額退還時不會出現 |
實收金額怎麼決定
- token 制影片:以後端回報的實際 token 用量重算。若後端未回報用量,則以送件時的估價結算。
- 秒制影片:以實際成品秒數重算;沒有回報時採用送件時的
seconds。 - 對話:以實際輸入/輸出 token 重算;後端沒回報用量或中途斷線時,以平台累計的產出量結算。
- 重算的結果就是
/v2/usage那一列的price,計費用量在同一列的billable。它可能略高或略低於送件時的price_reserved——差額不會另外列成一筆。
當實際用量高於估價(最常見於帶影片參考、實際長度大於你填的 seconds 時),結算會以實際金額計算,此時 available 可能變成負數。這是刻意的——帳要真實,不能因為超支就少記。請在估價時保守取高值,並監控 /v2/me 的 available。
對帳建議
- 你端以
job_id為主鍵記錄每個影片任務,並保存送件時回傳的price_reserved;對話則記錄回應中的price_charged。 - 定期拉
GET /v2/usage。用created_at圈出你要對的時間區間,並記下這一批裡最大的id——下次只處理id比它大的列,就不會重複也不會漏。 job_id是job_…的列(影片)直接對回你自己的任務紀錄,核對price與你保存的price_reserved。有差額是正常的:實收以實際用量重算,可能略高或略低。- 對話與圖片的列,
job_id就是回應的id(chatcmpl_…/img_…),用它對回你自己的請求紀錄,核對price與回應中的price_charged。 - 你送出了、卻在
/v2/usage找不到對應列的請求,只有兩種可能:還沒結算(仍在進行中)或已全額退還(送件失敗、生成失敗、零產出的串流)。兩者都不需要你做任何事——用GET /v2/videos/{job_id}查狀態就能分辨。只有某筆影片任務超過 2 小時仍停在processing,才是值得回報的異常(正常情況下最晚 2 小時內就會被系統自動判定為失敗並退還)。 - 核對
Σ price:帳戶下只有這一張憑證時,它應該等於/v2/me的used;有多張憑證時,各憑證的Σ price相加才等於used。
錯誤格式與狀態碼
{
"error": {
"code": "validation_error",
"message": "example-model 的長度需在 4–15 秒之間",
"param": "seconds",
"reason": "out_of_range",
"value": 20,
"allowed": {"min": 4, "max": 15}
}
}
/v2 的影片參數 400 validation_error 另外帶下列機器可讀的欄位。code 與 message 與以前完全相同(只新增欄位);請用新欄位分流,不要解析 message:
| 欄位 | 意義 |
|---|---|
param | 出錯的請求欄位,例如 seconds、resolution、size、aspect_ratio、input_references、frame_images |
reason | out_of_range、not_allowed、not_integer、mode_conflict、too_many |
value | 平台收到的值 |
allowed | 可接受的值:{"min", "max"}、{"values": [...]} 或 {"max"} |
mode | 規則來自 constraints.modes 的某個模式時才有 |
errors | 同時有多個錯誤時才有:每一項都是上面的形狀(含 message),頂層放第一項 |
該怎麼分支
| 狀態碼 | 代表 | 你該做的事 |
|---|---|---|
400 | 請求本身有問題 | 修參數再送。重試沒有意義。 |
401 | 憑證無效 | 檢查標頭格式與憑證是否已換發。不要自動重試。 |
402 | 額度不足 | 停止送件、通知負責人加值。若 reserved 很高,那是進行中的請求,等它們結束即可。 |
403 | 憑證被停用或已過期 | 聯繫 AppleToken。不要自動重試。 |
404 | 任務不存在、不屬於你,模型不在憑證的允許清單,或路徑/方法不存在 | 確認 job_id 與使用的憑證是否為送件時的同一張;確認 model 出現在 /v2/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 判斷責任歸屬:是你的參數問題,還是服務端的問題。
兩種例外(API v2.1 起,只限 /v2):後端判定參數不合法(upstream_code 為 invalid_request_error)時,code 是 validation_error,狀態碼與 message 照舊沿用後端;取件時成品已超過保存期限(完成後 30 天)或已無法取得,回 410 content_expired。兩者都照樣附上 upstream_message/upstream_code。
這類錯誤的 error 另外帶 upstream_message:後端回的原文(供應商名稱已替換成 upstream,Request id: … 完整保留);後端有給錯誤碼時再帶 upstream_code。message 可能是平台改寫過的說明(例如「素材網址無法被下載」),要知道後端實際說了什麼,請看 upstream_message。平台自己產生的錯誤(連線失敗、憑證異常、參數驗證)沒有這兩個欄位。
後端的 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} | 以 /v2/models 的 id 為準,不做模糊比對。 |
validation_error | 400 | seconds 必須是數字 | 傳了字串(如 "5")或非有限數值。表單取值最容易在這裡出錯。 |
validation_error | 400 | seconds 必須大於 0 | 調整 seconds。 |
validation_error | 400 | {id} 的長度需在 {min}–{max} 秒之間 / {id} 只接受 {list} 秒 | 調整 seconds。/v2 另帶 param: "seconds" 與 allowed;範圍來自模式子集時再帶 mode。 |
validation_error | 400 | {id} 支援的解析度為 … | 改用清單內的寫法(例如 "720p";有清單的模型不接受 "720")。/v2 另帶 param: "resolution" 與 allowed.values。 |
validation_error | 400 | {mode} 模式下比例跟隨輸入影片,不能指定 aspect_ratio | 只有宣告了 constraints.modes.<模式>.aspectRatio: "inherit" 的模型才會出現(reason: "mode_conflict")。拿掉 aspect_ratio 或填 "adaptive"。 |
validation_error | 400 | {id} 最多接受 {n} 個…參考素材 | /v2 另帶 reason: "too_many",allowed.max 是上限。 |
validation_error | 400 | 需要 seconds 才能報價… | 即使帶影片參考也必須提供 seconds 供估價。 |
validation_error | 400 | 需要 resolution 或 size 才能精算 token 用量 | token 制模型至少要有其中之一。對 chat-* 呼叫 /v2/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} 不是對話模型 | 對影片模型呼叫了 /v2/chat/completions。以 modality 分類。 |
model_ref_required | 400 | 模型名稱必須指定上游:'{model}' 已不再接受… | 送了裸名稱(v2.0 起)。改用模型 ID、slug 或「上游/模型」,回應附 param: "model"、candidates: [{ id, slug, ref }] 與 docs_url,範例見遷移指南。重試沒有意義。 |
fallback_unsupported | 400 | fallback 已不支援:換一家上游就是換一個模型… | 帶了 "fallback": true(v2.0 起)。拿掉它,需要備援請自行改送另一個模型;回應附 param: "fallback" 與 docs_url。 |
unauthorized | 401 | 憑證格式不正確 | 檢查 Bearer 前綴與空白。 |
unauthorized | 401 | 憑證無效 | 憑證不存在、機密錯誤或已被換發/撤銷。 |
quota_exceeded | 402 | 額度不足 | 附 needed/available/granted/used/reserved。 |
forbidden | 403 | 這組憑證已被停用 | 停用或已過期,兩者訊息相同。 |
ip_not_allowed | 403 | 這組憑證設定了 IP 白名單,來源 IP {ip} 不在名單內 | 把訊息裡的 IP 加進這把憑證的 IP 白名單,或請管理者清空名單。重試沒有意義。 |
not_found | 404 | 未知的模型 {model} | 憑證設有模型白名單而這個模型不在其中。與模型不存在回同一種錯誤,避免拿來探測目錄。影片與對話同樣適用。 |
not_found | 404 | 找不到這個任務 | 任務不存在,或屬於別張憑證。 |
not_found | 404 | 這個任務沒有可下載的內容 | 尚未完成,或成品已失效。 |
not_found | 404 | not found | 路徑或 HTTP 方法不存在(例如對 /v2/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 | 429 | 這個模型目前排隊中的任務已滿,請稍後再試 | 這個模型同時進行中的影片任務已達上限(部分上游一次只能處理少量任務)。送件前就擋下,額度未被扣除、不會建立任務。等手上的任務完成後再送,或改用同一個 family 的另一個模型。 |
upstream_error | 沿用後端 | 生成服務回報錯誤 | 後端拒絕了這次請求。訊息已清洗:上游原文裡的品牌字樣與主機名會被正規化或移除,也不含上游的任務 id、進價與金鑰。狀態碼不固定,請以 code 分支。 |
validation_error | 沿用後端(通常 400) | (沿用後端的訊息,已清洗) | 後端判定參數不合法(upstream_code 為 invalid_request_error,例如首幀生成時帶了 aspect_ratio)。依 message/upstream_message 修改參數後再送,額度未被扣除。 |
content_expired | 410 | 這支影片的成品已超過保存期限(完成後 30 天內可下載),無法再取得,請重新生成。 (平台沒存到、後端也已沒有時)這支影片的成品已無法取得(可能已超過保存期限),請重新生成。 | 取件(GET /v2/videos/{id}/content)時成品已超過保存期限(完成後 30 天),或平台沒能存到而後端也已經沒有這支成品。重試不會成功,請重新生成;由後端判定的會附 upstream_message(後端原文)。 |
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 | 這個模型暫時無法使用,請聯絡管理者 | 平台暫停了這個模型的新請求(例如這家上游當日的平台用量已達上限),通常隔天(UTC)自動恢復。已送出的影片任務照常完成。請稍後再試,或改用同一個 family 的另一個模型。 |
service_unavailable | 503 | 這個模型目前無法使用 | 對應的後端暫時未接上。請改用其他模型或稍後再試。 |
service_unavailable | 503 | 這個模型不支援串流 / 不支援非串流呼叫 | 換一種 stream 設定,或換模型。 |
service_unavailable | 503 | 平台尚未設定上游憑證,請聯絡管理者 | 平台端設定問題,請直接聯繫我們。 |
service_unavailable | 503 | 平台上游憑證異常,請聯絡管理者 | 指的是平台自己的上游憑證,不是你的 at_live_ 憑證——換發後者不會有幫助。你不需要、也不能綁定上游金鑰:用哪一家上游,由 model 的「上游/模型」決定(見遷移指南)。請直接聯絡我們。 |
service_unavailable | 503 | 平台上游服務暫時無法使用,請稍後再試 | 平台在這家上游的額度暫時不足,與你的帳戶餘額無關。回應不含 reason。請退避後重試;影片任務若已送出,平台會繼續追蹤,不會丟掉。 |
internal_error | 500 | 發生未預期的錯誤 | 平台端的問題,請回報時間與 job_id。常見的欄位型別錯誤(例如 seconds 傳成字串)在 v2 已改為 400 validation_error,若你仍收到 500,那不是你的參數造成的。 |
not_found | 404 | 未知的模型 <provider>/<model> | 模型 ID、slug 或「上游/模型」不存在(見遷移指南)。與白名單外/不存在的模型是同一種回應。 |
service_unavailable | 503 | 模型 … 目前暫停供應(reason: route_paused) | 你指定的模型暫停中。改用同一個 family 的另一個模型(見 /v2/models)。 |
service_unavailable | 503 | … 最近連續失敗,暫停接單中(reason: circuit_open) | 你指定的模型暫時被熔斷。稍後重試,或自行改送另一個模型。 |
回呼(Webhook)
影片(POST /v2/videos/generations)與音軌後處理(POST /v2/post-process,或送件時帶 post_process)進入終態時,平台會對你事先登記的網址發一個簽章過的 POST。只有 /v2;舊版 v1 API 不論怎麼帶都不會通知。圖片與對話是同步回應,沒有回呼。
回呼可能延遲、可能重複、極少數情況可能送不到(你的伺服器長時間掛掉)。取件與結果仍以 GET /v2/videos/{id} 為準;請保留低頻率的輪詢或用 GET /v2/jobs 定期對帳,當作備援。
設定:先登記,再帶入
- 在主控台 Webhook 頁登記網址:只接受
https、443 埠,主機必須解析到公開網路位址;最多 5 個啟用中的網址。第一次登記時會產生簽章 secret,原文只顯示一次。 - 送件時二選一:
- 什麼都不帶:通知「預設」網址(第一個登記的網址自動成為預設,可在主控台更換)。客戶端程式完全不用改。
- 帶
callback_url(必須完全等於已登記的網址)或callback_endpoint_id(主控台上的whe_…),指定這一筆要通知哪一個。
不在清單裡、格式不合(http、非 443、內部網段)一律回 400 validation_error,不收件、不預扣。callbackUrl(camelCase)不是平台的欄位,會被丟棄。兩階段確認時,回呼設定可以帶在第一階段(之後確認出來的那支影片會通知)或第二階段。
curl https://appletoken.app/v2/videos/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "siraya/Seedance-2.0-fast", "prompt": "…", "seconds": 5,
"callback_url": "https://example.com/appletoken/webhook"}'
事件與內容
| 事件 | 何時 |
|---|---|
video.completed / video.failed | 影片任務完成或失敗 |
postprocess.completed / postprocess.failed | 音軌後處理完成或失敗(訂閱了影片的通知,也會收到以這支影片為來源的後處理) |
內容是精簡版:只有平台自己的欄位(id、狀態、時間、error.code、取件路徑)。失敗原因的完整文字、價格與用量請用 GET 查詢。content 是要帶憑證才拿得到的 API 路徑,不是公開下載網址。
POST /appletoken/webhook HTTP/1.1
Content-Type: application/json
User-Agent: AppleToken-Webhooks/1
X-AppleToken-Event-Id: evt_3f9c2a71b0d84e5a6c1f2e90
X-AppleToken-Event-Type: video.completed
X-AppleToken-Signature: t=1791262582,v1=5b1d…e04a
{
"event_id": "evt_3f9c2a71b0d84e5a6c1f2e90",
"type": "video.completed",
"created_at": 1791262582,
"data": {
"id": "job_1a2b3c4d5e6f7a8b9c0d",
"object": "video",
"status": "completed",
"completed_at": 1791262579,
"error": null,
"content": "/v2/videos/job_1a2b3c4d5e6f7a8b9c0d/content"
}
}
驗證簽章
X-AppleToken-Signature 的格式是 t=<unix 秒>,v1=<hex>,v1 = HMAC-SHA256(secret, "<t>." + 原始 body)。請用收到的原始位元組計算、用定時比較,並拒絕時間差超過 5 分鐘的通知(防重放)。輪換 secret 後的 24 小時內,標頭會帶兩個 v1(新舊各一),符合任一即可。
import crypto from 'node:crypto';
// rawBody:收到的原始位元組(不要先 JSON.parse 再 stringify)
function verify(rawBody, header, secret) {
const t = Number(/t=(\d+)/.exec(header)?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return [...header.matchAll(/\bv1=([0-9a-f]{64})/g)].some((m) =>
crypto.timingSafeEqual(Buffer.from(m[1]), Buffer.from(expected)));
}
import hashlib, hmac, re, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
m = re.search(r't=(\d+)', header)
if not m or abs(time.time() - int(m.group(1))) > 300:
return False
expected = hmac.new(secret.encode(), m.group(1).encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for v in re.findall(r'v1=([0-9a-f]{64})', header))
送達、重試與去重
- 你的伺服器在 5 秒內回 2xx 才算送達;其餘狀態碼、逾時、重新導向(不會跟隨)都算失敗。
- 失敗後依序在 1 分、5 分、15 分、30 分、1 小時、2 小時後重試,之後放棄(約 4 小時)。主控台看得到每一筆的結果,可以手動重送。
- 至少送達一次:可能重複,請用
event_id去重;重送與重試都沿用同一個event_id。 - 不保證順序:影片與它的後處理是兩個事件,先後不固定;以
created_at判斷或直接 GET。 - 通知通常在任務終結後數秒內送出。回呼成功與否不影響任務狀態與扣款。
- 成品自完成起保存 30 天,期限內隨時可取件;過期會回
410 content_expired。
完整範例
影片:估價 → 送件 → 取件
以下三個範例做同一件事:估價 → 送件 → 輪詢到結束 → 下載成品。輪詢是為了知道結果並取件;額度的結算由平台在背景完成,不靠這段程式碼。
#!/usr/bin/env python3
"""AppleToken 最小客戶端 — 只用標準庫。"""
import json, os, time, urllib.request, urllib.error
BASE = "https://appletoken.app/v2"
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="siraya/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/v2'
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: 'siraya/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/v2/quote \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"siraya/Seedance-2.0-fast","seconds":5,"resolution":"720p"}'
# 2) 送件,取出 job id
JOB=$(curl -s https://appletoken.app/v2/videos/generations \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"siraya/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/v2/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/v2/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/v2"
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": "siraya/Seedance-2.0-fast", "seconds": 5, "resolution": "720p"},
{"model": "siraya/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 /v2/videos/{id},做法與上一個範例相同
// 同時開多張報價單 → 逐一確認送出。Node 18+ 內建 fetch,無需任何套件。
const BASE = 'https://appletoken.app/v2'
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: 'siraya/Seedance-2.0-fast', seconds: 5, resolution: '720p' },
{ model: 'siraya/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 /v2/videos/{id},做法與上一個範例相同
對話:串流讀取
重點只有兩個:每則事件都要檢查 object(錯誤不走狀態碼),以及從最後一則 chunk 取出 price_charged。
#!/usr/bin/env python3
"""AppleToken 對話串流 — 只用標準庫。"""
import json, os, urllib.request
BASE = "https://appletoken.app/v2"
KEY = os.environ["APPLETOKEN_KEY"]
def chat_stream(messages, model="siraya/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/v2'
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: 'siraya/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/v2/chat/completions \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"siraya/ByteDance-Seed-1.8","max_tokens":512,"stream":true,
"messages":[{"role":"user","content":"用三句話說明什麼是向量資料庫"}]}'
# 非串流:一次拿完整回應,price_charged 就在裡面
curl -s https://appletoken.app/v2/chat/completions \
-H "Authorization: Bearer $APPLETOKEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"siraya/ByteDance-Seed-1.8","max_tokens":256,
"messages":[{"role":"user","content":"用一句話說明什麼是向量資料庫"}]}'
上線檢查清單
正確性
- ☐ 憑證存在環境變數或密鑰服務,不在版控、不在前端。
- ☐ 每個送出的
job_id都持久化,程式重啟後仍會被查詢完並取件(成品保留時間有限)。 - ☐ 輪詢會走到
completed或failed才停,逾時的任務會被排入稍後續查。 - ☐ 送件前呼叫
/v2/quote,把預估費用顯示給使用者或記入你的成本帳。 - ☐
seconds確定是數字而不是字串(表單取值最容易在這裡出錯)。 - ☐
aspect_ratio只使用文件列出的六個值。 - ☐ 以
/v2/models的modality分類模型,不要靠 id 前綴猜。
對話
- ☐ 每個對話請求都帶
max_tokens,而且貼近實際需要——它決定預扣金額。 - ☐ 串流讀取器檢查每則事件的
object,遇到"error"當成失敗處理。 - ☐ 從最後一則 chunk(或非串流回應)取出
price_charged記入成本帳。 - ☐ 明白中途斷線仍會為已產生的內容付費,不要用「使用者按停止」當成本控制手段。
- ☐ HTTP 客戶端沒有在串流上套整體逾時(長回應是正常的)。
韌性
- ☐
429、502、503、504走指數退避重試;400、401、403一律不重試。 - ☐ HTTP 逾時設定足夠寬鬆(非串流建議 120 秒),但輪詢間隔不小於 3 秒。
- ☐ 送件的重試有冪等保護——重送等於再付一次錢,請確認上一次真的沒成立。
- ☐ 下載成品後立刻存入你自己的儲存空間,不要依賴平台長期保存。
成本控制
- ☐ 定期呼叫
/v2/me,在available低於門檻時告警。 - ☐ 限制併發數:
reserved是所有進行中請求的凍結總和,衝太高會讓後續請求撞到402。 - ☐ 以
/v2/usage的id為主鍵做每日對帳(有job_id的列再對回你的任務紀錄),並用modality拆分不同類型的成本。 - ☐ 正式與測試環境使用不同憑證,方便分開看用量、獨立停用。
常見問題
可以取消已送出的任務嗎?
不行。成本在送出當下就已產生,因此沒有取消端點。請在送件前用 /v2/quote 確認參數與費用。對話串流可以中止讀取,但已產生的部分照樣計費。
有 webhook 或回呼通知嗎?
有,只限 /v2:在主控台的 Webhook 頁登記網址後,影片與後處理完成或失敗時平台會主動通知,見回呼(Webhook)。舊版 v1 API 沒有回呼,請求中的 callbackUrl 類欄位會被丟棄。回呼只是提醒,取件仍以輪詢或 GET 為準。另外要澄清一個常見誤解:輪詢不是結算的前提。平台有背景程序負責結算與退還,你不查詢也不會有額度卡住。
生成失敗要付錢嗎?
影片任務失敗時凍結全額退還,used 不受影響;退還由平台自動完成,不需要你輪詢到 failed。
對話則要看有沒有產出:一個 token 都沒產出才全額退還;只要吐出了內容(即使只有半句話、即使後來斷線),那部分照實計費。
為什麼對話一定要給 max_tokens?
因為額度必須在呼叫後端之前凍結,而對話的用量到最後一刻才知道。max_tokens 是「這次最多產出多少」的上限,平台用它算出最壞情況的價格先凍結起來。結算一律以實際用量重算,差額當場退回,所以你付的永遠是實際用量。詳見對話的預扣與串流計費。
串流到一半我把連線關掉,會被收多少?
收「已經產生的部分」。不會整筆退還,也不會照 max_tokens 的預扣全收。差額在斷線的當下就結算完畢。要少花錢,正確作法是把 max_tokens 設小。
對話可以先估價嗎?
/v2/quote 只支援影片模型。對話的費用取決於實際產生多少 token,事前無法算準——但預扣公式是公開的,你可以自己算出「這次最多會花多少」,那正是平台凍結的金額。
可以同時送多個任務嗎?
可以,只要 available 足以覆蓋所有請求的預扣總和。每個請求各持有一筆獨立凍結。建議自行限制併發數,並對 429 做退避。
為什麼 calls 的數字比我的呼叫次數少?
calls 只計算成功建立凍結的請求——影片送件與對話請求都算,估價、查詢、下載、/v2/me 都不算。
用的是哪一家上游的模型?
就是你在 model 裡指定的那一家。v2.0 起上游是公開的:model 寫「上游/模型」(例如 siraya/Seedance-2.0-fast),或寫那個模型的 ID/slug;GET /v2/models 每一列都有 provider 與 upstream_model。平台不會替你換家——換一家上游就是換一個模型,售價與指標也各自不同,要換請自己改 model(見遷移指南)。模型的 ID、slug 與「上游/模型」永久不變;模型下架後會從 /v2/models 消失,呼叫它回 404。
價格會變嗎?
可能會。請從 /v2/models 讀取價格而非寫死,並以 /v2/quote 作為每次影片交易的報價依據。價格調整會事先通知,且既有的請求一律以送出當下生效的費率結算。
有速率限制嗎?
有。平台以 1 分鐘為時間窗做限流:帶 Authorization 標頭的請求是每分鐘 600 次,以憑證分別計數;未帶憑證的請求是每分鐘 20 次,以來源 IP 計數。/healthz 不受限。超過時回 429,error.code 是 rate_limited,訊息是「請求太頻繁,請稍後再試」,不會帶 retry_after。平台保留調整配額的權利,請不要把這些數字寫死進你的整合裡,該做的指數退避照樣要做。
429 有兩種來源,請用 error.code 分辨:rate_limited 是上面說的平台限流;upstream_error 則是上游服務的節流被原樣透傳。兩者的回應都不會帶 retry_after,額度也都不會被扣——請用自己的指數退避重試。
要留意的是整合寫錯造成的失控迴圈——那種情況下額度會在幾秒內燒光,而通常要等到帳單才會發現。請自行限制併發與重試次數。/v2/* 的每一個端點都需要憑證,GET /v2/models 也不例外。
額度不足不會回 429,而是回 402 quota_exceeded,並附上這次需要的金額與當下的可用餘額。那不是節流,重試也不會變好——請先加值,或降低同時進行的請求數。
版本紀錄
model 必須指定上游:改用模型 ID、slug 或「上游/模型」,裸名稱回 400 model_ref_required;GET /v2/models 改成一列一個模型,default_route/routes[] 已移除;fallback 已不支援。路徑改為 /v2;/v1 照舊可用、行為不變。見遷移指南。
每一版改了什麼、哪些整合需要跟著調整,都列在版本歷程。那一頁由後台直接維護,發版當天就會更新,不必等這份文件重新發布;目前的 API 版本與文件版本也標示在那裡。
回報問題時請附上時間(含時區)、job_id、憑證 id 前綴(例如 at_live_9f2c81ab_…)與收到的 error.code。對話請求的 job_id 就是回應的 id(chatcmpl_…);找不到的話請附 /v2/usage 裡對應那一列的 id 與 created_at。請勿在任何訊息中貼出完整憑證。