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 type | When it fires |
|---|---|
user.verified | A bootstrapped user successfully verifies their email after sign-up |
user.cancelled | A 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:
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 aPOST with Content-Type: application/json and a payload matching the type:
user.verified
user.cancelled
Cancellation reason enum (5 values)
reason | Meaning |
|---|---|
user_clicked_cancel | User explicitly cancelled their account from the dashboard |
squatting_defense | An unverified bootstrap account was cleaned up by Marea so a real user could sign up with the same email (PRD-4 squatting defense) |
30d_unverified | Bootstrapped account never verified their email; cleaned up after 30 days of inactivity |
90d_no_tos | Verified user never accepted updated Terms of Service after 90 days; cleaned up |
key_revoked | Developer key that bootstrapped the user was revoked; the user was cleaned up as part of revocation |
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 theX-Marea-Signature request header:
"<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
- Parse the
t=...andv1=...from the header - Reject if
|now - t| > 300(5-minute replay window) - Compute
HMAC-SHA256(secret, "<t>.<rawBody>")and hex-encode - Use a constant-time comparison against
v1 - If match: trust the payload. If not: respond
401.
Retry behavior
Marea expects a2xx response from your endpoint within 5 seconds. Anything else (non-2xx, timeout, connection error) triggers the retry path:
| Attempt | Delay from first dispatch |
|---|---|
| 1 | Immediate |
| 2 | +30 seconds |
| 3 | +5 minutes |
- Marks
users/{uid}.lastUserEvents[]withdeliveryFailed: true(visible on the bootstrapped-users dashboard) - Sends a delivery-failure email to the developer
- Fires an internal Telegram alert (
webhook_dispatch_failedP1 in our observability)
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 — usecrypto.timingSafeEqual(Node) orhmac.compare_digest(Python). - Test with
webhook.sitefirst: configure your webhook URL to point at a uniquewebhook.siteURL 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. ThedeveloperKeyId field in every payload tells you which key triggered the event, so you can route environments server-side:
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