# 表单提交 webhook：实时推送与验签｜Creght

> 在 Creght 注册表单提交 webhook：请求格式、X-Creght-Signature 的 HMAC-SHA256 验签、重试与幂等，以及用 Func 接收询盘写进数据表的完整示例。

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[查看 llms.txt](/llms.txt)

概览

- [Creght AI 编程指南](/api.md)

AI 可发现性

- [如何优化 llms.txt](/api/optimize-llms-txt.md)

站点配置

- [配置 talizen.config.ts](/api/talizen-config.md)
- [实现基于域名的多语言路由](/api/domain-locale-routing.md)

后端

- [在服务端调用外部 API 并管理缓存](/api/ssr-external-api-cache.md)
- [使用 Func 构建站点后端能力](/api/func-backend.md)
- [JSON 表：定义、读写与查询](/api/func-json-tables.md)
- [上传文件：直传与 Func 内生成](/api/func-assets-upload.md)
- [超时配置与流式响应](/api/func-timeout-streaming.md)
- [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)
- [表单提交 webhook：实时推送与验签](/api/form-webhook.md)

集成

- [使用集成发送邮件与验证码](/api/func-email-integration.md)
- [使用集成接入支付宝支付](/api/func-alipay-integration.md)
- [使用集成接入 Stripe 支付](/api/func-stripe-integration.md)
- [使用集成调用 OpenAI 模型](/api/func-ai-integration.md)
- [使用集成接入文字转语音](/api/func-tts-integration.md)

登录与用户

- [注册时验证邮箱](/api/auth-verified-registration.md)
- [实现找回密码与修改密码](/api/auth-password-reset.md)
- [在 Func 里实现登录](/api/auth-func-login.md)
- [在 Func 里查询用户](/api/func-user-directory.md)

本页目录

- [注册 webhook](#register)
- [请求格式](#request)
- [验证签名](#verify)
- [用 Creght Func 接收](#func)
- [重试与幂等](#retry)
- [排障](#debug)
- [边界](#limits)

后端/表单提交 webhook：实时推送与验签

# 表单提交 webhook：实时推送与验签

站点表单一有提交就 POST 到你的 https 地址：通过 MCP 注册，HMAC-SHA256 签名，失败自动重试，可以用另一个 Creght 项目的 Func 接收并按提交 id 幂等入库。

复制 Markdown 链接

站点表单每收到一条提交，Creght 立刻向你注册的 https 地址发一次 POST，请求用 HMAC-SHA256 签名。接收方可以是任何 https 服务，也可以是另一个 Creght 项目的 Func。

**本文范围**

注册与管理 webhook、请求格式、验签、重试与幂等，以及用 Func 接收。不需要实时的话，也可以用 MCP 的 `form_submissions` 定时增量拉取，两者返回的提交结构完全一样。

## 注册 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()` 读，见 [读取请求](/api/func-backend.md#request)。

把 `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` 幂等处理**。
- 不跟随重定向。地址改了请删掉重建。

## 排障

1. 注册后先调 `form_webhook_test`，看返回的 `status_code`： `0` 表示没拿到响应（连不上、超时、地址被拒）， `401` 通常是验签不过。
2. 真实提交没收到时调 `form_webhook_list`，看 `recent_deliveries` 里的 `status`（ `pending` 表示在等重试）、 `attempts`、 `error` 和 `response`。投递记录保留 30 天。

## 边界

- 只允许 https 地址，不能指向内网、回环或云厂商元数据地址。
- 每个项目最多 10 个 webhook。
- 只覆盖 Creght 站点的表单，旧版可视化编辑器的表单不会触发。
- 暂时只能通过 MCP 管理，编辑器里没有界面，CLI 也没有对应命令。

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

此网站使用 [Creght](/) 创建

![微信客服](https://fsu.creght.com/site/2066727200882692096/1785119134612__image.png?w=3072&fmt=webp)

微信客服

## 链接

- [SaaS 官网生成器](/saas-website-builder.md)
- [价格](/price.md)
- [解决方案](/solution.md)
- [客户案例](/customers.md)
- [AI 模型对比](/ai-models.md)
- [帮助中心](/help.md)
- [联系我们](/contact.md)
- [更新记录 & 博客](/blogs.md)
- [退款说明](/tuikuan.md)

## 资源

- [全部资源](/resources.md)
- [模板](/templates.md)
- [组件库](https://blocks.creght.com/)
- [动效库](/design/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API](/api.md)

## 产品对比

- [对比上线了](/creght-vs-sxl.md)
- [对比凡科建站](/creght-vs-fkw.md)
- [Creght vs 自己写代码](/compare/self-coding.md)
- [Creght vs 外包](/compare/outsourcing.md)
- [Creght vs Framer](/compare/framer.md)
- [Creght vs WordPress](/compare/wordpress.md)

## 协议

- [用户协议](/legal/terms.md)
- [隐私政策](/legal/privacy.md)
- [可接受使用政策](/legal/acceptable-use.md)

## 社交媒体

- [小红书](https://www.xiaohongshu.com/user/profile/5a38606811be10715f4895b6)
- [哔哩哔哩](https://space.bilibili.com/513308095)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)

免费方案评估

> 全站页面清单：[/llms.txt](/llms.txt)
