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:
type | Pays | What the run is made of |
|---|---|---|
Contractor | Contractors and vendors (1099 in the US, T4A in Canada) | Approved payables |
Employee | Employees (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.defaultFundingSourceon your payer settings. Finalizing fails with422without 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
| Status | Meaning |
|---|---|
Draft | Created. Payables are reserved for the run. No money has moved. |
PendingApprovals | Employee runs only. The approval window is open. |
Finalized | Employee runs only. Approvals are closed and amounts are locked. No money has moved yet. |
Processing | Funding has started and payouts are queued or in progress. |
Paid | The run is paid. |
PartiallyPaid | Part of the run is paid. |
Cancelled | Stopped before processing. |
Failed | Funding 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.scope | What it selects |
|---|---|
SpecificPayables | Exactly the IDs in selection.payableIds. If any named payable isn't eligible, the whole create fails with 422 and no run is created. |
ApprovedPayables | Every opened and approved payable you have in the run's currency, whatever its due date. |
Important:
ApprovedPayablesdoesn't look atdueDate. If you approve payables ahead of time, it pays them now. Filter by due date yourself and useSpecificPayableswhen 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:
- Checks that your payroll funding source is set up. If not, it returns
422and the run staysDraft. - Claims the run by moving it from
DrafttoProcessing. A repeated or concurrent finalize returns409, so retries can't pay twice. - 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
422and the run goes back toDraft. - Debits your funding source for the run.
- 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:
| Bucket | Meaning |
|---|---|
opened | Selected, not yet paid. |
paymentInTransit | Payment in progress. |
paid | Paid. |
pending | The payee wasn't eligible at pay time, such as a missing payout method or unmet requirement. Fix the payee; don't cancel the run. |
returned | The 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.code | What happened |
|---|---|
FundingSourceMissing | No payroll funding source was configured. |
FundingAccountNotReady | The funding account wasn't ready to be debited. |
InsufficientFunds | The funding account didn't have enough money. |
FundingSubmissionFailed | The funding debit couldn't be submitted. |
FundingDebitReturned | The 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
POST /v3/payments/payroll-runs/{payrollRunId}/cancelstops a run that hasn't started processing. It returns409once the run isProcessingorPaid.DELETE /v3/payments/payroll-runs/{payrollRunId}removes aDraftrun. Deleting aContractorrun releases its payables back to the approved pool. Anything other thanDraftreturns409; cancel it instead.
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]:EmployeeorContractorfilter[status][anyOf][]: any run statusfilter[cycleType][anyOf][]:OnCycleorOffCyclefilter[payDate][gte]andfilter[payDate][lte]: a pay-date rangefilter[externalId][eq]: your IDfilter[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
Employeeruns are created automatically from yourEmployeepayroll 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" }.Employeeruns are USD only. Only one off-cycle run that isn't finished can exist per pay period; a second returns409. - For an
Employeerun, finalize closes the approval window and locks amounts. No money moves at that point; processing follows on the schedule. Only oneEmployeerun per Account can beFinalizedorProcessingat a time, so a second finalize returns409until the first is paid. - Read the run's pay statements with
GET /v3/payments/pay-statementsfiltered byfilter[payrollRunId][eq], and pay periods withGET /v3/payments/pay-periods. expand=Breakdownon anEmployeerun 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 withfilter[payrollRunId][eq]. - Sweeping future-dated payables.
ApprovedPayablesignores due dates. - Retrying a failed run. A
Failedrun 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
Updated 10 days ago