웹훅
Direct API 생성 작업이 끝나면 서명된 알림을 받고, 서명을 검증한 다음, 생성 작업을 조회해 세부 정보를 확인합니다.
웹훅은 생성 작업이 최종 상태에 도달했을 때 Morphic이 호출하는 사용자의 HTTPS 엔드포인트입니다. 전달은 서명되고, 약 하루 동안 재시도되며, 로그로 남습니다. 각 이벤트는 간결한 알림입니다. 어떤 생성 작업이 어떻게 끝났는지만 알려 주므로, 에셋, 청구 내역, 오류 메시지는 키로 생성 작업을 조회해 확인합니다.
엔드포인트 등록
POST /v1/direct/webhooks에는 generations:write 범위가 필요합니다.
curl -s https://mcp.morphic.com/v1/direct/webhooks \
-H "Authorization: Bearer $MORPHIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/morphic",
"events": ["generation.succeeded", "generation.failed", "generation.cancelled"],
"description": "Production order pipeline"
}'| 필드 | 규칙 |
|---|---|
url | 공개 호스트의 HTTPS(IP 리터럴, 사설 또는 로컬 주소 불가), 최대 2,048자 |
events | 하나 이상의 이벤트 유형 |
description | 선택 사항, 최대 200자 |
응답은 201입니다.
{
"webhook": {
"id": "01929a10-0c3d-7f44-8a21-0b1c2d3e4f50",
"url": "https://hooks.example.com/morphic",
"events": ["generation.succeeded", "generation.failed", "generation.cancelled"],
"description": "Production order pipeline",
"created_at": "2026-10-25T08:00:00.000Z",
"signing_secret": "whsec_4b1e…"
}
}signing_secret은 이 응답에서만 표시됩니다. API 키처럼 보관하세요.- 비밀값을 교체하려면 새 엔드포인트를 등록하고, 호출이 새 엔드포인트의 ID를 사용하도록 바꾼 다음, 이전 엔드포인트를 삭제하세요.
- 조직은 엔드포인트를 최대 10개까지 보유할 수 있습니다.
엔드포인트 관리
| 요청 | 범위 | 기능 |
|---|---|---|
GET /v1/direct/webhooks | assets:read | 엔드포인트를 { "webhooks": [ … ] } 형태로 나열하며, 비밀값은 절대 포함하지 않음 |
DELETE /v1/direct/webhooks/{webhook_id} | generations:write | 엔드포인트를 삭제함. 해당 엔드포인트로 아직 보류 중인 전달은 중단됨 |
호출에 엔드포인트 연결
도구 호출 본문에 webhook_id를 전달하세요. 그 호출이 시작한 모든 생성 작업은 엔드포인트가 구독한 최종 상태에 도달하면 해당 엔드포인트로 전달됩니다. webhook_id가 없는 호출은 웹훅을 보내지 않습니다.
Morphic 서버에서 동영상을 렌더링하는 도구(video_compose, video_* 변환, burn_subtitles)는 아직 webhook_id를 받지 않습니다. 해당 생성 작업은 폴링하세요.
이벤트 유형
| 이벤트 | 전송 시점 |
|---|---|
generation.succeeded | 생성 작업이 출력과 함께 완료됨 |
generation.failed | 생성 작업이 실패함. 청구 금액은 환불됨 |
generation.cancelled | 생성 작업이 취소됨. 청구 금액은 환불됨 |
- 생성 작업마다 이벤트는 최대 하나만 발생합니다.
queued와in_progress는 전달되지 않습니다. 이 상태가 필요하면 폴링하세요.- 생성 작업은 시작 후 72시간 동안 감시됩니다. 그 이후에 끝나는 생성 작업은 이벤트를 보내지 않으므로, 직접 조회하세요.
전달 내용
POST /morphic HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: Morphic-Webhooks/1.0
Morphic-Event-Id: 0192a7c0-5d4e-7b3a-8c2d-1e0f9a8b7c6d
Morphic-Delivery-Id: 0192a7c0-6e5f-7c4b-9d3e-2f1a0b9c8d7e
Morphic-Event-Type: generation.succeeded
Morphic-Signature: t=1792919741,v1=5f8a0c…e21b
{
"id": "0192a7c0-5d4e-7b3a-8c2d-1e0f9a8b7c6d",
"type": "generation.succeeded",
"created_at": "2026-10-25T09:15:42.410Z",
"api_version": "v1",
"data": {
"generation": {
"object": "generation",
"id": "0192a7b3-a1c4-7e02-8f9d-1a2b3c4d5e6f",
"tool": "generate_video",
"status": "succeeded",
"completed_at": "2026-10-25T09:15:41.902Z",
"error_code": null,
"metadata": { "order_id": "4411" }
}
}
}data.generation에는 다음 필드만 포함됩니다.
| 필드 | 의미 |
|---|---|
object, id, tool, status, completed_at, metadata | 생성 작업 객체와 동일 |
error_code | null, 또는 failed 생성 작업의 코드 |
이벤트에는 출력, 링크, 청구 내역, 모델, 오류 메시지가 절대 포함되지 않으므로, 전달이 잘못된 곳이나 로그에 남더라도 노출되는 정보가 없습니다. 이벤트를 받으면 키로 생성 작업을 조회하세요. 하나는 GET /v1/direct/generations/{id}로, 여러 개는 일괄 조회로 읽습니다. 이벤트가 전송되는 시점에는 청구 내역에 더 이상 반영될 것이 없으므로, 이 조회 결과가 최종입니다.
| 헤더 | 의미 |
|---|---|
Morphic-Event-Id | 이벤트 ID. 모든 재시도에서 동일하므로 이 값으로 중복을 제거하세요 |
Morphic-Delivery-Id | 이번 한 번의 시도. 재시도마다 다름 |
Morphic-Event-Type | 본문의 type |
Morphic-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
이벤트는 보통 생성 작업이 최종 상태에 도달한 후 약 1분 안에 전송됩니다. 실시간 스트림이 아니므로, 기다리고 있는 생성 작업 하나를 가장 빨리 확인하려면 롱 폴링을 사용하세요.
서명 검증
v1은 엔드포인트의 signing_secret을 키로 하여 {t}.{raw request body} 문자열에 대해 계산한 16진수 HMAC-SHA256입니다.
- 헤더를
,로 나누어t와v1을 얻습니다. - JSON 파싱이나 재직렬화 전에, 받은 원본 바이트에 대해
HMAC_SHA256(secret, t + "." + raw_body)를 계산합니다. - 상수 시간 비교로
v1과 비교합니다. - 재전송 공격을 막기 위해,
t가 현재 시각과 5분 넘게 차이 나면 전달을 거부합니다.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/morphic', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('Morphic-Signature') ?? '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', process.env.MORPHIC_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
const valid =
parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(400);
const event = JSON.parse(req.body);
enqueue(event); // process asynchronously, keyed by event.id
res.sendStatus(204);
});검증하지 않는 엔드포인트는 열린 문과 같습니다. URL을 알게 된 사람은 누구든 생성 작업이 끝났다고 알릴 수 있습니다. 모든 서명을 검증하고, 5분보다 오래된 타임스탬프는 거부하세요.
응답과 재시도
- 10초 안에 아무
2xx로 응답한 다음, 작업을 처리하세요. - 그 외의 모든 경우는 실패한 시도입니다:
2xx가 아닌 상태, 타임아웃, 연결 거부, TLS 오류. - 리디렉션은 따라가지 않으며,
3xx는 실패한 시도입니다. - 순서는 보장되지 않으며 이벤트가 두 번 이상 도착할 수 있습니다. 핸들러를
Morphic-Event-Id기준으로, 또는 생성 작업마다 이벤트가 최대 하나이므로data.generation.id기준으로 멱등하게 만드세요.
실패한 시도는 다음 시간 후에 재시도됩니다.
| 재시도 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 이전 시도 후 대기 시간 | 1분 | 5분 | 15분 | 1시간 | 3시간 | 6시간 | 12시간 |
7번째 재시도 후, 즉 첫 시도로부터 약 22시간 후에는 이벤트가 중단됩니다. 생성 작업 자체에는 영향이 없으므로 GET /v1/direct/generations/{id}로 조회하세요.
전달 로그
GET /v1/direct/webhooks/{webhook_id}/events(범위 assets:read)는 엔드포인트의 최근 이벤트 20개를 반환하며, 각 이벤트에는 실제로 전송된 본문과 모든 시도의 결과가 포함됩니다. 생성 작업 하나의 이벤트만 보려면 ?generation_id=…를 추가하세요.
{
"webhook_events": [
{
"id": "0192a7c0-5d4e-…",
"type": "generation.succeeded",
"generation_id": "0192a7b3-…",
"status": "delivered",
"created_at": "2026-10-25T09:15:42.410Z",
"payload": { "…": "the body that was sent" },
"attempts": [
{ "id": "0192a7c0-6e5f-…", "attempted_at": "2026-10-25T09:15:42.900Z", "response_status": 204, "duration_ms": 131, "error": null, "response_body_excerpt": null }
]
}
]
}이벤트 status | 의미 |
|---|---|
pending | 아직 전달되지 않음. 시도가 더 이어질 수 있음 |
delivered | 엔드포인트가 2xx로 응답함 |
abandoned | 마지막 재시도가 실패했거나 엔드포인트가 삭제됨 |
skipped | 엔드포인트가 해당 이벤트 유형을 구독하지 않음 |
웹훅은 더 저렴하고 빠른 신호일 뿐, 보장은 아닙니다. 소식이 없는 생성 작업은 가끔 폴링하여 안전장치로 삼으세요.
문제가 있거나 궁금한 점이 있으면 언제든지 연락해 주세요. 기꺼이 도와드리겠습니다. 다음 주소로 이메일을 보내 주세요: support@morphic.com.