后端表单提交 webhook:实时推送与验签
表单提交 webhook:实时推送与验签
站点表单一有提交就 POST 到你的 https 地址:通过 MCP 注册,HMAC-SHA256 签名,失败自动重试,可以用另一个 Creght 项目的 Func 接收并按提交 id 幂等入库。
站点表单每收到一条提交,Creght 立刻向你注册的 https 地址发一次 POST,请求用 HMAC-SHA256 签名。接收方可以是任何 https 服务,也可以是另一个 Creght 项目的 Func。
注册 webhook
webhook 通过 Creght MCP(https://creght.cn/api/mcp)管理,编辑器里暂时没有界面。AI 助手或本地脚本连上 MCP 后调用下面四个工具:
| 工具 | 参数 | 返回 |
|---|---|---|
form_webhook_create | project_id;url(必须 https);form_ids(可选,表单 id 或 key,不填是项目下全部表单,包括以后新建的) | webhook(id、url、form_ids、secret_prefix、created_at)和 secret |
form_webhook_list | project_id | 项目下的 webhook,每个带最近 10 次投递结果 |
form_webhook_delete | project_id、webhook_id | { deleted: true },等待重试的投递一并放弃 |
form_webhook_test | project_id、webhook_id | 同步发一条测试事件,返回这次的状态码、错误和响应开头 |
secret只在创建时返回一次,之后只能看到开头几位(secret_prefix)用来核对。丢了就删掉重建。每个项目最多 10 个 webhook。
请求格式
每次投递是一个 POST,Content-Type: application/json:
{
"event": "form.submitted",
"event_id": "1287",
"webhook_id": "12",
"project_id": "p9k3n5y5hbbm",
"submission": {
"id": "3f9k2x",
"form_id": "8a1b2c",
"form_key": "contact",
"form_name": "联系我们",
"created_at": "2026-09-26T11:56:57.123456Z",
"form_url": "https://example.com/contact?utm_source=google&utm_campaign=q3",
"fields": { "name": "张三", "email": "z@x.com", "message": "想询价 500 件" },
"ua": "Mozilla/5.0 …",
"country": "US"
}
}
| 字段 | 说明 |
|---|---|
event | form.submitted(真实提交)或 form.test(form_webhook_test 发的测试事件) |
event_id | 这次投递的 id,重试时不变 |
submission.id | 提交 id,按它去重 |
submission.form_url | 提交所在页面的完整地址,含 query,来源参数(utm_*)在这里 |
submission.fields | 字段 key → 字符串值。多选用逗号连接,文件是可访问的 URL |
submission.country | 按提交者 IP 解析出的国家代码(ISO 3166-1 两位),解析不了时没有这个字段。IP 本身不发送 |
请求头:
| 请求头 | 说明 |
|---|---|
X-Creght-Event | 同 event |
X-Creght-Event-Id | 同 event_id |
X-Creght-Webhook-Id | 同 webhook_id |
X-Creght-Timestamp | 发送时间,unix 秒 |
X-Creght-Signature | v1=<hex>,见下一节 |
验证签名
hex = HMAC-SHA256(secret, "<X-Creght-Timestamp>.<原始请求体>")
X-Creght-Signature = "v1=" + hex
- 对原始字节验签。先把请求体解析成 JSON 再序列化回去,字节可能变,签名就对不上。
- 时间戳也在签名里,改了就对不上。再拒收超过 5 分钟的请求,就能防止别人重放截获的旧请求。
- 在 Node 等环境里比较签名时用常量时间比较(如
crypto.timingSafeEqual)。
用 Creght Func 接收
接收方可以是 另一个 Creght 项目的 Func,地址是 https://<站点域名>/func/<key>。比如站点项目的询盘推到运营后台项目,由那边的 Func 写进数据表。请求头用 ctx.request.headers.get() 读,原始请求体用 ctx.request.arrayBuffer() 读,见 读取请求。
把 secret 存到接收项目的环境变量(后端 → 环境变量),例如 CREGHT_WEBHOOK_SECRET:
export async function main(input, ctx) {
const ts = ctx.request.headers.get("x-creght-timestamp") || ""
const sig = ctx.request.headers.get("x-creght-signature") || ""
const body = new Uint8Array(await ctx.request.arrayBuffer())
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
ctx.response.status(400)
return { error: "stale request" }
}
const prefix = new TextEncoder().encode(ts + ".")
const msg = new Uint8Array(prefix.length + body.length)
msg.set(prefix)
msg.set(body, prefix.length)
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(process.env.CREGHT_WEBHOOK_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
)
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, msg))
const hex = Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("")
if (sig !== "v1=" + hex) {
ctx.response.status(401)
return { error: "bad signature" }
}
const evt = JSON.parse(new TextDecoder().decode(body))
if (evt.event === "form.test") return { ok: true }
const s = evt.submission
const existing = await ctx.db.query("leads", { where: { submission_id: s.id }, limit: 1 })
if (existing.total === 0) {
await ctx.db.insert("leads", {
submission_id: s.id,
form: s.form_key,
name: s.fields.name,
email: s.fields.email,
message: s.fields.message,
page: s.form_url,
country: s.country,
submitted_at: s.created_at,
})
}
return { ok: true }
}
- Func 地址默认不要求登录,验签就是唯一的门,不要省。
- 每次投递消耗接收项目一次 Func 调用额度。
- 接收项目需要有能访问的站点域名。
重试与幂等
- 接收方返回 2xx 算成功。每次请求超时 10 秒。
- 否则按 30 秒、2 分钟、10 分钟、1 小时、6 小时重试,最多 6 次。返回
410 Gone立即停止重试。 - 4xx 也会重试:接收方配置写错了,改好后后面的重试还能收到。
- 同一条提交可能收到不止一次(比如你处理成功了但响应超时),按
submission.id幂等处理。 - 不跟随重定向。地址改了请删掉重建。
排障
- 注册后先调
form_webhook_test,看返回的status_code:0表示没拿到响应(连不上、超时、地址被拒),401通常是验签不过。 - 真实提交没收到时调
form_webhook_list,看recent_deliveries里的status(pending表示在等重试)、attempts、error和response。投递记录保留 30 天。
边界
- 只允许 https 地址,不能指向内网、回环或云厂商元数据地址。
- 每个项目最多 10 个 webhook。
- 只覆盖 Creght 站点的表单,旧版可视化编辑器的表单不会触发。
- 暂时只能通过 MCP 管理,编辑器里没有界面,CLI 也没有对应命令。
