用 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',
);
