웹훅
작업 종료 알림과 서명 검증입니다.무엇을 보내나
생성 작업(이미지, 영상, 오디오)이 끝나면 등록한 주소로 알림을 보냅니다. 폴링 없이 종료를 알 수 있습니다.
| 이벤트 | 시점 |
|---|---|
job.succeeded | 작업이 성공으로 끝남 |
job.failed | 작업이 실패로 끝남 |
본인이 직접 취소한 작업은 알림을 보내지 않습니다. 환불이나 재시도 같은 세부 사유는 모두
job.failed로 합쳐지므로, 자세한 사유는 작업을 조회해 확인합니다.등록하기
설정의 웹훅 화면(
/settings/developer-webhooks)에서 등록합니다. 주소와 받을 이벤트를 하나 이상 고릅니다. 이 화면은 로그인한 사람이 씁니다. 개인 키로 부르는 웹훅 관리 경로는 없습니다.- 주소는
https만 쓸 수 있습니다. - 계정당 10개까지 등록할 수 있습니다.
- 사설망과 내부 주소는 등록할 때와 보낼 때 모두 거부합니다. 리다이렉트는 따라가지 않습니다.
- 비밀값(
whsec_로 시작)은 만든 직후 한 번만 보입니다. 서명 검증에 필요하므로 안전한 곳에 둡니다. 이후에는 끝 4자리 힌트만 보입니다. - 주소를 지우면 아직 가지 않은 알림은 보내는 시점에
dead가 됩니다.
요청 형식
POST로 JSON 본문을 보냅니다. 결과 링크는 담지 않으며, 수신 쪽이 작업 조회 경로로 결과를 읽습니다.알림 본문 — json{"id": "<이벤트 id>","type": "job.succeeded","createdAt": "2026-01-01T00:00:00+09:00","data": {"jobId": "<작업 id>","kind": "image","status": "succeeded","documentId": null}}
kind는 image, video, audio 중 하나입니다. status는 succeeded 또는 failed이고 type과 짝입니다. documentId는 작업이 캔버스 문서에 속할 때만 값이 있고 아니면 null입니다. 이벤트 id는 <kind>:<작업 id>:<status> 모양이라 같은 종료는 항상 같은 id입니다.받은 알림으로 결과를 읽을 때는
kind에 맞춰 GET /v1/images/<jobId>, GET /v1/videos/<jobId>, GET /v1/audio/<jobId>를 부릅니다.헤더
| 헤더 | 뜻 |
|---|---|
Mellowz-Signature | v1=<서명>. 회전 유예 중에는 v1=<서명>,v1=<서명> |
Mellowz-Timestamp | 서명 시각(초) |
Mellowz-Event-Id | 이벤트 id. 본문의 id와 같습니다 |
Mellowz-Event-Type | job.succeeded 또는 job.failed |
본문 형식은
Content-Type: application/json입니다.서명 검증
서명은
<타임스탬프>.<본문 원문>의 HMAC-SHA256(16진수)입니다. 비밀값 회전 직후 24시간 동안은 이전 비밀값의 서명이 하나 더 붙어 v1=<서명>,v1=<서명> 형태가 됩니다. 이 중 하나라도 맞으면 통과시킵니다.검증은 파싱하기 전의 원문 본문으로 합니다. 시각이 5분 넘게 어긋난 요청은 재전송 공격으로 보고 거절합니다.
import crypto from "node:crypto";export function verifyWebhook(secret, headers, rawBody) {const timestamp = headers["mellowz-timestamp"];const signatures = String(headers["mellowz-signature"] ?? "").split(",").map((part) => part.trim().replace(/^v1=/, ""));if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");return signatures.some((sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),);}
받는 쪽 규칙
- 2xx를 돌려주면 성공으로 기록합니다. 응답을 10초 안에 주세요.
- 이벤트 id(
Mellowz-Event-Id)가 같은 알림은 중복 전달될 수 있으니 id로 중복을 걸러 냅니다. - 알림은 종료를 알릴 뿐입니다. 성공 여부와 결과는 작업을 조회해 확인하세요.
전달과 재시도
| 상황 | 동작 |
|---|---|
| 2xx | 전달 완료(sent) |
| 408, 429, 5xx, 연결 실패 | 재시도합니다. 간격은 30초부터 두 배씩 늘어납니다. |
| 그 밖의 4xx, 3xx | 재시도해도 같으므로 바로 dead로 둡니다. 리다이렉트는 따라가지 않습니다. |
| 시도 10회 소진 | dead |
| 응답이 10초를 넘음 | 연결 실패와 같이 재시도합니다. |
| 주소가 사설 주소로 바뀜 | 접속하지 않고 dead |
간격은 30초에 2의 거듭제곱을 곱한 값입니다(30초, 60초, 120초…).
dead가 된 전달은 웹훅 화면의 전달 기록에서 다시 보내기로 같은 알림을 되살릴 수 있습니다. 시도 횟수가 0으로 돌아가 다시 10번까지 시도합니다. dead가 아닌 전달은 다시 보낼 수 없습니다. 전달 기록에는 주소마다 최근 50건이 보이고 상태(pending, sent, dead), 시도 횟수, 마지막 HTTP 상태와 오류가 있습니다.비밀값 회전
화면에서 회전하면 새 비밀값이 한 번 보이고, 이전 비밀값도 24시간 동안 서명에 함께 쓰입니다. 수신 쪽이 새 비밀값으로 갈아탈 시간을 줍니다.