Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mareaalcalina.com/llms.txt

Use this file to discover all available pages before exploring further.

Page webhooks

Marea POSTs JSON events to a URL each merchant configures for their store / digital menu / page. Use them to keep a POS, accounting integration, delivery tool, or automation platform (Zapier, Make.com, n8n) in sync with the lifecycle of every order — without polling.
Two webhook surfaces, do not confuse them:
  • Page webhooks (this page) — fire on merchant order events (order.created, order.status_updated, order.paid) for a single store / digital menu / page. Configured per-store by the merchant in the profile page at /menus/profile?section=integrations.
  • Agent webhooks — fire on user lifecycle events (user.verified, user.cancelled) for users a developer key bootstrapped. See Agent webhooks.

When do page webhooks fire?

Three events ship in v1:
Event typeWhen it fires
order.createdA customer completes checkout. Fires when the order transitions to orderStatus: 'pending' and a publicOrderId has been assigned.
order.status_updatedThe merchant changes the order’s status (e.g. pendinginProgresscompleted, or any custom-pipeline transition). One event per transition.
order.paidPayment is captured (Stripe Checkout success or PayPal capture success). Cash / custom payments do not fire this event in v1.
Granularity vs. coarse status: Marea’s internal orderStatus enum is coarse (pending | inProgress | completed | cancelled | refunded | abandonedCart | erased). Most merchants run a more granular per-store pipeline (e.g. received → preparing → ready → delivered). The order.status_updated payload carries BOTH the coarse orderStatus and the resolved customStatus { id, label }. Use whichever your integration cares about.

Configure your webhook

A merchant configures their webhook in their Marea profile at /menus/profile?section=integrations:
  1. Paste an HTTPS URL — your POS-vendor’s hook URL, your Zapier catch URL, your accounting integration endpoint, etc.
  2. Pick events — tick order.created / order.status_updated / order.paid. Marea only dispatches subscribed events.
  3. Save — Marea generates a 32-byte HMAC signing secret server-side and reveals it once in a modal. Copy it; you’ll need it to verify signatures on your receiver.
  4. [Probar webhook] — fire a synthetic test event to your endpoint. Receivers can identify test events via data.__test === true.
Marea requires HTTPS on a public host. Loopback (localhost, 127.0.0.1), private IP ranges (RFC1918), the cloud-metadata IP (169.254.169.254), .internal and .local DNS suffixes, and IPv6 ULA / link-local / IPv4-mapped private addresses are rejected.

Event payload shape

Every webhook is a POST with Content-Type: application/json and a payload wrapped in a common envelope:
interface PageWebhookEnvelope<T> {
  type: 'order.created' | 'order.status_updated' | 'order.paid';
  eventId: string;        // UUID v4 — idempotency key for your receiver
  timestamp: string;      // ISO-8601 dispatch time
  apiVersion: '2026-05-08';
  data: T;                // event-specific payload (see below)
}

order.created

The full order payload — items, pricing, customer, delivery, payment method:
{
  "type": "order.created",
  "eventId": "8f7c6d5e-1234-5678-90ab-cdef12345678",
  "timestamp": "2026-05-08T20:35:22.123Z",
  "apiVersion": "2026-05-08",
  "data": {
    "orderId": "order_abc123",
    "publicOrderId": 1042,
    "trackingToken": "trk_xyz789",
    "userId": "user_uid_lupita",
    "menuId": "menu_taqueria_main",
    "branchId": null,
    "branchName": null,
    "orderStatus": "pending",
    "customStatus": { "id": "received", "label": "Recibido" },
    "totalPrice": 285.50,
    "subtotalPrice": 250.00,
    "deliveryPrice": 35.50,
    "discountAmount": 0,
    "totalTaxAmount": 0,
    "taxes": null,
    "currency": "MXN",
    "currencySymbol": "$",
    "items": [
      {
        "title": "Taco al Pastor",
        "amount": 5,
        "price": 25.00,
        "salePrice": null,
        "variants": [{ "extras": ["Cebolla", "Cilantro"] }],
        "comment": "Sin chile"
      }
    ],
    "customer": {
      "name": "Juan Pérez",
      "phone": "+525512345678",
      "email": "juan@example.com"
    },
    "delivery": {
      "type": "deliveryTrue",
      "address": "Av. Insurgentes Sur 1234, Col. Del Valle, CDMX",
      "addressUrl": "https://maps.google.com/?q=...",
      "structuredAddress": {
        "addressLineOne": "Av. Insurgentes Sur 1234",
        "city": "CDMX",
        "state": "CDMX",
        "postalCode": "03100",
        "countryCode": "MX"
      },
      "numberOfTable": null
    },
    "paymentMethod": { "name": "Stripe" },
    "paymentStatus": "completed",
    "appliedPromotion": null,
    "generalComment": "Tocar al timbre 2 veces",
    "createdAt": "2026-05-08T20:35:18.456Z"
  }
}

order.status_updated

A slim payload — previousState → newState only. Receivers cache the order.created payload (which has full order context) and merge by orderId. PII is omitted from this event because status updates can fire many times per order.
{
  "type": "order.status_updated",
  "eventId": "9a8b7c6d-2345-6789-01bc-def234567890",
  "timestamp": "2026-05-08T20:42:11.789Z",
  "apiVersion": "2026-05-08",
  "data": {
    "orderId": "order_abc123",
    "publicOrderId": 1042,
    "trackingToken": "trk_xyz789",
    "userId": "user_uid_lupita",
    "menuId": "menu_taqueria_main",
    "branchId": null,
    "previousOrderStatus": "pending",
    "orderStatus": "inProgress",
    "previousCustomStatus": { "id": "received", "label": "Recibido" },
    "customStatus": { "id": "preparing", "label": "Preparando" },
    "changedAt": "2026-05-08T20:42:09.234Z",
    "reason": null
  }
}

order.paid

A finance-focused payload — pricing + provider-specific reconciliation IDs. Customer PII is omitted. Use this event for accounting integrations that don’t need order context.
{
  "type": "order.paid",
  "eventId": "ab9c8d7e-3456-7890-12cd-ef3456789012",
  "timestamp": "2026-05-08T20:35:20.456Z",
  "apiVersion": "2026-05-08",
  "data": {
    "orderId": "order_abc123",
    "publicOrderId": 1042,
    "userId": "user_uid_lupita",
    "menuId": "menu_taqueria_main",
    "paymentProvider": "stripe",
    "paymentMethod": { "name": "Stripe" },
    "paymentStatus": "completed",
    "totalPrice": 285.50,
    "subtotalPrice": 250.00,
    "deliveryPrice": 35.50,
    "discountAmount": 0,
    "totalTaxAmount": 0,
    "currency": "MXN",
    "stripeCheckoutSessionId": "cs_test_xxxxxxxxxx",
    "stripeInvoiceUrl": "https://invoice.stripe.com/i/...",
    "paypalOrderId": null,
    "paypalCaptureId": null,
    "paypalInvoiceUrl": null,
    "capturedAt": "2026-05-08T20:35:19.012Z"
  }
}

Verify signatures

Every webhook is signed with HMAC-SHA256. The signature is in the X-Marea-Signature request header:
X-Marea-Signature: t=1714867200,v1=2c8a1dd3... (64-char hex)
The signature is computed over the string "<timestamp>.<rawBody>" using your raw signing secret (the 32-byte hex string Marea revealed when you first saved the webhook).

Key difference from Agent webhooks

Agent webhooks derive the signing key from the developer-key hash via HKDF-SHA256. Page webhooks use the raw signing secret directly — there is no HKDF step. Use the X-Marea-Source header to disambiguate:
HeaderSourceSecret derivation
X-Marea-Source: developerAgent webhookHKDF-SHA256 from apiKeys/{keyId}.keyHash
X-Marea-Source: merchantPage webhookRaw 32-byte hex from users/{uid}.webhookSigningSecret
If your single endpoint receives both surfaces, branch on X-Marea-Source to pick the right verification path.

Verification flow

  1. Parse the t=... and v1=... from the header
  2. Reject if |now - t| > 300 (5-minute replay window)
  3. Compute HMAC-SHA256(secret, "<t>.<rawBody>") and hex-encode
  4. Use a constant-time comparison against v1
  5. If match: trust the payload. If not: respond 401.
Ready-to-paste implementations live at Page-webhook receiver helpers.
Always verify against the raw request body bytes, not a JSON-parsed and re-stringified version. Whitespace differences will break the signature match.

Headers reference

HeaderValue
Content-Typeapplication/json
X-Marea-Signaturet=<unix-ts>,v1=<hex-hmac-sha256>
X-Marea-Event-Typeorder.created / order.status_updated / order.paid
X-Marea-Event-IdUUID v4 (same value as data.eventId) — use for idempotency
X-Marea-Sourcemerchant for Page webhooks
User-Agentmarea-webhook/1.0

Retry behavior

Marea expects a 2xx response from your endpoint within 5 seconds. Anything else (non-2xx, timeout, connection error) triggers the retry path:
AttemptDelay from first dispatch
1Immediate
2+30 seconds
3+5 minutes
After attempt 3 fails, the event is dropped. The merchant sees the failure in the recent deliveries log in their integrations tab — outcome max_attempts_reached with the final HTTP status. There is no automatic replay in v1; if you need bulletproof delivery, queue events on your end (e.g. via Zapier / Make.com / n8n).

Recent deliveries log

The merchant’s profile page surfaces the last 50 deliveries (rolling 30-day window) with:
  • Outcome — success / failed / max_attempts_reached / pending
  • Response status — color-coded (green 2xx, yellow 4xx, red 5xx or max-attempts)
  • Latency — time to receive a response from your endpoint
  • Attempt count1/3, 2/3, 3/3
  • Error message — for failed deliveries (truncated to 500 chars)
Test fires (data.__test === true) are flagged in the log so they don’t pollute success-rate metrics.

Receiver implementation tips

  • Respond 200 immediately, then process asynchronously. Marea’s 5-second timeout is tight; if your business logic takes longer, ack quickly and process in a queue.
  • Idempotency: design your handler to dedupe on eventId — Marea retries on transient failures, so duplicate deliveries are possible.
  • Constant-time signature comparison: don’t use === or == on the HMAC — use crypto.timingSafeEqual (Node) or hmac.compare_digest (Python).
  • Test with webhook.site first: configure your webhook URL to point at a unique webhook.site URL while you wire up your real handler — you’ll see the exact payloads Marea sends.
  • Distinguish surfaces by X-Marea-Source if your endpoint also receives Agent webhooks.

Rotate the signing secret

If you suspect your secret has leaked, click Rotar secreto in the integrations tab. Marea generates a new 32-byte hex secret, reveals it once, and immediately invalidates the old one. There is no overlap window in v1 — update your receiver’s secret immediately. In-flight deliveries that were enqueued before the rotation are signed with the new secret at delivery time, so receivers configured with the new secret will accept them.

Limitations and roadmap

  • Three events only today (order.created, order.status_updated, order.paid) — menu.* / product.* / customer.* / subscription.* events are TIER 2.
  • No missed-event replay endpoint — events that hit max_attempts_reached are gone. Use a buffered receiver if you need stronger guarantees.
  • Single webhook URL per merchant — fan-out to multiple endpoints via Zapier/Make.com.
  • Cash / custom payments don’t fire order.paid in v1. Subscribe to order.created and look at paymentMethod if you need cash-order coverage.
  • Refund event (order.refunded) is TIER 2.
  • 24h secret-rotation overlap window is TIER 2.
For receiver-side helpers (signature verification code in JS + Python), see Page-webhook receiver helpers.