Webhook 回调

通过开放 API 提交的任务结束时,秒做向你的服务器发送一次带签名的 HTTP 回调。签名遵循 Standard Webhooks 规范,常见语言都能用标准库验签。

配置回调地址

  • 在开放平台的「Webhook」页设置账号默认回调地址,并查看签名密钥(whsec_ 开头)。
  • 提交任务时也可以用 webhook_url 为单个任务指定地址,优先级高于默认地址;两者都没有时不回调。
  • 回调地址必须是 HTTPS,不能是内网地址。
  • 只有通过开放 API(POST /v1/tasks)提交的任务会回调;网页创作和开放平台在线测试的任务不回调。
  • 在开放平台点「发送测试」可以向默认地址发一条 webhook.test 事件,用来验证地址和验签代码。

事件类型

事件触发时机
task.succeeded任务成功,结果文件已可下载
task.failed任务失败,冻结点数已退回
task.canceled任务被取消,冻结点数已退回
webhook.test在开放平台手动发送的测试事件,data 不是任务对象

每个任务只回调一次(进入终态时)。

回调请求

秒做以 POST 发送 JSON,请求体包含 type(事件类型)、timestamp(事件时间)和 data(与查询任务返回相同的任务对象)。

回调请求示例
POST /miaozuo/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Miaozuo-Webhook/1.0
webhook-id: msg_2Lp8sV3nQ6wE1rTyK9uX4bZm
webhook-timestamp: 1791537812
webhook-signature: v1,I5I4xkt2V/ecur0xuqXD8afNtIl6xjzXpfyApwg41fA=

{"type":"task.succeeded","timestamp":"2026-10-09T17:03:32+08:00","data":{ …任务对象… }}
请求头说明
webhook-id本条消息的唯一编号(msg_ 开头)。自动重试和手动重发时保持不变,可用于去重
webhook-timestamp本次发送时间,Unix 时间戳(秒)
webhook-signature签名列表,形如 v1,<Base64 签名>;轮换密钥期间有两个,用空格分隔
User-AgentMiaozuo-Webhook/1.0
测试事件的请求体
{
  "type": "webhook.test",
  "timestamp": "2026-10-09T17:30:00+08:00",
  "data": {
    "message": "这是一条来自秒做的测试回调,用于验证地址与签名",
    "account_webhook": true
  }
}

验证签名

  1. 取 whsec_ 之后的部分做 Base64 解码,得到签名密钥字节。
  2. 拼接待签内容:{webhook-id}.{webhook-timestamp}.{原始请求体}。
  3. 用 HMAC-SHA256 计算签名并做 Base64 编码。
  4. 与 webhook-signature 中每个 v1, 后的值做恒定时间比较,任一相同即通过。
  5. 检查 webhook-timestamp 与当前时间相差不超过 5 分钟,防止重放。
必须使用原始请求体

验签要用收到的原始字节。先解析 JSON 再重新序列化,字段顺序、空白或转义可能变化,导致验签失败。

import base64
import hashlib
import hmac
import os
import time

# 开放平台 → Webhook 中查看,形如 whsec_xxx
WEBHOOK_SECRET = os.environ["MIAOZUO_WEBHOOK_SECRET"]


def verify_webhook(headers: dict, body: bytes, tolerance: int = 300) -> bool:
    """headers 的键请统一转成小写;body 必须是原始请求体字节(如 Flask 的 request.get_data())。"""
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not (msg_id and timestamp and signatures and timestamp.isdigit()):
        return False
    if abs(time.time() - int(timestamp)) > tolerance:
        return False  # 超出 5 分钟,可能是重放

    secret = base64.b64decode(WEBHOOK_SECRET.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(secret, signed, hashlib.sha256).digest()).decode()

    # 轮换密钥期间可能同时带多个签名,用空格分隔,任一匹配即可
    for item in signatures.split(" "):
        version, _, signature = item.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return True
    return False

可以用下面这组数据核对你的实现(请求体为下方「成功事件的完整请求体」,不含末尾换行):

验签核对数据
签名密钥:whsec_bWlhb3p1by1kZW1vLXdlYmhvb2sta2V5
webhook-id:msg_2Lp8sV3nQ6wE1rTyK9uX4bZm
webhook-timestamp:1791537812
请求体:下方「成功事件的完整请求体」(不含末尾换行)
webhook-signature:v1,I5I4xkt2V/ecur0xuqXD8afNtIl6xjzXpfyApwg41fA=
成功事件的完整请求体
{"type":"task.succeeded","timestamp":"2026-10-09T17:03:32+08:00","data":{"id":"T261009170328CGM0GT","object":"task","model":"qwen-image-3.0","status":"succeeded","created_at":"2026-10-09T17:03:28+08:00","started_at":"2026-10-09T17:03:29+08:00","finished_at":"2026-10-09T17:03:32+08:00","input":{"prompt":"一只戴宇航员头盔的橘猫,蓝色星空背景","ratio":"1:1","resolution":"1k","count":1,"smart_prompt":true},"points":{"frozen":"33.000","charged":"33.000","refunded":"0.000"},"output":{"files":[{"url":"https://api.miaozuo.cn/ai/202610/104a3f61ba496027e7aebd892ec2fc86.png","type":"image","mime_type":"image/png","size":66589,"width":1024,"height":1024,"duration":null,"cover_url":null,"expires_at":"2026-11-08T17:03:32+08:00"}]},"error":null,"metadata":{"order_id":"1001"}}}

轮换签名密钥

在开放平台轮换密钥后,24 小时内新旧两个密钥同时签名(webhook-signature 里有两个值),这期间把服务器上的密钥换成新的即可,不会漏验。

响应与重试

  • 收到回调后请在 10 秒内返回任意 2xx 状态码,耗时处理请放到后台异步执行。
  • 返回非 2xx(包括 3xx,秒做不跟随重定向)、超时或连接失败都视为投递失败,按以下间隔重试,最多 5 次(连首次共 6 次):
  1. 第 1 次重试上次失败 1 分钟后
  2. 第 2 次重试上次失败 5 分钟后
  3. 第 3 次重试上次失败 30 分钟后
  4. 第 4 次重试上次失败 2 小时后
  5. 第 5 次重试上次失败 6 小时后
  • 每次投递都有记录,可在开放平台查看状态码与响应摘要,并手动重发(同一个 webhook-id,立即发送并返回结果,不再自动重试)。
  • 同一事件可能送达多次,请按 webhook-id 去重,保证处理幂等。
  • 回调只作为「提醒」。涉及发货、入账等关键操作,建议收到回调后再查询任务确认一次。