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 itWhen to use itResources
The contractor, on their own AccountThe 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 PayeeThe 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

  1. 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" }
  2. Open the bank-linking widget with linkToken. The contractor signs in to their bank and picks an account.

  3. Send the result to POST /v3/finance/external-bank-accounts. For Plaid, send publicToken and plaidAccountId:

    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.

  1. POST /v3/payments/payment-cards creates a Pending card record. The body is optional (externalId, metadata). Idempotency-Key is required.
  2. POST /v3/payments/payment-cards/{paymentCardId}/link returns a one-time clientToken (provider: Footprint) for the hosted card frame. The contractor enters the card there.
  3. POST /v3/payments/payment-cards/{paymentCardId}/activate with {"accepted": true, "consentVersion": "..."} runs a zero-amount card check and activates the card only if it matches. Send If-Match with the card's current ETag.

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"
  }'
FieldWhat it does
standardDestinationWhere standard payouts go. null clears it.
instantDestinationWhere instant payouts go, usually a PaymentCard. null clears it.
preferredPayoutSpeedStandard or Instant. Independent of which destinations exist.
accountInstantPayoutFeeRead-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.

  1. Create the bank account with subject set 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"
      }'

    isDefault must be false (or omitted) for a Payee subject.

  2. 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 link step.
  • Omitting accountHolderType. It's required on manual bank account create.
  • Using PATCH to change a Payee's payer-managed routing. Send the full list with POST /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 StepUpMfaRequired isn't a permanent denial. Complete the challenge and send the request again.

Related


Did this page help you?