> ## Documentation Index
> Fetch the complete documentation index at: https://docs.maple.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# 接收并处理订单

> 构建订单处理流程：连接门店、订阅 webhook、验证每次推送，并通过 accept、deny、ready 和 complete 调用推进每一笔订单。

这是 Maple 集成的核心。当顾客在您已连接的门店下单时，Maple 会把订单推送到您的 webhook；由您决定是否履行订单，并在订单推进时回报进度。**整个流程不涉及支付**，支付由 Maple 负责。

一个可用的订单集成包含两部分工作：

| # | 您要构建的内容 | 预计工作量 |
| - | - | - |
| 1 | 接收订单：一个带签名验证的 webhook 端点 | 约 1–2 天 |
| 2 | 处理订单：向 Maple 回调 accept / deny / status | 约 1–2 天 |

如果还没看过，请先浏览 [Maple 如何运作](/zh/developer-api/how-maple-works) 了解对象模型，并运行 [快速入门](/zh/developer-api/quickstart) 获取密钥并连接一个测试门店。

## 步骤 1：连接门店

只有当您的应用成为某个门店的已连接接收方后，订单才会路由给您：

```bash theme={null}
curl -X POST $MAPLE_BASE/locations/{locationId}/connection \
  -H "Authorization: Bearer $MAPLE_KEY"
```

每个环境下的连接是独占的：如果其他应用已占用该门店，您会收到 `409`。对同一路径使用 `GET` 和 `DELETE` 可以查看和移除您的连接。

## 步骤 2：订阅 webhook

注册一个 HTTPS 端点以及您需要的事件类型。完整目录见 `GET /v1/webhook_event_types`；订单流程通常需要以下事件：

```bash theme={null}
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.validation_requested", "order.cancelled"]
  }'
```

响应中包含一个**一次性签名密钥**（`mwhsec_…`）。请妥善保存，它不会再次显示。URL 必须是公开的 HTTPS 地址，私有和内部地址会被拒绝。

<Note>
  以上是订单相关事件。目录中还包括 `order.created`、`order.paid`、`store.provisioned`、
  `store.deprovisioned`、`store.status.changed` 以及菜单同步事件。只订阅您会处理的事件。完整列表和负载请见
  [Webhooks](/zh/developer-api/concepts/webhooks)。
</Note>

## 步骤 3：验证每一次推送

每次推送都是 Stripe 风格的信封：

```json theme={null}
{
  "object": "event",
  "id": "evt_...",
  "type": "order.notification",
  "created": 1765432100,
  "data": { "order_id": "ord_...", "location_id": "str_...", "...": "the order content — see Step 4" }
}
```

每次推送带有两个请求头：`maple-webhook-id`（事件 ID）和 `maple-webhook-signature`，格式为 `t=<unix_seconds>,v1=<hex_hmac>`。被签名的字符串是 `{timestamp}.{subscription_id}.{notification_url}.{raw_body}`。请对原始、未解析的请求体进行验证：

```ts theme={null}
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));
}
```

上面的检查会拒绝过期的时间戳（超过约 5 分钟），以防重放。除此之外，请**按信封中的 `id` 去重**，因为推送保证至少送达一次。完整的可靠性约定，包括重试计划和事件记录，见[Webhooks](/zh/developer-api/concepts/webhooks)。

### 停机后补齐订单

如果您的接收端曾不可用（本地软件关闭或网络中断），请按以下步骤对账：

1. 使用您上一次成功的检查点调用 `GET /v1/orders?since=<timestamp>&limit=100`（不含该时间点，ISO 8601 UTC）。使用 `since` 时，结果按从旧到新排序，`has_more` 表示还有下一页。
2. 当 `has_more` 为 true 时，把 `starting_after` 设为上一页最后一个订单 ID 并重复请求。直到积压处理完毕，再推进您的检查点。
3. 当您需要 Maple 为某个错过 webhook 的有效订单重新发布一条 `order.notification` 时，调用 `POST /v1/orders/{orderId}/resend`。它需要 `webhooks:write` 权限范围，会创建一次新的、已签名且带重试的推送，并且不会改变订单状态。终态订单（`REJECTED`、`CUSTOMER_CANCELLED` 或 `STORE_CANCELLED`）无法重发或恢复，会返回 `400`；请通过 `GET /v1/orders?since=...` 和 `GET /v1/orders/{orderId}` 对账。
4. 如果需要保留原始事件身份，请使用[事件记录](/zh/developer-api/concepts/webhooks)，而不是 `resend`。

对于短时间的中断，等待也可以：[重试计划](/zh/developer-api/concepts/webhooks)会在大约 10 小时内重新投递失败的事件，之后才算彻底失败。

<Tip>
  在有真实流量之前，`POST /v1/webhook_subscriptions/{id}/test` 会发送一条已签名的 `webhook.test` 事件，
  让您验证处理程序能否正确验签并响应。
</Tip>

## 步骤 4：读取订单

`order.notification` 负载中的 `data` 是**订单内容**，包括 ID、顾客、商品明细和合计，因此您可以直接根据 webhook 开始备餐，无需再次调用。它遵循 `GET /v1/orders/{orderId}` 的结构，但不含实时的 `status` 和 `payment`。您可以随时获取订单资源，以得到权威的当前状态，包括 `status` 和 `payment`：

```json theme={null}
{
  "object": "order",
  "id": "ord_...",
  "livemode": false,
  "created": 1765432100,
  "status": "PENDING",
  "fulfillment_type": "delivery",
  "scheduled_for": null,
  "location_id": "str_...",
  "delivery_address": {
    "street": "123 Main Street",
    "unit": "Apt 4B",
    "city": "New York",
    "state": "NY",
    "zip": "10001",
    "notes": "Leave at the side door"
  },
  "customer": { "name": "Alex", "phone": "+14155551234", "phone_last_four": "1234" },
  "line_items": [
    {
      "name": "Latte",
      "menu_entity_id": "itm-latte",
      "quantity": 1,
      "base_price": 450,
      "tax": 36,
      "modifiers": [{ "name": "Oat milk", "menu_entity_id": "mod-oat", "quantity": 1, "price": 75 }]
    }
  ],
  "totals": { "currency": "USD", "subtotal": 525, "tax": 42, "surcharge": 0, "tip": 0, "delivery_fee": 599, "delivery_tip": 0, "total": 1166 },
  "external_id": null,
  "payment": { "provider": "stripe", "status": "unpaid", "payment_link_url": "https://..." }
}
```

请务必牢记以下几点：

* **金额是以美分为单位的整数（USD）。** `450` 即 \$4.50。切勿将其解析为浮点数。`totals` 和商品明细中的所有金额都遵循这一约定。
* **合计已预先计算。** `totals.total` 是权威值，您无需重新计价。
* **商品明细通过 `menu_entity_id` 引用您的菜单**，其值是您为该菜品发布的 `externalId`。先发布菜单，这些值就能与您自己的商品目录对应。（菜单负载使用 camelCase；订单和 webhook 负载使用 snake\_case，见[约定](/zh/developer-api/api-reference)。）
* **加料选项（modifier）是直接选择的第一级选项。** 更深层嵌套的选项在 v1 中不会展开到订单资源里。
* **顾客数据刻意保持精简。** 您会得到顾客的姓名和电话号码（`phone` 为 E.164 格式，另有 `phone_last_four` 用于显示）。外送订单还包含 `delivery_address`，内有街道、单元、城市、州、邮编和配送说明；自取订单或 Maple 没有已存地址时，其值为 `null`。只要该块有内容，六个地址键就都会存在。
* **出单前先检查 `scheduled_for`。** ASAP 订单该值为 `null`。有值时（ISO 8601，UTC），表示顾客选定了取餐/送达时间；您会立即收到订单，因此应按 `scheduled_for` 安排备餐，而不是在收到订单时就开始。

## 步骤 5：做出决定并回报进度

随着订单推进，通过决定端点作出响应。除特别说明外，都不需要请求体：

| 调用 | 何时使用 |
| - | - |
| `POST /v1/orders/{orderId}/accept` | 您可以履行订单 |
| `POST /v1/orders/{orderId}/deny` | 您无法履行，可选 `{ "reason": "…" }` |
| `POST /v1/orders/{orderId}/ready` | 已备好，可取餐或交给骑手 |
| `POST /v1/orders/{orderId}/complete` | 已履行完成 |
| `POST /v1/orders/{orderId}/cancel` | 接单后您必须取消，可选 `{ "reason": "…" }` |
| `POST /v1/orders/{orderId}/status` | 细粒度状态转换（见下文） |

```bash theme={null}
curl -X POST $MAPLE_BASE/orders/{orderId}/accept \
  -H "Authorization: Bearer $MAPLE_KEY"
```

```json Response theme={null}
{ "object": "order_decision", "order_id": "ord_...", "decision": "accept", "status": "received" }
```

如果您更喜欢使用单个端点，`POST /v1/orders/{orderId}/status` 接受明确的状态转换：

```json theme={null}
{ "status": "ACCEPTED" }
```

有效值：`ACCEPTED`、`READY`、`IN_DELIVERY`、`FULFILLED`、`REJECTED`、`STORE_CANCELLED`。各状态的含义及合法的状态转换，请参阅[订单生命周期](/zh/developer-api/concepts/order-lifecycle)。

<Info>
  **决定调用可安全重放。** 重复调用会返回 `{ "status": "received" }`，不会产生重复的副作用，因此遇到网络波动时随时可以放心重试。见[幂等性与重放安全](/zh/developer-api/concepts/idempotency)。
</Info>

## 可选：预先验证订单

预先验证是**可选的，通过订阅 `order.validation_requested` 来开启**：

* **如果您不订阅**，就没有验证步骤。Maple 直接发送 `order.notification`，您的 accept/deny 是唯一的关卡。
* **如果您订阅**，Maple 会在发送通知*之前*，请您确认每笔订单是否可履行（菜品供应、价格可行性、能否写入 POS），并等待您的答复。因此选择启用就是一项承诺：您未及时验证的订单会被拒绝（见下文）。

机制就是这些：订阅即开关。

订阅后，请对每个 `order.validation_requested` 回复结果：

```bash theme={null}
curl -X POST $MAPLE_BASE/orders/{orderId}/validation_result \
  -H "Authorization: Bearer $MAPLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "valid", "partner_reference": "pos_quote_123" }'
```

若要阻止该订单：

```json theme={null}
{
  "status": "invalid",
  "reason": { "code": "ITEM_AVAILABILITY", "explanation": "86'd", "item_external_ids": ["itm-latte"] }
}
```

`valid` 结果会让订单继续进入通知阶段；`invalid` 会在向顾客收款之前拦下订单。您有约 **5 分钟**的时间答复，请求随后过期（资源中带有 `expires_at`，约 300 秒后）。如果过期，Maple 会**再请求一次**验证；若第二次请求仍无答复，订单会被拒绝且不发送通知，因此请在您的处理程序能够可靠地按时答复之后再订阅。这与您在接单时进行的可用性检查相同，只是提前了，这样顾客就不会为您做不出的东西付款。

## “完成”是什么样子

| 您构建的内容 | 您得到的结果 |
| - | - |
| 一个负责验证、去重和路由的 webhook 处理程序 | 实时推送的订单，合计已预先计算 |
| Accept / deny / ready / complete / status 调用 | 顾客可看到订单的实时进度 |
| *（可选）* 验证响应 | 问题订单在向顾客收款之前就被拦下 |

一个 webhook 加上少量决定调用。支付和定价由我们负责，Maple 团队在您的整个构建过程中都会提供支持。

## 下一步

<CardGroup cols={2}>
  <Card title="发布菜单" icon="book-open" href="/zh/developer-api/guides/publish-menu">
    让每个商品明细上的 `menu_entity_id` 都对应到您自己的商品目录。
  </Card>

  <Card title="深入了解 Webhooks" icon="bell" href="/zh/developer-api/concepts/webhooks">
    投递保证、重试、事件记录和重放。
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.