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.

Agent webhooks

Marea POSTs JSON events to a URL you configure for your developer key. Use them to keep your agent (or downstream CRM / integration platform) in sync with the lifecycle of users you bootstrapped — without polling.
Two webhook surfaces, do not confuse them:
  • Agent webhooks (this page) — fire on user lifecycle events (user.verified, user.cancelled) for users your developer key bootstrapped. Configured in the developer dashboard at /developers/webhooks.
  • Page webhooks — 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. See Page webhooks.

When do agent webhooks fire?

Two events fire today:
Event typeWhen it fires
user.verifiedA bootstrapped user successfully verifies their email after sign-up
user.cancelledA user cancels their account OR is hard-deleted by Marea (the reason field tells you which)
Order events ship under Page webhooks, not here. order.created, order.status_updated, and order.paid are merchant-side events tied to a specific store / page; they are configured per-store by the merchant, not by the developer key. See Page webhooks.

Configure your webhook URL

In the developer dashboard at /developers/webhooks, set a single Default URL that receives events from all your developer keys:
https://your-server.com/marea-webhook
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 matching the type:

user.verified

{
  "type": "user.verified",
  "userId": "QqV9pK3nL...",
  "developerKeyId": "kid_abc123",
  "verifiedAt": "2026-05-08T19:42:11.823Z"
}

user.cancelled

{
  "type": "user.cancelled",
  "userId": "QqV9pK3nL...",
  "developerKeyId": "kid_abc123",
  "cancelledAt": "2026-05-08T22:03:45.117Z",
  "reason": "user_clicked_cancel"
}

Cancellation reason enum (5 values)

reasonMeaning
user_clicked_cancelUser explicitly cancelled their account from the dashboard
squatting_defenseAn unverified bootstrap account was cleaned up by Marea so a real user could sign up with the same email (PRD-4 squatting defense)
30d_unverifiedBootstrapped account never verified their email; cleaned up after 30 days of inactivity
90d_no_tosVerified user never accepted updated Terms of Service after 90 days; cleaned up
key_revokedDeveloper key that bootstrapped the user was revoked; the user was cleaned up as part of revocation
Note: when a user.cancelled event fires, the user’s data has already been deleted from Marea by the time the event reaches you. There is no follow-up user.deleted event.

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 a secret derived from your developer key. You must verify the signature on every incoming webhook before trusting the payload.

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 in JavaScript and Python live in 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.

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. Marea also:
  1. Marks users/{uid}.lastUserEvents[] with deliveryFailed: true (visible on the bootstrapped-users dashboard)
  2. Sends a delivery-failure email to the developer
  3. Fires an internal Telegram alert (webhook_dispatch_failed P1 in our observability)
There is no automatic replay. If you need bulletproof delivery, queue events on your end (e.g. via a managed integration platform like Zapier or Make.com) so a transient receiver outage doesn’t lose events.

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: Marea may retry, so design your handler to be idempotent on (type, userId, developerKeyId, verifiedAt|cancelledAt). Duplicate deliveries are rare but 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.

What about per-key URLs?

The dashboard configures a single Default URL per developer. All your keys’ events POST to that URL. The developerKeyId field in every payload tells you which key triggered the event, so you can route environments server-side:
function handleMareaWebhook(req, res) {
  if (!verifySignature(req)) return res.status(401).end();
  const env = KEY_TO_ENV[req.body.developerKeyId];  // 'prod' | 'staging' | 'dev'
  routeToHandler(env, req.body);
  res.status(200).end();
}
For developers who genuinely need a separate URL per key (rare), Marea’s API offers a per-key override: POST /v1/webhooks/userEvents with { keyId, url } using the developer key in the Authorization: Bearer mk_dev_... header. The dispatcher checks per-key URLs first, then falls back to the developer-level Default URL.

Limitations and roadmap

  • Two events only today — order events (order.*) ship as Page webhooks, a separate merchant-configured surface
  • No replay endpoint — once 3 retries fail, the event is gone. Use a buffered receiver if you need stronger guarantees
  • Single Default URL per developer in the dashboard. Per-key overrides via API only
  • No signing-secret rotation UI — to rotate, revoke the developer key and issue a new one
For receiver-side helpers (signature verification code in JS + Python), see Webhook receiver helpers.