PopNowAPI 文档
用 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 最多重试一两次,然后把错误交给人处理。

完整示例

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

本页内容