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/webhooksassets: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_codenull,或 failed 生成任务的错误代码

事件从不包含输出、链接、计费信息、模型或错误消息,因此即使某次投递落到了错误的地方或被写入日志,也不会泄露任何内容。收到事件后,请用你的密钥读取该生成任务:GET /v1/direct/generations/{id},多个任务则使用 批量读取。事件发送时,其计费已不会再有变化,因此这次读取得到的就是最终结果。

请求头含义
Morphic-Event-Id事件的 ID,每次重试都相同;用它去重
Morphic-Delivery-Id本次投递尝试;每次重试都不同
Morphic-Event-Type请求体的 type
Morphic-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

事件通常会在生成任务达到最终状态后约一分钟内发送。它不是实时流:如果你在等待某一个生成任务并希望延迟最低,请 对它进行长轮询。

验证签名

v1 是以你的端点的 signing_secret 为密钥、对字符串 {t}.{raw request body} 计算出的十六进制 HMAC-SHA256。

  1. 按 , 拆分请求头,得到 t 和 v1。
  2. 在进行任何 JSON 解析或重新序列化之前,对你收到的原始字节计算 HMAC_SHA256(secret, t + "." + raw_body)。
  3. 以恒定时间将其与 v1 比较。
  4. 如果 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,因为每个生成任务最多只有一个事件。

失败的尝试会在以下时间后重试:

重试1234567
距上一次尝试的等待时间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.

本页内容