POST request to your configured webhook endpoint with the event body.
Configuring your endpoint
Your endpoint must be publicly reachable over HTTPS and respond with any 2xx status code. Configure its URL with your Meru contact or via the dashboard. On creation you receive a signing secret (prefixed withwhsec_) — store it securely; you need it to verify signatures.
Event structure
Every delivery body has the same top-level shape:The event type. One of
payout.update or balance.updated.ISO 8601 timestamp of when the event was emitted.
The event payload. Its shape depends on
type (see below).Delivery headers
Each request includes these headers:| Header | Description |
|---|---|
Content-Type | application/json |
webhook-id | Unique message/delivery ID. Use it for idempotency. |
webhook-timestamp | Unix timestamp (seconds) of the delivery, used for signature verification. |
webhook-signature | Space-separated list of v1,<base64> signatures (more than one during secret rotation). |
Verifying signatures
Each request is signed with HMAC-SHA256 so you can confirm it came from us. The signed content is the string{webhook-id}.{webhook-timestamp}.{rawBody}, keyed with your signing secret (base64-decoded after stripping the whsec_ prefix), and the result is base64-encoded into the webhook-signature header.
To verify: recompute the signature over the raw request body (before any JSON parsing) and compare it against the header in constant time. Reject deliveries whose webhook-timestamp is more than 5 minutes old.
Event: payout.update
Sent whenever a payout changes state. The event type is always payout.update regardless of the transition — read data.state (and data.previousState) to know what happened.
data fields
The payout identifier.
Your company identifier.
Your external reference, if one was provided.
The end user/entity the payout was made for.
The state before this transition.
null on the first event (creation).The current payout state. See the table below.
The payout amount, in minor units (e.g. cents).
The destination currency.
One of
wallet, bank, or qr_code.Present only when the payout failed.
On-chain transaction hash. Present for
wallet and bank destinations.Bank-rail metadata. Present only for
destinationType = bank.ISO 8601 creation timestamp.
ISO 8601 last-update timestamp.
Payout states (data.state)
| State | Meaning |
|---|---|
created | Payout created |
in_review | In review (pre-partner) |
awaiting_funds | Awaiting funds |
funds_received | Funds received |
payment_submitted | Submitted / in progress |
payment_processed | Completed successfully |
undeliverable | Undeliverable |
returned | Returned |
refunded | Refunded |
canceled | Canceled |
error | Error / failed |
Event: balance.updated
Sent when a crypto deposit changes state. Its data shape differs from payout.update: it carries on-chain deposit details and does not include onBehalfOf, destinationType, or extraData.
data fields
The deposit identifier (named
payoutId for transport consistency).Your company identifier.
Your external reference, if one was provided.
Always
null for deposit events.The current deposit state.
The deposit amount, in minor units.
The deposit currency (e.g.
usdc).The blockchain the deposit settled on.
On-chain transaction hash, when available.
Failure reason, when applicable.
ISO 8601 last-update timestamp.
Idempotency
The same event may be delivered more than once. Use thewebhook-id header as the idempotency key: store processed IDs and skip duplicates.
Retries
If your endpoint does not return a2xx status (or times out), we retry delivery automatically with exponential backoff over several hours, honoring Retry-After on error responses. Acknowledge quickly (under a few seconds) and process heavy work asynchronously. Endpoints that fail persistently may be disabled.
Best practices
- Always verify the signature against the raw body before processing.
- Respond fast with a
2xx, then process asynchronously. - Be idempotent using
webhook-id. - Don’t assume order — rely on
state/previousStateandupdatedAt, not delivery order. - Log the
webhook-id,type, anddata.statefor every delivery.
Example handler
Testing
Expose your local server with a tunnel (e.g. ngrok) and register the public URL as your webhook endpoint:Troubleshooting
- Invalid signature: verify against the raw body (not re-serialized JSON), use the correct
whsec_secret, and read thewebhook-id/webhook-timestamp/webhook-signatureheaders as-is. - Duplicate events: deduplicate using
webhook-id. - Missed events: ensure your endpoint returns
2xxquickly; non-2xx responses and timeouts are retried, but persistent failures can disable delivery. - SSL errors: your endpoint needs a valid TLS certificate.
webhook-id.