createOrderPayoutsWorkflow will pay an account, and what each webhook does.
Account statuses
An account’s state is thestatus field on PayoutAccount, typed by the PayoutAccountStatus enum from @mercurjs/types. It defaults to pending.
The account also mirrors the provider’s flags:
payouts_enabled, charges_enabled, details_submitted, disabled_reason, and a derived kyc_status (pending_submission, pending_approval, approved, refused). createOrderPayoutsWorkflow checks payouts_enabled and holds the payout until it’s true.
How the status is derived
Mercur doesn’t take a status from the webhook body. It asks the provider for the account withretrieveAccount and derives the status from what comes back:
rejectedwhen the provider says so, ordisabled_reasonstarts withrejectedpendingwhiledetails_submittedisfalseactivewhen payouts and charges are enabled and there’s nodisabled_reasonrestrictedin every other case
kyc_status follows the requirements: anything currently or past due is pending_submission, anything pending verification is pending_approval, a rejected.* disabled reason is refused, and an account with nothing outstanding is approved.
Webhooks refresh the account
The provider’sgetWebhookActionAndData turns a raw webhook into an action and the provider’s account id. processPayoutForWebhookWorkflow finds the account by external_id and runs syncPayoutAccountWorkflow, which:
- Re-reads the account from the provider and stores the derived status, flags, and
kyc_status - Swaps the onboarding link for a provider dashboard login link once details are submitted
- Emits
payout_account.updatedwith the account id
account.* action runs the same sync, so the provider’s current view always wins, whatever order the webhooks arrive in. You can run the same sync by hand with POST /admin/payout-accounts/:id/sync.
The Stripe Connect provider handles Stripe’s account.updated event and reports account.rejected, account.activated, account.restricted, or account.updated, depending on the account it receives.
Payout statuses
Each payout and each reversal carries aPayoutStatus:
status_reason holds a stable code that says why a payout or reversal is on_hold, failed, or aborted, from PayoutStatusReason in @mercurjs/types. The details behind it, such as an id, a status, or the provider’s error message, are in data.status_detail, for example Order is not ready yet, status is not_fulfilled.
on_hold and failed payouts are re-decided by the next createOrderPayoutsWorkflow run, and operators can retry one, or a pending one, with POST /admin/payouts/:id/retry. A canceled order’s payout is aborted even when the seller has no usable account yet. paid, processing, and aborted payouts are never sent again.
The Stripe Connect provider returns paid as soon as Stripe creates the transfer, so no webhook is needed to confirm it. Transfers are platform-account events and never reach the Connect endpoint.
Related
- Process a provider webhook to see the webhook path end to end
- The payout pipeline for what happens once an account is active
- Workflows reference for every step name