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创建账号打开 anyhook.net/sign-up,创建一个免费账号。无需绑定信用卡。
- 2创建一个 App一个 App 代表一个 Webhook 来源。新账号会直接打开创建表单:给它起个名字(例如“Stripe Production”),还可以选填你自己的服务器 URL,AnyHook 收到每个事件后会转发到这里。留空的话,这个 App 只记录所有事件,不转发。转发目标可以随时添加,Free 套餐每个 App 最多 3 个。
- 3把发送方指向 AnyHookApp 页面会显示你的接收 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_...)。
| 来源 | 签名请求头 | 算法 |
|---|---|---|
| Stripe | Stripe-Signature | HMAC-SHA256 + 时间戳 |
| GitHub | X-Hub-Signature-256 | HMAC-SHA256 |
| Meta (Instagram / Facebook / Threads) | X-Hub-Signature-256 | HMAC-SHA256 |
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 (Base64) |
| Discord | X-Signature-Ed25519 | Ed25519 |
| Slack | X-Slack-Signature | HMAC-SHA256 + 时间戳 |
| LINE | x-line-signature | HMAC-SHA256 (Base64) |
| Xero | x-xero-signature | HMAC-SHA256 (Base64) |
| Linear | Linear-Signature | HMAC-SHA256 |
| Sentry | Sentry-Hook-Signature | HMAC-SHA256 |
| Vercel | x-vercel-signature | HMAC-SHA1 |
| Paddle | Paddle-Signature | HMAC-SHA256 + 时间戳 |
| SendGrid | X-Twilio-Email-Event-Webhook-Signature | ECDSA P-256 |
| Twilio | X-Twilio-Signature | HMAC-SHA1 |
| Intercom | X-Hub-Signature | HMAC-SHA1 |
| HubSpot | X-HubSpot-Signature-v3 | HMAC-SHA256 (v3) / SHA-256 (v1/v2) |
| Standard Webhooks (Svix, OpenAI, Replicate) | webhook-signature / Svix-Signature | HMAC-SHA256 + 时间戳 |
| Clerk | Svix-Signature | HMAC-SHA256(通过 Svix) |
| Resend | Svix-Signature | HMAC-SHA256(通过 Svix) |
| Lemon Squeezy | X-Signature | HMAC-SHA256 |
| WooCommerce | X-WC-Webhook-Signature | HMAC-SHA256 (Base64) |
| PayPal | PAYPAL-TRANSMISSION-SIG | RSA-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: Meta | App settings → Basic |
| App Secret | 从这里复制到 AnyHook |
| Verify Token(可选) | Webhooks → Verify token:填入同一个字符串 |
| 你的接收 URL | Webhooks → Callback URL:粘贴到这里 |
Meta 会在 36 小时内以逐渐降低的频率重试失败的投递,之后就丢弃。因为 AnyHook 在尝试投递到你的端点之前就已经保存了事件,你这边的部署或宕机不会消耗这个时间窗口。
投递机制
AnyHook 采用基于队列的异步架构,把 Webhook 发送方和你的后端解耦:
- 边缘接收:Cloudflare Workers 接收 POST 请求,验证签名,在事件持久化保存后返回 200,整个过程约 1 秒。
- 入队:事件被发布到持久化消息队列(Upstash QStash)。此时发送方的 HTTP 连接已经关闭。
- 转发:QStash 把事件投递给 AnyHook 的转发器,转发器再把原始请求体 POST 到你的转发目标端点。
- 记录:每一步都会记录下来,包括收到的请求头/请求体、投递状态、你的端点返回的状态码和响应体,以及延迟。
413 Payload Too Large 拒收,从不悄悄丢弃,也从不计费。App 页面会统计它们的数量。429,AnyHook 会退避并重试。投递保证
我们把约定直接写清楚,因为一个对自身保证含糊其辞的转发服务,会把这份含糊推给你的代码:
- 至少一次,而不是精确一次。重试本身就是这个产品,而响应丢失后的重试与一次新的投递无法区分。你偶尔会收到同一个事件两次;没有哪个转发服务能做出相反的承诺,那些这样承诺的,说的只是一切顺利时的情况。你的处理程序必须幂等,我们在
X-AnyHook-Delivery中转发的事件 ID 就是去重用的键。 - 不保证顺序。由于重试和并发投递,较晚的事件可能比仍在重试中的较早事件先到。大多数发送方在我们的上游也不承诺顺序。按请求体里的字段排序(版本号、
updated_at),永远不要按到达时间排序。 - 不要按签名去重。
AnyHook-Signature在每次尝试时都会用新的时间戳重新签名,这是有意为之,这样重试永远不会超出你的时间容差。同一个事件的两次投递带着不同的签名;保持不变的是事件id。 - 已接收即已持久化。边缘节点只在事件持久化之后才返回 200;队列之后还有两个备用存储,都失败了才会改为返回 503。接收 URL 返回 503 意味着什么都没有保存,这时发送方自己的重试就是安全网,这也正是我们返回 503 而不是假装成功的原因。
重试计划
当你的转发目标返回可重试错误或超时,AnyHook 会按指数退避自动重试:
| 尝试 | 延迟 | 套餐 |
|---|---|---|
| 1 | 立即 | 全部 |
| 2 | 5 分钟后 | 全部 |
| 3 | 30 分钟后 | 全部 |
| 4 | 3 小时后 | Pro、Scale、Enterprise |
| 5 | 18 小时后 | Pro、Scale、Enterprise |
| 6-10 | 每次间隔 24 小时 | Scale、Enterprise |
HTTP 202 Accepted,再异步处理。AnyHook 收到 202 就把投递标记为成功。哪些响应会触发重试?
并非所有错误都可以重试。AnyHook 会对你的端点的响应分类,避免把尝试次数或你的计算额度花在再试也解决不了的错误上:
| 状态码 | 处理方式 | 原因 |
|---|---|---|
| 2xx | 成功:投递完成 | 你的端点接受了这个事件。 |
| 3xx | 不可重试:立即标记为失败 | 不会跟随重定向;把转发目标设为最终 URL。 |
| 408 Request Timeout | 按退避策略重试 | 暂时性超时,下一次尝试可能成功。 |
| 429 Too Many Requests | 按退避策略重试 | 你的端点正在限流,给它一些时间。 |
| 4xx(其他:400、401、403、404、422……) | 不可重试:立即标记为失败 | 客户端错误:请求格式有误或未授权。重试同样的请求体没有帮助。 |
| 5xx | 按退避策略重试 | 服务器错误,很可能是暂时的。 |
| 网络错误/超时 | 按退避策略重试 | 无法连接到你的端点。 |
400 Bad Request 或 422 Unprocessable Entity 说明你的处理程序拒绝了这个请求体,重试也不会改变结果。立即把它标记为失败,你能更快收到通知,重试不会消耗计算额度,修好处理程序后还可以重放这个事件。每次尝试都会记录完整的响应体和状态码。不可重试的 4xx 失败和可重试的失败都会计入熔断器,任何类型的失败连续 20 次都会触发熔断器。
失败通知
AnyHook 在特定阈值发送邮件告警,而不是每个事件发一封,这样你的邮箱不会被告警淹没:
- 第 1 次失败:立即告警,附上状态码和转发目标。
- 连续第 5 次失败:升级告警,附上处理建议。
- 连续第 20 次失败:熔断器触发(见下文),发送熔断告警。
上面每个阈值各发一封邮件,之后熔断器的每一级(每次暂停和停用)各发一封,绝不会为每个事件发一封。任何一次投递成功都会重置计数。
熔断器
如果 AnyHook 检测到同一个转发目标连续 20 次投递失败(包括不可重试的 4xx),熔断器就会触发,并开始逐级升级:
| 触发次数 | 暂停时长 | 处理 |
|---|---|---|
| 1 | 10 分钟 | 暂停转发目标,发送邮件 |
| 2 | 30 分钟 | 暂停转发目标,发送邮件 |
| 3 | 2 小时 | 暂停转发目标,发送邮件 |
| 4 | 24 小时 | 暂停转发目标,发送邮件 |
| 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:灵活。
is_active: false),它的接收 URL 返回 202,但不保存任何内容:平台不会因此禁用端点,但这些事件就没了。在你需要的流量到来之前,先取消暂停。控制台重放
日志里的任何事件都可以在控制台中一键重放。重放会把保存的请求体逐字节发送到该事件原来的转发目标;如果你之后改过这个转发目标的 URL,就发送到新的 URL。
- 打开 Dashboard → Events
- 找到该事件,点击 Replay
- 系统会创建一个带
is_replay: true的新事件,并关联到原始事件
API 重放(单个事件)
把原始事件的请求体发布到 QStash,再次投递。新事件通过 replay_of 关联到原始事件。
curl -X POST https://anyhook.net/api/v1/events/{event_id}/replay \
-H "Authorization: Bearer {api_key}"成功时返回 200 和新事件的 ID。如果事件已超出重放窗口,返回 410 Gone。
未送达的事件
返回状态为 failed、circuit_open 或 quota_exceeded 的事件。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| since | ISO 8601 | 24 小时前 | 只返回在这个时间戳之后创建的事件 |
| limit | integer | 100 | 最多返回的事件数(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": "..." },
...
]
}批量重放
重放符合条件的失败事件,从最旧的开始,每次调用不超过上限。已经重放过的事件,或已超出重放窗口的事件,会被跳过。
| 请求体参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| since | ISO 8601 | 24 小时前 | 只重放在这个时间戳之后创建的事件 |
| limit | integer | 50 | 最多重放的事件数(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" },
...
]
}本地开发
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 转发过去,重试、日志和签名请求头都完全一样。
https://in.anyhook.net/you/app)。真实事件会被记录在你的日志里,你也可以在穿透地址和生产环境之间切换转发目标,完全不用动 Stripe。2. 捕获事件并重放到 localhost
让真实的生产环境事件在 AnyHook 里积累,需要时把其中任何一个重放到你的本地环境。不用手动触发测试事件,用真实的请求体测试,次数只受每日重放上限限制。
- 把转发目标切换为你的穿透地址
- 打开 Dashboard → Events,在任意事件上点击 Replay
- 或者调用 API:
POST /api/v1/events/{id}/replay - 原始请求体会逐字节发送到你当前的转发目标
这在调试边界情况时特别有用:在生产日志里找到出问题的事件,在本地重放,反复修改,直到你的处理程序正确运行。
静态加密
所有 Webhook 请求体数据在写入数据库之前,都会用 AES-256-GCM 进行静态加密。这包括收到的请求头、请求体,以及每个转发目标的签名密钥。
加密使用一个专用密钥(AES_SECRET_KEY),与数据库凭据分开存放。数据库泄露而加密密钥没有泄露时,攻击者只能拿到密文:没有明文的 Webhook 数据,没有签名密钥,也没有客户信息。
加密的内容
- Webhook 请求头和请求体(每个事件)
- 平台签名密钥(Stripe Webhook 密钥等)
- 每个转发目标的 AnyHook 签名密钥
边缘签名验证
AnyHook 在 Cloudflare 边缘节点验证 Webhook 签名,在事件进入队列之前就完成验证。App 设置了签名密钥后,签名缺失或无效的请求会被拒收并返回 401,从不占用你的配额;没有密钥时,请求会不经验证就转发。
签名检测根据请求头自动进行。创建 App 并选择来源类型后,AnyHook 会保存签名密钥(静态加密),并对该平台使用正确的算法。
不在列表中的来源或自定义来源
如果你的 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 列里显示的值相同。
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-DeliveryID,attempt从 1 开始计数,测试运行时为 0。 - 失败。抛出异常会让这次投递失败,之后按套餐的尝试次数重试(重试计划)。如果某个事件的运行已经成功,重试不会再运行它;但如果运行在某个调用已经完成之后才失败,重试会再运行一次,所以在 API 允许的地方,用
delivery_id作为这类调用的幂等键。 - Pipedream 步骤。
export default defineComponent({ async run({ steps, $ }) {} })也能运行:steps.trigger.event就是上面的事件,$.export和$.flow.exit都可用,每个data_storeprop 都是这个 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保存,移除那个转发目标之后再开启代码。两种更改都需要最多一分钟才能同步到所有边缘节点。
认证
所有 API 请求都需要在 Authorization 请求头中携带 Bearer 令牌:
Authorization: Bearer ahk_live_xxxxxxxxxxxxxxxx
在 Dashboard → Settings → API Keys 中生成 API 密钥。密钥的作用范围是你的账号,可以是完全访问或只读访问(创建时选择),并且可以随时吊销。通过 MCP 登录(anyhook.net/mcp)连接的应用,可以用它的 OAuth 访问令牌调用同样的接口,权限以你批准的范围为限。
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
创建一个 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
提供一个 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 设置里。
事件
列出事件时的查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| app_slug | string | 按 App 的 slug 过滤 |
| status | string | 按状态过滤(queued、success、retrying、failed、rejected、circuit_open、quota_exceeded) |
| limit | integer | 最多返回的结果数(默认 50,最多 200) |
重放(API)
完整说明和示例见上文的 API 重放。
未送达(API)
完整说明和示例见上文的未送达的事件。
批量重放(API)
完整说明和示例见上文的批量重放。
套餐限制
| Free | Pro($20/月) | Scale($90/月) | Enterprise | |
|---|---|---|---|---|
| 事件数/月 | 10,000 | 50,000 | 250,000 | 灵活 |
| 每日上限 | 1,000 | 无 | 无 | 无 |
| App 数量 | 1 | 10 | 50 | 灵活 |
| 每个 App 的转发目标数 | 3 | 20 | 50 | 灵活 |
| 投递次数 | 3 | 5 | 10 | 灵活 |
| 保留期 | 3 天 | 1 年 | 7 年 | 灵活 |
| 重放窗口 | 3 天 | 30 天 | 90 天 | 最多 365 天 |
| 投递超时 | 60 秒 | 120 秒 | 5 分钟 | 灵活 |
| 请求体上限 | 512 KB | 2 MB | 4 MB | 定制 |
| 超额计费 | 无 | $0.15/1k | $0.17/1k | 按合同 |
| 每日重放上限 | 100 | 500 | 1,000 | 不限 |
| 代码运行次数/月 | 无 | 3,000 | 12,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 拒收,不保存任何内容,以此限制失控的来源。
quota_exceeded 状态出现在你的事件日志里,升级后可以重放。超过之后,它们会像超过每日上限那样被拒收。升级到 Pro 套餐即可使用按量计费的超额。超额计费
月度配额用完后,超额计费让事件继续投递,而不是被暂存。仅 Pro 和 Scale 套餐可用。
工作方式
- 在 Settings 中设置每月支出上限($5 / $10 / $20 / $50,或 Off)。
- 达到套餐的事件配额后,AnyHook 会检查超额计费是否已开启、支出上限是否还没用完。
- 如果允许,事件会正常投递,并标记为超额。
- 下个月初,超额费用会作为一个单独的条目出现在你的 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。 - 密钥只在创建时显示一次。把它保存在安全的地方。
需要帮助?
发邮件到 hello@anyhook.net:每封邮件我们都会回复。在 status.anyhook.net 查看实时服务状态。