Pay employees
Turn the hours your employees log into pay statements, and follow each Employee payroll run from draft to paid with the Wingspan V3 API.
Wingspan pays employees in Employee payroll runs. Your employee pay schedule creates a pay period and an on-cycle run for it, employees log their hours against that period, and each work log you approve becomes a pay statement in the run. Wingspan then pays the run on its pay date. This page shows you how each step works and what to do at each one.
How pay periods and runs work
Each pay period on your employee schedule gets one on-cycle payroll run. Wingspan creates the next pay period and its run as each period closes, so there's always a run open for the current period. Find it with GET /v3/payments/payroll-runs.
curl -G https://api.wingspan.app/v3/payments/payroll-runs \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
--data-urlencode "filter[type][eq]=Employee"// 200 OK (trimmed)
{
"data": [
{
"id": "TVDpIcgEQZ0E64NXJeC2Ps",
"type": "Employee",
"status": "Draft",
"cycleType": "OnCycle",
"payrollNumber": "K4TQZ-001-PR",
"currency": "USD",
"payPeriodId": "ha2GMh5tQiSpEyCNFbzd5D",
"payPeriodStartDate": "2026-11-06T12:00:00.000Z",
"payPeriodEndDate": "2026-11-12T12:00:00.000Z",
"payDate": "2026-11-13T12:00:00.000Z",
"approvalDeadline": "2026-11-12T12:00:00.000Z",
"totalAmount": 0
}
],
"pagination": { "nextPageToken": "" }
}| Field | What it means |
|---|---|
payPeriodId | The pay period the run pays. Use it when you log hours and preview pay. |
payDate | The day employees are paid. |
approvalDeadline | The last time pay statements can be added to or changed in the run. Approve hours before it. |
totalAmount | What Wingspan debits from your payroll funding source for the run: gross pay plus your employer taxes. |
List pay periods themselves with GET /v3/payments/pay-periods.
Log hours
Hours go on a work log for one assignment and one pay period. Employees can log their own hours in Wingspan or in your app, then submit the work log for approval with POST /v3/payments/work-logs/{workLogId}/submit. When the pay period ends, Wingspan moves any work logs that are still being edited into review, so nothing is left out of the run.
To send hours from your own system, such as a scheduling or timekeeping tool, create the work log yourself with POST /v3/payments/work-logs. A work log you create is ready for your approval right away: it starts in InReview, so you don't submit it.
curl -X POST https://api.wingspan.app/v3/payments/work-logs \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"payeeEngagementId": "j88P41LYjxNCLZ6CmGNHqH",
"payPeriodId": "ha2GMh5tQiSpEyCNFbzd5D",
"externalId": "timesheet-1042-2026-11-12"
}'// 201 Created (trimmed)
{
"id": "mvG1MkSioJyHDrW5vRGlMf",
"workLogNumber": "Q2NVR-001L",
"status": "InReview",
"payeeEngagementId": "j88P41LYjxNCLZ6CmGNHqH",
"payPeriodId": "ha2GMh5tQiSpEyCNFbzd5D",
"totalAmount": 0,
"currency": "USD"
}Then add each entry as a work item with POST /v3/payments/work-items. The attributes follow the engagement's work definition. The default hourly work definition takes hours and overtimeHours.
curl -X POST https://api.wingspan.app/v3/payments/work-items \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"workLogId": "mvG1MkSioJyHDrW5vRGlMf",
"workDefinitionId": "RG230mb8G9wPIKxfsUR3lu",
"timestamp": "2026-11-10T16:00:00Z",
"attributes": { "hours": 8, "overtimeHours": 2 }
}'// 201 Created (trimmed)
{
"id": "aixxMYORNYvz7kcS0CjYUK",
"workLogId": "mvG1MkSioJyHDrW5vRGlMf",
"status": "Active",
"attributes": { "hours": 8, "overtimeHours": 2 },
"calculations": {
"result": 220,
"items": [
{ "earningType": "Regular", "hoursAttribute": "hours", "interpolatedFormula": "8 * 20", "result": 160 },
{ "earningType": "Overtime", "hoursAttribute": "overtimeHours", "interpolatedFormula": "2 * 30", "result": 60 }
]
}
}Wingspan prices each entry from the assignment's rate card and labels each part with the earning type it becomes on the pay statement. The work log's totalHours and totalAmount update as you add entries. Work logs and rate cards covers work definitions, rate cards, and requesting edits.
Approve hours
Approve a work log with POST /v3/payments/work-logs/{workLogId}/approve. Approving it creates a pay statement for the employee in that pay period's run, and the run's totalAmount goes up by the statement's gross pay plus your employer taxes. For the work log above, that's $220.00 of gross pay plus $25.85 of employer taxes, so the run's totalAmount becomes 245.85.
curl -X POST https://api.wingspan.app/v3/payments/work-logs/mvG1MkSioJyHDrW5vRGlMf/approve \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"// 200 OK (trimmed)
{
"id": "mvG1MkSioJyHDrW5vRGlMf",
"payeeEngagementId": "j88P41LYjxNCLZ6CmGNHqH",
"payPeriodId": "ha2GMh5tQiSpEyCNFbzd5D",
"status": "Approved",
"totalAmount": 220.00,
"currency": "USD",
"convertedResourceType": "PayStatement",
"convertedResourceId": "GUAJztqobp9aoa1fCRJ92f"
}Approve every work log before the run's approvalDeadline. To have Wingspan approve submitted work logs for you, set autoApproveWorkLogs.enabled to true with PATCH /v3/payments/work-logging-configs. See Work logs and rate cards.
Before you approve, you can see what the employee will take home with a gross-to-net preview. See Pay statements.
To keep hours from being paid, reject the work log with POST /v3/payments/work-logs/{workLogId}/reject and a reason. A rejected work log is final.
Employee run statuses
stateDiagram-v2 [*] --> Draft Draft --> PendingApprovals: pay period ends PendingApprovals --> Finalized: approval deadline, or finalize Finalized --> Processing: funding debit starts Processing --> Paid Processing --> PartiallyPaid Draft --> Cancelled PendingApprovals --> Cancelled Finalized --> Cancelled Processing --> Failed
| Status | What it means |
|---|---|
Draft | The pay period is in progress. Approving a work log adds its pay statement to the run. |
PendingApprovals | The run is open for approval. Approve the period's work logs before the approvalDeadline. |
Finalized | Approvals are closed and the amounts are locked. No money has moved yet. |
Processing | Wingspan has debited your funding source and is paying employees. Deposits land on the pay date. |
Paid | Employees have been paid. Each pay statement shows Paid. |
PartiallyPaid | Some of the run's pay statements were paid. Check each statement's status. |
Cancelled | The run was cancelled and won't be paid. |
Failed | Funding failed. failureReason says why. |
At the approval deadline, Wingspan finalizes the run for you. To close approvals early, call POST /v3/payments/payroll-runs/{payrollRunId}/finalize. If you call with a person's login session, finalizing needs a recent MFA step-up: without one it returns 403 StepUpMfaRequired. Create and verify an MFA challenge with requiredFor: "HighRiskWriteAction", then retry. A backend calling with an API key has no session to step up. Your account can have one Employee run Finalized or Processing at a time.
Read a run's totals with ?expand=Breakdown on GET /v3/payments/payroll-runs/{payrollRunId}.
curl https://api.wingspan.app/v3/payments/payroll-runs/TVDpIcgEQZ0E64NXJeC2Ps?expand=Breakdown \
-H "Authorization: Bearer $WINGSPAN_TOKEN"// 200 OK (trimmed)
{
"id": "TVDpIcgEQZ0E64NXJeC2Ps",
"type": "Employee",
"status": "Draft",
"totalAmount": 245.85,
"breakdown": {
"payStatementsCount": 1,
"grossPay": 220,
"netPay": 200.31,
"grossLiabilityAmount": 245.85,
"payeeTaxes": [
{ "taxCodeId": "OhbVrpoiVgRV5IfLBcbfno", "amount": 13.64, "ytdAmount": 13.64, "qtdAmount": 13.64, "mtdAmount": 13.64 }
],
"payerTaxes": [
{ "taxCodeId": "kIUpkDyr7OSJoRu1XXdo0c", "amount": 13.64, "ytdAmount": 13.64, "qtdAmount": 13.64, "mtdAmount": 13.64 }
]
}
}grossLiabilityAmount is what the run costs you: gross pay plus your employer taxes. Each tax entry carries the run's amount and the year-, quarter-, and month-to-date totals.
Funding and failures
Wingspan debits the run's totalAmount from your payroll funding source. If the debit can't go through, the run moves to Failed with one of these failureReason codes.
failureReason code | What happened | What to do |
|---|---|---|
FundingSourceMissing | The account has no payroll funding source. | Set one. See Payroll settings and funding. |
FundingAccountNotReady | The funding account isn't verified or active yet. | Finish setting up the funding account, then run payroll again. |
InsufficientFunds | The funding account didn't have enough money. | Add funds, then run payroll again. |
FundingSubmissionFailed | Wingspan couldn't submit the debit. | Run payroll again. |
FundingDebitReturned | The debit went through and your bank later returned it. | Contact support to pay the run again. |
For every code except FundingDebitReturned, no money left your account.
Off-cycle runs
To pay outside the on-cycle run for a pay period, create an off-cycle run with POST /v3/payments/payroll-runs. It starts in Draft.
curl -X POST https://api.wingspan.app/v3/payments/payroll-runs \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "type": "Employee", "payPeriodId": "ha2GMh5tQiSpEyCNFbzd5D" }'// 201 Created (trimmed)
{
"id": "4IWrXPvhsBkDa9U4UqGWlG",
"type": "Employee",
"status": "Draft",
"cycleType": "OffCycle",
"payrollNumber": "K4TQZ-002-PR",
"payPeriodId": "ha2GMh5tQiSpEyCNFbzd5D",
"payDate": "2026-11-13T12:00:00.000Z",
"approvalDeadline": "2026-11-12T12:00:00.000Z",
"totalAmount": 0
}A pay period can have one open off-cycle Employee run at a time. Creating a second returns 409 ResourceConflict. Delete an off-cycle run while it's still a draft with DELETE /v3/payments/payroll-runs/{payrollRunId}.
Cancel a run
Cancel a run that hasn't started paying with POST /v3/payments/payroll-runs/{payrollRunId}/cancel. To take one employee out of a run instead, cancel their pay statement. See Pay statements.
Track a run
To follow a run, read it. Its events object records when it reached each status: draftAt, pendingApprovalsAt, finalizedAt, processingAt, paidAt, cancelledAt, and failedAt. Once the run is paid, each pay statement's own events records when the employee's deposit was sent (paidAt) and when it settled (depositedAt).
What can go wrong
| Status and code | Cause | Fix |
|---|---|---|
422 payments.WorkLogPayrollProfileMissing on approve | The employee's account isn't linked, or there's no worksite on the assignment and no home address or primary worksite to tax them from. | Link the employee's account, or add a worksite. The work log stays InReview until you approve it again. |
403 StepUpMfaRequired on finalize | The login session has no recent MFA step-up. | Complete an MFA challenge with requiredFor: "HighRiskWriteAction", then retry. |
409 on finalize | Another Employee run is Finalized or Processing, or this run isn't in a status that can be finalized. | Wait for the other run to be paid, or check this run's status. |
409 ResourceConflict on an off-cycle run | The pay period already has an open off-cycle Employee run. | Use the open run, or wait until it's paid. |
409 on cancel | The run is already Processing or Paid. | Contact support. |
403 payments.WorkLogDeleteForbidden | You tried to delete a work log as the employer. | Reject it with a reason instead. |
422 on submit | The work log is already InReview. Work logs you create start there. | Approve it. You only submit work logs the employee is still editing. |
Related pages
Updated 11 days ago