> ## 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.

# Receive and decide orders

> Build the order loop: connect a location, subscribe to webhooks, verify deliveries, and drive each order with accept, deny, ready, and complete calls.

This is the core of a Maple integration. When a customer orders at a location you’re connected to, Maple pushes the order to your webhook; you decide whether to fulfill it and report progress as it moves. **None of it involves payments** — Maple owns that.

A working ordering integration is two pieces of work:

| # | What you build                                                    | Typical effort |
| - | ----------------------------------------------------------------- | -------------- |
| 1 | Receive orders — one webhook endpoint with signature verification | \~1–2 days     |
| 2 | Decide orders — accept / deny / status calls back to Maple        | \~1–2 days     |

If you haven’t yet, skim [How Maple works](https://docs.maple.inc/developer-api/how-maple-works.md) for the object model, and run the [Quickstart](https://docs.maple.inc/developer-api/quickstart.md) to get a key and connect a test location.

## Step 1 — Connect a location

Orders route to you only once your app is a location’s connected receiver:

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

Connections are exclusive per environment — if another app already holds the location you get a `409`. `GET` and `DELETE` on the same path inspect and remove your connection.

## Step 2 — Subscribe to webhooks

Register an HTTPS endpoint and the event types you want. The full catalog is at `GET /v1/webhook_event_types`; for the order loop you’ll typically want these:

```bash
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"]
  }'
```

The response carries a **one-time signing secret** (`mwhsec_…`). Store it; it is never shown again. The URL must be public HTTPS — private and internal addresses are rejected.

> [!NOTE]
>
> These are the order events. The catalog also includes `order.created`, `order.paid`, `store.provisioned`, `store.deprovisioned`, `store.status.changed`, and the menu sync events. Subscribe only to what you act on. See the full list and payloads in [Webhooks](https://docs.maple.inc/developer-api/concepts/webhooks.md#event-types).

## Step 3 — Verify every delivery

Each delivery is a Stripe-style envelope:

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

Deliveries carry two headers: `maple-webhook-id` (the event id) and `maple-webhook-signature` in the form `t=<unix_seconds>,v1=<hex_hmac>`. The signed string is `{timestamp}.{subscription_id}.{notification_url}.{raw_body}`. Verify it against the raw, unparsed body:

```ts
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));
}
```

The check above rejects stale timestamps (older than \~5 minutes) to prevent replay. Beyond that, **dedupe on the envelope `id`** — delivery is at-least-once. The full reliability contract, including the retry schedule and the ledger, is in [Webhooks](https://docs.maple.inc/developer-api/concepts/webhooks.md).

### Catching up after downtime

When your receiver was unavailable — the local software was off, or the network was down — reconcile like this:

1. Call `GET /v1/orders?since=<timestamp>&limit=100` with your last successful checkpoint (exclusive, ISO 8601 UTC). With `since`, results are oldest first and `has_more` indicates another page.
2. While `has_more` is true, repeat the request with `starting_after` set to the last order id from the previous page. Continue until the backlog is drained, then advance your checkpoint.
3. Call `POST /v1/orders/{orderId}/resend` when you need Maple to publish a fresh `order.notification` for one live order that missed its webhook. It requires the `webhooks:write` scope, creates a new signed, retried delivery, and does not change the order’s state. Terminal orders (`REJECTED`, `CUSTOMER_CANCELLED`, or `STORE_CANCELLED`) cannot be resent or revived and return `400`; reconcile them with `GET /v1/orders?since=...` and `GET /v1/orders/{orderId}`.
4. Use the [event ledger](https://docs.maple.inc/developer-api/concepts/webhooks.md#the-event-ledger) instead of `resend` when preserving the original event identity matters.

Waiting it out also works for a short outage: the [retry schedule](https://docs.maple.inc/developer-api/concepts/webhooks.md#retries-and-auto-disable) redelivers a failed event over roughly 10 hours before it counts as fully failed.

> [!TIP]
>
> Before you have real traffic, `POST /v1/webhook_subscriptions/{id}/test` sends a signed `webhook.test` event so you can prove your handler verifies and responds correctly.

## Step 4 — Read the order

The `order.notification` payload’s `data` is the **order’s content** — its IDs, customer, line items, and totals — so you can start fulfilling straight from the webhook without another call. It uses the same field names and integer-cent amounts as the order resource, but it is a smaller object: `order_id` (not `id`), `location_id`, `fulfillment_type`, `scheduled_for`, `delivery_address`, `customer`, `line_items`, and `totals`. It has no `object`, `id`, `livemode`, `created`, `status`, `payment_status`, `external_id`, or `payment`, and line items carry no per-item `tax`. `order.validation_requested` carries the same `data` plus `validation_id`:

```json
{
  "order_id": "ord_...",
  "location_id": "str_...",
  "fulfillment_type": "delivery",
  "scheduled_for": null,
  "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,
      "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 }
}
```

`GET /v1/orders/{orderId}` returns the full order resource instead, adding `object`, `id`, `livemode`, `created`, `status`, `payment_status`, `external_id`, `payment`, and per-item `tax`. Fetch it any time for the authoritative current state:

```json
{
  "object": "order",
  "id": "ord_...",
  "livemode": false,
  "created": 1765432100,
  "status": "PENDING",
  "payment_status": "unpaid",
  "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://..." }
}
```

A few things to internalize:

- **Money is integer USD cents.** `450` is $4.50. Never parse it as a float. Every amount in `totals` and on line items follows this convention.
- **Totals are precomputed.** `totals.total` is authoritative. You do not re-price anything.
- **Line items reference your menu** by `menu_entity_id`, whose value is the `externalId` you published for that item. Publish a menu first and these line up with your own catalog. (Menu payloads are camelCase; order and webhook payloads are snake\_case — see [Conventions](https://docs.maple.inc/developer-api/api-reference.md#resource-conventions).)
- **Modifiers are the directly-selected, first-level options.** Deeper nested modifier selections aren’t expanded into the order resource in v1.
- **Customer data is intentionally minimal.** You get the customer’s name and phone number (`phone` in E.164 format, plus `phone_last_four` for display). Delivery orders also include `delivery_address` with the street, unit, city, state, ZIP code, and delivery instructions; it is `null` for pickup orders or when Maple has no stored address. All six address keys are present whenever the block is populated.
- **Check `scheduled_for` before firing the ticket.** It’s `null` for ASAP orders. When set (ISO 8601, UTC), the customer chose that pickup/delivery time — you receive the order immediately, so schedule preparation for `scheduled_for`, not on receipt.

## Step 5 — Decide and report progress

Respond through the decision endpoints as the order moves. None take a body unless noted:

| Call                                 | When                                                           |
| ------------------------------------ | -------------------------------------------------------------- |
| `POST /v1/orders/{orderId}/accept`   | You can fulfill it                                             |
| `POST /v1/orders/{orderId}/deny`     | You can’t — optional `{ "reason": "…" }`                       |
| `POST /v1/orders/{orderId}/ready`    | Ready for pickup or courier handoff                            |
| `POST /v1/orders/{orderId}/complete` | Fulfilled                                                      |
| `POST /v1/orders/{orderId}/cancel`   | You must cancel after accepting — optional `{ "reason": "…" }` |
| `POST /v1/orders/{orderId}/status`   | Granular transition (see below)                                |

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

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

If you prefer one endpoint, `POST /v1/orders/{orderId}/status` takes an explicit transition:

```json
{ "status": "ACCEPTED" }
```

Valid values: `ACCEPTED`, `READY`, `IN_DELIVERY`, `FULFILLED`, `REJECTED`, `STORE_CANCELLED`. For what each status means and the legal transitions between them, see the [Order lifecycle](https://docs.maple.inc/developer-api/concepts/order-lifecycle.md).

> [!NOTE]
>
> **Decision calls are replay-safe.** Repeating one returns `{ "status": "received" }` with no double side effects, so retrying on a network blip is always safe. See [Idempotency and replay safety](https://docs.maple.inc/developer-api/concepts/idempotency.md).

## Optional — pre-validate orders

Pre-validation is **opt-in, and you turn it on by subscribing to `order.validation_requested`**:

- **If you don’t subscribe**, there’s no validation step. Maple sends `order.notification` directly, and your accept/deny is the only gate.
- **If you subscribe**, Maple asks you to confirm each order is fulfillable (item availability, pricing feasibility, POS injectability) *before* it sends the notification, and waits for your answer. Opting in is therefore a commitment: an order you don’t validate in time is rejected (see below).

That’s the whole mechanism — the subscription is the switch.

When subscribed, respond to each `order.validation_requested` with a result. Echo the `validation_id` from that event’s `data` so the result is bound to that attempt; a result for an expired or older attempt is rejected with `404`:

```bash
curl -X POST $MAPLE_BASE/orders/{orderId}/validation_result \
  -H "Authorization: Bearer $MAPLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "validation_id": "<validation_id from the event>", "status": "valid", "partner_reference": "pos_quote_123" }'
```

To block the order instead:

```json
{
  "validation_id": "<validation_id from the event>",
  "status": "invalid",
  "reason": { "code": "ITEM_AVAILABILITY", "explanation": "86'd", "item_external_ids": ["itm-latte"] }
}
```

A `valid` result lets the order proceed to notification; `invalid` blocks it before the customer is charged. You have about **5 minutes** to respond before a request expires (the resource carries `expires_at`, \~300 seconds out). If it expires, Maple requests validation **once more**; if that second request also goes unanswered, the order is rejected without a notification — so only subscribe once your handler reliably answers in time. It’s the same availability check you’d run at accept time, only earlier, so customers don’t pay for something you can’t make.

## What “done” looks like

| You build                                              | You get                                           |
| ------------------------------------------------------ | ------------------------------------------------- |
| One webhook handler that verifies, dedupes, and routes | Real-time orders pushed with precomputed totals   |
| Accept / deny / ready / complete / status calls        | Customers see live order progress                 |
| *(Optional)* validation responses                      | Bad orders blocked before the customer is charged |

One webhook and a handful of decision calls. Payments and pricing stay on our side, and the Maple team is available throughout your build.

## Next

- [Publish a menu](https://docs.maple.inc/developer-api/guides/publish-menu.md): Make menu\_entity\_id on every line item map to your own catalog.
- [Webhooks in depth](https://docs.maple.inc/developer-api/concepts/webhooks.md): Delivery guarantees, retries, the event ledger, and replay.
