> ## 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](/developer-api/how-maple-works) for the object model, and run the [Quickstart](/developer-api/quickstart) 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 theme={null}
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 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"]
  }'
```

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](/developer-api/concepts/webhooks#event-types).
</Note>

## Step 3 — Verify every delivery

Each delivery is a Stripe-style envelope:

```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" }
}
```

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

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](/developer-api/concepts/webhooks).

<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.
</Tip>

## 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 follows the `GET /v1/orders/{orderId}` shape, but omits the live `status` and `payment`. Fetch the order resource any time for the authoritative current state, including `status` and `payment`:

```json theme={null}
{
  "object": "order",
  "id": "ord_...",
  "livemode": false,
  "created": 1765432100,
  "status": "PENDING",
  "fulfillment_type": "pickup",
  "location_id": "str_...",
  "customer": { "name": "Alex", "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": 0, "delivery_tip": 0, "total": 567 },
  "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](/developer-api/api-reference#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 minimal.** You get a name and the last four digits of the phone number, never the full number.

## 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 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" }
```

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

```json theme={null}
{ "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](/developer-api/concepts/order-lifecycle).

<Info>
  **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](/developer-api/concepts/idempotency).
</Info>

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

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

To block the order instead:

```json theme={null}
{
  "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

<CardGroup cols={2}>
  <Card title="Publish a menu" icon="book-open" href="/developer-api/guides/publish-menu">
    Make `menu_entity_id` on every line item map to your own catalog.
  </Card>

  <Card title="Webhooks in depth" icon="bell" href="/developer-api/concepts/webhooks">
    Delivery guarantees, retries, the event ledger, and replay.
  </Card>
</CardGroup>
