This endpoint lets you specify the classified hours, per worker, for a pay period. You can include as many or as few workers as you want in a single call (subject to the size limit, below), and we only change the hours of the workers you include.
How to use this endpoint
The purpose of this endpoint is to synchronize a timesheet of hours, for many workers, at the same time. To do this accurately, follow the rules below for adding, editing, and removing hours.
This is a "synchronize" operation, not an "add" operationEach call replaces the hours we have on file; it never adds to them. For every worker you include, send the complete set of hours for the pay period each time, whether or not anything changed since your last sync.
Replacement is scoped to the worker, the pay period, and the work location. When you send hours for a work location (or for no work location, which means the worker's default location), we remove everything we already have for that worker, in that pay period, at that location, and replace it with the hours you sent. Hours the worker has at work locations you did not include are left untouched.
Let's say you send us 40 hours at Location A for Jane Appleseed for the January 3 - January 9 pay period. Later, you send us 8 hours for Jane at Location A for the same pay period. We will remove the 40 hours and keep only the 8. We do not combine the previous hours with the new hours, so if you split a worker's hours for one location across several calls, only the last call's hours are kept. If the second call were for Location B instead, Jane would have 40 hours at Location A and 8 hours at Location B.
Finally, if Jane was already paid for that pay period and you've authorized us to process corrections, we'll determine the difference between the old hours and the new hours, then handle the consequences automatically.
DifferentearningDatevalues do not prevent a collisionReplacement ignores the individual date. If you send Jane's Monday shift with
earningDateMonday, then her Tuesday shift withearningDateTuesday, both at Location A in the same pay period, we keep only the Tuesday hours and the Monday hours are lost. The call still returns200, so nothing warns you that hours were replaced. To avoid this, build oneclassifiedHourslist per worker covering every shift in the period, and send it in one call.
Rules for each call
- List each worker once per call. A call that includes the same worker more than once is rejected with a
400error. Put all of a worker's hours, across all classifications, pay rates, and work locations, in that worker's singleclassifiedHourslist. - Send all of a worker's hours for a work location together. If your system holds hours as separate timesheets (for example, one per shift or per client) for the same worker and work location in the same pay period, combine them before sending. This includes shifts on different days. Combine them into one list before sending, even when your system tracks each day separately.
- Set
earningDateto the last date of the pay period.earningDatetells us which pay period the hours belong to, and we recommend always sending the period's final day (for a Sunday–Saturday period, the Saturday) so the same value identifies the period on every call. Any date inside the period is currently accepted and resolves to the same pay period, but it does not scope the hours to that day. Do not send one call per shift date. A worker's hours are replaced by pay period and work location, so two calls with differentearningDatevalues in the same period still replace each other. Send the worker's complete hours for the whole period in a single call. Hours sent with anearningDatein a pay period that has already been paid are rejected unless you authorize corrections (see below). - Workers you leave out are not changed. If you do not include a worker, we leave that worker's existing hours in place.
Adding and editing hours
To add or change a worker's hours, send that worker with the complete, correct set of hours for each work location that changed. We replace the worker's existing hours for those locations in the pay period with what you sent.
Removing hours
If you do not include a worker in the call, we make no changes to that worker's hours, because you haven't told us to remove anything.
To remove all of a worker's hours for the pay period, include the worker with every ...HoursWorked field set to "0.00". That explicitly instructs us to remove the hours we have on file.
But what if payroll has already been run?
This endpoint supports our automated Timesheet Corrections system. We can accept changes to hours for pay periods that have already been paid-out and automatically process the resulting additional payout or future reduction of wages. You just need to authorize the change using the correction-authorized=true parameter.
You should consider authorizing corrections!If you authorize us to process corrections to paid-out periods, it creates a seamless and automated workflow for payroll admins, and eliminates the need for manual double-entry in your system and ours. This is a nice experience!
How to structure the data
Typically, you'll need to send us some combination of regular, overtime, and double-time hours. You specify these in the appropriate field: regularHoursWorked, for instance.
But notice how each entry in a worker's classifiedHours list also has a payRate field. When using this endpoint, you're expected to provide a pay rate and exactly one of the ...HoursWorked fields for each entry. This allows you to provide the exact pay rate for each set of hours at each classification.
This is important because employers are required to pay overtime and double time at premium rates, and we won't calculate that premium rate automatically on your behalf, since we don't have enough information to do that.
Here are some examples of good and bad inputs:
// 👍 This is good! 👍
{
"payRate": { "amount": "20.00", "currency": "USD" },
"regularHoursWorked": "18.00"
}
// => 200 OK
// 👍 This is good! 👍
{
"payRate": { "amount": "65.00", "currency": "USD" },
"overtimeHoursWorked": "4.00"
}
// => 200 OK
// ❌ This is ambiguous and confusing. This will produce an error. ❌
{
"payRate": { "amount": "20.00", "currency": "USD" },
"regularHoursWorked": "12.00",
"overtimeHoursWorked": "2.25"
}
// => 400 Bad Request: "More than one type of hours is not allowed."In summary: split regular, overtime, and double time hours into separate entries in the worker's classifiedHours list, to make sure you're sending us exactly the right pay rate + hours combination for each classification. A worker appears once in the request; their entries go inside that one worker object.
Example: one worker, two work locations, one call
This is the correct way to send a worker who worked at two locations in the same pay period. The worker appears once, with one entry per classification and location:
{
"earningDate": "2026-01-09",
"classifiedHoursPerWorker": [
{
"workerId": "0ee89b64-e8e4-4d80-baf2-a7dece28528a",
"classifiedHours": [
{ "workLocationId": "1001", "payRate": { "amount": "20.00", "currency": "USD" }, "regularHoursWorked": "32.00" },
{ "workLocationId": "1001", "payRate": { "amount": "30.00", "currency": "USD" }, "overtimeHoursWorked": "4.00" },
{ "workLocationId": "1002", "payRate": { "amount": "20.00", "currency": "USD" }, "regularHoursWorked": "8.00" }
]
}
]
}Sending this worker again later with only the Location 1001 entries would replace the hours at 1001 and leave the hours at 1002 as they are.
Data size limitations
This endpoint is limited to a maximum of 200 entries in the classifiedHoursPerWorker list per call. If you need to provide hours for more than 200 employees, you will need to call this endpoint multiple times until you have provided hours for all employees with hours in the period. Split the workers across calls, never one worker's hours: each worker's complete hours for a work location must go in a single call, because a later call for the same worker and location replaces the earlier one.