Skip to content
Maple

Core concepts

How Maple makes retries safe without an idempotency key — replay-safe decisions, diff-applied menus, idempotent replay, and dedupe on your side.

Networks fail mid-request. The question that matters is: if you retry, will something happen twice? With Maple, the writes you’ll retry most are built to be safe to repeat — so you can retry on a timeout without reconciling first.

Order decisions are replay-safe

Repeating an order decision — accept, deny, ready, complete, cancel, or status — returns the same acknowledgement without applying the side effect twice:

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

If you send accept and the response times out, send it again. The order is accepted exactly once, and you get { "status": "received" } either way. This makes a simple retry-on-transient-error loop correct without any bookkeeping on your side.

Publishing a menu is a diff

A menu publish describes the desired state of the whole menu and is applied as a diff against what’s currently published, keyed by your externalIds:

  • Re-publishing an identical document is a no-op.
  • Object identities stay stable across publishes by externalId, so republishing doesn’t recreate or churn objects.
  • Validation is all-or-nothing — a 400 changes nothing, so a failed publish leaves the menu exactly as it was.

So if a publish times out, re-sending the same document is safe. See Publish a menu.

Booking writes carry an idempotency_key

Creating, changing, or cancelling a booking (POST /v1/bookings, POST /v1/bookings/{id}/change, POST /v1/bookings/{id}/cancel), cancelling one as the venue (POST /v1/bookings/{id}/venue_cancel), and seating a walk-in (POST /v1/locations/{locationId}/walk_ins) require an idempotency_key in the body, scoped to your app. Walk-in keys, like waitlist joins, are your app’s across all its locations. Keys must contain 1–255 characters. For Maple-native bookings, the ledger records the write and its replay outcome in one transaction:

  • Retrying with the same key and the same request replays the original outcome — you get the same booking back, and nothing is booked, moved, or cancelled twice.
  • The same key with a different request (a different guest, slot, or note) answers 409 with code: "idempotency_key_reused". Generate a fresh key per intended write; a UUID is fine.

A retry returns the same booking identity without reapplying the mutation; the response is read back from the current booking. Keys have a 24-hour replay window from the first claim. Replays are checked before expected_version, which will often be stale after the original write succeeds.

Native and provider holds are persisted and owned by the app that acquired them. Repeating the same slot_token replays the app’s live hold without taking another lock. Another app cannot consume or release it, even at a jointly granted location. Unknown provider holds release their quota reservation after a conservative 24-hour inactivity quarantine, independently of their retained audit record; expired operations cannot be revived by retrying.

Provider-backed writes

For OpenTable, Maple durably claims the key and request digest before provider I/O and uses one stable provider request ID. A pending change or cancellation also reserves the mirrored booking against other partner writes. On success, Maple records the mirror and replay outcome together. Transient mirror failures are retried synchronously; a background reconciler repairs outstanding work.

A provider timeout returns 409 operation_in_progress while the outcome is unresolved. Retry the same request and key after details.retry_after seconds. Maple performs a reconciliation read before repeating a booking write with the original provider request ID. Completed writes replay without calling the provider again.

Booking configuration uses content-addressed publication instead: identical configuration is a no-op without an explicit key. The status venue verbs (confirm, decline, seat, complete, no-show) take expected_version, not an idempotency key; after an ambiguous response, read the booking before taking another action. Venue-side cancellation and walk-ins do take an idempotency_key, scoped to your app like every other key.

{ "hold_id": "bkh_…", "guest": { "first_name": "Alex", "last_name": "Quinn", "phone": "+12125551234" }, "idempotency_key": "6f1c…" }

Replaying an event is idempotent

POST /v1/webhook_events/{eventId}/replay re-delivers an event only to subscriptions that haven’t already received it. Subscriptions that already got it are skipped, so replaying after an outage won’t double-deliver to healthy endpoints.

Your side: dedupe deliveries

The one place you must add idempotency is your webhook handler. Delivery is at-least-once, so the same event can arrive more than once. Dedupe on the envelope id:

async function handleEvent(event: { id: string; type: string; data: unknown }) {
if (await alreadyProcessed(event.id)) return; // no-op on a repeat
await process(event);
await markProcessed(event.id);
}

Make the work itself idempotent where you can — for example, key your own order records by Maple’s order_id so a duplicate order.notification updates rather than duplicates. Don’t rely on event ordering; reconcile against GET /v1/orders/{orderId} when sequence matters. See Webhooks.

Summary

Operation Safe to retry? Why
Order decisions Yes Replay-safe; repeats return received with no double effect
Menu publish Yes Diff-applied; identical re-publish is a no-op
Event replay Yes Skips subscriptions already delivered to
Native booking create / change / cancel Yes, with your idempotency_key Same key + same body replays; same key + different body is 409 idempotency_key_reused
Walk-ins and venue-side cancellation Yes, with your idempotency_key Same key + same body replays the same booking, even after its table is taken; same key + different body (or, for walk-ins, another location) is 409 idempotency_key_reused
Native booking holds While the original hold is live Re-presenting its slot_token returns the live hold
Provider booking writes / holds With the same key / quote Durable claims and stable provider request IDs; unresolved outcomes can require reconciliation
Booking configuration Yes Identical configuration is a content-addressed no-op
Receiving webhooks You dedupe At-least-once delivery — dedupe on event id