跳转到内容
Maple

核心概念

订阅事件类型、验证 HMAC 签名的推送、对至少一次投递进行去重,并从事件记录中重放。

Webhook 会将受支持的业务事件通知给您的集成。注册一个公开的 HTTPS 端点以及您要处理的事件类型;Maple 会为每次推送签名并记录结果。持久化的业务事件推送在失败时会重试。已签名的测试和手动重放都是一次性操作,不会自行安排自动重试。

在控制台中管理推送

受邀的组织成员无需 API 密钥,即可打开 Developers → app → Webhooks。应用需要 webhooks:read 权限用于诊断,需要 webhooks:write 权限用于变更。出站操作还要求应用处于激活状态。

  • Endpoints: 创建或编辑端点,保存一次性显示的签名密钥,主动发送测试,或禁用/重新启用端点。建议优先禁用,以保留推送历史。已暂停的应用仍可查看,其端点也仍可被禁用。
  • Deliveries: 可按端点、事件类型、结果和时间范围筛选普通推送和已签名的测试推送。您可以查看汇总状态、尝试次数、最近一次响应状态、安全的错误信息和时间戳。这不是逐次尝试的时间线。显示的 URL 和端点状态是当前配置,而非历史目标地址的快照。
  • Events: 查看可重放的业务事件元数据。Review event 会预览事件级别的重试;Confirm retry 会尝试向符合条件的目标重新推送,已成功的组合、已禁用的端点以及不再订阅的端点会被跳过。如果目标发生变化,请在确认前重新查看最新的预览。

历史记录默认显示 24 小时,最多支持 30 天,每页最多 100 行;游标在 15 分钟后失效。此读取窗口并不表示更早的记录已被删除。一次确认的控制台重放最多支持十个符合条件的端点,预览最多 100 个目标;超出的扇出会在发送前被拒绝。

控制台的测试和重放各有独立的服务端配额:每位用户/应用/环境每分钟三次,每个应用/环境每分钟十次。请遵守返回的冷却时间;界面不会自动重新提交。如果结果未知,请先检查您的接收端并刷新历史记录,然后再重试。

测试推送显示“已送达”,仅表示您的端点返回了 2xx,并不表示其签名已被验证。没有可重放业务事件的测试推送会提供 Send another test。待处理的事件记录或手动操作的失败,都不能证明还会有另一次尝试被安排。

控制台只显示元数据,不显示完整的事件负载、接收端响应正文、密钥或内部追踪信息。下文经过身份验证的公共事件 API 与此相互独立。Webhook format and signature verification 指南只包含可复制的合成示例;打开它不会发送请求,也不会用您的密钥生成签名。

测试完整的订单工作流

对于订单集成,请在沙盒控制台中打开 Developers → app → Test orders。选择一个已授权的测试门店,然后确认 Create test order。应用需要 orders:write 权限,必须处于激活状态,并且必须是该门店当前处于测试模式的订单接收方。Maple 必须能够准备一个有库存且没有必选加料选项的菜单项。

这会通过正常工作流创建一笔真实的到店付款自取订单,包括已配置的 webhook 和打印机集成。该操作不会向银行卡扣款。请使用独立的沙盒门店;它与 webhook 端点上的 Send test 不是一回事。创建频率按门店限制(默认每分钟三次),如果无法检查配额存储,则不可用。

具有 orders:read 权限时,结果跟踪器会显示与该应用、环境和门店关联的最近十笔订单的验证、通知投递和合作伙伴决定。新订单会在工作流记录验证或 webhook 事件后出现。如果结果不确定,请先刷新再重新发送;列表为空并不能证明创建失败。此工具不能替代使用您自己的密钥验证 API 请求。

商户的 Location settings → Integrations → Connected apps 仍保留连接和访问控制。开发者测试工具现已移至组织级控制台。

事件信封

每次推送都使用相同的 Stripe 风格信封。data 负载因事件而异:

{
"object": "event",
"id": "evt_...",
"type": "order.notification",
"created": 1765432100,
"data": { "order_id": "ord_...", "location_id": "str_..." }
}

id

string

唯一的事件 id。请基于它去重——推送为至少一次。

type

string

事件类型。根据它来路由事件。

created

number

事件创建时的 Unix 时间戳(秒)。

data

object

事件专属的负载。order.notification 包含完整的订单内容;生命周期事件(order.created、order.paid、order.cancelled)包含摘要字段。各负载详见Webhook 事件,如需权威的当前状态,请获取 GET /v1/orders/{orderId}。

事件类型

只订阅您会处理的事件。实时目录始终位于 GET /v1/webhook_event_types:

类型 触发时机
order.validation_requested Maple 在通知您之前,请您验证订单
order.notification 订单交给您履约——您需要处理的事件
order.created 订单已创建——用于分析的生命周期信号,信息比通知事件更少
order.paid 付款已结算(可能晚于订单交接)
order.cancelled 订单已取消——停止履约
booking.created 预订已创建,或首次从其提供方获取到
booking.confirmed 预订已确认
booking.updated 预订已变更
booking.cancelled 预订已取消
booking.declined 预订被餐厅拒绝
booking.seated 一组客人已入座
booking.completed 预订已完成
booking.no_show 预订被标记为爽约
waitlist_entry.created 一组客人加入候位名单
waitlist_entry.notified 已通知候位的客人桌位已准备好
waitlist_entry.seated 候位的客人已作为现场客人入座
waitlist_entry.removed 客人离开候位名单,或被餐厅移除
waitlist_entry.expired 已被通知的客人未在规定时间内回应
store.provisioned 某个门店已开通给您的应用
store.deprovisioned 某个门店已从您的应用撤销
store.status.changed 已连接门店的状态发生变化
menu.sync.completed 某个门店的菜单同步已完成
menu.sync.failed 某个门店的菜单同步失败

测试端点也会按需推送 webhook.test,让您在真实流量到来之前验证处理程序。

关于每个订单事件包含什么内容以及应订阅哪些,请参阅Webhook 事件。预订事件需要 bookings:read 权限,并会针对已授权的门店推送;请参阅Webhook 事件中的预订事件部分。

订阅

Terminal window
curl -X POST $MAPLE_BASE/webhook_subscriptions \
-H "Authorization: Bearer $MAPLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"notification_url": "https://your-app.example.com/maple/webhooks",
"event_types": ["order.notification", "order.cancelled"]
}'
{
"object": "webhook_subscription.created",
"subscription": { "object": "webhook_subscription", "id": "dws_...", "environment": "test", "status": "enabled", "...": "..." },
"signing_secret": "mwhsec_..."
}

notification_url 必须是公开的 HTTPS 地址——私有和内部地址会被拒绝。可对 /v1/webhook_subscriptions/{id} 使用 GET、PATCH(更新 URL、事件类型,或 enabled/disabled 状态)和 DELETE 来管理订阅。更新订阅不会改变其签名密钥。

多个订阅

您可以注册多个订阅。每个 enabled 且 event_types 包含已触发事件的订阅,都会收到各自独立签名的推送,并使用各自的签名密钥——因此允许多个订阅的 event_types 相互重叠。这样您可以按目的地分流:例如,将订单事件发送到您的履约服务,将 menu.sync.* 事件发送到另一个后台端点。重放遵循相同的路由,并会跳过已收到该事件的订阅。

验证签名

每次推送都带有两个标头:

  • maple-webhook-id——事件 id。
  • maple-webhook-signature——t=<unix_seconds>,v1=<hex_hmac>。

HMAC-SHA256 签名是针对一个签名字符串计算的,该字符串将时间戳、订阅和目标 URL 与正文绑定在一起——因此截获的签名无法被重放到其他订阅或 URL 上:

{timestamp}.{subscription_id}.{notification_url}.{raw_body}

请使用您的签名密钥重新计算,并以恒定时间进行比较。务必使用未经解析的原始请求正文——重新序列化 JSON 会改变字节内容,导致签名失效。

import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyMapleWebhook(params: {
signatureHeader: string; // maple-webhook-signature
subscriptionId: string; // your subscription id (dws_…)
notificationUrl: string; // the exact URL you registered
rawBody: string; // the unparsed request body
signingSecret: string; // mwhsec_…
toleranceSeconds?: number; // default 300
}): boolean {
const parts = Object.fromEntries(params.signatureHeader.split(',').map((p) => p.split('=')));
const timestamp = Number(parts.t);
const provided = parts.v1 ?? '';
// Reject stale deliveries to prevent replay.
const skew = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
if (!Number.isFinite(timestamp) || skew > (params.toleranceSeconds ?? 300)) return false;
const signed = `${timestamp}.${params.subscriptionId}.${params.notificationUrl}.${params.rawBody}`;
const expected = createHmac('sha256', params.signingSecret).update(signed).digest('hex');
return expected.length === provided.length && timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
}

投递保证

  • 至少一次。 同一事件可能多次到达。请基于信封中的 id 去重,并确保您的处理程序是幂等的。
  • 快速响应。 请尽快返回 2xx,再异步处理耗时的工作。任何非 2xx 响应(或超时)都算作投递失败。
  • 不保证顺序。 不要假定事件按发生顺序到达;当顺序很重要时,请以订单资源为准进行核对。

重试与自动禁用

持久化的业务事件推送默认最多尝试 9 次——第一次立即进行,之后退避间隔逐渐增大。前几次尝试落在六分钟的 POS 决定窗口之内,而较长的尾部则用于在真正的故障之后进行对账:

尝试 与上一次的间隔
1 立即
2 10 秒
3 30 秒
4 2 分钟
5 5 分钟
6 15 分钟
7 1 小时
8 3 小时
9 6 小时

非 2xx 响应(或超时——每次尝试的上限为 15 秒)会使该次尝试失败。一个事件在 全部 9 次尝试中都失败,算作一次完全失败的推送。在 连续 5 次完全失败的推送(即五个不同的事件,每个都用尽了所有尝试)之后,该订阅会被自动禁用;只要有一次推送成功,计数器就会归零。待您的端点恢复正常后,通过 PATCH 将 status 改回 enabled 即可重新启用,然后重放您错过的内容。

事件记录

已发布的业务事件会被记录下来,用于对账和重放。合成的已签名测试推送有推送记录,但没有可重放的事件记录;请改为再次主动发送测试。

  • GET /v1/webhook_events——您最近的事件,按时间倒序(最多 50 条)。适用于对账和调试。
  • GET /v1/webhook_events/{eventId}——单个事件,包含其完整的 data 负载。
  • POST /v1/webhook_events/{eventId}/replay——尝试向当前匹配的已启用订阅推送,跳过已记录成功的组合。它会复用事件 ID,且不会强制重新推送已成功的内容。并发尝试或网络/持久化结果不确定时,仍可能产生重复;接收端必须去重。手动尝试失败即为终态,不会安排自动重试。

如果您的服务宕机时间过长而错过了订单,当您希望保留原始事件标识时,请优先使用事件记录重放。若要直接补齐,请以 since 作为您上一次成功的检查点并向前读取:请求 GET /v1/orders?since=<checkpoint>&limit=100,当 has_more 为 true 时,使用 starting_after=<last-order-id> 重复请求。使用 since 时,结果按从旧到新排序,因此可以读完全部积压订单,而不会漏掉时间戳相同的订单。获取权威的订单资源,然后仅对错过 webhook 的有效订单调用 POST /v1/orders/{orderId}/resend。重发会创建一个新的已签名 order.notification,带有新的事件标识,且绝不会改变订单状态。处于终态的订单(REJECTED、CUSTOMER_CANCELLED 或 STORE_CANCELLED)无法重发或恢复;请使用 GET /v1/orders?since=... 和 GET /v1/orders/{orderId} 对账。

测试您的处理程序

Terminal window
curl -X POST $MAPLE_BASE/webhook_subscriptions/{id}/test \
-H "Authorization: Bearer $MAPLE_KEY"

这会推送一个已签名的 webhook.test 事件,并返回是否送达以及您的端点返回的 HTTP 状态:

{ "object": "webhook_test_result", "event_id": "evt_...", "delivered": true, "response_status": 200 }

用它来检查可达性和 2xx 响应,并在您自己的接收端验证签名。该响应不能证明签名处理正确。这只是一次尝试;delivered 为 false 或响应状态为 null,不应被理解为有自动重试正在等待。

检查清单