Payout methods
Add a bank account or debit card, choose standard and instant payout destinations, and set up payer-managed payout routing for a Payee who hasn't linked.
Add a bank account or debit card for a contractor, choose their payout destinations, or route payouts for an unlinked Payee.
For the payer's side of funding payments (the account payroll draws from), see Payment and payout methods.
What's different from V1
V1 required changing banking details in the Wingspan app; there was no API for it. In V3 you can manage payout methods through the API. Because these changes move money, they require a session with recent multi-factor authentication (see Step-up authentication).
Two ways a payout destination gets set
| Who sets it | When to use it | Resources |
|---|---|---|
| The contractor, on their own Account | The contractor has linked an Account. This is the usual case. | ExternalBankAccount or PaymentCard owned by their Account, plus their Account's PayoutSettings |
| The payer, for a Payee | The Payee hasn't linked, or you collect bank details in your own system. | ExternalBankAccount owned by the payer with subject: { type: Payee }, plus PayoutSettings for that Payee |
The Payee's payoutDestinationPolicy decides which one Wingspan uses: PayeeSupplied, PayerSupplied, or PayeeSuppliedFallbackToPayer (the default for new Payees, which uses the payee's own destination and falls back to yours when theirs is missing, invalid, or inaccessible). Wingspan picks one complete configuration and never merges the two.
For contractors: add a bank account
Option A: link through your bank
-
Get a link token with
POST /v3/finance/external-bank-accounts/link:curl -X POST https://api.wingspan.app/v3/finance/external-bank-accounts/link \ -H "Authorization: Bearer $CONTRACTOR_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "redirectUri": "https://app.example.com/payouts/linked" }'// 201 Created { "linkToken": "...", "expiresAt": "2026-09-24T17:40:00Z" } -
Open the bank-linking widget with
linkToken. The contractor signs in to their bank and picks an account. -
Send the result to
POST /v3/finance/external-bank-accounts. For Plaid, sendpublicTokenandplaidAccountId:curl -X POST https://api.wingspan.app/v3/finance/external-bank-accounts \ -H "Authorization: Bearer $CONTRACTOR_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "publicToken": "public-...", "plaidAccountId": "..." }'
A linked account can start Verified right away, or Pending if the bank needs micro-deposits.
Option B: enter routing and account numbers
curl -X POST https://api.wingspan.app/v3/finance/external-bank-accounts \
-H "Authorization: Bearer $CONTRACTOR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"routingNumber": "000000000",
"accountNumber": "000000000",
"accountType": "Checking",
"accountHolderType": "Individual",
"nickname": "Main checking"
}'// 201 Created (trimmed)
{
"id": "gHv05B09HF_msN5pygl4Ga",
"subject": { "type": "Account", "id": "qY0t19FmElxN3ur1EiW2Ov" },
"status": "Pending",
"accountName": "Example Bank ...0000",
"accountType": "Checking",
"accountHolderType": "Individual",
"accountNumberMask": "0000",
"verificationMethod": "MicroDeposit",
"capabilities": ["Payout"]
}accountHolderType (Individual or Business) is required; it decides how the ACH payment is classified. For a non-US bank, set country and currency, and use GET /v3/finance/external-bank-accounts/{bankAccountId}/missing-fields to see what the corridor needs.
When the micro-deposits arrive, confirm them with POST /v3/finance/external-bank-accounts/{bankAccountId}/verify and {"amounts": [0.12, 0.34]}.
Idempotency-Key is required on bank account create, so a retry never creates a duplicate account or a second set of micro-deposits.
Bank account statuses: Pending, Verified, Disconnected, Failed, Removed. A Disconnected linked account can be reconnected with POST .../{bankAccountId}/reconnect. Remove one with DELETE /v3/finance/external-bank-accounts/{bankAccountId}.
For contractors: add a debit card for instant payouts
Card numbers never pass through the API. Cards are captured in a hosted frame.
POST /v3/payments/payment-cardscreates aPendingcard record. The body is optional (externalId,metadata).Idempotency-Keyis required.POST /v3/payments/payment-cards/{paymentCardId}/linkreturns a one-timeclientToken(provider: Footprint) for the hosted card frame. The contractor enters the card there.POST /v3/payments/payment-cards/{paymentCardId}/activatewith{"accepted": true, "consentVersion": "..."}runs a zero-amount card check and activates the card only if it matches. SendIf-Matchwith the card's currentETag.
An active card reports status: Active and capabilities such as Payout.
For contractors: choose payout destinations
Payout destinations for an Account live on its PayoutSettings record. Find it with GET /v3/payments/payout-settings, then update it with PATCH /v3/payments/payout-settings/{payoutSettingsId}:
curl -X PATCH https://api.wingspan.app/v3/payments/payout-settings/bApklsdKrY_o0cRD7GZ3EU \
-H "Authorization: Bearer $CONTRACTOR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"standardDestination": { "type": "ExternalBankAccount", "id": "gHv05B09HF_msN5pygl4Ga" },
"instantDestination": { "type": "PaymentCard", "id": "BZkFhjOT_GALf5_PXMgmj8" },
"preferredPayoutSpeed": "Standard"
}'| Field | What it does |
|---|---|
standardDestination | Where standard payouts go. null clears it. |
instantDestination | Where instant payouts go, usually a PaymentCard. null clears it. |
preferredPayoutSpeed | Standard or Instant. Independent of which destinations exist. |
accountInstantPayoutFee | Read-only. The Account's default instant payout fee, as a percentage. |
Destination type is one of ExternalBankAccount, InternalAccount (a Wingspan wallet), or PaymentCard. Omitted fields stay the same.
Setting a destination completes a PayoutMethod requirement if the payer uses one.
For payers: route payouts for a payee
If a Payee hasn't linked their Account, or you already hold their bank details, you can set up payer-managed payout routing. The bank account belongs to your Account and names the Payee as its subject, so it stays valid while the Payee is unlinked.
-
Create the bank account with
subjectset to the Payee:curl -X POST https://api.wingspan.app/v3/finance/external-bank-accounts \ -H "Authorization: Bearer $WINGSPAN_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "subject": { "type": "Payee", "id": "M9ISYbzJElXs4zIHv76rjT" }, "routingNumber": "000000000", "accountNumber": "000000000", "accountType": "Checking", "accountHolderType": "Individual", "accountHolderName": "Priya Shah" }'isDefaultmust befalse(or omitted) for a Payee subject. -
Route the Payee's payouts to it with
POST /v3/payments/payout-settings. This creates or replaces the Payee's routing.curl -X POST https://api.wingspan.app/v3/payments/payout-settings \ -H "Authorization: Bearer $WINGSPAN_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "subject": { "type": "Payee", "id": "M9ISYbzJElXs4zIHv76rjT" }, "destinations": [ { "type": "ExternalBankAccount", "id": "gHv05B09HF_msN5pygl4Ga", "percentage": 100 } ] }'
You can list 1 to 10 destinations. Each must be one of your own bank accounts with the same Payee subject, and the percentages must add up to 100. shouldSendRemainderToWallet is always false for a Payee, because an unlinked Payee has no wallet.
To change the routing, send the full list again with POST /v3/payments/payout-settings. PATCH doesn't change Payee allocations.
To find the bank accounts you hold for a Payee, call GET /v3/finance/external-bank-accounts with both filter[subjectType][eq]=Payee and filter[subjectId][eq]=<payeeId>.
Step-up authentication
Creating, changing, verifying, or removing a bank account, and changing payout settings, require a session that completed multi-factor authentication recently. If it hasn't, you get:
// 403 Forbidden (trimmed)
{
"status": 403,
"code": "StepUpMfaRequired",
"extensions": { "challengeUri": "...", "challengeRequiredFor": "PayoutMethodChange" }
}Complete the challenge at extensions.challengeUri, then retry the same request. Plan for this in your UI: ask for the second factor before showing the bank form, not after the contractor submits it.
Webhooks
PayoutSettings.* and ExternalBankAccount.* events aren't available for subscription yet. Read the resources after each change instead.
Common mistakes
- Collecting card numbers in your own form. The API never accepts them. Use the hosted frame from the
linkstep. - Omitting
accountHolderType. It's required on manual bank account create. - Using
PATCHto change a Payee's payer-managed routing. Send the full list withPOST /v3/payments/payout-settings. - Percentages that don't add up to 100. Payee routing must allocate exactly 100%.
- Not retrying after step-up. A
403 StepUpMfaRequiredisn't a permanent denial. Complete the challenge and send the request again.
Related
Updated 10 days ago