Makernice API 文件:快速開始
- 適合
- 在自己的程式碼中透過 REST 呼叫影片與圖片生成的開發者:先報價、再建立,然後輪詢或接收 Webhook。
- 不適合
- Makernice API 不適合不寫程式碼就製作影片或圖片;這種情況請使用控制台的「創作」區,模型、價格和餘額都與 API 相同。
只要一個 API 金鑰、三個請求就能生成影片:取得報價、建立生成任務,然後輪詢直到完成。
1. 建立 API 金鑰
在控制台開啟 開發者 → API 金鑰,建立一個金鑰。每個請求都以 Bearer token 的形式帶上它。生成費用從你的錢包扣款;你可以為每個金鑰設定消費上限。
2. 取得報價
curl https://api.makernice.com/v1/generations/quote \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"seedance-2.5","mode":"t2v","prompt":"緩緩穿過溫室的推軌鏡頭","duration":5,"resolution":"720p"}'
# → (數值僅供示意){ "id": "qt_…", "amount": "500000", "expires_at": "…", "facts": { "quantity": 5, … } }金額以美元計,單位為百萬分之一美元(micro-USD):1,000,000 = $1.00。報價會將價格鎖定 12 分鐘。
3. 建立生成任務
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Idempotency-Key: 7c1e9a2b-order-1842" \
-H "Content-Type: application/json" \
-d '{"quote_id":"qt_…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"緩緩穿過溫室的推軌鏡頭","duration":5,"resolution":"720p"}}'
# → 202 { "id": "gen_…", "status": "queued", "billing": "held", … }這筆金額會從你的餘額中凍結。如果生成失敗,或你在排隊中取消,凍結金額會全額釋放。使用相同的 Idempotency-Key 重試,會傳回同一個生成任務,而不會重複扣款。
4. 輪詢直到完成
curl https://api.makernice.com/v1/generations/gen_… \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# status:queued → generating → moderating → succeeded | failed | canceled
# 成功時:帶上你的金鑰 GET outputs[0].url,會收到指向檔案的 302 重新導向5. 下載檔案
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
-H "Authorization: Bearer $MAKERNICE_API_KEY"Python:取得報價、建立與輪詢
先設定 MAKERNICE_API_KEY,再執行:
import os
import time
import uuid
from decimal import Decimal
import requests
API = "https://api.makernice.com/v1"
ORIGIN = "https://api.makernice.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MAKERNICE_API_KEY']}"}
TIMEOUT_SECONDS = 30
POLL_INTERVAL_SECONDS = 5
MAX_WAIT_SECONDS = 30 * 60
FINAL = {"succeeded", "failed", "canceled"}
def call(method, path, headers=None, **kwargs):
# Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; raises on any other error.
for _ in range(5):
r = requests.request(method, API + path, headers={**HEADERS, **(headers or {})}, timeout=TIMEOUT_SECONDS, **kwargs)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
if not r.ok:
raise RuntimeError(f"{method} {path} -> {r.status_code}: {r.text}")
return r.json()
raise RuntimeError(f"{method} {path}: still rate limited")
job = {
"model": "seedance-2.5",
"mode": "t2v",
"prompt": "A slow dolly shot through a greenhouse",
"duration": 5,
"resolution": "720p",
}
# 1. Quote: the exact amount, locked for 12 minutes
quote = call("POST", "/generations/quote", json=job)
usd = Decimal(quote["amount"]).scaleb(-6) # micro-USD integer string -> dollars, no float rounding
print(f"Quoted ${usd} (price list v{quote['price_version']}), valid until {quote['expires_at']}")
# 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
idempotency_key = str(uuid.uuid4())
gen = call("POST", "/generations", json={"quote_id": quote["id"], "input": job}, headers={"Idempotency-Key": idempotency_key})
# 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
deadline = time.monotonic() + MAX_WAIT_SECONDS
while gen["status"] not in FINAL:
if time.monotonic() > deadline:
raise SystemExit(f"{gen['id']} is still {gen['status']}; poll it again later")
time.sleep(POLL_INTERVAL_SECONDS)
gen = call("GET", f"/generations/{gen['id']}")
if gen["status"] != "succeeded":
# billing is "released": the hold went back to your balance
raise SystemExit(f"{gen['status']}, billing {gen['billing']}: {gen['error']}")
# 4. Download: outputs[].url is relative to the API origin and redirects (302) to the file
output = gen["outputs"][0]
if output["state"] == "blocked":
raise SystemExit("Output blocked by moderation (the run is charged); contact support for a manual review")
r = requests.get(ORIGIN + output["url"], headers=HEADERS, timeout=120) # requests drops Authorization on the redirect
r.raise_for_status()
with open("output.mp4", "wb") as f:
f.write(r.content)
print(f"Saved output.mp4, charged ${Decimal(gen['amount']).scaleb(-6)}")Node.js:取得報價、建立與輪詢
需要 Node.js 18 或更高版本,無需安裝套件。存成 quickstart.mjs,設定 MAKERNICE_API_KEY 後執行 node quickstart.mjs:
// Node.js 18+ (built-in fetch, no packages). Save as quickstart.mjs and run: node quickstart.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
const API = "https://api.makernice.com/v1";
const ORIGIN = "https://api.makernice.com";
const HEADERS = { Authorization: `Bearer ${process.env.MAKERNICE_API_KEY}` };
const TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 5_000;
const MAX_WAIT_MS = 30 * 60 * 1000;
const FINAL = new Set(["succeeded", "failed", "canceled"]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// micro-USD integer string -> dollars, no float rounding
const usd = (micro) => `${BigInt(micro) / 1_000_000n}.${(BigInt(micro) % 1_000_000n).toString().padStart(6, "0")}`;
class Stop extends Error {}
async function call(method, path, { body, headers } = {}) {
// Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; throws on any other error.
for (let attempt = 0; attempt < 5; attempt++) {
const r = await fetch(API + path, { signal: AbortSignal.timeout(TIMEOUT_MS),
method,
headers: { ...HEADERS, ...(body ? { "Content-Type": "application/json" } : {}), ...headers },
body: body ? JSON.stringify(body) : undefined,
});
if (r.status === 429) {
await sleep(Number(r.headers.get("Retry-After") ?? "5") * 1000);
continue;
}
if (!r.ok) throw new Stop(`${method} ${path} -> ${r.status}: ${await r.text()}`);
return r.json();
}
throw new Stop(`${method} ${path}: still rate limited`);
}
async function main() {
const job = {
"model": "seedance-2.5",
"mode": "t2v",
"prompt": "A slow dolly shot through a greenhouse",
"duration": 5,
"resolution": "720p",
};
// 1. Quote: the exact amount, locked for 12 minutes
const quote = await call("POST", "/generations/quote", { body: job });
console.log(`Quoted $${usd(quote.amount)} (price list v${quote.price_version}), valid until ${quote.expires_at}`);
// 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
const idempotencyKey = randomUUID();
let gen = await call("POST", "/generations", { body: { quote_id: quote.id, input: job }, headers: { "Idempotency-Key": idempotencyKey } });
// 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
const deadline = Date.now() + MAX_WAIT_MS;
while (!FINAL.has(gen.status)) {
if (Date.now() > deadline) throw new Stop(`${gen.id} is still ${gen.status}; poll it again later`);
await sleep(POLL_INTERVAL_MS);
gen = await call("GET", `/generations/${gen.id}`);
}
if (gen.status !== "succeeded") {
// billing is "released": the hold went back to your balance
throw new Stop(`${gen.status}, billing ${gen.billing}: ${JSON.stringify(gen.error)}`);
}
// 4. Download: outputs[].url is relative to the API origin and answers 302 with a signed file URL.
// Follow it yourself so your API key is never sent to the file host.
const [output] = gen.outputs;
if (output.state === "blocked") throw new Stop("Output blocked by moderation (the run is charged); contact support for a manual review");
const hop = await fetch(ORIGIN + output.url, { headers: HEADERS, redirect: "manual", signal: AbortSignal.timeout(TIMEOUT_MS) });
const location = hop.headers.get("Location");
if (hop.status < 300 || hop.status > 399 || !location) throw new Stop(`GET ${output.url} -> ${hop.status}`);
const file = await fetch(new URL(location, ORIGIN + output.url), { signal: AbortSignal.timeout(120_000) });
if (!file.ok) throw new Stop(`download -> ${file.status}`);
await writeFile("output.mp4", Buffer.from(await file.arrayBuffer()));
console.log(`Saved output.mp4, charged $${usd(gen.amount)}`);
}
main().catch((err) => {
console.error(err instanceof Stop ? err.message : err);
process.exit(1);
});想同時執行多個生成任務,可以並行發起(例如使用執行緒池、asyncio 搭配非同步 HTTP 用戶端,或 Promise.all)。每個帳戶預設同時進行中的生成任務為 2 個;超出時 API 會傳回 429 concurrency_limit_reached(附帶 Retry-After),兩個範例中的 call() 都會依它等待後重試。如需提高上限,請聯絡客服。
驗證
除了 GET /v1/models 和 GET /v1/pricing,每個請求都需要在 Authorization 標頭中帶上 API 金鑰。金鑰的格式是 mn- 後接 48 個字母與數字。
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY"- 完整金鑰只在建立時顯示一次。我們只儲存它的指紋,因此遺失的金鑰無法找回:請建立新的金鑰,再刪除舊的。
- 金鑰缺失、格式錯誤、不存在、已停用、已過期或已刪除時,一律得到相同的
401 invalid_api_key回應。停用或刪除金鑰會從下一個請求起生效。 - 請從你的伺服器呼叫 API。API 不接受瀏覽器的跨來源請求(不支援 CORS);切勿把金鑰放在前端程式碼中。
- 同一帳戶中的每個金鑰看到的是相同的生成任務:任何金鑰都能查詢、列出和取消帳戶中的所有生成任務,包括在控制台建立的。
- 同一帳戶中的所有金鑰都從同一份預付餘額扣款。目前一個 Makernice 帳戶只有一個登入,團隊透過多個 API 金鑰共用同一份餘額。
- API 金鑰頁面會顯示每個金鑰的累計消費,用量頁面可依金鑰和模型查看 API 消費。
- 在控制台中,你可以為金鑰設定到期日與消費上限。金鑰的消費額是它所建立的生成任務中已扣款金額加上目前凍結中的金額;會超出上限的建立請求會得到
402 key_spend_limit_reached。失敗和已取消的生成任務不計入。
價格與計費
- 以美元計費。所有金額都是 micro-USD(百萬分之一美元)的整數字串:
1,000,000= $1.00。請以整數或十進位小數(decimal)解析,不要用浮點數。 - 影片按秒計價(數量為請求的時長;含參考影片片段的範本也計入片段的秒數),圖片按請求計價(每次生成一張圖片)。價格 = 單價 × 數量。
- 報價會將價格鎖定 12 分鐘(
expires_at)。即使期間價格有變動,生成任務仍按報價金額扣款。
GET /v1/pricing 列出目前價目表版本的單價。它不需要 API 金鑰,結果最多可能快取 60 秒。若有 official_unit_price,代表同一組合的官方定價,僅供比較;實際一律按 unit_price 計費。
{
"price_version": 3,
"data": [
{ "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "unit": "second", "unit_price": "100000", "unit_price_usd": "0.10", "official_unit_price": "120000" }
]
}生成失敗會扣費嗎?
不用。凍結金額會全額釋放——失敗的生成一律不扣費,但若完成的輸出因違反政策而被輸出審核攔截,仍會扣費。你可以聯絡客服申請人工審核。
建立生成任務時,我們會凍結報價金額。生成完成時,凍結金額轉為扣款;生成失敗和逾時會全額釋放,排隊中取消同樣全額釋放。釋放凍結不是退款,這筆錢從未離開你的餘額。
| 發生了什麼 | billing | 扣費嗎? |
|---|---|---|
| 生成成功 | charged | 扣,按報價金額 |
生成失敗(error.code 為 generation_failed、upstream_unavailable 或 invalid_request) | released | 不扣 |
超過最長執行時間(error.code: timeout) | released | 不扣 |
在 queued 時取消 | released | 不扣 |
建立時提示詞被攔截(422 content_rejected,reason: prompt) | — | 不扣;不會建立生成任務,也不凍結 |
輸入圖片或模型服務商的安全政策拒絕(content_rejected,reason 為 input_image 或 model) | released | 不扣 |
完成的輸出被輸出審核攔截(輸出 state: blocked) | charged | 扣;可聯絡客服申請人工審核 |
正在確認結果(status: checking) | held | 確認成功才扣 |
取得報價
傳送生成輸入,回應會告訴你這次生成的確切費用。每個報價只能用於一次生成。
| 欄位 | 類型 | 說明 |
|---|---|---|
model | string | 必填。來自 GET /v1/models 的模型 id。 |
mode | string | 必填。t2v、i2v、flf、r2v、t2i 或 i2i;每個模型會列出它支援的模式。 |
prompt | string | 必填。預設 1–4,000 個字元;部分模型的上限較低(見 GET /v1/models 中的 maxPromptChars)。 |
duration | integer | 僅限影片。單位為秒,須在模型允許的範圍內;預設為模型的預設值。 |
resolution | string | 模型 resolutions 中的一個值;預設為 defaultResolution。模型未列出任何解析度時不可傳送。 |
aspect_ratio | string | 該模式 aspectRatios 中的一個值。 |
image_url | string | 首影格。i2v 和 flf 必填。 |
last_image_url | string | 尾影格。flf 必填。 |
reference_images | string[] | r2v 和 i2i 必填;最多為該模式的 maxReferences 張。 |
seed | integer | 選填。0–2,147,483,647。相同的 seed 搭配相同的設定,結果會更接近。 |
generate_audio | boolean | 選填。是否為影片加上音軌。 |
prompt_expansion | string | 選填。off、balanced 或 quality:模型可改寫你的提示詞的程度。 |
negative_prompt | string | 選填。最多 2,000 個字元,描述要避免的內容。 |
watermark | boolean | 選填。加上供應商的 AI 生成浮水印。 |
最後五個欄位是選填的額外參數,每個模型只支援其中一部分:GET /v1/models 會在 prices[].options 中列出每個模式與解析度可用的欄位,以及允許值和預設值。沒有任何模型路由能套用的欄位會傳回 400 invalid_request;與其他欄位一樣,變更選填欄位也需要重新取得報價。
圖片為公開的 https 網址(最多 2,048 個字元)。該模式不使用的欄位與未知欄位會以 400 invalid_request 拒絕;對於輸入欄位,error.details 會以 { path, message } 逐一列出每個問題(其他 invalid_request 錯誤的 details 結構可能不同,或沒有 details)。沒有價格的組合會傳回 422 price_unavailable。
每張圖片都必須是可公開存取的 https 網址,並直接在該網址提供靜態的 JPEG、PNG 或 WebP 圖片(不支援動態 WebP 和 GIF),大小不超過 10 MB:我們不會追隨重新導向。每張圖片我們只抓取一次並保留副本;內容審核與模型都使用這份副本。若圖片無法使用,生成任務會以 invalid_request 失敗,error.input 會指出是哪張圖片,凍結金額也會釋放。
curl https://api.makernice.com/v1/generations/quote \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"seedance-2.5","mode":"t2v","prompt":"緩緩穿過溫室的推軌鏡頭","duration":5,"resolution":"720p"}'{
"id": "qt_6f1c…",
"amount": "500000",
"price_version": 3,
"expires_at": "2026-10-09T09:24:41.000Z",
"facts": { "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "variant": "default", "unit": "second", "quantity": 5 }
}報價會鎖價多久?
每次報價都會鎖定價格 12 分鐘,生成任務一律按報價金額扣款。
| 欄位或情形 | 意思 |
|---|---|
expires_at | 報價建立後 12 分鐘。請在此之前建立生成任務。 |
amount | 單價 × facts.quantity(影片為秒數,圖片為 1),單位為 micro-USD。建立生成任務時凍結這筆金額,成功時按它扣款。 |
price_version | 報價所用的價目表版本。建立生成任務前即使發布了新價目表,仍按報價金額扣款。 |
| 超過 12 分鐘 | 用這個報價建立生成任務會傳回 410 quote_expired。沒有凍結任何金額,重新取得報價即可。 |
| 更改了模型、模式、解析度或時長 | 422 quote_mismatch。只更改提示詞,報價仍然有效。 |
| 報價已使用過 | 409 quote_already_used。每個報價只能用於一次生成。 |
建立生成任務
用報價與相同的輸入建立生成任務。Idempotency-Key 標頭為必填(見 冪等性)。
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Idempotency-Key: 7c1e9a2b-order-1842" \
-H "Content-Type: application/json" \
-d '{"quote_id":"qt_6f1c…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"緩緩穿過溫室的推軌鏡頭","duration":5,"resolution":"720p"}}'- 回應
202與生成任務(status: queued、billing: held)。使用相同的 Idempotency-Key 重試時,會以200回應原本的生成任務。 - 你可以修改提示詞,但變更任何影響價格的參數(模型、模式、解析度、時長)會傳回
422 quote_mismatch。 - 已使用的報價傳回
409 quote_already_used,已過期的傳回410 quote_expired,不在你帳戶中的傳回404 quote_not_found。 - 餘額不足傳回
402 insufficient_balance;超出金鑰的消費上限傳回402 key_spend_limit_reached。 - 選填的頂層欄位
callback_url(https,最多 2048 個字元,網址中不得含憑證,不得為私有或保留位址):生成任務結束時,我們會把它 POST 到該網址。它的優先順序高於你帳戶的 Webhook 位址。見 Webhook。
查詢狀態
輪詢生成任務,直到它進入最終狀態。每隔幾秒輪詢一次沒有問題;讀取限制見「速率限制」。一次影片生成可能需要幾分鐘,長影片和高解析度會更久。超過最長執行時間的生成任務會判定為失敗,凍結金額全額釋放。
| 狀態 | 意義 |
|---|---|
queued | 等待開始,仍可取消。 |
generating | 模型正在處理。 |
moderating | 生成已結束且金額已扣款;輸出正在進行安全檢查,檢查完成前 outputs 保持為空。請繼續輪詢。 |
succeeded | 已完成。金額已扣款,outputs 已可使用。 |
failed | 未能完成。凍結金額已釋放;error 包含錯誤碼與訊息。 |
canceled | 在排隊中被取消。凍結金額已釋放。 |
checking | 我們正在確認結果。確認完成前,金額維持凍結。 |
失敗生成任務的 error.code 為以下之一:invalid_request(模型拒絕了請求;請調整提示詞或設定)、content_rejected(未通過內容審核或模型供應商的安全政策;凍結金額已釋放)、timeout、upstream_unavailable 或 generation_failed。對於 content_rejected,error.reason 說明被拒絕的對象:prompt(提示詞)、input_image(參考圖)或 model(模型供應商的安全政策)。提交的提示詞被攔截時,422 回應中也會出現相同的 reason。當 invalid_request 是由無法使用的參考圖造成時,error.input 會指出是哪一張:image_url、last_image_url 或 reference_images[0](從 0 起算)。 請替換該圖片,並建立新的生成任務。
{
"id": "gen_3b9e…",
"type": "video",
"model": "seedance-2.5",
"mode": "t2v",
"status": "succeeded",
"prompt": "緩緩穿過溫室的推軌鏡頭",
"params": { "duration": 5, "resolution": "720p", "aspectRatio": null },
"amount": "500000",
"billing": "charged",
"price_version": 3,
"outputs": [
{
"index": 0,
"url": "/v1/generations/gen_3b9e…/outputs/0",
"state": "stored",
"contentType": "video/mp4",
"expiresAt": "2026-11-08T09:14:03.000Z"
}
],
"error": null,
"created_at": "2026-10-09T09:12:41.000Z",
"finished_at": "2026-10-09T09:14:03.000Z"
}生成任務結束 90 天後,依我們的資料保留政策,其請求內容會被清除:prompt 會變成空字串。計費資料(model、params、amount、status 與時間戳記)會保留。
GET /v1/generations/{id}/events 傳回生成任務的時間軸。代碼:created、submitted、generating、delivered、failed_refunded、canceled_refunded、checking、requeued。
{
"data": [
{ "code": "created", "at": "2026-10-09T09:12:41.000Z" },
{ "code": "submitted", "at": "2026-10-09T09:12:43.000Z" },
{ "code": "generating", "at": "2026-10-09T09:12:50.000Z" },
{ "code": "delivered", "at": "2026-10-09T09:14:03.000Z" }
]
}下載輸出
outputs 中的每一項都有一個 url,例如 /v1/generations/gen_…/outputs/0,相對於 https://api.makernice.com。請帶上你的 API 金鑰請求它;回應是 302 重新導向,指向檔案的短期連結。每次需要檔案時,請重新請求這個端點,不要儲存重新導向的目標。
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
-H "Authorization: Bearer $MAKERNICE_API_KEY"- 加上
?download=1可讓檔案以附件形式提供。檔案為stored後才有效;在pending或degraded狀態時,重新導向會指向原始檔案,此參數不起作用。 curl -L不會把你的Authorization標頭傳送給重新導向的主機,因此你的金鑰只會留在我們這裡。- 只有生成任務
succeeded後才會有輸出;否則端點傳回404 output_not_found。 state:pending(仍在儲存中;連結已可使用)、stored(保存至expiresAt)、degraded(儲存未完成;連結可能較早失效,請盡快下載)、expired(儲存的檔案已過期;端點傳回410 output_expired)、blocked(輸出未通過內容審核;url為空字串,請聯絡客服申請人工審核)。
取消
只有 queued 的生成任務可以取消;凍結金額會全額釋放,回應為已取消的生成任務。生成任務進入 generating 後就無法停止,取消會傳回 409 not_cancelable。
curl -X POST https://api.makernice.com/v1/generations/gen_…/cancel \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# → { "id": "gen_…", "status": "canceled", "billing": "released", … }列出生成任務
列出你帳戶中的生成任務,最新的排在最前面,包括在控制台建立的。
| 查詢參數 | 說明 |
|---|---|
limit | 每頁筆數,1–100。預設 20。 |
cursor | 上一頁的 next_cursor。 |
status | 一個或多個狀態,以逗號分隔,例如 queued,generating。 |
type | video 或 image。 |
model | 模型 id。 |
q | 生成任務 id 的一部分,或提示詞中包含的文字(皆不區分大小寫)。 |
curl "https://api.makernice.com/v1/generations?status=queued,generating&limit=50" \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# → { "data": [ …generations… ], "next_cursor": "gen_…" | null, "total": 12 }total 為符合篩選條件的生成任務總數。next_cursor 為 null 時,表示已到最後一頁。
Webhook
除了輪詢,你也可以讓我們在生成任務進入最終狀態時,把它 POST 到你的伺服器。我們會傳送到建立生成任務時指定的 callback_url;若未指定,則傳送到你帳戶的 Webhook 位址(在 控制台設定中設定)。兩者皆無時,不會傳送任何內容。
| 事件 | 傳送時機 |
|---|---|
generation.succeeded | 生成成功並已扣款。 |
generation.failed | 生成失敗;凍結金額已釋放。 |
generation.canceled | 生成已取消;凍結金額已釋放。 |
請求
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
Makernice-Signature: t=1791364443,v1=5f0c2d…e81a
Makernice-Event-Id: evt_1b7f…{
"id": "evt_1b7f…",
"type": "generation.succeeded",
"created_at": "2026-10-09T09:14:03.000Z",
"data": {
"id": "gen_3b9e…",
"status": "succeeded",
"billing": "charged",
"outputs": [
{ "index": 0, "url": "https://api.makernice.com/v1/generations/gen_3b9e…/outputs/0", "state": "stored", … }
],
…
}
}data與GET /v1/generations/{id}傳回的生成物件相同,只是outputs[].url為絕對網址。Makernice-Event-Id等於本文中的id,重試時保持不變。可用它忽略重複的傳送。
驗證簽章
Makernice-Signature 的格式為 t=<unix seconds>,v1=<hex>,其中 v1 是以你的簽章密鑰對 t + "." + raw body 計算的 HMAC-SHA256。請以收到的原始位元組計算、以常數時間比較,並拒絕 t 與你的時鐘相差超過 5 分鐘的請求。簽章密鑰位於控制台設定;透過 callback_url 傳送的請求也以同一個密鑰簽署。輪替密鑰後,尚未送出的傳送會改用新密鑰簽署。
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the body exactly as received (Buffer). Don't re-serialize parsed JSON.
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => [p.slice(0, p.indexOf("=")), p.slice(p.indexOf("=") + 1)]));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib
import hmac
import time
# raw_body: the body exactly as received (bytes). Don't re-serialize parsed JSON.
def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))傳送與重試
- 僅限 HTTPS。不會追隨重新導向。每次嘗試在 10 秒後逾時。
- 任何 2xx 回應都視為已送達。請盡快回應,繁重的處理稍後再做。
- 否則我們會依序在 1 min、5 min、30 min、2 h、6 h、12 h 後重試(共 7 次嘗試),之後將該次傳送標記為失敗。最近的傳送紀錄及其最後一次回應會顯示在控制台設定中。
- 每個生成任務通常只傳送一個事件,而同一次傳送可能不只送達一次(例如你那端逾時之後)。若被內容審核暫扣的輸出之後被放行,我們會以新的事件 id 再次傳送該生成任務,因此你可能會收到兩個事件;以最新的一個為準。
模型
GET /v1/models 列出所有模型,以及你可以使用的模式、解析度和時長。只會列出有價格的模式,價格位於 modes[].prices。它不需要 API 金鑰,結果最多可能快取 60 秒。
resolutions/defaultResolution:resolution的允許值。空清單表示該模型的解析度固定,請省略resolution(即使傳送的是固定值,也會傳回400 invalid_request)。固定的解析度等級即modes[].prices[].resolution所顯示的值:例如seedance-2.0-fast一律輸出 720p。列出的解析度在某些模式下可能沒有價格:請查看modes[].prices;為它取得報價會傳回422 price_unavailable。durations:影片為以秒計的{ min, max, default },圖片為null。modes[].needs:該模式需要哪些圖片(image→image_url、lastImage→last_image_url、references→reference_images,最多maxReferences張)。modes[].aspectRatios:允許的aspect_ratio值。modes[].prices:每種解析度的單價。
{
"data": [
{
"id": "seedance-2.5",
"type": "video",
"resolutions": ["480p", "720p", "1080p", "4k"],
"defaultResolution": "720p",
"durations": { "min": 4, "max": 30, "default": 5 },
"modes": [
{
"mode": "t2v",
"needs": { "image": false, "lastImage": false, "references": false },
"maxReferences": null,
"aspectRatios": ["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"],
"prices": [{ "resolution": "720p", "unit": "second", "unitPrice": "100000" }]
}
]
}
]
}能呼叫哪些模型?
一個 API 金鑰可以呼叫全部 17 個模型:來自字節跳動(ByteDance)、快手(Kuaishou)、MiniMax 和阿里巴巴(Alibaba)的 13 個影片模型和 4 個圖片模型,包括 Seedance、可靈 Kling、Wan 和 MiniMax H3。所有模型使用同一種請求格式,把 model 設為下表的 API 模型 id 即可。
| 模型 | 廠商 | API 模型 id | 類型 |
|---|---|---|---|
| Seedance 2.5 | 字節跳動 | seedance-2.5 | 影片 |
| Seedance 2.0 | 字節跳動 | seedance-2.0 | 影片 |
| Seedance 2.0 Fast | 字節跳動 | seedance-2.0-fast | 影片 |
| MiniMax H3 | MiniMax | minimax-h3 | 影片 |
| MiniMax H3 Max | MiniMax | minimax-h3-max | 影片 |
| MiniMax H3 Max Turbo | MiniMax | minimax-h3-max-turbo | 影片 |
| Wan 3.0 | 阿里巴巴 | wan-3.0 | 影片 |
| Wan 3.0 Prime | 阿里巴巴 | wan-3.0-prime | 影片 |
| Kling 3.0 Pro | 快手 | kling-3.0-pro | 影片 |
| Kling 3.0 Standard | 快手 | kling-3.0-standard | 影片 |
| Kling 3.0 4K | 快手 | kling-3.0-4k | 影片 |
| Kling 4.0 | 快手 | kling-4.0 | 影片 |
| Happy Horse 1.1 | 阿里巴巴 | happy-horse-1.1 | 影片 |
| Seedream 5.0 Pro | 字節跳動 | seedream-5.0-pro | 圖片 |
| Seedream 5.0 | 字節跳動 | seedream-5.0 | 圖片 |
| Qwen3 Image | 阿里巴巴 | qwen3-image | 圖片 |
| Qwen3 Pro Image | 阿里巴巴 | qwen3-pro-image | 圖片 |
依任務查看支援的模式和範例價格(以下頁面僅有英文版):圖片生成影片(英文)、AI 影片生成器(英文)和AI 圖片生成器(英文)。
錯誤
錯誤以 JSON 傳回:{ "error": { "code", "message", "reason"?, "details"?, "request_id" } }。請依 code 判斷處理分支;message 可能變動。每個回應都帶有 x-request-id 標頭(與 request_id 的值相同);聯絡客服時請附上,方便我們找到該請求。
| 狀態碼 | 代碼 | 處理方式 |
|---|---|---|
| 400 | invalid_request | 請求本文、查詢參數或標頭無效。details 會逐一列出每個欄位與問題,請據此修正請求。 |
| 400 | template_conflict | 範本請求只接受 template、images,以及選填的 prompt、resolution 和 aspect_ratio;模型、模式、時長等其他設定由範本固定。請移除其他欄位。 |
| 401 | invalid_api_key | 金鑰缺失、格式錯誤、已停用、已過期或已刪除。 |
| 402 | insufficient_balance | 請為錢包儲值。 |
| 402 | key_spend_limit_reached | 請調高此金鑰的消費上限,或改用其他金鑰。 |
| 402 | daily_spend_limit_reached | 你的帳戶已達每日消費上限,上限於每天 00:00 UTC 重設。如需調高,請聯絡客服。 |
| 403 | org_frozen | 你的帳戶已被凍結,請聯絡客服。 |
| 404 | quote_not_found | 你的帳戶中沒有這個報價,請重新取得報價。 |
| 404 | task_not_found | 你的帳戶中沒有這個生成任務,請檢查 id。 |
| 404 | output_not_found | 生成尚未成功,或沒有這個 index 的輸出。 |
| 404 | template_not_found | 沒有這個 id 的範本。請用 GET /v1/templates 列出可用的範本。 |
| 404 | not_found | 端點不存在,請檢查請求方法與路徑。 |
| 409 | quote_already_used | 請重新取得報價。 |
| 409 | not_cancelable | 只有排隊中(queued)的生成任務可以取消。 |
| 410 | quote_expired | 請重新取得報價。 |
| 410 | output_expired | 儲存的檔案已過期,無法再下載。 |
| 410 | template_retired | 這個範本已不再提供。請從 GET /v1/templates 另選一個;你已建立的生成任務不受影響。 |
| 413 | payload_too_large | 請求本文超過 64 KB。請縮短提示詞,或減少圖片網址的數量。 |
| 422 | idempotency_conflict | 這個 Idempotency-Key 已用於另一個請求,請改用新的 Idempotency-Key。 |
| 422 | quote_mismatch | 輸入變更了影響價格的參數(模型、模式、解析度或時長)。請用這組輸入重新取得報價。 |
| 422 | price_unavailable | 這個組合沒有價格。請選擇 GET /v1/models 列出的組合。 |
| 422 | content_rejected | 未通過內容審核或模型供應商的安全政策;error.reason 會說明被拒絕的是哪一項(prompt、input_image 或 model)。請修改提示詞或圖片;本次未扣款。 |
| 429 | rate_limited | 請等待 Retry-After 指定的秒數後再重試。 |
| 429 | concurrency_limit_reached | 你的帳戶同時進行中的生成任務過多。請等待其中一個完成(參考 Retry-After),或聯絡客服調高上限。 |
| 500 | internal_error | 非預期的伺服器錯誤。請以退避方式重試;聯絡客服時請附上 request_id。 |
遇到非預期的 5xx 回應或網路錯誤時,請以退避方式重試。重試建立請求時,請沿用相同的 Idempotency-Key,這樣就不會被重複扣款。
速率限制
限制以每分鐘計算。超出時傳回 429 rate_limited,並附帶 Retry-After 標頭(單位為秒)。
| 範圍 | 限制 |
|---|---|
| 每個金鑰:寫入(報價、建立、取消) | 60 次 / 分鐘 |
| 每個金鑰:讀取(查詢、列出、事件、輸出) | 600 次 / 分鐘 |
| 每個帳戶:所有金鑰合計的寫入 | 120 次 / 分鐘 |
| 每個 IP:模型、價格,以及金鑰被拒絕的請求 | 120 次 / 分鐘 |
每個金鑰同時進行中的建立請求最多 4 個;超出的請求同樣傳回 429。限制可能調整。
每個帳戶另有兩項用量限制,預設為同時進行 2 個生成任務,以及每個 UTC 日 US$100 的消費。超出時傳回 429 concurrency_limit_reached(附 Retry-After)或 402 daily_spend_limit_reached。如需調高,請聯絡客服。生成任務處於 queued、generating 或 checking 時,會佔用「同時進行中」的名額,不論由哪個模型、哪個金鑰建立;進入 moderating、succeeded、failed 或 canceled 後就不再佔用。在網頁版中發起的生成也計入同一個名額。
冪等性
POST /v1/generations需要Idempotency-Key標頭:1–200 個可列印的 ASCII 字元,不含空格。每個新的生成任務都請使用新的值, 例如 UUID。- 這些值在你的帳戶內唯一,並由帳戶的所有 API 金鑰共用。
- 以相同的值、報價與輸入重複請求,會以
200傳回原本的生成任務,不會再次扣款。 - 以相同的值搭配不同的
quote_id、不同的input(包括提示詞與圖片網址)或不同的callback_url,會傳回422 idempotency_conflict。 - 報價和取消不接受 Idempotency-Key。重複一個已成功的取消會傳回
409 not_cancelable;請以GET查看狀態。
常見問題
報價需要付費嗎?
POST /v1/generations/quote 只計算你輸入內容的價格;在你用這個報價建立生成任務之前,不會凍結也不會扣任何費用。每個模型都需要單獨的 API 金鑰嗎?
model 欄位。