Payroll runs

Pay a set of approved payables in one funded Wingspan V3 payroll run, then reconcile each contractor's payment.

This page shows you how to pay many approved payables in one run: create a payroll run, finalize it to move money, and reconcile the result payable by payable. It replaces V1's POST /payments/pay-approved.

What a payroll run is

A payroll run selects payables and pays them in one funding movement. Wingspan debits your payroll funding source once for the run, then pays each payee from that funding. One run pays one currency.

Every run has a type:

typePaysWhat the run is made of
ContractorContractors and vendors (1099 in the US, T4A in Canada)Approved payables
EmployeeEmployees (W-2 in the US, T4 in Canada)Pay statements for a pay period

Earlier versions of the API called the Contractor type ContractorVendor. Update any code that sends or compares the old value. Most of this page covers Contractor runs. Employee runs are covered in Employee runs.

Before you begin

  • A payroll funding source. Set payrollSettings.defaultFundingSource on your payer settings. Finalizing fails with 422 without one. See Payroll settings and funding.
  • Approved payables. A payable is eligible for a run when it belongs to you, is opened, has payerApprovalStatus: Approved, is in the run's currency, and isn't already in another run. See Create a payable.
  • Eligible payees. Payees whose engagements have incomplete requirements can't be paid. See Find incomplete payables.

Run statuses

StatusMeaning
DraftCreated. Payables are reserved for the run. No money has moved.
PendingApprovalsEmployee runs only. The approval window is open.
FinalizedEmployee runs only. Approvals are closed and amounts are locked. No money has moved yet.
ProcessingFunding has started and payouts are queued or in progress.
PaidThe run is paid.
PartiallyPaidPart of the run is paid.
CancelledStopped before processing.
FailedFunding failed. Read failureReason.

A Contractor run goes Draft, then Processing when you finalize it. It stays Processing until its payouts settle. The payables in the run are the authoritative record of what each contractor was paid, so reconcile against them rather than waiting on the run status.

Step 1: Choose what to pay

Decide which payables go in the run. List your approved payables:

curl -g "https://api.wingspan.app/v3/payments/payables?filter[payerApprovalStatus][eq]=Approved&filter[status][eq]=Opened&page[size]=100" \
  -H "Authorization: Bearer $WINGSPAN_TOKEN"

A run selects payables in one of two ways:

selection.scopeWhat it selects
SpecificPayablesExactly the IDs in selection.payableIds. If any named payable isn't eligible, the whole create fails with 422 and no run is created.
ApprovedPayablesEvery opened and approved payable you have in the run's currency, whatever its due date.

Important: ApprovedPayables doesn't look at dueDate. If you approve payables ahead of time, it pays them now. Filter by due date yourself and use SpecificPayables when timing matters.

A third scope, PayPeriod, appears in the schema but isn't available yet.

Step 2: Create the run

Call POST /v3/payments/payroll-runs.

curl -X POST "https://api.wingspan.app/v3/payments/payroll-runs" \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Idempotency-Key: nw-run-2026-09-26" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "Contractor",
    "currency": "USD",
    "externalId": "NW-PAYROLL-2026-09-26",
    "selection": {
      "scope": "SpecificPayables",
      "payableIds": ["Hb4Xk9Qw2Rt7Vn1Mz8Pc3L", "Jd8Rw3Nz6Kq1Tx5Vb0Lm7Y"]
    }
  }'
// (trimmed)
{
  "id": "Wc5Ty2Hn8Qp4Zr1Xk7Mb9D",
  "type": "Contractor",
  "status": "Draft",
  "cycleType": "OffCycle",
  "currency": "USD",
  "externalId": "NW-PAYROLL-2026-09-26",
  "totalAmount": 3528.00,
  "events": { "draftAt": "2026-09-24T17:10:00Z" }
}

No money moves at this step. Each selected payable now carries the run's ID in payrollRunId.

If a SpecificPayables create fails with 422, the response doesn't say which payable was the problem. An ineligible payable, one already in another run, and an unknown ID look the same. Re-read the payables, drop any that are no longer opened and approved, and retry.

Step 3: Review the run

Before money moves, read the run with its breakdown using GET /v3/payments/payroll-runs/{payrollRunId}:

curl "https://api.wingspan.app/v3/payments/payroll-runs/Wc5Ty2Hn8Qp4Zr1Xk7Mb9D?expand=Breakdown" \
  -H "Authorization: Bearer $WINGSPAN_TOKEN"
// (trimmed)
{
  "id": "Wc5Ty2Hn8Qp4Zr1Xk7Mb9D",
  "status": "Draft",
  "totalAmount": 3528.00,
  "breakdown": {
    "payablesAmount": 3528.00,
    "payablesCount": 2,
    "deductionsCount": 0,
    "deductionsAmount": 0,
    "payablesStatuses": {
      "opened": { "count": 2, "amount": 3528.00 },
      "paymentInTransit": { "count": 0, "amount": 0 },
      "paid": { "count": 0, "amount": 0 },
      "pending": { "count": 0, "amount": 0 },
      "returned": { "count": 0, "amount": 0 }
    }
  }
}

There's no separate preview endpoint; this read is the dry run. To back out, cancel the run (see Cancel or delete a run) and the payables go back to the approved pool.

Step 4: Finalize the run

Finalizing with POST /v3/payments/payroll-runs/{payrollRunId}/finalize funds the run and queues a payout for each payable. This is the only step where money moves.

curl -X POST "https://api.wingspan.app/v3/payments/payroll-runs/Wc5Ty2Hn8Qp4Zr1Xk7Mb9D/finalize" \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Idempotency-Key: nw-run-2026-09-26-finalize"
// (trimmed)
{
  "id": "Wc5Ty2Hn8Qp4Zr1Xk7Mb9D",
  "status": "Processing",
  "events": { "draftAt": "2026-09-24T17:10:00Z", "processingAt": "2026-09-24T17:14:22Z", "estimatedDepositAt": "2026-09-28T14:00:00Z" }
}

What finalize does, in order:

  1. Checks that your payroll funding source is set up. If not, it returns 422 and the run stays Draft.
  2. Claims the run by moving it from Draft to Processing. A repeated or concurrent finalize returns 409, so retries can't pay twice.
  3. Re-checks each payable. Any payable that stopped being eligible since you created the run (it was paid directly, cancelled, or unapproved) is released and not funded. If nothing is left, finalize returns 422 and the run goes back to Draft.
  4. Debits your funding source for the run.
  5. Queues one payout per payable. Payouts wait for the funding to clear, so no one is paid before your debit settles.

events.estimatedDepositAt is when the run's funding is estimated to clear, which is when money starts moving to contractors. It isn't the date money lands in their accounts. payDate stays the scheduled date.

Finalize is a high-risk action. Without recent multi-factor authentication it returns 403 with code: StepUpMfaRequired.

Step 5: Reconcile

List the run's payables to see each contractor's outcome:

curl -g "https://api.wingspan.app/v3/payments/payables?filter[payrollRunId][eq]=Wc5Ty2Hn8Qp4Zr1Xk7Mb9D&page[size]=100" \
  -H "Authorization: Bearer $WINGSPAN_TOKEN"

Each payable moves through PaymentInTransit to Paid, or to Returned. breakdown.payablesStatuses on the run gives the counts and totals per status:

BucketMeaning
openedSelected, not yet paid.
paymentInTransitPayment in progress.
paidPaid.
pendingThe payee wasn't eligible at pay time, such as a missing payout method or unmet requirement. Fix the payee; don't cancel the run.
returnedThe payment was returned. Create a new payable for the replacement.

Payables that were cancelled or marked paid off-platform while in the run aren't counted in any bucket, so the buckets can add up to less than payablesCount.

For push updates, subscribe to Payable.Paid, Payable.PartiallyPaid, and Payable.Returned. Payroll runs don't have subscribable webhooks yet.

Paid is Wingspan's internal state. DepositConfirmed means Wingspan's originating bank or provider reported its terminal processed state. It never means the recipient's bank received the money, funds are available, or the payment can't be returned. See Paid vs DepositConfirmed.

When a run fails

A run in Failed carries failureReason with a code and a message you can show to users as is.

failureReason.codeWhat happened
FundingSourceMissingNo payroll funding source was configured.
FundingAccountNotReadyThe funding account wasn't ready to be debited.
InsufficientFundsThe funding account didn't have enough money.
FundingSubmissionFailedThe funding debit couldn't be submitted.
FundingDebitReturnedThe debit settled and your bank later returned it. This is the only code where your money actually moved. Contact Wingspan support to reprocess.

message names the cause and the fix. Treat an unrecognized code as a generic failure and show message. For every code except FundingDebitReturned, no money left your account, so you can fix the cause and run payroll again. Failed is terminal: the run's payables are released back to the approved pool, and you select them into a new run.

List failed runs with GET /v3/payments/payroll-runs?filter[failureCode][anyOf][]=InsufficientFunds.

Cancel or delete a run

You can change a run's externalId and metadata with PATCH /v3/payments/payroll-runs/{payrollRunId}. Nothing else on a run is editable.

List runs

GET /v3/payments/payroll-runs lists runs of both types, most recent payDate first. Filters:

  • filter[type][eq]: Employee or Contractor
  • filter[status][anyOf][]: any run status
  • filter[cycleType][anyOf][]: OnCycle or OffCycle
  • filter[payDate][gte] and filter[payDate][lte]: a pay-date range
  • filter[externalId][eq]: your ID
  • filter[failureCode][anyOf][]: failed runs by reason

To change the order, sort by one field: sort[payDate]=asc or desc, or sort[totalAmount]=asc or desc. Send the same sort on every page request, because the page token is tied to it.

Employee runs

Employee runs pay W-2 or T4 employees from pay statements. They follow a longer path because amounts are approved and locked before money moves: Draft, PendingApprovals, Finalized, Processing, then Paid.

  • On-cycle Employee runs are created automatically from your Employee payroll schedule. You can't create one through the API.
  • You can create an off-cycle run against an existing pay period: { "type": "Employee", "payPeriodId": "Pp9Lm3Wx6Qk1Zt8Rv2Nb4J" }. Employee runs are USD only. Only one off-cycle run that isn't finished can exist per pay period; a second returns 409.
  • For an Employee run, finalize closes the approval window and locks amounts. No money moves at that point; processing follows on the schedule. Only one Employee run per Account can be Finalized or Processing at a time, so a second finalize returns 409 until the first is paid.
  • Read the run's pay statements with GET /v3/payments/pay-statements filtered by filter[payrollRunId][eq], and pay periods with GET /v3/payments/pay-periods.
  • expand=Breakdown on an Employee run returns gross pay, net pay, employer and employee tax entries, and the pay statement count.

Employee payroll needs employer payroll setup with Wingspan before you can use it. Contact your Wingspan account manager to get started.

Common mistakes

  • Waiting for the run to reach Paid. Reconcile per payable with filter[payrollRunId][eq].
  • Sweeping future-dated payables. ApprovedPayables ignores due dates.
  • Retrying a failed run. A Failed run can't be finalized again. Create a new run with the released payables.
  • Mixing currencies. One run pays one currency. Create one run per currency.
  • Cancelling a run because a payee is pending. Fix the payee's eligibility instead. See Find incomplete payables.

Related pages


Did this page help you?