Void a Payroll

Void a Payroll

Reverses payments from a payroll that has already been finalized — for example, when a paystub was calculated incorrectly or paid to the wrong person. Voiding doesn't cancel or delete the original paystub. It reverses it: Worklio starts a new payroll run that contains a negative copy of each paystub you void, and optionally a replacement paystub. The original paystub isn't edited and stays in the original payroll with its original amounts.

🚧

Voiding a direct deposit paystub can debit the employee's bank account. Read What happens to the payment before you call this endpoint.

Requires a bearer access token (see How to Get API Access). Only Admin and System level users can call the void endpoints; employee-level tokens get a 403 Forbidden, even for their own company. CLIENT_ID is the company identifier returned as id when you create the company.

Steps at a glance

StepWhat you doRequired
1. Find the payroll to voidGet the payroll's runId and pay date.Yes
2. Find the employee's paystubGet the employeeId and paystubId to void.Yes
3. Start the void runCreate the void run. You get a new runId.Yes
4. Change the pay on the replacement paystubGet the replacement paystub and PATCH its pay lines.Optional. Skip it to keep the original pay.
5. Advance and finalize the runComplete the void.Yes

How voiding works

When you void a paystub, Worklio:

  1. Creates a void paystub in the new void run. It is a copy of the original with every amount negated, including gross pay, taxes, deductions, and the payment itself. It keeps the original check number.
  2. Depending on the mode you choose, creates a replacement paystub in the same run.
  3. When you finalize the void run, flags the original paystub as voided. The original payroll's overview and paystub amounts don't change. You can tell a paystub has been voided because it no longer appears in the lookup endpoints, the void paystub is in the void run, and voiding it again fails with AlreadyVoided.

There are three modes. The mode is set by the reissue field and by whether the original payroll's pay date is in the same year as the void run's payDate.

ModeWhenWhat the run contains
Void onlyreissue is false, and the original paystub is from the same year as the void run's payDate.A void paystub only.
Void and re-keyreissue is true (the default), and the original paystub is from the same year as the void run's payDate.A void paystub, plus a new paystub that starts from the original's pay lines and is recalculated in the void run. The run pauses at the timesheet step. You can change the pay on the new paystub before it is paid, or skip that step to keep the original pay.
Void and reissueThe original paystub is from an earlier year than the void run's payDate. reissue must be true.A void paystub, plus a full, unchanged copy of the original paystub.

In the API, reissue: true covers both replacement modes. Whether you get a re-key or a reissue depends only on the year of the original paystub.

A paystub that is itself a void can't be reissued. Voiding it again with reissue set to false reverses the void.

What happens to the payment

The void paystub reverses the original payment. What that means depends on how the employee was paid.

Original paymentVoid onlyVoid and re-keyVoid and reissue
CheckThe check is marked void. No money moves.The check is marked void and a new check is issued for the re-keyed paystub.The check is marked void and a new check is issued for the reissued paystub.
Direct depositA negative payment is created for the full amount. It is sent as a debit to the employee's bank account.A negative payment for the original amount and a new payment for the re-keyed amount are created with the same pay date. The net change in the employee's account is the difference between the two.A negative payment and an identical positive payment are created. The net change in the employee's account is zero.

Notes:

  • Voiding a check doesn't recover a check the employee has already cashed.
  • The debit goes to the bank account that received the original payment. The replacement payment goes to the employee's pay allocations as they are when the void run is calculated. If the employee's pay allocations have changed since the original payroll, the debit and the replacement payment can go to different accounts and won't offset each other.
  • A void doesn't cancel a payment that was already sent. It creates a new, opposite payment.

Example: voiding a $2,000.00 paycheck

An employee was paid on August 14, 2026. The paystub has $2,000.00 gross pay and $700.00 of employee taxes, so net pay is $1,300.00, paid by direct deposit. The employer's share of Social Security and Medicare is $153.00. On October 15, 2026, you void the paystub with reissue set to false.

Original paystub (August 14)Void paystub (October 15)Total
Gross pay2,000.00−2,000.000.00
Employee taxes700.00−700.000.00
Net pay (direct deposit)1,300.00−1,300.000.00
Employer Social Security and Medicare153.00−153.000.00
  • The original paystub isn't edited. It stays in the August payroll with its original amounts.
  • The void paystub belongs to the October run. Its taxes are negative amounts dated on the void run's pay date, not in August.
  • The employee's account is debited $1,300.00.

If you void and re-key instead, and the corrected paystub comes to $1,800.00 gross, $630.00 of employee taxes, and $1,170.00 net, the $1,300.00 debit and the $1,170.00 new payment are in the same run. The employee's account is debited the $130.00 difference. The steps below follow this re-key example.

1. Find the payroll to void

Find the payroll that contains the paystub you want to void. All lookup endpoints in steps 1 and 2 are GET requests with no body, and they only return payrolls from the payroll policy in the path.

PurposeEndpoint
Years that have voidable payrollsGET /wep/companies/{CLIENT_ID}/policies/{POLICY_ID}/void
Payrolls in a yearGET /wep/companies/{CLIENT_ID}/policies/{POLICY_ID}/void/{YEAR}/payrolls
Payrolls in a yearGET /wep/companies/{CLIENT_ID}/payroll/void/{YEAR}/payrolls

POLICY_ID is the payroll policy's id — see Payroll Policies. YEAR is a four-digit year, such as 2026.

Only finalized payrolls and paystubs that haven't been voided are returned.

Response fields

Years — an array of integers, newest first.

Payrolls — an array of objects:

FieldTypeDescription
runIdintegerThe payroll run's ID.
payDaystring (date, YYYY-MM-DD)The date employees were paid for the run.
payrollTypeinteger (enum)The type of the original run.
totalGrossnumberTotal gross pay for the run.
deadlinestring (date-time)Processing deadline for the run.
daysForProcessintegerNumber of days allowed to process payments.

Note the payDay of the payroll you are voiding. In step 2, each paystub has its own payDay, which you use to pick the paystub that belongs to this payroll.

Example request

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID;
const url = `https://api.worklio.com/wep/companies/${CLIENT_ID}/payroll/void/2026/payrolls`;


async function getPayrollsToVoid() {
  const response = await fetch(url, {
    method: "GET",
    headers: {
      accept: "application/json",
      "api-version": "2.0",
      authorization: `Bearer ${TOKEN}`,
      "content-type": "application/json",
      "x-api-version": "2.0"
    },
  });

  console.log(response.status);
  const raw = await response.text();
  console.log(JSON.stringify(JSON.parse(raw), null, 2));
}

getPayrollsToVoid();
# TODO: add example request — list payrolls in a year

Example response

[
  {
    "runId": 1002171,
    "payDay": "2026-10-09",
    "deadline": "2026-10-05T21:00:00Z",
    "daysForProcess": 4,
    "payrollType": 2,
    "totalGross": 90000
  }
]

2. Find the employee's paystub

You need the employeeId and paystubId of each paystub you want to void.


GET /wep/companies/{CLIENT_ID}/payroll/void/{YEAR}/employees/{EMPLOYEE_ID}

Response fields

Employees — an array of objects:

FieldTypeDescription
idintegerThe employee's ID. Pass it as employeeId in paystubs.
firstNamestringFirst name.
middleNamestring | nullMiddle name.
lastNamestringLast name.

Paystubs — an array of objects, newest first:

FieldTypeDescription
paystubIdintegerThe paystub's ID. Pass it as paystubId in paystubs.
payrollNumberstringThe paystub's check number, formatted.
payDaystring (date, YYYY-MM-DD)The date the employee was paid.
grossnumberGross pay on the paystub.
netPaynumberNet pay on the paystub.
deadlinestring (date-time)Processing deadline for the original run.
daysForProcessintegerNumber of days allowed to process payments.

Example request

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID;
const EMPLOYEE_ID = process.env.EMPLOYEE_ID
const url = `https://api.worklio.com/wep/companies/${CLIENT_ID}/payroll/void/2026/employees/${EMPLOYEE_ID}`;


async function getEmployeePaystubsToVoid() {
  const response = await fetch(url, {
    method: "GET",
    headers: {
      accept: "application/json",
      "api-version": "2.0",
      authorization: `Bearer ${TOKEN}`,
      "content-type": "application/json",
      "x-api-version": "2.0"
    },
  });

  console.log(response.status);
  const raw = await response.text();
  console.log(JSON.stringify(JSON.parse(raw), null, 2));
}

getEmployeePaystubsToVoid()();
# TODO: add example request — list one employee's paystubs for a year

Example response

[
  {
    "paystubId": 10239,
    "payDay": "2026-10-09",
    "deadline": "2026-10-05T21:00:00Z",
    "daysForProcess": 4,
    "payrollNumber": "00105800010000004",
    "netPay": 1300,
    "gross": 2000
  }
]

3. Start the void run

POST https://api.worklio.com/wep/companies/{CLIENT_ID}/payroll/void

Parameters

FieldTypeRequiredDescription
voidYearint32 | nullYes, in practiceThe year of the payrolls you are voiding. Defaults to 2024 if you omit it, which gives wrong results for any other year — always send it.
startDatestring (date, YYYY-MM-DD)YesPay period start date of the void run.
endDatestring (date, YYYY-MM-DD)YesPay period end date of the void run.
payDatestring (date, YYYY-MM-DD)YesPay date of the void run. Replacement and offsetting payments use this date.
reissueboolean | nullNotrue (the default) creates a replacement paystub for each voided paystub. false voids only. Must be true when voidYear is earlier than the year of payDate. See How voiding works.
paystubsarray of objectsYesThe paystubs to void. See below.

startDate, endDate, and payDate describe the void run, not the original payroll.

paystubs object

FieldTypeRequiredDescription
employeeIdint32YesThe employee's ID from step 2.
paystubIdint32YesThe ID of the paystub to void, from step 2.

Example request

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID;
const url = `https://api.worklio.com/wep/companies/${CLIENT_ID}/payroll/void`;

const payload = {
  voidYear: 2026,
  startDate: "2026-10-15",
  endDate: "2026-10-15",
  payDate: "2026-10-15",
  reissue: true,
  paystubs: [
    {
      employeeId: 4281,
      paystubId: 10238
    }
  ]
};

async function startVoidPayroll() {
  const response = await fetch(url, {
    method: "POST",
    headers: {
      accept: "application/json",
      "api-version": "2.0",
      authorization: `Bearer ${TOKEN}`,
      "content-type": "application/json",
      "x-api-version": "2.0"
    },
    body: JSON.stringify(payload)
  });

  console.log(response.status);
  const raw = await response.text();
  console.log(JSON.stringify(JSON.parse(raw), null, 2));
}

startVoidPayroll();
curl -s -X POST "https://api.worklio.com/wep/companies/$CLIENT_ID/payroll/void" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "content-type: application/json" \
  -H "x-api-version: 2.0" \
  -d '{
    "voidYear": 2026,
    "startDate": "2026-10-15",
    "endDate": "2026-10-15",
    "payDate": "2026-10-15",
    "reissue": true,
    "paystubs": [
      {
        "employeeId": 4281,
        "paystubId": 10238
      }
    ]
  }' | jq .

Example response

{
  "companyId": 1030,
  "runId": 1002191,
  "payrollType": 2,
  "startedOn": "2026-10-15",
  "payDay": "2026-10-15",
  "periodStart": "2026-10-15",
  "periodEnd": "2026-10-15",
  "daysToProcessPayments": 4,
  "deadline": "2026-10-11T21:00:00Z",
  "currentUIStep": 0,
  "currentProcStep": 501,
  "currentProcStepDesc": "Payroll Engine Activated",
  "currentProcStepProgress": 0,
  "currentProcStepStatus": 1,
  "direction": 0,
  "isPayrollBlocked": false,
  "canUserOverride": false,
  "errors": [],
  "warnings": []
}

The response is the same run status object returned by Run Payroll. runId is the ID of the void run. It is a new ID, not the runId of the payroll you found in step 1. Use it for every later call.

To void without a replacement paystub, send "reissue": false and skip step 4.

Error response — reissue not set for a prior-year void

{
  "status": 0,
  "code": "500",
  "errorCode": null,
  "message": "10/15/2026 - 11:14:55 AM : Reissue is required for voiding payroll from previous year",
  "stackTrace": "",
  "pagination": {
    "pageNo": 0,
    "pageSize": 0,
    "totalRecords": 0,
    "totalPages": 0,
    "dataToken": ""
  },
  "validationErrors": null
}

Returned when reissue is false and voidYear isn't the same year as payDate.

4. (Optional) Change the pay on the replacement paystub

You can skip this step. If you don't change anything, the replacement paystub keeps the original paystub's pay and you go straight to step 5.

Use this step when the original paystub was paid too much or too little and you want the replacement paystub to carry the correct pay. Skip it if you only want to void, or if the replacement paystub should have the same pay as the original.

This step applies only to a void and re-key run: reissue is true and voidYear is the same year as payDate. A void and reissue run, and a void-only run, have no editable paystub.

Before you start:

  • You can change pay only while the run is paused at the timesheet step. After you advance past it, the request fails with TimeEntryIsNotEditable.
  • Void paystubs are read-only and aren't returned by this step's endpoints.
  • You can change only the pay lines you can edit in a regular payroll. System-generated pay lines can't be changed.
  • Worklio recalculates taxes on the new amounts. You don't enter taxes.

4a. Get the replacement paystub

GET https://api.worklio.com/wep/companies/{CLIENT_ID}/payroll/{RUN_ID}/paystubs

RUN_ID is the runId of the void run from step 3. The response lists the editable paystubs in the void run, which are the replacement paystubs. Note the paystub id and the id of each pay line you want to change.

The paystub id returned here belongs to the replacement paystub in the void run. It is not the paystubId of the original paystub you voided in step 3.

Example request

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID;
const RUN_ID = 1002191;
const url = `https://api.worklio.com/wep/companies/${CLIENT_ID}/payroll/${RUN_ID}/paystubs`;

async function getVoidRunPaystubs() {
  const response = await fetch(url, {
    method: "GET",
    headers: {
      accept: "application/json",
      "api-version": "2.0",
      authorization: `Bearer ${TOKEN}`,
      "x-api-version": "2.0"
    }
  });

  console.log(response.status);
  const raw = await response.text();
  console.log(JSON.stringify(JSON.parse(raw), null, 2));
}

getVoidRunPaystubs();
curl -s -X GET "https://api.worklio.com/wep/companies/$CLIENT_ID/payroll/1002191/paystubs" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0" | jq .

Example response

[
  {
    "employeeName": "Brown, Michael",
    "id": 10264,
    "compensation": 1,
    "allowedNetPayAlloc": true,
    "employeeId": 4281,
    "paymentMethod": 1,
    "paylines": [
      {
        "id": 10291,
        "rate": 50,
        "salaryWithHours": {
          "amount": 2000,
          "hours": 40
        },
        "wcCode": "0000"
      }
    ]
  }
]

4b. Update the pay lines

PATCH https://api.worklio.com/wep/companies/{CLIENT_ID}/payroll/{RUN_ID}/paystubs/{PAYSTUB_ID}

PAYSTUB_ID is the paystub id from step 4a.

Send only the pay lines you want to change. Each pay line is identified by its id, and the body contains the fields for the line's pay type.

FieldTypeRequiredDescription
paylinesarray of objectsYesThe pay lines to change.
paylines[].idintegerYesThe pay line's ID from the step 4a response.
paylines[].ratenumberNoThe pay rate.
paylines[].hourly.hoursnumberNoHours on an hourly pay line.
paylines[].overtime.hoursnumberNoHours on an overtime pay line.
paylines[].pto.hoursnumberNoHours on a paid time off pay line.
paylines[].sickTime.hoursnumberNoHours on a sick time pay line.
paylines[].unitsWithHoursobjectNohours and units on a piece-rate pay line.
paylines[].salaryWithHoursobjectNoamount and hours on a salary pay line.
paylines[].bonus.amount, commission.amount, reimbursement.amount, other.amountnumberNoThe amount on a bonus, commission, reimbursement, or other pay line.

The body can also contain paymentMethod and deductions.

Example request

In this example, the original weekly salary of $2,000.00 should have been $1,800.00.

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID;
const RUN_ID = 1002191;
const PAYSTUB_ID = 10264;
const url = `https://api.worklio.com/wep/companies/${CLIENT_ID}/payroll/${RUN_ID}/paystubs/${PAYSTUB_ID}`;

const payload = {
  paylines: [
    {
      id: 10291,
      salaryWithHours: { amount: 1800 }
    }
  ]
};

async function updatePaystub() {
  const response = await fetch(url, {
    method: "PATCH",
    headers: {
      accept: "application/json",
      "api-version": "2.0",
      authorization: `Bearer ${TOKEN}`,
      "content-type": "application/json",
      "x-api-version": "2.0"
    },
    body: JSON.stringify(payload)
  });

  console.log(response.status);
  const raw = await response.text();
  console.log(JSON.stringify(JSON.parse(raw), null, 2));
}

updatePaystub();
curl -s -X PATCH "https://api.worklio.com/wep/companies/$CLIENT_ID/payroll/1002191/paystubs/10264" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "content-type: application/json" \
  -H "x-api-version: 2.0" \
  -d '{
    "paylines": [
      {
        "id": 10291,
        "salaryWithHours": { "amount": 1800 }
      }
    ]
  }' | jq .

Example response

The response is the updated paystub. Only salaryWithHours.amount changed. hours and rate are returned as they were.

{
  "employeeName": "Brown, Michael",
  "id": 10264,
  "compensation": 1,
  "allowedNetPayAlloc": true,
  "employeeId": 4281,
  "paymentMethod": 1,
  "paylines": [
    {
      "id": 10291,
      "rate": 50,
      "salaryWithHours": {
        "amount": 1800,
        "hours": 40
      },
      "wcCode": "0000"
    }
  ]
}

To add or remove a pay line on the paystub, use these endpoints:

PurposeEndpoint
Add a pay linePOST https://api.worklio.com/wep/companies/{CLIENT_ID}/payroll/{RUN_ID}/paystubs/{PAYSTUB_ID}/paylines/add
Remove a pay lineDELETE https://api.worklio.com/wep/companies/{CLIENT_ID}/payroll/{RUN_ID}/paystubs/{PAYSTUB_ID}/paylines/{PAYLINE_ID}

5. Advance and finalize the run

The void run is processed in the background. Once you have runId (and have changed the pay in step 4, if you needed to), advance and finalize it the same way as a regular payroll run — see steps 2 and 3 in Run Payroll. Finalizing is what completes the void: the original paystub is flagged as voided and payments are released.

If the run is a void and re-key, it stops at the timesheet step until you advance it. After you finalize the run, it appears in Payroll History, and its reports are available through Payroll Reports.

When a paystub can't be voided

The call to start the run can return 200 even if a paystub was already void, check warnings to see.

{
  "companyId": 1030,
  "runId": 1002198,
  "payrollType": 2,
  "startedOn": "2026-10-15",
  "payDay": "2026-10-15",
  "periodStart": "2026-10-15",
  "periodEnd": "2026-10-15",
  "daysToProcessPayments": 4,
  "deadline": "2026-10-11T21:00:00Z",
  "currentUIStep": 0,
  "currentProcStep": 701,
  "skipUISteps": [],
  "currentProcStepDesc": "Crunching time entry data …",
  "currentProcStepProgress": 0,
  "currentProcStepStatus": 1,
  "direction": 0,
  "isPayrollBlocked": false,
  "canUserOverride": false,
  "errors": [],
  "warnings": [
    "E: AlreadyVoided - The source pay statement was voided already."
  ]
}
CodeMeaning
NotFoundNo paystub with that paystubId exists.
AlreadyVoidedThe paystub has already been voided.
PaRunNotFinalizedThe paystub belongs to a payroll that hasn't been finalized.
MethodNotAllowedThe paystub is from a previous year and reissue is false, or reissue is true and the paystub is itself a void.
EmployeeIsNotInCurrentPaRunThe paystub's employee isn't in the void run, for example because the employee is in another payroll that is in progress.
SourceCantBeSetupThe paystub comes from a setup payroll. Setup paystubs can't be voided here.
SourceCantBeSickPayThe paystub comes from a third-party sick pay payroll. Sick pay paystubs can't be voided here.
UncollectedTaxInAnotherPayrollThe paystub has uncollected-tax records on tips or group term life that are tied to another payroll that hasn't been finalized.

Notes

  • Not idempotent. Each call starts a new void run. If a call may have succeeded but you didn't get the response, check Payroll History for a void run before you retry.
  • Changing pay is optional and limited to the timesheet step. Make PATCH calls before you advance the run. See step 4.
  • Pay date. If the user behind the token isn't allowed to back-date pay dates, a payDate earlier than today is refused.
  • One void run at a time per employee. An employee can't be included in a void run while they're in another payroll that hasn't been finalized.
  • Webhooks. Starting a void run sends the Payroll Started event. See Webhook Event Types.
  • What a void run leaves out. It doesn't apply flat-amount deductions, arrears, scheduled pay, or adjustments, and it doesn't send a payday announcement email.

Did this page help you?