AnyHook 文档

Webhook 转发服务。把发送方指向 AnyHook,就能获得自动重试、完整的事件日志、统一的签名验证和一键重放。不需要任何 SDK。

快速开始

AnyHook 不需要 SDK,也不需要安装任何包:你只需在发送方的控制台里替换 Webhook URL,你的处理程序收到的还是同样的请求。有一点需要提前考虑:重试和重放带的是发送方的原始时间戳,所以对于签名会过期的发送方(Stripe、Slack,以及 /providers 上标有时间容差的其他平台),要验证 AnyHook-Signature 才能接受这些请求。

最快的方式(无需账号):运行 curl -X POST https://anyhook.net/api/v1/quickstart,立即拿到一个可用的接收 URL 和 API 密钥,适合 AI 智能体和快速测试。这个端点 7 天后过期,除非通过返回的 claim_url 认领。把发送方指向返回的 inbound_url,再用 GET /api/v1/events 和 GET /api/v1/events/{id} 读取事件;响应里的 next_steps 列出了每一条命令。

  1. 1
    创建账号
    打开 anyhook.net/sign-up,创建一个免费账号。无需绑定信用卡。
  2. 2
    创建一个 App
    一个 App 代表一个 Webhook 来源。新账号会直接打开创建表单:给它起个名字(例如“Stripe Production”),还可以选填你自己的服务器 URL,AnyHook 收到每个事件后会转发到这里。留空的话,这个 App 只记录所有事件,不转发。转发目标可以随时添加,Free 套餐每个 App 最多 3 个。
  3. 3
    把发送方指向 AnyHook
    App 页面会显示你的接收 URL,以及常见发送方在哪里填写它(例如 Stripe → Developers → Webhooks)。把它粘贴到那里,替换原来的 Webhook URL:
    https://in.anyhook.net/{user-slug}/{app-slug}
    这个页面会实时监听:第一个请求几秒内就会出现在页面上。还没有可以发送请求的来源?在同一页面上点击 Send a test event。

接收 URL 格式

每个 App 都有一个唯一的接收 URL:

https://in.anyhook.net/{user-slug}/{app-slug}
  • user-slug:你的账号标识,可以在控制台的设置中看到。
  • app-slug:每个 App 唯一,根据 App 名称自动生成,可以修改。

接收端点接受任何 POST 请求,也会应答配置时的验证握手:带有 hub.mode=subscribe 的 GET 请求(Meta 的 hub.challenge 校验)会被自动回显,即使 App 还没配置好。其他方法一律返回 405。网页和普通的 HTML 表单也可以向它提交(见代码处理程序)。事件一旦持久化入队,AnyHook 就会返回 HTTP 200,约需 1 秒,不受你的转发目标端点速度的影响,所以发送方从不需要等它。

内容类型。 对于文本请求体,比如 application/json、application/x-www-form-urlencoded、XML 或纯文本,AnyHook 会逐字节保存,并带着原始的 Content-Type 转发。请求体按文本存储,所以二进制请求体或 multipart 表单里的文件无法完整送达。

支持的来源

AnyHook 根据收到的请求头自动识别发送方,并在边缘节点验证请求体签名。要启用验证,创建 App 时选择你的 Webhook 平台,然后粘贴该平台的 Webhook 签名密钥(例如 Stripe 的 whsec_...)。

强烈建议。 没有签名密钥,AnyHook 就无法验证收到的请求是否真的来自你的平台。未经验证的请求仍会被转发;添加密钥后,AnyHook 可以在伪造的请求进入你的处理链路之前,就在边缘节点拒收它们。
来源签名请求头算法
StripeStripe-SignatureHMAC-SHA256 + 时间戳
GitHubX-Hub-Signature-256HMAC-SHA256
Meta (Instagram / Facebook / Threads)X-Hub-Signature-256HMAC-SHA256
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256 (Base64)
DiscordX-Signature-Ed25519Ed25519
SlackX-Slack-SignatureHMAC-SHA256 + 时间戳
LINEx-line-signatureHMAC-SHA256 (Base64)
Xerox-xero-signatureHMAC-SHA256 (Base64)
LinearLinear-SignatureHMAC-SHA256
SentrySentry-Hook-SignatureHMAC-SHA256
Vercelx-vercel-signatureHMAC-SHA1
PaddlePaddle-SignatureHMAC-SHA256 + 时间戳
SendGridX-Twilio-Email-Event-Webhook-SignatureECDSA P-256
TwilioX-Twilio-SignatureHMAC-SHA1
IntercomX-Hub-SignatureHMAC-SHA1
HubSpotX-HubSpot-Signature-v3HMAC-SHA256 (v3) / SHA-256 (v1/v2)
Standard Webhooks (Svix, OpenAI, Replicate)webhook-signature / Svix-SignatureHMAC-SHA256 + 时间戳
ClerkSvix-SignatureHMAC-SHA256(通过 Svix)
ResendSvix-SignatureHMAC-SHA256(通过 Svix)
Lemon SqueezyX-SignatureHMAC-SHA256
WooCommerceX-WC-Webhook-SignatureHMAC-SHA256 (Base64)
PayPalPAYPAL-TRANSMISSION-SIGRSA-SHA256
Generic/自定义无不验证

未通过验证的请求会在边缘节点被拒收,永远不会到达你的端点。App 页面会统计它们的数量,每天的前 5 个会以 rejected 状态保存在事件日志里,请求头和请求体都在,方便你查看原因。

Meta:Instagram、Facebook、Threads

这三者用的是同一套 Graph API Webhook 约定,所以一个 App 就能覆盖。Meta 用 X-Hub-Signature-256 签名,和 GitHub 用的是同一个请求头;AnyHook 通过 X-Github-Event 区分两者:GitHub 总会发送它,Meta 从不发送。

回调 URL 必须先应答一个带有 hub.mode=subscribe 的 GET 请求,把 hub.challenge 原样作为整个响应体返回,Meta 才会保存这个 URL。AnyHook 会替你应答,所以接收 URL 第一次就能保存成功。

在 AnyHook 中在 Meta App Dashboard 中
Source: MetaApp settings → Basic
App Secret从这里复制到 AnyHook
Verify Token(可选)Webhooks → Verify token:填入同一个字符串
你的接收 URLWebhooks → Callback URL:粘贴到这里
Verify Token 的方向正好相反。 这里的其他密钥都是从平台复制出来的。这一个则由你自己设定,再填进 Meta。它不是 App Secret,也不应该和它用同一个字符串:它会出现在配置握手的查询字符串里,而签名密钥不应该出现在 URL 中。留空的话,订阅照样能完成;设置了的话,AnyHook 会先检查它再应答。

Meta 会在 36 小时内以逐渐降低的频率重试失败的投递,之后就丢弃。因为 AnyHook 在尝试投递到你的端点之前就已经保存了事件,你这边的部署或宕机不会消耗这个时间窗口。

投递机制

AnyHook 采用基于队列的异步架构,把 Webhook 发送方和你的后端解耦:

  1. 边缘接收:Cloudflare Workers 接收 POST 请求,验证签名,在事件持久化保存后返回 200,整个过程约 1 秒。
  2. 入队:事件被发布到持久化消息队列(Upstash QStash)。此时发送方的 HTTP 连接已经关闭。
  3. 转发:QStash 把事件投递给 AnyHook 的转发器,转发器再把原始请求体 POST 到你的转发目标端点。
  4. 记录:每一步都会记录下来,包括收到的请求头/请求体、投递状态、你的端点返回的状态码和响应体,以及延迟。
为什么这很重要。 你的发送方(Stripe、GitHub 等)约 1 秒就能收到 200。之后你的后端可以在套餐的投递超时内响应(Free 套餐 60 秒,Pro 套餐 120 秒,Scale 套餐 5 分钟),发送方不会重试,因为它已经收到了 200。等待超过第一秒会消耗你的计算额度。
请求体大小上限。 每个事件的请求体上限:512 KB(Free)、2 MB(Pro)、4 MB(Scale)、定制(Enterprise)。超出上限的请求会在边缘节点以 413 Payload Too Large 拒收,从不悄悄丢弃,也从不计费。App 页面会统计它们的数量。
投递模式。 默认情况下,AnyHook 会在事件到达时立即转发,没有延迟,也没有并发上限。在 Pro 及以上套餐,转发目标可以设置每秒限流和首次投递的延迟。如果你的端点返回 429,AnyHook 会退避并重试。

投递保证

我们把约定直接写清楚,因为一个对自身保证含糊其辞的转发服务,会把这份含糊推给你的代码:

  1. 至少一次,而不是精确一次。重试本身就是这个产品,而响应丢失后的重试与一次新的投递无法区分。你偶尔会收到同一个事件两次;没有哪个转发服务能做出相反的承诺,那些这样承诺的,说的只是一切顺利时的情况。你的处理程序必须幂等,我们在 X-AnyHook-Delivery 中转发的事件 ID 就是去重用的键。
  2. 不保证顺序。由于重试和并发投递,较晚的事件可能比仍在重试中的较早事件先到。大多数发送方在我们的上游也不承诺顺序。按请求体里的字段排序(版本号、updated_at),永远不要按到达时间排序。
  3. 不要按签名去重。AnyHook-Signature 在每次尝试时都会用新的时间戳重新签名,这是有意为之,这样重试永远不会超出你的时间容差。同一个事件的两次投递带着不同的签名;保持不变的是事件 id。
  4. 已接收即已持久化。边缘节点只在事件持久化之后才返回 200;队列之后还有两个备用存储,都失败了才会改为返回 503。接收 URL 返回 503 意味着什么都没有保存,这时发送方自己的重试就是安全网,这也正是我们返回 503 而不是假装成功的原因。
实际该怎么做。 把处理程序写成同一个事件处理两次也无害,并把到达顺序当作噪声。我们按保护力度把这些模式排了序,写在同一个 Webhook 收到两次:这是约定,不是 bug这篇文章里。

重试计划

当你的转发目标返回可重试错误或超时,AnyHook 会按指数退避自动重试:

尝试延迟套餐
1立即全部
25 分钟后全部
330 分钟后全部
43 小时后Pro、Scale、Enterprise
518 小时后Pro、Scale、Enterprise
6-10每次间隔 24 小时Scale、Enterprise
投递超时。 AnyHook 最多等待你的端点响应 60 秒(Free)、120 秒(Pro)或 5 分钟(Scale+)。如果你的处理程序需要更长时间,比如多步骤的 AI 智能体,就立即返回 HTTP 202 Accepted,再异步处理。AnyHook 收到 202 就把投递标记为成功。

哪些响应会触发重试?

并非所有错误都可以重试。AnyHook 会对你的端点的响应分类,避免把尝试次数或你的计算额度花在再试也解决不了的错误上:

状态码处理方式原因
2xx成功:投递完成你的端点接受了这个事件。
3xx不可重试:立即标记为失败不会跟随重定向;把转发目标设为最终 URL。
408 Request Timeout按退避策略重试暂时性超时,下一次尝试可能成功。
429 Too Many Requests按退避策略重试你的端点正在限流,给它一些时间。
4xx(其他:400、401、403、404、422……)不可重试:立即标记为失败客户端错误:请求格式有误或未授权。重试同样的请求体没有帮助。
5xx按退避策略重试服务器错误,很可能是暂时的。
网络错误/超时按退避策略重试无法连接到你的端点。
为什么 4xx 不可重试。 400 Bad Request 或 422 Unprocessable Entity 说明你的处理程序拒绝了这个请求体,重试也不会改变结果。立即把它标记为失败,你能更快收到通知,重试不会消耗计算额度,修好处理程序后还可以重放这个事件。

每次尝试都会记录完整的响应体和状态码。不可重试的 4xx 失败和可重试的失败都会计入熔断器,任何类型的失败连续 20 次都会触发熔断器。

失败通知

AnyHook 在特定阈值发送邮件告警,而不是每个事件发一封,这样你的邮箱不会被告警淹没:

  • 第 1 次失败:立即告警,附上状态码和转发目标。
  • 连续第 5 次失败:升级告警,附上处理建议。
  • 连续第 20 次失败:熔断器触发(见下文),发送熔断告警。

上面每个阈值各发一封邮件,之后熔断器的每一级(每次暂停和停用)各发一封,绝不会为每个事件发一封。任何一次投递成功都会重置计数。

熔断器

如果 AnyHook 检测到同一个转发目标连续 20 次投递失败(包括不可重试的 4xx),熔断器就会触发,并开始逐级升级:

触发次数暂停时长处理
110 分钟暂停转发目标,发送邮件
230 分钟暂停转发目标,发送邮件
32 小时暂停转发目标,发送邮件
424 小时暂停转发目标,发送邮件
5永久停用转发目标,发送邮件

工作方式

  • 照常接收。你的接收 URL 始终可用,来自 Stripe、GitHub 等平台的事件仍会被接收和保存。暂停的只是对你的转发目标的投递。
  • 每次暂停结束后,AnyHook 会发起一次半开探测:用下一个事件测试你的转发目标。如果成功,熔断器闭合,一切重置。
  • 任何一次成功都会重置一切:连续失败次数和触发次数都归零。
  • 暂停期间收到的事件会以 circuit_open 状态保存,转发目标恢复后可以重放。
  • 每升级一级,你都会收到一封邮件通知,包括转发目标 URL、失败次数和暂停时长。
  • 如果转发目标已停用(第 5 次触发),你必须在控制台中手动重新启用它。

这种逐级升级的方式给你的服务器留出恢复时间,同时让你的计算额度免于被失效端点上的重试白白消耗。

事件日志

到达你的接收 URL 的每个事件都会被记录,不管它的投递结果如何。日志从你把平台指向 AnyHook 时开始:在那之前失败的事件只存在于平台那边(Stripe 的情况见如何重放 Webhook)。每条日志包括:

  • 完整的请求头和请求体(与从发送方收到的一致)
  • 投递状态:queued → success / retrying / failed / rejected / circuit_open / quota_exceeded。rejected 表示 AnyHook 拒收了这个事件(签名验证失败,或转发目标 URL 被屏蔽);它从未被尝试投递,也不能重放。
  • 你的转发目标返回的 HTTP 状态码和响应体(每次尝试)
  • 延迟(毫秒)
  • 尝试次数和下次重试时间
  • 该事件是否为重放(is_replay: true)

事件保留多久取决于套餐。Free:3 天,期间都可以重放,之后删除。Pro:1 年:前 30 天可以重放,之后转入只读的加密归档。Scale:7 年:前 90 天可以重放,之后转入只读的加密归档。Enterprise:灵活。

暂停的 App 不保存任何东西。 App 暂停期间(is_active: false),它的接收 URL 返回 202,但不保存任何内容:平台不会因此禁用端点,但这些事件就没了。在你需要的流量到来之前,先取消暂停。

控制台重放

日志里的任何事件都可以在控制台中一键重放。重放会把保存的请求体逐字节发送到该事件原来的转发目标;如果你之后改过这个转发目标的 URL,就发送到新的 URL。

  • 打开 Dashboard → Events
  • 找到该事件,点击 Replay
  • 系统会创建一个带 is_replay: true 的新事件,并关联到原始事件
配额。 重放不计入你的月度事件配额,只有被暂存(quota_exceeded)的事件例外,它的重放会像新事件一样计一次。重放到 App 的代码时,计为一次代码运行。

API 重放(单个事件)

POST/api/v1/events/{id}/replay按 ID 重放单个事件

把原始事件的请求体发布到 QStash,再次投递。新事件通过 replay_of 关联到原始事件。

curl -X POST https://anyhook.net/api/v1/events/{event_id}/replay \
  -H "Authorization: Bearer {api_key}"

成功时返回 200 和新事件的 ID。如果事件已超出重放窗口,返回 410 Gone。

未送达的事件

GET/api/v1/apps/{slug}/events/undelivered列出投递失败的事件

返回状态为 failed、circuit_open 或 quota_exceeded 的事件。

参数类型默认值说明
sinceISO 860124 小时前只返回在这个时间戳之后创建的事件
limitinteger100最多返回的事件数(1–200)
curl "https://anyhook.net/api/v1/apps/stripe-prod/events/undelivered?since=2026-04-11T00:00:00Z&limit=50" \
  -H "Authorization: Bearer {api_key}"
{
  "count": 3,
  "events": [
    { "id": "evt_abc", "status": "failed", "created_at": "..." },
    ...
  ]
}

批量重放

POST/api/v1/apps/{slug}/replay-failed重放某个 App 的失败事件

重放符合条件的失败事件,从最旧的开始,每次调用不超过上限。已经重放过的事件,或已超出重放窗口的事件,会被跳过。

请求体参数类型默认值说明
sinceISO 860124 小时前只重放在这个时间戳之后创建的事件
limitinteger50最多重放的事件数(1–100)
curl -X POST https://anyhook.net/api/v1/apps/stripe-prod/replay-failed \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"since": "2026-04-11T00:00:00Z", "limit": 100}'
{
  "replayed": 3,
  "events": [
    { "original_id": "evt_abc", "new_id": "evt_xyz" },
    ...
  ]
}
每日重放上限。 Free:100/天,Pro:500/天,Scale:1,000/天,Enterprise:不限。

本地开发

AnyHook 开箱即支持本地开发,不需要特殊模式或 CLI。有两种做法:

1. 通过内网穿透实时转发

把 App 的转发目标设成指向你本机的内网穿透地址。任何内网穿透工具都可以:

# ngrok
ngrok http 3000
# → https://abc123.ngrok.io

# cloudflared
cloudflared tunnel --url http://localhost:3000

# localhost.run
ssh -R 80:localhost:3000 nokey@localhost.run

在控制台里把穿透地址填到 App 的转发目标中。AnyHook 会像对待其他端点一样把 Webhook 转发过去,重试、日志和签名请求头都完全一样。

提示。 开发期间,让你的 Webhook 平台继续指向生产环境的接收 URL(https://in.anyhook.net/you/app)。真实事件会被记录在你的日志里,你也可以在穿透地址和生产环境之间切换转发目标,完全不用动 Stripe。

2. 捕获事件并重放到 localhost

让真实的生产环境事件在 AnyHook 里积累,需要时把其中任何一个重放到你的本地环境。不用手动触发测试事件,用真实的请求体测试,次数只受每日重放上限限制。

  • 把转发目标切换为你的穿透地址
  • 打开 Dashboard → Events,在任意事件上点击 Replay
  • 或者调用 API:POST /api/v1/events/{id}/replay
  • 原始请求体会逐字节发送到你当前的转发目标

这在调试边界情况时特别有用:在生产日志里找到出问题的事件,在本地重放,反复修改,直到你的处理程序正确运行。

重放不计入配额。 重放不计入你的月度事件配额。重放会消耗计算额度,并计入每日重放上限(Free 套餐 100,Pro 套餐 500,Scale 套餐 1,000)。

静态加密

所有 Webhook 请求体数据在写入数据库之前,都会用 AES-256-GCM 进行静态加密。这包括收到的请求头、请求体,以及每个转发目标的签名密钥。

加密使用一个专用密钥(AES_SECRET_KEY),与数据库凭据分开存放。数据库泄露而加密密钥没有泄露时,攻击者只能拿到密文:没有明文的 Webhook 数据,没有签名密钥,也没有客户信息。

加密的内容

  • Webhook 请求头和请求体(每个事件)
  • 平台签名密钥(Stripe Webhook 密钥等)
  • 每个转发目标的 AnyHook 签名密钥
数据库里没有任何明文密钥。 每个密钥和每个 Webhook 请求体在存储前都经过 AES-256-GCM 加密。即使拥有数据库的完整访问权限,攻击者看到的也只有密文。

边缘签名验证

AnyHook 在 Cloudflare 边缘节点验证 Webhook 签名,在事件进入队列之前就完成验证。App 设置了签名密钥后,签名缺失或无效的请求会被拒收并返回 401,从不占用你的配额;没有密钥时,请求会不经验证就转发。

签名检测根据请求头自动进行。创建 App 并选择来源类型后,AnyHook 会保存签名密钥(静态加密),并对该平台使用正确的算法。

无效请求。 签名验证失败的请求会以 HTTP 401 拒收,永远不会到达你的转发目标。App 页面会统计它们的数量,每天的前 5 个会保存在事件日志里,请求头和请求体都在。

不在列表中的来源或自定义来源

如果你的 Webhook 来源不在支持列表里,创建 App 时选择 Generic。AnyHook 会:

  • 接收每一个发来的 POST 并放入队列,不验证收到的签名。
  • 把逐字节不变的请求体和平台的原始请求头转发到你的转发目标,每次尝试都附上新的 AnyHook-Signature。在我们的边缘节点之前添加的基础设施请求头(cf-* 之类)会被去掉。
  • 附加一个 AnyHook-Signature 请求头,让你的端点仍然可以验证请求经过了 AnyHook。

Generic 类型的 App 不验证收到的签名,无论设置了什么密钥。AnyHook-Signature 证明的是请求经过了 AnyHook,而不是谁把它发给了 AnyHook;如果你需要后者,就保留你自己对发送方原始请求头的验证(这个请求头会原样转发)。如果发送方用 Standard Webhooks 签名(webhook-id、webhook-timestamp、webhook-signature,OpenAI 和 Replicate 就是这样),改选这个来源:它会在边缘节点验证。

AnyHook-Signature

每个转发的请求都带有 AnyHook-Signature 请求头:用你的转发目标的签名密钥生成的、带时间戳的 HMAC-SHA256 签名。

AnyHook-Signature: t=1712934567,v1=5257a869...

在你的服务器上这样验证:

// Node.js / TypeScript
import crypto from "crypto";

function verifyAnyHook(header: string, body: string, secret: string): boolean {
  const [tPart, vPart] = header.split(",");
  const timestamp = Number(tPart?.replace("t=", ""));
  // Re-signed on every attempt, so a genuine signature is never older than this.
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const signature = Buffer.from(vPart?.replace("v1=", "") ?? "", "hex");

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${body}`)
    .digest();

  // timingSafeEqual throws on a length mismatch, so check that first.
  return signature.length === expected.length && crypto.timingSafeEqual(signature, expected);
}

签名密钥按转发目标分别生成,可以在控制台的 App → Destinations → Signing secret 下找到。你可以随时通过 API 轮换它。

自定义请求头

你可以按转发目标,为发出的请求附加自定义请求头。常见用途:

  • Authorization: Bearer ...:向你的 API 认证
  • x-api-key: ...:API 密钥认证
  • 你的端点需要的任何请求头

自定义请求头的值经过静态加密,从不出现在事件日志里。在 App → Destinations → Custom headers 中配置。

保留原始请求头。 平台的原始请求头(Stripe-Signature 等)会和你的自定义请求头、AnyHook-Signature 一起逐字节转发,所以你现有的 SDK 验证在首次投递时就能通过。重试和重放带的是平台的原始时间戳,Stripe 的 SDK 默认在 5 分钟后拒绝它:改为验证 AnyHook-Signature,它在每次尝试时都会重新签名。

投递控制

每个转发目标有三项可选设置,Pro 及以上套餐都可以用。在 App → Destinations → Delivery controls 中设置,或者在转发目标对象上用 PATCH /api/v1/apps/{slug} 设置。Free 账号会保留付费期间设置的控制项,但不能修改。

{
  "destinations": [{
    "url": "https://api.example.com/hooks/stripe",
    "event_types": ["invoice.*", "checkout.session.completed"],
    "rate_limit": 20,
    "delay_seconds": 5
  }]
}

事件类型过滤

event_types 是一组模式;转发目标只接收类型与其中某个模式匹配的事件。* 匹配任意长度的字符,所以 invoice.* 是所有 invoice 事件,checkout.session.completed 则只匹配这一个。匹配针对整个类型,不区分大小写。有两个转发目标时,你可以按类型扇出:计费事件发往一个服务,其他事件发往另一个。

  • 在边缘节点判定,在签名验证之后、配额计数之前:被过滤掉的事件不保存、不投递,也不计数。
  • 发送方会收到 200 {"filtered":true,"event_type":"…"},Stripe 和 Shopify 会在它们自己的 Webhook 日志里显示这个响应。
  • 类型从哪里读取取决于平台:GitHub、Shopify、WooCommerce 和 Lemon Squeezy 从请求头读取;Stripe、Clerk、Resend、Vercel 和 Svix 风格的发送方从请求体的 type 读取;Paddle 和 PayPal 从 event_type 读取。它和事件日志 Type 列里显示的值相同。
未知类型总会投递。 AnyHook 读不出类型的事件(没有 type 字段的通用请求体、平台的 ping)会投递到每个转发目标,不受过滤规则影响。这个转发服务的承诺是什么都不丢;一个吞掉未知类型的过滤器会打破这个承诺。

投递限流

rate_limit 把发往某个转发目标的投递限制在每秒这么多次(1 到 1,000)。超出上限的事件在队列里等待,有余量时按顺序发出;不会丢弃任何事件,重试也计入同一个上限。当发送方的突发速度超过你的服务器能承受的速度时使用它:Shopify 批量更新商品、Stripe 月初的一波订阅续费、GitHub monorepo 的一次 push 扇出成大量 CI 事件。

批量重放同样遵守这个上限,而这正是它最重要的时候:以每秒 20 个的速度重放 500 个失败事件,就是持续 25 秒的稳定流量,而不是一次又会失败的流量尖峰。

投递延迟

delay_seconds 推迟首次投递尝试(1 秒到 1 天)。常见原因是发送方的 Webhook 和它自己的 API 赛跑:Stripe 的 checkout.session.completed 可能在你的成功页面写完订单之前就到达,推迟五秒,这场竞态就不再是问题。重放从不延迟;首次尝试之后的重试计划不变。

Slack/Discord 告警

在 Pro 及以上套餐,AnyHook 发出的每封告警邮件,也会发到你在 Settings → Notifications 中设置一次的 Incoming Webhook:投递失败(第 1 次和连续第 5 次)、熔断器触发和停用、拒收激增及其恢复,以及 50% / 90% / 100% 的配额预警。保存 URL 时会发一条测试消息,让你确认它能收到。Slack、Discord、Mattermost、Rocket.Chat 和 Teams 的 Incoming Webhook 连接器都可以用;请求体是 {"text": …}(Discord 用 content)。关闭“Email on failure”只会停发邮件;频道仍会收到告警。

代码处理程序

在 Pro 和 Scale 套餐上,App 可以对收到的每个事件运行你自己的代码:在沙箱里运行,不需要你自己的服务器。保存代码后,App 会多出一个运行这段代码的转发目标,所以重试、重放、熔断器和事件日志的工作方式与其他转发目标完全一样,每次运行的控制台输出和返回值就是那次投递的响应。在 App 的 Code 部分编写代码,或使用下面的接口。常见操作的模板在 /code-templates.md,从 Pipedream 迁移有专门的指南。

export default async function (event, { env, store }) {
  // event: { method, headers, body, raw_body, delivery_id, attempt }
  console.log(event.body);
}
  • 事件。body 是解析后的 JSON、表单字段(urlencoded,或 multipart 表单中的文本字段)或原始文本,raw_body 是收到时的原始文本。method 始终是 POST;查询字符串、路径和调用方的 IP 都不会保留。delivery_id 是事件的 X-AnyHook-Delivery ID,attempt 从 1 开始计数,测试运行时为 0。
  • 失败。抛出异常会让这次投递失败,之后按套餐的尝试次数重试(重试计划)。如果某个事件的运行已经成功,重试不会再运行它;但如果运行在某个调用已经完成之后才失败,重试会再运行一次,所以在 API 允许的地方,用 delivery_id 作为这类调用的幂等键。
  • Pipedream 步骤。export default defineComponent({ async run({ steps, $ }) {} }) 也能运行:steps.trigger.event 就是上面的事件,$.export 和 $.flow.exit 都可用,每个 data_store prop 都是这个 App 的存储,app prop 的 $auth.field 读取密钥 APP_FIELD,其他 prop 读取以它命名的密钥(channelId 读取 CHANNEL_ID)。Pipedream 迁移指南列出了哪些不能沿用。
  • npm 包。代码导入的包会在保存时安装,用 Pipedream 的方式锁定版本(import axios from "axios@1.7.9"),最多 20 个包。需要安装新包的保存可能要几分钟;安装失败则不会保存任何内容。
  • 密钥。用 env.NAME 或 process.env.NAME 读取,最多 20 个,名称由字母、数字和下划线组成,不能以数字开头。PATH, HOME, NODE_OPTIONS, NODE_PATH, PWD 以及以 ANYHOOK_ 开头的名称归运行时所有,每个值最多 4,000 个字符。值永远不会离开服务器。
  • 存储。store.get、set({ ttl } 以秒为单位)、has、delete、keys、entries 和 clear 在事件之间保存状态,每个 App 一个存储:最多 1,000 个键,每个键最长 256 个字符,每个值序列化为 JSON 后最多 64,000 字节。导入时接受 {"entries": {key: value}},会覆盖已存在的键,导入的键都没有 TTL。
  • 定时任务。schedule 是五个 cron 字段,默认使用 UTC,除非加上 CRON_TZ=Area/City 前缀。每次触发都会向 App 自己的 URL 发送一个 anyhook.schedule 事件,所以它既计为一次运行,也计为一个事件,并且只在代码开启时触发。这个 App 的来源必须是 generic 或 auto,且不能设置签名密钥:给定时代码单独建一个 App。
  • 邮件。每个 App 还可以在 {user-slug}.{app-slug}@anyhook.net 收邮件,邮件会变成一个 email.received 事件,请求体包含 from、to、subject、message_id、date、size,以及作为 raw 的 MIME 邮件原文。和定时任务一样,它需要一个来源为 generic 或 auto、没有签名密钥的 App;超过套餐请求体上限的邮件会被退回。
  • 网页和表单。网页可以用 fetch() 请求接收 URL:边缘节点会应答 CORS 预检请求,每个响应都带有 Access-Control-Allow-Origin: *。普通的 HTML 表单需要 method="post"。在表单的 action 后加上 ?redirect=https://your-site.com/thanks,事件保存后,访客会被 303 重定向到那里,前提是这个地址和提交表单的页面在同一个站点上;否则访客会看到一个简短的“Received”页面。文本字段会出现在 event.body 中;上传的文件无法保留。
  • 限制。代码最多 64,000 字节,每次运行在 Pro 套餐最长 105 秒,在 Scale 套餐最长 285 秒。每次运行保留控制台输出的最后 16,000 个字符。每月的运行次数见套餐对比表,每次运行都计数,包括重试、测试运行、重放和定时任务触发。没有超额计费:超出当月运行次数后,发往代码的投递会返回 429 并被暂存(circuit_open),不重试,等到下个月或升级套餐后即可重放。
  • 测试与开关。POST .../code/test 用 App 最近保存的事件、你指定的 event_id 或你传入的 body 运行已保存的代码,并返回日志、返回值和耗时。它计为一次运行,不投递任何东西,但代码自己发出的请求确实会发出。只有在代码开启时,事件才会运行它。控制台保存新代码时默认关闭;而 PUT .../code 保存第一个版本时会直接开启,除非你传入 "enabled": false;之后的保存保持当前状态。如果另一个转发目标仍在处理同样的事件(比如迁移期间),用 "enabled": false 保存,移除那个转发目标之后再开启代码。两种更改都需要最多一分钟才能同步到所有边缘节点。
GET/api/v1/apps/{slug}/code代码、密钥名称、定时任务和包,事件是否会运行它,以及本月已用的运行次数
PUT/api/v1/apps/{slug}/code保存代码、密钥(合并;null 删除一个)、定时任务(null 删除)和 enabled
DELETE/api/v1/apps/{slug}/code删除代码、它的密钥和定时任务
POST/api/v1/apps/{slug}/code/test立即运行已保存的代码,使用一个已保存的事件或你传入的请求体
GET/api/v1/apps/{slug}/storeApp 存储中所有未过期的键
POST/api/v1/apps/{slug}/store导入键:{"entries": {key: value}}
DELETE/api/v1/apps/{slug}/store用 ?key= 删除一个键,或删除所有键

认证

所有 API 请求都需要在 Authorization 请求头中携带 Bearer 令牌:

Authorization: Bearer ahk_live_xxxxxxxxxxxxxxxx

在 Dashboard → Settings → API Keys 中生成 API 密钥。密钥的作用范围是你的账号,可以是完全访问或只读访问(创建时选择),并且可以随时吊销。通过 MCP 登录(anyhook.net/mcp)连接的应用,可以用它的 OAuth 访问令牌调用同样的接口,权限以你批准的范围为限。

限流。 API 和 MCP 调用每个账号每个 UTC 日有上限:Free 套餐 300,Pro 套餐 1,000,Scale 套餐 3,000。超过上限后,你会收到 429 Too Many Requests,并带有 Retry-After 响应头,直到 UTC 时间 00:00。

AI 智能体(MCP)

AnyHook 是一个位于 https://anyhook.net/mcp 的 MCP 服务器,智能体可以替你创建 App、读取事件日志、重放失败的事件,在 Pro 和 Scale 套餐上还能部署和测试 App 的代码(anyhook_code_deploy、anyhook_code_test、anyhook_code_get)。Claude Code、claude.ai、Codex和VS Code 只需这个 URL 和浏览器登录就能连接,不用复制密钥:各客户端的步骤。

Cursor、Windsurf 和 Claude Desktop 使用密钥连接。在 Dashboard → Settings → API Keys 下创建一个,页面会显示下面这段配置,你的密钥已经填好:

{
  "mcpServers": {
    "anyhook": {
      "command": "npx",
      "args": [
        "-y",
        "anyhook-mcp"
      ],
      "env": {
        "ANYHOOK_API_KEY": "ahk_live_xxxxxxxxxxxxxxxx"
      }
    }
  }
}

Cursor:Settings → MCP → Add new MCP server,或 ~/.cursor/mcp.json。Windsurf:~/.codeium/windsurf/mcp_config.json。Claude Desktop:Settings → Developer → Edit Config。代码相关工具需要 anyhook-mcp 0.4.0 或更高版本(npx -y 会自动获取),或者使用 https://anyhook.net/mcp 上的服务器。

然后对它说:“为我的 Stripe Webhook 创建一个 AnyHook App,并把它的 URL 给我。”这些内容智能体都可以在 /llms.txt 中读到。

App

GET/api/v1/apps列出所有 App
POST/api/v1/apps创建新 App
GET/api/v1/apps/{slug}获取 App 详情
PATCH/api/v1/apps/{slug}更新 App
DELETE/api/v1/apps/{slug}删除 App

创建一个 App:

curl -X POST https://anyhook.net/api/v1/apps \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe Production",
    "source": "stripe",
    "destination_url": "https://api.example.com/webhooks"
  }'
{
  "id": "app_xxxxx",
  "name": "Stripe Production",
  "slug": "stripe-production",
  "inbound_url": "https://in.anyhook.net/you/stripe-production",
  "destinations": [
    { "url": "https://api.example.com/webhooks", "enabled": true }
  ]
}

连接 Stripe 或 GitHub

POST/api/v1/connect用平台自己的密钥,在平台上注册 App 的 URL

提供一个 Stripe 密钥或 GitHub 令牌,AnyHook 就会在平台上把 App 的接收 URL 添加为 Webhook,保存平台返回的签名密钥,并从第一个事件开始验证签名。平台根据密钥自动识别。密钥只用于这些调用,不会保存。不传 app 时会创建一个新 App;再次运行会替换原来的注册,而不是再添加一个。

最小权限就够了:带 Webhook Endpoints: Write 权限的 Stripe 受限密钥,或者对该仓库有 Webhooks: Read and write 权限的 GitHub 细粒度令牌。

# Stripe: a restricted key, connecting an existing app
curl -X POST https://anyhook.net/api/v1/connect \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{ "key": "rk_live_...", "app": "payments" }'

# GitHub: name the repository (or an organization); no app, so one is created
curl -X POST https://anyhook.net/api/v1/connect \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{ "key": "github_pat_...", "target": "acme/api" }'

Webhook 一建好,GitHub 就会发送一个已签名的 ping,所以第一个事件几秒内就会出现。其他平台:用 POST /api/v1/apps 创建 App,再把它的接收 URL 粘贴到平台的 Webhook 设置里。

事件

GET/api/v1/events列出事件摘要,最新的在前(不含请求体)
GET/api/v1/events/{id}获取事件详情,包括完整的请求体

列出事件时的查询参数:

参数类型说明
app_slugstring按 App 的 slug 过滤
statusstring按状态过滤(queued、success、retrying、failed、rejected、circuit_open、quota_exceeded)
limitinteger最多返回的结果数(默认 50,最多 200)

重放(API)

POST/api/v1/events/{id}/replay重放单个事件

完整说明和示例见上文的 API 重放。

未送达(API)

GET/api/v1/apps/{slug}/events/undelivered列出失败/未送达的事件

完整说明和示例见上文的未送达的事件。

批量重放(API)

POST/api/v1/apps/{slug}/replay-failed重放某个 App 的所有失败事件

完整说明和示例见上文的批量重放。

套餐限制

FreePro($20/月)Scale($90/月)Enterprise
事件数/月10,00050,000250,000灵活
每日上限1,000无无无
App 数量11050灵活
每个 App 的转发目标数32050灵活
投递次数3510灵活
保留期3 天1 年7 年灵活
重放窗口3 天30 天90 天最多 365 天
投递超时60 秒120 秒5 分钟灵活
请求体上限512 KB2 MB4 MB定制
超额计费无$0.15/1k$0.17/1k按合同
每日重放上限1005001,000不限
代码运行次数/月无3,00012,000灵活
代码运行时长无105 秒285 秒灵活
计算额度每月 $0.15每月 $3.00每月 $11.00灵活
API 和 MCP 调用每天 300 次每天 1,000 次每天 3,000 次灵活

查看完整的价格详情 →

用量与配额

AnyHook 按自然月统计用量(例如“2026-04”)。计数在每个自然月开始时重置。

  • 每次投递计一次:一个事件发到两个转发目标计为两次;大事件按请求体和请求头的大小,每开始一个 64 KB 计一次。没有转发目标的 App 上的事件计一次。被过滤掉的事件和签名验证失败的事件不计数。Free 套餐的每日上限更简单:边缘节点每接收一个请求计一次,不管它发往多少个转发目标。
  • 只计首次尝试:重试不计入你的配额。
  • 重放不计数:带 is_replay: true 的事件不计费;只有超出配额后被暂存的事件,重放时会像新事件一样计一次。重放消耗的是计算额度和套餐的每日重放上限。
  • 计算额度:AnyHook 在每个事件的首次尝试之外为你的账号花费的成本,按成本价计算:重试、重放、代码运行、包安装、定时任务触发、故障演练,以及等待转发目标超过第一秒的时间(重试一个 1 KB 的事件约消耗 $0.00002,一次几秒钟的代码运行约 $0.0008)。额度用完后,这些操作会停止,直到下个月 1 日(UTC):失败的投递不再重试,重放和代码运行会被拒绝,定时任务暂停,转发目标只有一秒钟来响应。事件仍会到达,并获得首次尝试。
  • API 和 MCP 调用:用 API 密钥或 MCP 客户端发起的每次调用计一次,超过 32 KB 的响应按每开始一个 32 KB 计一次;控制台不计数。超过当天上限后,API 会返回 429 和 Retry-After,直到 UTC 时间 00:00。GET /api/v1/usage(或通过 MCP 调用 anyhook_usage)会显示每项限制以及哪些操作用掉了它。
  • 暂存,而不是丢弃:超出套餐包含的事件数后(如果设置了超额上限,则是超出上限后),每个事件都会以 quota_exceeded 状态记录,不投递。升级或设置上限后,在控制台里一键重放它们。在上限所能购买的量之外,暂存的事件最多为配额的 20%;超过之后,边缘节点会返回 429 拒收,不保存任何内容,以此限制失控的来源。
Free 套餐限制。 超过每日上限(1,000)的事件会在边缘节点被拒收。超过月度上限(10,000)的事件会被暂存,最多再暂存月度上限的 20%:它们以 quota_exceeded 状态出现在你的事件日志里,升级后可以重放。超过之后,它们会像超过每日上限那样被拒收。升级到 Pro 套餐即可使用按量计费的超额。
付费套餐的超额。 Pro 套餐的超额按 $0.15/1k 个事件计费,Scale 套餐按 $0.17/1k,向上取整到美分。我们从不悄悄丢弃付费流量。

超额计费

月度配额用完后,超额计费让事件继续投递,而不是被暂存。仅 Pro 和 Scale 套餐可用。

工作方式

  1. 在 Settings 中设置每月支出上限($5 / $10 / $20 / $50,或 Off)。
  2. 达到套餐的事件配额后,AnyHook 会检查超额计费是否已开启、支出上限是否还没用完。
  3. 如果允许,事件会正常投递,并标记为超额。
  4. 下个月初,超额费用会作为一个单独的条目出现在你的 Stripe 账单上:Pro 套餐每 1,000 个事件 $0.15,Scale 套餐每 1,000 个事件 $0.17(按事件计费,向上取整到美分),总额不超过你的支出上限。

按 App 开关

每个 App 都有一个超额计费设置:Inherit default、Enabled 或 Disabled。默认情况下,App 沿用你的账号级设置。在 App Settings → Overage Billing 中按 App 单独覆盖。

账号默认值

在 Settings → Overage Billing 中,只要支出上限高于 Off,所有设为 Inherit default 的 App 都会开启超额计费,已有的 App 也包括在内;设为 Disabled 的 App 保持关闭。

支出上限是硬性限制。 超额费用一旦达到上限,后续事件就不再投递:每个事件都以 quota_exceeded 状态保存,等你提高上限或进入下个月后即可重放。超出上限的部分不会计费。超过上限后,最多还会暂存相当于配额 20% 的事件;其余的由边缘节点返回 429 拒收,不保存任何内容。

API 密钥

API 密钥用于认证你对 AnyHook REST API 的请求。在 Dashboard → Settings → API Keys 中管理。

  • 生产环境的密钥以 ahk_live_ 为前缀。
  • 你可以创建多个密钥并给它们加标签(例如“CI/CD”“Agent”)。
  • 密钥可以立即吊销,被吊销的密钥下次使用时会返回 401。
  • 密钥只在创建时显示一次。把它保存在安全的地方。
GET/api/v1/api-keys列出所有 API 密钥(密钥值已遮盖)
POST/api/v1/api-keys创建新的 API 密钥
PATCH/api/v1/api-keys/{id}/revoke吊销一个 API 密钥

需要帮助?

发邮件到 hello@anyhook.net:每封邮件我们都会回复。在 status.anyhook.net 查看实时服务状态。