Setup Payroll

When a company moves to Worklio part-way through a year, it has already paid its employees for part of that year. Set Up Payroll is how you enter that earlier payroll, so that year-to-date wages, deductions and taxes are correct when the first payroll runs in Worklio.

The Set Up Payroll wizard is not a payroll run. You enter what each employee was paid before the move, Worklio processes it, and you finish the wizard. In the API Reference these endpoints are listed under YTD Payroll Setup. The sections up to Endpoints explain the concepts and rules. The Walkthrough shows each call in order.

Requires a bearer access token (see How to Get API Access). CLIENT_ID is the company identifier returned as id when you create the company.

When to use it

Run the wizard when at least one employee was paid, outside Worklio, in the year of the company's first Worklio pay day. For example, a company that joins in October and has already run payroll since January needs to enter January through September.

If no employee was paid in that year, start the wizard with eesPaidInCurrentYear=false. Worklio creates the wizard in the Finished status and there is nothing to enter.

Before you start

The wizard needs the following to be in place:

  • A completed company tax setup.
  • A payroll policy with a pay schedule. The wizard year is derived from the schedule.
  • Each employee to import has an employment record that is valid on 1 January of the wizard year (or, if there is none, the first one that starts after it), and an employee tax setup that does not start after the wizard year. Employees that don't meet this are not offered by the wizard.

Contractors (compensation type 1099) are included and are marked isContractor: true.

Wizard year and deadline

The wizard year is the calendar year of the company's earliest scheduled pay day that has not been run yet. The deadline is 4 days before that pay day.

Call Get setup payroll status before you start the wizard to read the year and deadline that apply to the company.

SituationWhat happens
The deadline has passed when you start the wizardThe wizard starts and the response contains a warning.
The deadline has passed when you call processThe call fails with a message to contact support.
eesPaidInCurrentYear=true, the first pay day is in the current calendar year, and today is 16 December or laterThe wizard can't be started this year.
The wizard was already startedThe call fails.

Wizard status

The wizard moves through the statuses below. status is returned by Get setup payroll status.

statusNameMeaning
0NotStartedThe wizard has not been started.
1DataEntryYou are entering paystubs and quarter totals.
2OverviewYou have requested the overview of what was entered.
3ProcessingWorklio is processing the data in the background.
4ProcessingFinishedProcessing is done. Check errors and warnings, then finish.
5FinishedThe wizard is complete.

How the status changes:

  • Starting the wizard sets DataEntry, or Finished when eesPaidInCurrentYear=false.
  • DataEntry and Overview switch automatically. Calling Get quarters overview sets Overview. Calling any data-entry endpoint sets DataEntry again.
  • Calling process sets Processing. Worklio sets ProcessingFinished when the background job ends. You don't set it.
  • Calling finish sets Finished. It is refused while processing errors exist.
  • You can't go back once Processing has started. Discard is available only before that.

Quarters: totals or paystubs

The wizard covers the quarters of the wizard year from Q1 up to the quarter that contains the first Worklio pay day. For each employee and each quarter you enter data in one of two ways.

ModeWhat you enterUsed for
TotalsOne set of aggregate wages, deductions and taxes for the whole quarter.Earlier quarters.
PaystubsOne entry per paystub, each with its own pay period and pay day.The quarter of today's date and any later quarter.

Rules:

  • The current quarter and later quarters must be entered as paystubs. A quarter is "current" by today's date, not by the date of the first pay day.
  • An earlier quarter can be either mode. Within one employee and quarter, don't mix totals with paystubs.
  • If an employee wasn't paid in a quarter, mark the quarter as having no wages instead of entering zeros.

GET .../quarter/{quarter}/ tells you which mode a quarter is in: totalsQuarter is true for totals and false for paystubs.

What the wizard creates

When processing runs, Worklio creates finalized payroll runs from the data you entered, with payrollType 7 (Setup), one run per pay period.

Until the wizard is Finished, the company dashboard shows "Import prior payroll history", and payroll policies with autoRun enabled don't start automatically.

Endpoints

All paths start with /wep/companies/{CLIENT_ID}/setuppayroll.

MethodPathValid in statusDescription
GET``AnyGet setup payroll status. Also works before the wizard starts.
POST?eesPaidInCurrentYear=trueNotStartedStart the wizard. All eligible employees are added.
POST/discardBefore ProcessingDelete everything entered in the wizard.
GET/eligibleemployeesDataEntryList employees and contractors that can still be added.
GET/employeesDataEntryList employees in the wizard, with progress per quarter.
POST/employeesDataEntryAdd employees to the wizard.
DELETE/employees/{employeeId}DataEntryRemove an employee and the data entered for them.
GET/employees/{employeeId}/quarter/{quarter}/DataEntryGet a quarter: the saved paystubs, or the totals form.
POST/employees/{employeeId}/quarter/{quarter}/wages?received=DataEntryMark a quarter as paid or not paid.
GET/employees/{employeeId}/quarter/{quarter}/fields/DataEntryGet the blank paystub form and a token.
GET/employees/{employeeId}/quarter/{quarter}/fields/{paystubId}/DataEntryGet the form for a saved paystub.
POST/employees/{employeeId}/quarter/{quarter}/addpaystubDataEntrySave a new paystub.
PUT/employees/{employeeId}/quarter/{quarter}/{paystubId}/DataEntryReplace a saved paystub.
DELETE/employees/{employeeId}/quarter/{quarter}/{paystubId}/DataEntryDelete a saved paystub.
POST/employees/{employeeId}/quarter/{quarter}/DataEntrySave quarter totals. Replaces earlier totals.
GET/quartersoverviewDataEntry, OverviewGet everything entered, per quarter and in total.
POST/processOverviewStart processing.
POST/finishProcessingFinishedFinish the wizard.

Walkthrough

Every request below sends these headers:

accept: application/json
api-version: 2.0
x-api-version: 2.0
authorization: Bearer {ACCESS_TOKEN}
content-type: application/json

The flow

  1. Check the status to read the wizard year and deadline.
  2. Start the wizard.
  3. Check the employees that were added.
  4. Enter each quarter, for every employee and every quarter of the wizard year up to the quarter of the first Worklio pay day.
  5. Change or remove anything entered by mistake.
  6. Review the overview.
  7. Process the data.
  8. Check the processing result.
  9. Finish the wizard.

Step 1: Check the status

GET /setuppayroll

Returns the wizard's current status. Before the wizard starts, year and deadline show when it must be done. After process, call this endpoint until status is 4.

Response fields

FieldTypeDescription
statusint32 (enum)Wizard status. See Wizard status.
yearint32 | nullThe wizard year.
wizardStarteddate-time | nullWhen the wizard was started.
deadlinedate-time | nullLast date to process the data. See Wizard year and deadline.
startedByint32 | nullID of the user who started the wizard.
processingProgressint32 | nullProgress of the background processing, out of processingProgressTotal.
processingProgressTotalint32 | nullTotal used to measure processingProgress.
errorsarray of stringProcessing errors. If any are present, finish is refused and you need to contact Worklio support.
warningsarray of stringProcessing warnings. Warnings don't block finish.
require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll
`

async function getSetupPayroll() {
  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));

}

getSetupPayroll();
curl -s -X GET "https://api.worklio.com/wep/companies/$CLIENT_ID/setuppayroll" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0" | jq .
{
  "status": 0,
  "year": 2026,
  "deadline": "2026-11-06T00:00:00Z",
  "errors": [],
  "warnings": []
}

Step 2: Start the wizard

POST /setuppayroll?eesPaidInCurrentYear=true

Parameters

NameTypeRequiredDescription
eesPaidInCurrentYearboolean (query)No, defaults to truetrue if at least one employee was paid in the wizard year before the first Worklio pay day. false if nobody was. With false, the wizard is created in the Finished status and you can skip the remaining steps.

Example request

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll?eesPaidInCurrentYear=true
`
async function startSetup() {
  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"
    },

  });

  console.log(response.status);

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

}

startSetup();
curl -s -X POST "https://api.worklio.com/wep/companies/$CLIENT_ID/setuppayroll?eesPaidInCurrentYear=true" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0" | jq .
{
  "status": 1,
  "deadline": "2026-11-06T00:00:00Z",
  "errors": [],
  "warnings": []
}

All eligible employees and contractors are added to the wizard when it starts. The status becomes DataEntry.

The call fails if the company tax setup is missing or incomplete, if the wizard was already started, if the company has no payroll policy schedule, or if it is 16 December or later and the first pay day is in the current year. If the deadline has passed, the call succeeds and the response includes a warning.

Step 3: Check the employees

GET /setuppayroll/employees

Returns the employees and contractors that are in the wizard, with the state of each of their quarters.

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const EMPLOYEE_ID = process.env.EMPLOYEE_ID
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll/employees
`

async function getEmployeesInSP() {
  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));

}

getEmployeesInSP();
curl -s -X GET "https://api.worklio.com/wep/companies/$CLIENT_ID/setuppayroll/employees" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0" | jq .
{
  "employees": [
    {
      "id": 4387,
      "firstName": "Maria",
      "lastName": "Lopez",
      "middleName": "",
      "maskedSSN": "*-1003",
      "ssn": "999-58-1003",
      "isContractor": false,
      "quarters": [
        {
          "totalsQuarter": true,
          "noWages": false,
          "paystubsCount": 0
        },
        {
          "totalsQuarter": true,
          "noWages": false,
          "paystubsCount": 0
        },
        {
          "totalsQuarter": true,
          "noWages": false,
          "paystubsCount": 0
        },
        {
          "totalsQuarter": false,
          "noWages": false,
          "paystubsCount": 0
        }
      ]
    }
  ],
  "contractors": [],
  "eligibleEmployees": true,
  "eligibleContractors": false
}

GET /setuppayroll/eligibleemployees

Returns the employees and contractors that can be added but are not in the wizard yet. Because the wizard adds everyone eligible when it starts, this returns [] straight after the start.

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll/eligibleemployees
`
async function getEligibleEE() {
  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));

}

getEligibleEE();
curl -s -X GET "https://api.worklio.com/wep/companies/$CLIENT_ID/setuppayroll/eligibleemployees" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0" | jq .
[
  {
    "id": 4388,
    "firstName": "Rodrigo",
    "lastName": "Lopez",
    "middleName": "",
    "maskedSSN": "*-1002",
    "isContractor": false,
    "quarters": [
      null,
      null,
      null,
      null
    ]
  }
]

Step 4: Enter each quarter

For every employee in the wizard, enter every quarter from Q1 to the quarter of the first Worklio pay day. Start by reading the quarter:

GET /setuppayroll/employees/{employeeId}/quarter/{quarter}/

quarter is 1 to 4, in the wizard year.

Response fields

FieldTypeDescription
totalsQuarterbooleantrue if the quarter is entered as one set of totals. false if it is entered paystub by paystub.
receivedWagesbooleanfalse if the quarter is marked as not paid.
paystubsarray | nullPaystubs saved so far, for a paystub quarter. Each has id, startDate, endDate, payDay, gross and netPay. Empty for a totals quarter.
totalDataobject | nullThe totals form and any saved totals, for a totals quarter. null for a paystub quarter.

Which of the three cases applies:

The current quarter and later quarters are always paystub quarters. An earlier quarter starts as a totals quarter. You can switch an empty one to paystubs by adding a paystub to it.

Quarter totals

The GET for a totals quarter returns the form for the totals, and a token that you send back when you save.

{
  "totalsQuarter": true,
  "receivedWages": true,
  "totalData": {
    "period": {
      "startDate": "2026-01-01",
      "endDate": "2026-03-31",
      "payDay": "2026-03-31",
      "isRegular": true
    },
    "wageItems": [
      {
        "title": "Hourly wages",
        "rows": [
          {
            "title": "Hourly",
            "hide": true,
            "required": 1,
            "fields": [
              { "id": "PC11873W", "label": "Wages", "type": 0, "mathOp": 1 },
              { "id": "PC11873H", "label": "Hours", "type": 2, "mathOp": 0 }
            ]
          },
          {
            "title": "Salary With Hours",
            "hide": false,
            "required": 1,
            "fields": [
              { "id": "PC11878W", "label": "Wages", "type": 0, "mathOp": 1 },
              { "id": "PC11878H", "label": "Hours", "type": 2, "mathOp": 0 }
            ]
          }
        ]
      },
      {
        "title": "Salary and other flat amount earnings",
        "rows": [
          {
            "title": "Bonus",
            "hide": false,
            "required": 1,
            "fields": [
              { "id": "PC11869W", "label": "", "type": 0, "mathOp": 1 }
            ]
          }
        ]
      }
    ],
    "deductionItems": [
      {
        "title": "Deductions",
        "rows": [
          {
            "title": "MISC: Garnishment Fee",
            "required": 0,
            "fields": [
              { "id": "CDC9118-1EE", "label": "Employee", "type": 0, "mathOp": -1 },
              { "id": "CDC9118-1ER", "label": "Employer", "type": 0, "mathOp": 0 }
            ]
          }
        ]
      }
    ],
    "taxItems": [
      {
        "title": "Taxes",
        "rows": [
          { "title": "Federal Income Tax", "required": 2, "fields": [{ "id": "T5", "type": 0, "mathOp": -1 }] },
          { "title": "Social Security Tax", "required": 2, "fields": [{ "id": "T4", "type": 0, "mathOp": -1 }] },
          { "title": "Medicare Tax", "required": 2, "fields": [{ "id": "T6", "type": 0, "mathOp": -1 }] },
          { "title": "Additional Medicare", "required": 2, "fields": [{ "id": "T7", "type": 0, "mathOp": -1 }] },
          { "title": "New York State Tax", "required": 2, "fields": [{ "id": "T3604", "type": 0, "mathOp": -1 }] },
          { "title": "New York SDI", "required": 2, "fields": [{ "id": "T3603", "type": 0, "mathOp": -1 }] },
          { "title": "New York Paid Family Leave Insurance", "required": 2, "fields": [{ "id": "T3605", "type": 0, "mathOp": -1 }] },
          { "title": "New York City Tax", "required": 2, "fields": [{ "id": "T3641", "type": 0, "mathOp": -1 }] },
          { "title": "Employer Social Security Tax", "required": 2, "fields": [{ "id": "T1", "type": 0, "mathOp": 0 }] },
          { "title": "Employer Medicare Tax", "required": 2, "fields": [{ "id": "T3", "type": 0, "mathOp": 0 }] },
          { "title": "Federal Unemployment Tax", "required": 2, "fields": [{ "id": "T2", "type": 0, "mathOp": 0 }] },
          { "title": "New York Re-Employment Service Fund", "required": 2, "fields": [{ "id": "T3602", "type": 0, "mathOp": 0 }] },
          { "title": "New York Employer SDI", "required": 2, "fields": [{ "id": "T3606", "type": 0, "mathOp": 0 }] }
        ]
      }
    ],
    "reimbursementItems": [
      {
        "title": "Flat reimbursements",
        "rows": [
          {
            "title": "Reimbursement",
            "hide": true,
            "required": 1,
            "fields": [
              { "id": "PC11877W", "label": "", "type": 0, "mathOp": 1 }
            ]
          }
        ]
      }
    ],
    "errors": [],
    "warnings": [],
    "token": "tVv3vCoaWSwoPv1hhVNnViVGqBPYKUeVV8SNtADPVCNT5YpBEqZ4E-1Vji4C5kbYqjx-..."
  }
}

This example is shortened: it omits the other wage and reimbursement rows, and the token is much longer. The field ids depend on the company's pay codes, deductions and the employee's taxes. Never hard-code them. Read them from the form you just requested.

The form

totalData has four lists of sections: wageItems, deductionItems, taxItems and reimbursementItems. Each section has rows, and each row has one or more fields. The id of a field is the key you use in fields when you save.

Id patternMeaning
PC{payCodeId}WWages (amount) for a pay code.
PC{payCodeId}HHours for a pay code.
PC{payCodeId}UUnits for a pay code.
CDC{id}-1EEDeduction, employee amount.
CDC{id}-1ERDeduction, employer contribution.
T{taxId}Tax amount. Taxes with mathOp -1 are withheld from the employee. Taxes with mathOp 0 are paid by the employer.
Row propertyValues
required0: optional. 1: fill in every field in the row, or none of them. 2: required. Every tax row is 2.
hidetrue for a row that doesn't suit the employee's compensation type, such as hourly rows for a salaried employee. Leave hidden rows empty.
Field propertyValues
type0: amount. 1: units. 2: hours.
mathOp1: adds to gross pay. -1: subtracts from net pay. 0: not part of net pay.

Save the totals

POST /setuppayroll/employees/{employeeId}/quarter/{quarter}/

Saving replaces any totals saved earlier for the quarter.

FieldTypeRequiredDescription
receivedWagesbooleanYestrue to save totals. false to mark the quarter as not paid (no other field is needed).
payDaydateYes, if receivedWages is trueThe pay day for the quarter's data. It must be in the wizard year and in the quarter in the path.
fieldsobjectYes, if receivedWages is trueField id to amount, hours or units. Send a value for every required field.
netPaynumberYes, if receivedWages is trueNet pay for the quarter. It must equal the fields with mathOp 1 added together, minus the fields with mathOp -1.
tokenstringYes, if receivedWages is trueThe token from the GET of this employee and quarter.

In the example form, the totals for the quarter are 22,500.00 in wages and 6,496.35 in employee taxes. Net pay is 22,500.00 − 6,496.35 = 16,003.65.

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const EMPLOYEE_ID = process.env.EMPLOYEE_ID
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll/employees/${EMPLOYEE_ID}/quarter/1
`

  const payload = {
    receivedWages: true,
    payDay: "2026-03-31",
    fields: {
      PC11873W: 22500.0,
      PC11873H: 520,
      T5: 2900.0,
      T4: 1395.0,
      T6: 326.25,
      T7: 0,
      T3604: 1000.0,
      T3603: 7.8,
      T3605: 87.3,
      T3641: 780.0,
      T1: 1395.0,
      T3: 326.25,
      T2: 42.0,
      T3602: 0,
      T3606: 0
    },
    netPay: 16003.65,
    token: `${YOUR_TOKEN}`
  };


async function saveQuarter() {
  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));

}

saveQuarter();
curl -X POST "${URL}/wep/companies/${CLIENT_ID}/setuppayroll/employees/${EMPLOYEE_ID}/quarter/1" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "x-api-version: 2.0" \
  -H "authorization: Bearer ${API_KEY}" \
  -H "content-type: application/json" \
  -d '{
    "receivedWages": true,
    "payDay": "2026-09-30",
    "fields": {
      "PC11873W": 22500.0,
      "PC11873H": 520,
      "T5": 2900.0,
      "T4": 1395.0,
      "T6": 326.25,
      "T7": 0,
      "T3604": 1000.0,
      "T3603": 7.8,
      "T3605": 87.3,
      "T3641": 780.0,
      "T1": 1395.0,
      "T3": 326.25,
      "T2": 42.0,
      "T3602": 0,
      "T3606": 0
    },
    "netPay": 16003.65,
    "token": "YOUR_TOKEN"
  }'

{
  "totalsResult": {
    "totalsQuarter": true,
    "noWages": false,
    "paystubsCount": 1
  }
}

Incorrect Pay Date will result in an Error

{
  "status": 3,
  "code": "DataValidationError",
  "errorCode": "",
  "message": "One or more validation errors occurred.\r\n: [The supplied value is invalid.]",
  "stackTrace": "",
  "pagination": {
    "pageNo": 0,
    "pageSize": 0,
    "totalRecords": 0,
    "totalPages": 0,
    "dataToken": ""
  },
  "validationErrors": {
    "": [
      [
        "validation_error",
        "The supplied value is invalid."
      ]
    ]
  }
}
{
  "status": 0,
  "code": "500",
  "errorCode": null,
  "message": "10/15/2026 - 7:31:45 AM : Invalid Pay day.",
  "stackTrace": "",
  "pagination": {
    "pageNo": 0,
    "pageSize": 0,
    "totalRecords": 0,
    "totalPages": 0,
    "dataToken": ""
  },
  "validationErrors": null
}
FieldTypeDescription
totalsResult.totalsQuarterbooleantrue if the quarter is a totals quarter.
totalsResult.noWagesbooleantrue if the quarter is marked as not paid.
totalsResult.paystubsCountint32Number of saved data sets in the quarter. A totals quarter with saved totals returns 1.

Repeat for every earlier quarter. Request the form again for each quarter, because the token is specific to the employee and quarter.

Employee was not paid in the quarter

If the employee wasn't paid at all in a quarter, for example because they joined the company after it, mark the quarter instead of entering zeros.

POST /setuppayroll/employees/{employeeId}/quarter/{quarter}/wages?received=false
NameTypeRequiredDescription
receivedboolean (query)Yesfalse if the employee wasn't paid in the quarter. true to mark it as paid again.
require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const EMPLOYEE_ID = process.env.EMPLOYEE_ID
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll/employees/${EMPLOYEE_ID}/quarter/3/wages?received=false
`


async function setWagesReceived() {
  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"
    },

  });

  console.log(response.status);


}

setWagesReceived();
curl -s -X POST "https://api.worklio.com/wep/companies/$CLIENT_ID/setuppayroll/employees/$EMPLOYEE_ID/quarter/3/wages?received=false" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0"
No Body

Data already saved in the quarter is kept when you change this flag. This works for totals quarters and paystub quarters. For a totals quarter you can also send receivedWages: false in the totals save, which doesn't need a token.

Paystubs

A paystub quarter has no totals form. An employee in the current quarter, for example, starts like this:

GET /setuppayroll/employees/{employeeId}/quarter/4/
{
  "totalsQuarter": false,
  "receivedWages": true,
  "paystubs": []
}

paystubs is empty until you add a paystub. For each paystub, request a form for its pay period, then save it.

Get the paystub form

GET /setuppayroll/employees/{employeeId}/quarter/{quarter}/fields/?startDate=2026-10-01&endDate=2026-10-07&payDay=2026-10-09
{
  "period": {
    "startDate": "2026-10-01",
    "endDate": "2026-10-07",
    "payDay": "2026-10-09",
    "isRegular": true
  },
  "availablePeriods": [],
  "wageItems": [
    {
      "title": "Hourly wages",
      "rows": [
        {
          "title": "Hourly",
          "hide": true,
          "required": 1,
          "fields": [
            {
              "id": "PC11886W",
              "label": "Wages",
              "type": 0,
              "mathOp": 1
            },
            {
              "id": "PC11886H",
              "label": "Hours",
              "type": 2,
              "mathOp": 0
            }
          ]
        },
        .
        . //more types of wages
        .
  ],
  "deductionItems": [
    {
      "title": "Deductions",
      "rows": [
        {
          "title": "MISC: Garnishment Fee",
          "required": 0,
          "fields": [
            {
              "id": "CDC9118-1EE",
              "label": "Employee",
              "type": 0,
              "mathOp": -1
            },
            {
              "id": "CDC9118-1ER",
              "label": "Employer",
              "type": 0,
              "mathOp": 0
            }
          ]
        },
        .
        . //more deductions
        .
      ]
    }
  ],
  "taxItems": [
    {
      "title": "Taxes",
      "rows": [
        {
          "title": "Federal Income Tax",
          "required": 2,
          "fields": [
            {
              "id": "T5",
              "type": 0,
              "mathOp": -1
            }
          ]
        },
        {
          "title": "Social Security Tax",
          "required": 2,
          "fields": [
            {
              "id": "T4",
              "type": 0,
              "mathOp": -1
            }
          ]
        },
        .
        . //more taxes
        .
      ]
    }
  ],
  "reimbursementItems": [
    {
      "title": "Flat reimbursements",
      "rows": [
        {
          "title": "Reimbursement",
          "hide": true,
          "required": 1,
          "fields": [
            {
              "id": "PC11890W",
              "label": "",
              "type": 0,
              "mathOp": 1
            }
          ]
        }
      ]
    }
  ],
  "errors": [],
  "warnings": [],
  "token": "0LI1-mZgn5UppjrrsiaIj2my7nGFy12J_-TWYFLPBbIPNReDIvW..."
}
NameTypeRequiredDescription
startDatedate (query)NoPay period start.
endDatedate (query)NoPay period end.
payDaydate (query)NoPay day.

The response has the same form as in Quarter totals, with these additions:

FieldTypeDescription
idint32 | nullThe paystub id, when the form is for a saved paystub.
periodobjectThe pay period the form is for: startDate, endDate, payDay and isRegular. If you send no dates, one of the available periods that has no data yet for this employee can be preselected here.
availablePeriodsarrayThe pay periods that can be entered, each with startDate, endDate, payDay and isRegular.
tokenstringSend this back when you save. It is valid for this pay period only.

The rows and fields on the form depend on the pay period. If you change the pay period, request the form again and use the new token.

To read a saved paystub with its values, use GET .../quarter/{quarter}/fields/{paystubId}/. It takes the same query parameters.

Save a paystub

POST /setuppayroll/employees/{employeeId}/quarter/{quarter}/addpaystub
FieldTypeRequiredDescription
startDatedateYesPay period start. Must match the period of the form the token came from.
endDatedateYesPay period end. Must match the period of the form the token came from, and can't be before startDate.
payDaydateYesPay day. Must match the period of the form the token came from. It must be in the wizard year and in the quarter in the path.
isRegularbooleanNoWhether this is a regular pay period. The wizard offers regular periods when you enter other employees.
fieldsobjectYesField id to amount, hours or units. Send a value for every required field.
netPaynumberYesNet pay for the paystub. It must equal the fields with mathOp 1 added together, minus the fields with mathOp -1.
tokenstringYesThe token from the most recent form request for this pay period.

Send dates as YYYY-MM-DD, with no time.

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const EMPLOYEE_ID = process.env.EMPLOYEE_ID
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll/employees/${EMPLOYEE_ID}/quarter/4/addpaystub
`

  const payload = {
    startDate: "2026-10-01",
    endDate: "2026-10-07",
    payDay: "2026-10-09",
    isRegular: true,
    fields: {
      PC11886W: 1730.77,
      PC11886H: 40,
      T5: 223.08,
      T4: 107.31,
      T6: 25.1,
      T7: 0,
      T3604: 76.92,
      T3603: 0.6,
      T3605: 6.72,
      T3641: 60.0,
      T1: 107.31,
      T3: 25.1,
      T2: 3.23,
      T3602: 0,
      T3606: 0
    },
    netPay: 1231.04,
    token: YOUR_TOKEN
  };


async function addPaystub() {
  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));

}

addPaystub();
curl -X POST "${URL}/wep/companies/${CLIENT_ID}/setuppayroll/employees/${EMPLOYEE_ID}/quarter/4/addpaystub" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "x-api-version: 2.0" \
  -H "authorization: Bearer ${API_KEY}" \
  -H "content-type: application/json" \
  -d '{
    "startDate": "2026-10-01",
    "endDate": "2026-10-07",
    "payDay": "2026-10-09",
    "isRegular": true,
    "fields": {
      "PC11886W": 1730.77,
      "PC11886H": 40,
      "T5": 223.08,
      "T4": 107.31,
      "T6": 25.1,
      "T7": 0,
      "T3604": 76.92,
      "T3603": 0.6,
      "T3605": 6.72,
      "T3641": 60.0,
      "T1": 107.31,
      "T3": 25.1,
      "T2": 3.23,
      "T3602": 0,
      "T3606": 0
    },
    "netPay": 1231.04,
    "token": "YOUR_TOKEN"
  }'
{
  "paystubsResult": [
    {
      "id": 4,
      "startDate": "2026-10-01T00:00:00Z",
      "endDate": "2026-10-07T00:00:00Z",
      "payDay": "2026-10-09T00:00:00Z",
      "gross": 1730.77,
      "netPay": 1231.04
    }
  ]
}
{
  "status": 0,
  "code": "500",
  "errorCode": null,
  "message": "10/15/2026 - 7:14:23 AM : Validation errors. Entered Net Pay amount 1231.04 is different from Net Pay calculated from entered fields (-499.73)",
  "stackTrace": "",
  "pagination": {
    "pageNo": 0,
    "pageSize": 0,
    "totalRecords": 0,
    "totalPages": 0,
    "dataToken": ""
  },
  "validationErrors": null
}

Add one paystub for each pay period the employee was paid in the quarter. Only the current quarter and later quarters need one paystub per pay period. For earlier quarters, totals are enough.

Step 5: Change or remove entered data

ToCall
Replace a paystubPUT /employees/{employeeId}/quarter/{quarter}/{paystubId}/. The body is the same as Save a paystub. Request the form with GET .../fields/{paystubId}/ first to get the token.
Delete a paystubDELETE /employees/{employeeId}/quarter/{quarter}/{paystubId}/
Replace quarter totalsSave the totals again. See Save the totals.
Remove an employee and everything entered for themDELETE /employees/{employeeId}
Add an employee backPOST /employees with a JSON array of employee ids, for example [4387]. The employee must be in the list from GET /eligibleemployees.
Start overPOST /discard. See Discard the wizard.

Step 6: Review the overview

GET /setuppayroll/quartersoverview

Returns what was entered for every employee, per quarter and in total. Calling it sets the status to Overview. Use it to check the numbers before you process.

{
  "quarterData": [
    [
      {
        "employeeId": 4387,                    //Q1
        "firstName": "Maria",
        "lastName": "Lopez",
        "middleName": "",
        "grossWages": 22500,    
        "netPay": 16003.65,
        "eeTaxes": 6496.35,
        "erTaxes": 1763.25,
        "eeDeductions": 0,
        "erContributions": 0
      }
    ],
    [
      {
        "employeeId": 4387,                 //Q2
        "firstName": "Maria",
        "lastName": "Lopez",
        "middleName": "",
        "grossWages": 22500,   
        "netPay": 16003.65,
        "eeTaxes": 6496.35,
        "erTaxes": 1763.25,
        "eeDeductions": 0,
        "erContributions": 0
      }
    ],
    [],                                     //Q3
    [
      {
        "employeeId": 4387,                 //Q4
        "firstName": "Maria",
        "lastName": "Lopez",
        "middleName": "",
        "grossWages": 1730.77,
        "netPay": 1231.04,
        "eeTaxes": 499.73,
        "erTaxes": 135.64,
        "eeDeductions": 0,
        "erContributions": 0
      }
    ]
  ],
  "totalsData": [
    {
      "employeeId": 4387,
      "firstName": "Maria",
      "lastName": "Lopez",
      "middleName": "",
      "grossWages": 46730.77,
      "netPay": 33238.34,
      "eeTaxes": 13492.43,
      "erTaxes": 3662.14,
      "eeDeductions": 0,
      "erContributions": 0
    }
  ]
}
FieldTypeDescription
quarterDataarray of arraysOne array per quarter, Q1 first. Each has one entry per employee with data in that quarter. A quarter without wages is an empty array, as Q3 is in the example.
totalsDataarrayOne entry per employee, with the amounts for all quarters added together.

Each entry has employeeId, the employee's name, and the amounts grossWages, netPay, eeTaxes, erTaxes, eeDeductions and erContributions.

Step 7: Process the data

POST /setuppayroll/process

Starts processing as a background job. The status becomes Processing. After this point you can't change or discard the data. The call fails if the deadline has passed. In that case contact Worklio support.

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const EMPLOYEE_ID = process.env.EMPLOYEE_ID
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll/process
`


async function processData() {
  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"
    },

  });

  console.log(response.status);

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

}

processData();
curl -s -X POST "https://api.worklio.com/wep/companies/$CLIENT_ID/setuppayroll/process" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0" | jq .
{
  "status": 1,
  "code": "200",
  "errorCode": "",
  "message": "OK",
  "stackTrace": "",
  "pagination": {
    "pageNo": 0,
    "pageSize": 0,
    "totalRecords": 0,
    "totalPages": 0,
    "dataToken": ""
  }
}

A 200 response means processing started, not that it has finished. The response contains no progress information. Read the progress from the status endpoint.

Step 8: Finish the wizard

POST /setuppayroll/finish

Finishes the wizard. The call succeeds with 204 No Content and no body, and the status becomes Finished. It is refused if processing hasn't finished or ended with errors.

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID
const URL = process.env.URL;
const EMPLOYEE_ID = process.env.EMPLOYEE_ID
const url = `${URL}/wep/companies/${CLIENT_ID}/setuppayroll/finish
`


async function finishSetup() {
  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"
    },

  });

  console.log(response.status);

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

}

finishSetup();
curl -s -i -X POST "https://api.worklio.com/wep/companies/$CLIENT_ID/setuppayroll/finish" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "x-api-version: 2.0"
No Body

After the wizard is Finished, the "Import prior payroll history" item leaves the company dashboard, and payroll policies with autoRun enabled can start automatically.

Discard the wizard

POST /setuppayroll/discard

Deletes all data entered in the wizard. This can't be undone, and it is refused once processing has started. The deadline is not checked.

Validation rules

A save fails when any of these rules is broken. The reason is returned in message (see Errors).

RuleCauseFix
TokenThe token is missing, was changed, or belongs to a different company, employee or quarter.Request the form again and send the new token.
Period (paystub)startDate, endDate or payDay differs from the period of the form the token came from.Send the dates you requested the form with, as YYYY-MM-DD.
Pay daypayDay is missing, is not in the wizard year, or is not in the quarter in the path.Use a pay day inside the quarter.
DatesendDate is before startDate.Correct the dates.
Required taxA tax row has no value. The message reads Missing required value for Tax item #{id}.Send every tax id from the form. Send 0 for a tax that doesn't apply.
Required pay codeOne field of a row with required 1 has a value and another doesn't. The message reads Required value missing for pay code {id}.Send every field in the row, or none of them.
Net paynetPay doesn't match the fields. The message reads Entered Net Pay amount X is different from Net Pay calculated from entered fields (Y).Send netPay equal to the added fields (mathOp 1) minus the subtracted fields (mathOp -1).
EmployeeThe employee is not in the wizard.Add the employee, see Step 5.
Quarter modeYou sent totals for a paystub quarter, or paystubs mixed with totals in one quarter.Use the mode that totalsQuarter returns for the quarter.

Did this page help you?