Skip to main content
Webhooks are how Maple pushes things to you — a new order, a status change, a finished menu sync. You register an HTTPS endpoint and the event types you care about; Maple signs each event and delivers it, retrying on failure and keeping a ledger you can replay from. You never poll.

The event envelope

Every delivery has the same Stripe-style envelope. The data payload is event-specific:
{
  "object": "event",
  "id": "evt_...",
  "type": "order.notification",
  "created": 1765432100,
  "data": { "order_id": "ord_...", "location_id": "str_..." }
}
id
string
Unique event id. Dedupe on this — delivery is at-least-once.
type
string
The event type. Switch on it to route the event.
created
number
Unix seconds when the event was created.
data
object
Event-specific payload. order.notification carries the full order content; the lifecycle events (order.created, order.paid, order.cancelled) carry summary fields. See Webhook events for each payload, and fetch GET /v1/orders/{orderId} for authoritative current state.

Event types

Subscribe only to what you act on. The live catalog is always at GET /v1/webhook_event_types:
TypeWhen it fires
order.validation_requestedMaple asks you to validate an order before notifying you
order.notificationAn order is handed to you to fulfill — the event you act on
order.createdAn order was created — a lifecycle signal for analytics, thinner than the notification
order.paidPayment settled (can be after the handoff)
order.cancelledThe order was cancelled — stop fulfilling it
store.provisionedA location was provisioned to your app
store.deprovisionedA location was deprovisioned from your app
store.status.changedA connected location’s status changed
menu.sync.completedA menu sync completed for a location
menu.sync.failedA menu sync failed for a location
webhook.test is also delivered on demand by the test endpoint, so you can verify a handler before any real traffic.
These are discrete events, not a status feed. Maple doesn’t send a webhook for every order status change — the transitions you drive yourself (accept, ready, complete) aren’t echoed back, and there’s no per-transition event. For an order’s current state, read GET /v1/orders/{orderId}. See the Order lifecycle.
For what each order event carries and which to subscribe to, see Webhook events.

Subscribing

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"]
  }'
Response
{
  "object": "webhook_subscription.created",
  "subscription": { "object": "webhook_subscription", "id": "dws_...", "environment": "test", "status": "enabled", "...": "..." },
  "signing_secret": "mwhsec_..."
}
The signing_secret (mwhsec_…) is returned once, at creation, and never again. Store it securely now. If you lose it, rotate by creating a new subscription.
The notification_url must be public HTTPS — private and internal addresses are rejected. Manage subscriptions with GET, PATCH (update the URL, event types, or enabled/disabled status), and DELETE on /v1/webhook_subscriptions/{id}. Updating a subscription leaves its signing secret unchanged.

Multiple subscriptions

You can register more than one subscription. Every enabled subscription whose event_types include a fired event gets its own signed delivery, each with its own signing secret — so overlapping event_types across subscriptions are allowed. This lets you fan out by destination: for example, point order events at your fulfillment service and menu.sync.* events at a separate back-office endpoint. Replay follows the same routing and skips any subscription that already received the event.

Verifying the signature

Each delivery carries two headers:
  • maple-webhook-id — the event id.
  • maple-webhook-signaturet=<unix_seconds>,v1=<hex_hmac>.
The HMAC-SHA256 signature is computed over a signed string that binds the timestamp, the subscription, and the destination URL to the body — so a captured signature can’t be replayed against a different subscription or URL:
{timestamp}.{subscription_id}.{notification_url}.{raw_body}
Recompute it with your signing secret and compare in constant time. Always use the raw, unparsed request body — re-serializing JSON will change the bytes and break the signature.
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));
}

Delivery guarantees

  • At-least-once. The same event can arrive more than once. Dedupe on the envelope id and make your handler idempotent.
  • Respond fast. Return a 2xx quickly, then do slower work asynchronously. Any non-2xx (or a timeout) counts as a failed delivery.
  • Order is not guaranteed. Don’t assume events arrive in the order they occurred; reconcile against the order resource when sequence matters.

Retries and auto-disable

Each event is delivered with up to 6 attempts — the first immediately, then with increasing backoff:
AttemptDelay after previous
1immediate
21 minute
35 minutes
430 minutes
52 hours
66 hours
A non-2xx response (or a timeout — the per-attempt limit is 15 seconds) fails that attempt. An event that fails all 6 attempts counts as one fully-failed delivery. After 5 consecutive fully-failed deliveries — five separate events that each exhausted every attempt — the subscription is automatically disabled; a single successful delivery resets the counter to zero. Re-enable it with a PATCH setting status back to enabled once your endpoint is healthy, then replay anything you missed.

The event ledger

Every event is recorded, so a missed or failed delivery is recoverable.
  • GET /v1/webhook_events — your recent events, most recent first (up to 50). Useful for reconciliation and debugging.
  • GET /v1/webhook_events/{eventId} — a single event, including its full data payload.
  • POST /v1/webhook_events/{eventId}/replay — re-deliver the event to currently-matching subscriptions. Idempotent: subscriptions that already received it are skipped, so replaying is safe.

Testing your handler

curl -X POST $MAPLE_BASE/webhook_subscriptions/{id}/test \
  -H "Authorization: Bearer $MAPLE_KEY"
This delivers a signed webhook.test event and returns whether it was delivered and the HTTP status your endpoint returned:
{ "object": "webhook_test_result", "event_id": "evt_...", "delivered": true, "response_status": 200 }
Use it to confirm — before any real order — that your endpoint is reachable, verifies the signature, and returns 2xx.

Checklist

Verify the HMAC signature on every delivery against the raw body, and reject stale timestamps.
Dedupe on the envelope id; make handlers idempotent.
Return 2xx fast; process asynchronously.
Subscribe only to the event types you handle.
Monitor for auto-disabled subscriptions and replay from the ledger after an outage.