API로 만들기
본문멱등성
타임아웃 뒤에도 두 번 생성되거나 두 번 과금될 걱정 없이 재시도하기.
멱등 키는 "주문 8814의 영상 렌더링"처럼 한 가지 일에 붙이는 이름표입니다. 같은 이름표가 붙은 요청이 PopNow에 두 번 도착해도 일은 한 번만 처리됩니다.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1왜 필요한가
타임아웃이 나면 판단하기 어렵습니다. 요청이 아예 도착하지 않았을 수도 있고, 도착했는데 응답만 사라졌을 수도 있습니다. 키가 없으면 포기하거나(사용자는 헛되이 기다림) 재시도하거나(두 번 생성되고 두 번 과금될 수 있음) 둘 중 하나입니다. 키가 있으면 재시도는 언제나 안전합니다. 새 작업이 생기거나, 방금 만든 작업을 돌려받거나 둘 중 하나입니다.
동작 방식
- 같은 API 키에서 같은 키로 다시 보내면 원래 작업이 현재 상태로 돌아옵니다. 작업도, 홀드도, 과금도 늘지 않습니다.
- 키는 보낸 내용(
model,input전체,callBackUrl)에 묶입니다. 필드 순서나 공백은 상관없지만 실제로 내용이 바뀌면 다릅니다. 바뀐 요청을 예전 키로 보내면409가 오니 새 키를 쓰세요. - 키는 처음 쓴 때부터 24시간 기억됩니다. 그 뒤에는 같은 키라도 새로 과금되는 작업이 만들어지니 자동 재시도는 그보다 훨씬 전에 멈추세요.
- 키는 128자까지이며 계정 안에서만 고유하면 됩니다. 계정의 다른 API 키는 이 키를 다시 쓸 수 없으며, 원래 작업 ID 없이
409를 받습니다.
| 엔드포인트 | 같은 키로 다시 보내면… |
|---|---|
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',
);
