active.
Before you begin
You need:- A payout provider registered on the payout module, such as Stripe Connect
- For Stripe Connect, a webhook signing secret set as the provider’s
webhookSecretoption - The Stripe CLI for local testing
Receive webhooks
Mercur ships the route, the subscriber, and the workflow. You point the provider at the route and check the result.1
Point the provider at /hooks/payout
In production, add a Stripe webhook endpoint for events on connected accounts, with the URL The CLI prints a signing secret. Use it as the provider’s
https://your_backend_url/hooks/payout and the account.updated event. The Stripe connector plugin also needs the bank payout.* events, see its webhooks. Locally, forward Connect events with the Stripe CLI:webhookSecret.2
Let the subscriber run the workflow
The route emits Stripe Connect verifies the
payout.webhook_received with the body, raw body, and headers. It delays the event by 5000 ms and retries up to 3 times. The shipped payout-webhook subscriber then parses it and runs the workflow:src/subscribers/payout-webhook.ts (in @mercurjs/core)
stripe-signature header against webhookSecret. A bad signature throws, and the event bus retries.3
Check the account status
Read the account to confirm the update landed:The workflow re-read the account from Stripe, so
status, payouts_enabled, kyc_status, and last_synced_at match Stripe’s current view.Run the workflow directly
processPayoutForWebhookWorkflow takes a PayoutWebhookResult, so you can apply a provider event without a webhook, for example in a test or a backfill script:
data.id is the provider’s id. An account.* action looks the account up by external_id and re-syncs it from the provider. payout.paid and payout.failed carry Mercur’s payout id in data.payout_id, which providers echo from the payout_id metadata sent with the payout, and set that payout’s status. payout.paid also stores data.id as the payout’s data.provider.id, which a later reversal is sent against; payout.failed stores status_reason: "provider_error" with the webhook’s data.status_reason in data.status_detail. Any other action, an unknown id, or a missing data.id changes nothing.
Map webhooks in your own provider
When you write a provider,getWebhookActionAndData decides what each webhook means. Return the action and the provider’s own id for the account or payout:
src/providers/acme-payouts/service.ts
payload.rawData and payload.headers before you trust the body. The route keeps the raw body for this purpose.
Emit the event from another route
If your provider must post to a different URL, emitpayout.webhook_received yourself with the same shape. The shipped subscriber picks it up, and so does any other subscriber on that event, such as the Stripe connector plugin’s handler for bank payout events:
preserveRawBody on that route’s body parser, or signature checks will fail.
Next steps
- Account lifecycle for every action and the status it sets
- Events reference for the
payout.webhook_receivedpayload