Webhook
在 Direct API 生成任务完成时接收签名通知,验证通知,并读取生成任务以获取其详情。
webhook 是你的一个 HTTPS 端点,Morphic 会在生成任务达到最终状态时调用它。每次投递都经过签名,会在大约一天内重试,并记录在日志中。每个事件都是一条精简通知:它说明哪个生成任务已经结束以及结果如何,你需要用自己的密钥读取该生成任务,以获取其素材、计费信息和错误消息。
注册端点
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 的调用不会发送 webhook。
在 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> |
事件通常会在生成任务达到最终状态后约一分钟内发送。它不是实时流:如果你在等待某一个生成任务并希望延迟最低,请 对它进行长轮询。
验证签名
v1 是以你的端点的 signing_secret 为密钥、对字符串 {t}.{raw request body} 计算出的十六进制 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 | 该端点未订阅此事件类型 |
webhook 是更便宜、更快速的信号,但并非保证。对于尚未收到通知的生成任务,请保留偶尔的轮询作为兜底。
如果您遇到任何问题或有任何疑问,欢迎随时联系我们。我们很乐意提供帮助。请发送邮件至 support@morphic.com.