Skip to main content
Connect your payout provider’s webhooks to Mercur so account and payout statuses follow the provider. You need this before any Stripe Connect account can become 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 webhookSecret option
  • 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 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:
The CLI prints a signing secret. Use it as the provider’s webhookSecret.
2

Let the subscriber run the workflow

The route emits 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 Connect verifies the 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
Verify the provider’s signature here with 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, emit payout.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:
Enable preserveRawBody on that route’s body parser, or signature checks will fail.

Next steps