用 API 開發
主要內容冪等
逾時後安心重試,不會重複生成,也不會重複扣點。
冪等鍵是你為一件工作貼上的標籤,例如「為訂單 8814 算圖」。同一個貼了標籤的請求就算送到 PopNow 兩次,這件工作也只會做一次。
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1為什麼需要它
逾時會讓你進退兩難:可能請求根本沒送到,也可能送到了只是回應遺失。沒有冪等鍵,你只能放棄(讓使用者白等)或重試(可能生成兩次、扣點兩次)。有了它,重試永遠是安全的:要嘛建立新任務,要嘛拿回剛才建立的那一個。
運作方式
- 同一把 API 金鑰用同一個鍵重送,拿回的是原本的任務及其目前狀態——不會多出任務、多保留或多扣點。
- 鍵與你送出的內容綁定:
model、整個input與callBackUrl。欄位順序與空白無所謂,但內容真的改了就不行。用舊鍵送出修改過的請求會回傳409;請改用新鍵。 - 鍵從第一次使用起保留 24 小時。超過之後,同一個鍵會建立一個新的、要收費的任務,所以自動重試要在那之前提早停止。
- 鍵最長 128 個字元,只需在你的帳戶內唯一。帳戶中另一把 API 金鑰無法重用它:會收到
409,且不會回傳原任務的 ID。
| 端點 | 用同一個鍵重送時… |
|---|---|
POST /api/v1/jobs/createTask | 回傳原本的任務 |
POST /api/v1/jobs/retry | 回傳那次重試所建立的任務(鍵的作用範圍限於來源任務) |
POST /v1/videos | 回傳原本的任務 |
POST /api/v1/assets | 回傳已經儲存的素材(這裡必須帶這個標頭) |
讀取類端點以及產生上傳、下載連結的端點會忽略這個標頭。jobs/purge 不需要它:重複呼叫本來就沒有影響。
如何挑選鍵
- 一件工作一個鍵,而不是每次 HTTP 嘗試一個。每次重試都必須沿用它。
- 從你已經儲存的資料推導:
order-8814-render-1在程式當掉後還能重新算出;隨機 UUID 除非先存起來,否則找不回來。 - 如果使用 UUID,請在第一次嘗試之前產生,並與對應的工作存放在一起。
千萬不要在重試迴圈裡產生鍵
// Wrong: a new key per attempt defeats the whole mechanism
for (let i = 0; i < 3; i++) await createTask(body, crypto.randomUUID());
// Right: one key, created once
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) await createTask(body, key);衝突
以下情況都會回傳 code: 409:
| 發生了什麼 | 要重試嗎? |
|---|---|
| 兩個帶同一個鍵的請求同時抵達 | 要——等幾百毫秒,用同一個鍵重送 |
| 這個鍵已用於別的請求 | 不要——請求變了,需要新鍵 |
| 帳戶中另一把 API 金鑰已經用過這個鍵 | 不要——請用第一次嘗試時的那把 API 金鑰重試 |
程式不應依 msg 分支,所以 409 最多重試一兩次,之後就把錯誤交給人處理。
完整範例
const RETRYABLE = new Set([429, 500, 503, 50301]); // business codes from the body
async function createTaskOnce(body, idempotencyKey) {
for (let attempt = 0; attempt < 4; attempt++) {
try {
const res = await fetch('https://api.popnow.ai/api/v1/jobs/createTask', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POPNOW_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(body),
});
const json = await res.json();
if (json.code === 200) return json.data; // a new task, or the original one
if (!RETRYABLE.has(json.code)) throw new Error(`${json.code} ${json.msg} (${json.request_id})`);
} catch (err) {
if (!(err instanceof TypeError)) throw err; // network error: safe to retry with the same key
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 500 + Math.random() * 300));
}
throw new Error('createTask did not succeed after 4 attempts');
}
const task = await createTaskOnce(
{ model: 'alibaba/z-image-turbo/text-to-image', input: { prompt: 'A paper boat on a calm lake' } },
'order-8814-render-1',
);
