Time tracking

Manage time tracking shifts for Global Payroll employees using the time tracking API

With the time tracking API, you can manage the time worked by employees, and add, update, retrieve and delete their shifts.

The time tracking API only works for Global Payroll.

Independent contractors use timesheets to track their time. For more information, see Timesheets.

Before you begin

Before managing shifts, note the following:

Shifts are linked to contracts. Retrieve the contract ID from the GET list of contracts endpoint.

Categorized shifts reference a shift rate by its external_id. Create the shift rates you need before you submit any categorized shifts.

Shifts are processed and compensated at the end of each payroll cycle. For more information, see Shifts and payroll cycles.

If a shift’s payroll cycle reaches its cutoff date, the shift is not rejected by default; it automatically moves to the next payroll cycle and is compensated then. To detect this instead of relying on automatic rollover, see Preventing late submissions with payroll_cycle_ref.date.

How a shift is compensated depends on the type of the shift rate it references:

  • PER_HOUR_FLAT_RATE and PER_UNIT_FLAT_RATE: the rate value is applied directly to time_amount. The worker’s base salary is not used.
  • MULTIPLIER_PERCENTAGE: the rate value is applied as a percentage of the worker’s hourly base salary (or equivalent hourly rate, for non-hourly contracts).

See Manage shift rates for the formula used by each rate type.

If a request fails, check Common errors before troubleshooting further. Several error messages, particularly those related to payroll cycles and corrections, are easy to misread.

How the pieces fit together

At a high level, setting up and running time tracking for an employee follows this sequence:

1

Create shift rates

Create one shift rate for each type of compensation, for example, a flat hourly rate, an overtime multiplier, or a per-unit rate for piecework. Each rate gets an external_id that you reference later.

2

Submit shifts

Submit categorized shifts that reference a shift rate’s external_id, or submit uncategorized (raw) shifts that only capture start time, end time, and breaks. Raw shifts do not reference a shift rate.

3

Shift is matched to a payroll cycle

The shift is associated with a payroll cycle based on date_of_work, or explicitly via payroll_cycle_ref.date if you want to guard against late submissions.

4

Submission must land before cutoff

For shifts created through this API, the submission timestamp is what counts; there is no separate manager approval step. The shift must be submitted before the payroll cycle’s cutoff date to be processed in that cycle. See Shifts and payroll cycles for what happens if it lands after the cutoff.

5

Cycle is compensated

At the cutoff date, all shifts submitted for the cycle are compensated according to their shift rate.

6

Adjust with corrections, if needed

Once a shift has been processed for payroll, you can no longer update or delete it directly. Use correction shifts to adjust the payable amount without touching historical payroll data.

Shift types

Shifts can be submitted in two formats:

Categorized shifts

Categorized shifts are suited for when shifts are categorized outside Deel and the shift rates (pay codes/category) are known. To use this API, you must first create shift rates in Deel and then reference those shift rates when submitting categorized shifts.

Following is an example of a categorized shift:

1{
2 "external_id": "shift_123",
3 "date_of_work": "2024-04-01",
4 "summary": {
5 "shift_rate_external_id": "rate123",
6 "time_unit": "HOUR",
7 "time_amount": 15.50
8 }
9}

Uncategorized (raw) shifts

Uncategorized shifts are used to capture the shift information in a more granular way. Unlike categorized shifts, which capture summary information such as total hours worked, uncategorized shifts capture granular start time, end time, and break information. There is no need to set up a shift rate for uncategorized shifts.

1{
2 "external_id": "shift_456",
3 "date_of_work": "2024-04-01",
4 "meta": {
5 "start": {
6 "date": "2024-04-01",
7 "time": "09:00",
8 "is_rest_day": false,
9 "is_public_holiday": false
10 },
11 "end": {
12 "date": "2024-04-01",
13 "time": "17:00",
14 "is_rest_day": false,
15 "is_public_holiday": false
16 },
17 "breaks": [
18 {
19 "end": {
20 "date": "2024-04-01",
21 "time": "12:00"
22 },
23 "start": {
24 "date": "2024-04-01",
25 "time": "11:00"
26 },
27 "is_paid": true
28 }
29 ],
30 "approval_date": "2024-04-03"
31 }
32}

Shifts and payroll cycles

When you submit a shift, it’s automatically associated with the relevant payroll cycle based on the submission timestamp and the cycle’s cutoff date. For shifts created through this API, only the submission timestamp counts; there is no separate manager approval step. The cutoff date marks the last day you can submit a shift for processing within the current payroll cycle.

  • If the shift is submitted before the cutoff date, it is processed within the current cycle
  • If the shift is submitted after the cutoff date, it is processed in the next cycle

The payroll calendar is configured per entity by your Deel representative when you onboard. That is when you decide the cutoff dates. If you have questions about your cutoff dates, contact your Deel representative. For more information, see Understanding the Deel Global Payroll Calendar.

Shifts cutoff date

Contact support for late shift submissions

If the payroll cutoff date has already passed and waiting for the next cycle for the shift to be processed is not an option for you, you can contact support to process the shift in a special off-cycle payroll run. Off-cycle payroll runs have an additional fee that depends on your contract agreement.

Make sure to provide the shift details via a CSV file through the support channel.

Preventing late submissions with payroll_cycle_ref.date

You can include the optional payroll_cycle_ref.date parameter when submitting a shift to declare your intended payroll cycle. The system compares the request’s timestamp against that cycle’s cutoff date, so the shift is only accepted into the specified cycle if the submission is timely; otherwise, the request is rejected instead of silently rolling over.

The following table compares this to omitting the parameter:

ApproachIf the cycle’s cutoff has already passedUse when
Omit payroll_cycle_refThe shift is silently moved to the next payroll cycle. The request still succeeds.Automatic rollover is acceptable, and your integration does not need to be notified when it happens.
Include payroll_cycle_ref.dateThe request is rejected, so your integration can detect the miss immediately instead of finding out only when compensation is late.You want to guarantee a shift lands in a specific cycle, or want to catch late submissions programmatically.

A shift that rolls over to the next payroll cycle is compensated with that cycle, not the one its date_of_work originally fell in. In practice, this means payment lands roughly one full payroll cycle later than expected.

The following example shows how to use the payroll_cycle_ref.date parameter:

Format of the payroll_cycle_ref

The payroll_cycle_ref.date follows the ISO 8601 date and time format. You can use any date within the cycle’s start and end dates, but we recommend using the cycle’s end date for clarity.

1{
2 "data": {
3 "contract_id": "123456",
4 "shifts": [
5 {
6 "external_id": "shift_123456",
7 "description": "This is a sample shift description.",
8 "date_of_work": "2023-10-01",
9 "payroll_cycle_ref": {
10 "date": "2023-10-31T00:00:00.000Z"
11 },
12 "summary": {
13 "shift_rate_external_id": "rate1234",
14 "time_unit": "HOUR",
15 "time_amount": 15.50
16 }
17 }
18 ]
19 }
20}

Manage shifts

This section covers how to add, update, and delete shifts for an employee.

Add shifts

This section covers how to add shifts for an employee. There are different endpoints available for adding shifts based on the shift type:

Add categorized shifts

You can add multiple categorized shifts for a single contract by providing an array of shifts. You can add a shift using any of the following time units: HOUR , DAY , WEEK , and MONTH.

1

Create a shift rate

Before submitting a categorized shift, create a shift rate and note its external_id.

2

Submit the shift

Make a POST request to the Create a time tracking shift endpoint.

1import requests
2import os
3
4url = "https://api.letsdeel.com/rest/time_tracking/shifts"
5
6headers = {
7 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
8 "Content-Type": "application/json"
9}
10
11payload = {
12 "data": {
13 "contract_id": "123456",
14 "shifts": [
15 {
16 "external_id": "shift_123456",
17 "description": "This is a sample shift description.",
18 "date_of_work": "2023-10-01",
19 "payroll_cycle_ref": {
20 "date": "2023-10-31T00:00:00.000Z"
21 },
22 "summary": {
23 "shift_rate_external_id": "rate1234",
24 "time_unit": "HOUR",
25 "time_amount": 15.50
26 }
27 }
28 ]
29 }
30}
31
32response = requests.post(url, json=payload, headers=headers)
33print(response.json())

In the body:

NameRequiredTypeFormatDescriptionExample
contract_idtruestring-Unique identifier of the contract for which shifts are being submitted123456
descriptiontruestring-Description of shift. Use it to describe what kind of work is done during the shift.This is a sample shift description.
external_idtruestring-User-defined ID of the shiftshift_123456
date_of_worktruestringdateDate on which shift is performed. It is used to identify the payroll cycle of the shift2023-10-01
payroll_cycle_ref.datefalsestringdate-time (ISO 8601)Reference date of the payroll cycle in which shift should be processed (We recommend to send the payroll cycle end date as the payroll cycle reference)2023-10-31T00:00:00.000Z
summarytrueobject-Object containing numerical data about the shift. This data is used to calculate the amount to be paid for the shift.-
summary.shift_rate_external_idtruestring-ID of the shift rate. Use it to link the shift to a shift rate you created.rate1234
summary.time_unittruestring-Time unit for the shift. Possible values: HOUR, DAY, WEEK, MONTH.HOUR
summary.time_amountfalsenumber-Length of the shift, expressed in the selected time unit15.50
3

Verify the response

A successful response (200) returns the details of the shift created.

1{
2 "data": [
3 {
4 "external_id": "95c35493-41aa-44f8-9154-5a25cbbc1865",
5 "organization_id": 0,
6 "description": "string",
7 "date_of_work": "2019-08-24T14:15:22Z",
8 "contract_id": "string",
9 "payroll_cycle_ref": {
10 "date": "2023-10-31T00:00:00.000Z"
11 },
12 "summary": {
13 "shift_rate_external_id": "rate1234",
14 "time_unit": "HOUR",
15 "time_amount": 15.50,
16 "total_payable_hours": 15.50
17 },
18 "created_at": "2022-05-24T09:38:46.235Z",
19 "updated_at": "2022-05-24T09:38:46.235Z"
20 }
21 ]
22}

Where:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined ID of the shiftshift_123456
organization_idtruenumber-The ID of your organization123456
descriptiontruestring-Description of shiftThis is a sample shift description.
date_of_worktruestringdate-timeDate of the shift2019-08-24T14:15:22Z
payroll_cycle_ref.datefalsestringdate-time (ISO 8601)Reference date of the payroll cycle in which shift will be processed2023-10-31T00:00:00.000Z
contract_idtruestring-Unique identifier of the contract that shifts were submitted for123456
summarytrueobject-Object containing numerical data about the shift. This data is used to calculate the amount to be paid for the shift.-
created_attruestringdate-timeDate on which the shift is created2022-05-24T09:38:46.235Z
updated_attruestringdate-timeDate on which the shift is updated2022-05-24T09:38:46.235Z

The total_payable_hours is set by default by the API when time_unit is HOUR.

Add categorized shifts (Legacy)

This method is still supported, but it is recommended to use Add categorized shifts instead.

This shift type adds a shift without requiring time_amount and time_unit in the request body. By default, time_amount is set to summary.total_payable_hours and time_unit is set to HOUR. You can perform the same operations on these shifts as you would with categorized shifts by using the same payload.

1

Create a shift rate

Before submitting a categorized shift, create a shift rate and note its external_id.

2

Submit the shift

Make a POST request to the Create a time tracking shift endpoint.

1import requests
2import os
3
4url = "https://api.letsdeel.com/rest/time_tracking/shifts"
5
6headers = {
7 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
8 "Content-Type": "application/json"
9}
10
11payload = {
12 "data": {
13 "contract_id": "123456",
14 "shifts": [
15 {
16 "external_id": "shift_123456",
17 "description": "This is a sample shift description.",
18 "date_of_work": "2023-10-01",
19 "payroll_cycle_ref": {
20 "date": "2023-10-31T00:00:00.000Z"
21 },
22 "summary": {
23 "shift_rate_external_id": "rate1234",
24 "shift_duration_hours": 8,
25 "total_break_hours": 1,
26 "payable_break_hours": 0.5,
27 "total_payable_hours": 7.5
28 }
29 }
30 ]
31 }
32}
33
34response = requests.post(url, json=payload, headers=headers)
35print(response.json())

In the body:

NameRequiredTypeFormatDescriptionExample
contract_idtruestring-Unique identifier of the contract for which shifts are being submitted123456
descriptiontruestring-Description of shift. Use it to describe what kind of work is done during the shift.This is a sample shift description.
external_idtruestring-User-defined ID of the shiftshift_123456
date_of_worktruestringdateDate on which shift is performed. It is used to identify the payroll cycle of the shift2023-10-01
payroll_cycle_ref.datefalsestringdate-time (ISO 8601)Reference date of the payroll cycle in which shift should be processed (We recommend to send the payroll cycle end date as the payroll cycle reference)2023-10-31T00:00:00.000Z
summarytrueobject-Object containing numerical data about the shift. This data is used to calculate the amount to be paid for the shift.-
summary.shift_rate_external_idtruestring-ID of the shift rate. Use it to link the shift to a shift rate you created.rate1234
summary.shift_duration_hoursfalsenumber-Total time of the shift in hours8
summary.total_break_hoursfalsenumber-Total break time in hours1
summary.payable_break_hoursfalsenumber-Total breaks hours that must be paid0.5
summary.total_payable_hourstruenumber-Total hours that need to be paid using the shift rate provided above7.5
3

Verify the response

A successful response (200) returns the details of the shift created.

1{
2 "data": [
3 {
4 "external_id": "95c35493-41aa-44f8-9154-5a25cbbc1865",
5 "organization_id": 0,
6 "description": "string",
7 "date_of_work": "2019-08-24T14:15:22Z",
8 "contract_id": "string",
9 "payroll_cycle_ref": {
10 "date": "2023-10-31T00:00:00.000Z"
11 },
12 "summary": {
13 "shift_rate_external_id": "rate1234",
14 "time_unit": "HOUR",
15 "time_amount": 7.5,
16 "shift_duration_hours": 8,
17 "total_break_hours": 1,
18 "payable_break_hours": 0.5,
19 "total_payable_hours": 7.5
20 },
21 "created_at": "2022-05-24T09:38:46.235Z",
22 "updated_at": "2022-05-24T09:38:46.235Z"
23 }
24 ]
25}

Where:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined ID of the shiftshift_123456
organization_idtruenumber-The ID of your organization123456
descriptiontruestring-Description of shiftThis is a sample shift description.
date_of_worktruestringdate-timeDate of the shift2019-08-24T14:15:22Z
payroll_cycle_ref.datefalsestringdate-time (ISO 8601)Reference date of the payroll cycle in which shift will be processed2023-10-31T00:00:00.000Z
contract_idtruestring-Unique identifier of the contract that shifts were submitted for123456
summarytrueobject-Object containing numerical data about the shift. This data is used to calculate the amount to be paid for the shift.-
created_attruestringdate-timeDate on which the shift is created2022-05-24T09:38:46.235Z
updated_attruestringdate-timeDate on which the shift is updated2022-05-24T09:38:46.235Z

Add uncategorized (raw) shifts

You can add multiple uncategorized shifts for a single contract by providing an array of shifts.

1

Submit the shift

Make a POST request to the Create uncategorized (raw) shift endpoint.

1import requests
2import os
3
4url = "https://api.letsdeel.com/rest/time_tracking/shifts/raw"
5
6headers = {
7 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
8 "Content-Type": "application/json"
9}
10
11payload = {
12 "data": {
13 "contract_id": "123456",
14 "shifts": [
15 {
16 "external_id": "shift_123456",
17 "description": "This is a sample shift description.",
18 "date_of_work": "2024-04-01",
19 "meta": {
20 "start": {
21 "date": "2024-04-01",
22 "time": "09:00",
23 "is_rest_day": False,
24 "is_public_holiday": False
25 },
26 "end": {
27 "date": "2024-04-01",
28 "time": "17:00",
29 "is_rest_day": False,
30 "is_public_holiday": False
31 },
32 "breaks": [
33 {
34 "end": {
35 "date": "2024-04-01",
36 "time": "12:00"
37 },
38 "start": {
39 "date": "2024-04-01",
40 "time": "11:00"
41 },
42 "is_paid": True
43 }
44 ],
45 "approval_date": "2024-04-03"
46 }
47 }
48 ]
49 }
50}
51
52response = requests.post(url, json=payload, headers=headers)
53print(response.json())

In the body:

NameRequiredTypeFormatDescriptionExample
contract_idtruestring-Unique identifier of the contract for which shifts are being submitted123456
external_idtruestring-User-defined ID of the shiftshift_123456
descriptiontruestring-Description of shift. Use it to describe what kind of work is done during the shift.This is a sample shift description.
date_of_worktruestringdateDate on which shift is performed. It is used to identify the payroll cycle of the shift2024-04-01
metatrueobject-Object containing detailed start/end times, breaks, and approval metadata-
meta.start.datetruestringdateDate when the shift starts2024-04-01
meta.start.timetruestringtime (HH:mm)Start time of the shift09:00
meta.start.is_rest_daytrueboolean-Indicates if the shift start is on a rest dayfalse
meta.start.is_public_holidaytrueboolean-Indicates if the shift start is on a public holidayfalse
meta.end.datetruestringdateDate when the shift ends2024-04-01
meta.end.timetruestringtime (HH:mm)End time of the shift17:00
meta.end.is_rest_daytrueboolean-Indicates if the shift end is on a rest dayfalse
meta.end.is_public_holidaytrueboolean-Indicates if the shift end is on a public holidayfalse
meta.breaksfalsearray-List of breaks taken during the shift-
meta.breaks[].start.datetruestringdateBreak start date2024-04-01
meta.breaks[].start.timetruestringtime (HH:mm)Break start time11:00
meta.breaks[].end.datetruestringdateBreak end date2024-04-01
meta.breaks[].end.timetruestringtime (HH:mm)Break end time12:00
meta.breaks[].is_paidfalseboolean-Indicates whether the break is paidtrue
meta.approval_datefalsestringdateDate when the shift was approved by a manager2024-04-03
2

Verify the response

A successful response (200) returns the details of the shift created.

1{
2 "data": [
3 {
4 "external_id": "shift_example05",
5 "description": "This is a sample shift description 5",
6 "date_of_work": "2025-06-01",
7 "created_at": "2025-06-30T19:31:54.402Z",
8 "updated_at": "2025-06-30T19:31:54.402Z",
9 "contract_id": "mjgd99e",
10 "meta": {
11 "start": {
12 "date": "2024-02-12",
13 "time": "08:00",
14 "is_rest_day": false,
15 "is_public_holiday": false
16 },
17 "end": {
18 "date": "2024-02-12",
19 "time": "16:00",
20 "is_rest_day": false,
21 "is_public_holiday": false
22 },
23 "approval_date": "2024-12-11"
24 }
25 }
26 ]
27}

Where:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined ID of the shiftshift_example05
descriptiontruestring-Description of shiftThis is a sample shift description 5
date_of_worktruestringdateDate of the shift2025-06-01
contract_idtruestring-Unique identifier of the contract that shifts were submitted formjgd99e
created_attruestringdate-timeDate on which the shift is created2025-06-30T19:31:54.402Z
updated_attruestringdate-timeDate on which the shift is updated2025-06-30T19:31:54.402Z
metatrueobject-Object containing detailed start/end times and approval date-
meta.start.datetruestringdateDate when the shift starts2024-02-12
meta.start.timetruestringtime (HH:mm)Start time of the shift08:00
meta.start.is_rest_daytrueboolean-Indicates if the shift start is on a rest dayfalse
meta.start.is_public_holidaytrueboolean-Indicates if the shift start is on a public holidayfalse
meta.end.datetruestringdateDate when the shift ends2024-02-12
meta.end.timetruestringtime (HH:mm)End time of the shift16:00
meta.end.is_rest_daytrueboolean-Indicates if the shift end is on a rest dayfalse
meta.end.is_public_holidaytrueboolean-Indicates if the shift end is on a public holidayfalse
meta.approval_datefalsestringdateDate when the shift was approved by a manager2024-12-11

List shifts in your organization

You can list the shifts in your organization and sort them by the time of creation.

1

Make the request

Make a GET request to the List time tracking shifts endpoint.

1import requests
2import os
3
4url = "https://api.letsdeel.com/rest/time_tracking/shifts"
5
6headers = {
7 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}"
8}
9
10params = {
11 "limit": 10,
12 "offset": 20,
13 "contract_id[]": ["abcd", "abcd2"],
14 "from_date": "2023-10-01",
15 "to_date": "2023-10-02"
16}
17
18response = requests.get(url, headers=headers, params=params)
19print(response.json())

In the query:

NameRequiredTypeFormatDescriptionExample
limitfalsenumberNumber of rows that must be returned in one API call100
offsetfalsenumberNumber of rows that must be skipped when returning the results10
contract_id[]falsestring[]array paramFilter shifts by one or more contract IDscontract_id[]=abcd&contract_id[]=efgh
from_datefalsestringYYYY-MM-DDFilter shifts from this date (inclusive)2023-10-01
to_datefalsestringYYYY-MM-DDFilter shifts until this date (inclusive)2023-10-02

Use the array syntax contract_id[]=value to filter for contract IDs. For multiple contract IDs, you can use contract_id[]=value1&contract_id[]=value2.

2

Review the response

A successful response (200) returns the list of shifts available in your organization and matching any filters applied.

1{
2 "data": [
3 {
4 "external_id": "d3m0d3m0-d3m0-d3m0-d3m0-d3m0d3m0d3m0",
5 "organization_id": 0,
6 "description": "string",
7 "date_of_work": "2019-08-24T14:15:22Z",
8 "contract_id": "string",
9 "summary": {
10 "shift_rate_external_id": "rate1234",
11 "time_unit": "HOUR",
12 "time_amount": 15.50,
13 "total_payable_hours": 15.50
14 },
15 "created_at": "2022-05-24T09:38:46.235Z",
16 "updated_at": "2022-05-24T09:38:46.235Z"
17 }
18 ],
19 "page": {
20 "total_rows": 0,
21 "items_per_page": 1,
22 "offset": 999999999
23 }
24}

Where:

NameRequiredTypeFormatDescriptionExample
datatruearray-The list of shifts available-
pagetrueobject-Contains information to navigate to the next set of results, if applicable-

Retrieve a single shift

You can retrieve the information of a single shift using the external_id of the shift.

1

Make the request

Make a GET request to the Retrieve a single time tracking shift endpoint.

1import requests
2import os
3
4external_id = "shift_123456"
5url = f"https://api.letsdeel.com/rest/time_tracking/shifts/{external_id}"
6
7headers = {
8 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}"
9}
10
11response = requests.get(url, headers=headers)
12print(response.json())
2

Review the response

A successful response (200) returns the information of the requested shift.

1{
2 "external_id": "d3m0d3m0-d3m0-d3m0-d3m0-d3m0d3m0d3m0",
3 "organization_id": 0,
4 "description": "string",
5 "date_of_work": "2019-08-24T14:15:22Z",
6 "contract_id": "string",
7 "summary": {
8 "shift_rate_external_id": "rate1234",
9 "time_unit": "HOUR",
10 "time_amount": 15.50,
11 "total_payable_hours": 15.50
12 },
13 "created_at": "2022-05-24T09:38:46.235Z",
14 "updated_at": "2022-05-24T09:38:46.235Z"
15}

Update shifts

You can update a shift before it is processed for payroll. After a shift is processed, your ability to amend it depends on the shift type:

  • You can amend a categorized shift using correction shifts
  • You cannot amend uncategorized (raw) shifts

This section explains how to update shifts that have not been processed for payroll:

Update categorized shift for an employee

You can update the information of a categorized shift.

You can only update shifts that have not been processed for payroll. Shifts are processed for payroll at the cutoff date.

1

Make the request

Make a PATCH request to the Update a time tracking shift endpoint.

1import requests
2import os
3
4external_id = "shift_123456"
5url = f"https://api.letsdeel.com/rest/time_tracking/shifts/{external_id}"
6
7headers = {
8 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
9 "Content-Type": "application/json"
10}
11
12payload = {
13 "data": {
14 "description": "This is a sample shift description.",
15 "date_of_work": "2023-10-01",
16 "payroll_cycle_ref": {
17 "date": "2023-10-31T00:00:00.000Z"
18 },
19 "summary": {
20 "time_amount": 15.50
21 }
22 }
23}
24
25response = requests.patch(url, json=payload, headers=headers)
26print(response.json())

In the path:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined ID of the shiftshift_123456

In the body:

NameRequiredTypeFormatDescriptionExample
datatrueobject-Contains the information of the shift that must be updated.-
2

Review the response

A successful response (200) returns the updated shift.

1{
2 "external_id": "95c35493-41aa-44f8-9154-5a25cbbc1865",
3 "organization_id": 0,
4 "description": "string",
5 "date_of_work": "2019-08-24T14:15:22Z",
6 "contract_id": "string",
7 "payroll_cycle_ref": {
8 "date": "2023-10-31T00:00:00.000Z"
9 },
10 "summary": {
11 "shift_rate_external_id": "rate1234",
12 "time_unit": "HOUR",
13 "time_amount": 15.50,
14 "total_payable_hours": 15.50
15 },
16 "created_at": "2022-05-24T09:38:46.235Z",
17 "updated_at": "2022-05-24T09:38:46.235Z"
18}

Update uncategorized (raw) shift for an employee

You can update the information of an uncategorized shift.

You can only update shifts that have not been processed for payroll. Shifts are processed for payroll at the cutoff date.

1

Make the request

Make a PATCH request to the Update a raw time tracking shift endpoint.

1import requests
2import os
3
4external_id = "shift_123456"
5url = f"https://api.letsdeel.com/rest/time_tracking/shifts/raw/{external_id}"
6
7headers = {
8 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
9 "Content-Type": "application/json"
10}
11
12payload = {
13 "data": {
14 "description": "This is a sample shift updated now again.",
15 "date_of_work": "2023-10-01",
16 "meta": {
17 "start": {
18 "date": "2024-02-12",
19 "time": "08:00",
20 "is_rest_day": False,
21 "is_public_holiday": False
22 },
23 "end": {
24 "date": "2024-02-12",
25 "time": "16:00",
26 "is_rest_day": False,
27 "is_public_holiday": False
28 },
29 "approval_date": "2024-12-11"
30 }
31 }
32}
33
34response = requests.patch(url, json=payload, headers=headers)
35print(response.json())

In the path:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined ID of the shiftshift_123456

In the body:

NameRequiredTypeFormatDescriptionExample
datatrueobject-Contains the information of the shift that must be updated.-
2

Review the response

A successful response (200) returns the updated shift.

1{
2 "external_id": "95c35493-41aa-44f8-9154-5a25cbbc1865",
3 "description": "string",
4 "date_of_work": "2019-08-24T14:15:22Z",
5 "contract_id": "string",
6 "payroll_cycle_ref": {
7 "date": "2023-10-31T00:00:00.000Z"
8 },
9 "meta": {
10 "start": {
11 "date": "2024-02-12",
12 "time": "08:00",
13 "is_rest_day": false,
14 "is_public_holiday": false
15 },
16 "end": {
17 "date": "2024-02-12",
18 "time": "16:00",
19 "is_rest_day": false,
20 "is_public_holiday": false
21 },
22 "approval_date": "2024-12-11"
23 },
24 "created_at": "2022-05-24T09:38:46.235Z",
25 "updated_at": "2022-05-24T09:38:46.235Z"
26}

Delete shift for a contract

You can delete a shift for a contract by using the external_id of the shift.

You can only delete shifts that have not been processed for payroll. Shifts are processed for payroll at the cutoff date.

1

Make the request

Make a DELETE request to the Delete a time tracking shift endpoint.

1import requests
2import os
3
4external_id = "shift_123456"
5url = f"https://api.letsdeel.com/rest/time_tracking/shifts/{external_id}"
6
7headers = {
8 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}"
9}
10
11response = requests.delete(url, headers=headers)
12print(response.status_code)

Where:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined ID of the shiftshift_123456
2

Verify the response

A successful response (204) returns an empty body.

Correction shifts

When you need to adjust hours for shifts that have already been processed for payroll, you can use correction shifts. Corrections create a new shift entry that adjusts the payable hours of the original shift. The correction will be processed in the next payroll cycle, ensuring accurate compensation without modifying historical payroll data.

The sequence below covers when a correction is accepted or rejected. See Common errors for the exact rejection messages.

Two additional rules to keep in mind:

  • Correction shifts cannot be submitted for another correction shift.
  • Correction shifts cannot be updated but can be deleted.

To submit a correction, use the same Create a time tracking shift endpoint, by specifying CORRECTION_DELTA as the shift_type and including a corrections array with additional fields to specify the correction details.

1

Identify the shift to correct

For example, you may have previously created a shift with 10 total payable hours and this shift has been already processed for payroll. If you later discover that the actual hours worked were 7.5, which requires a reduction of 2.5 hours, note the original shift’s external_id.

2

Submit the correction

Make a POST request to the Create a time tracking shift endpoint with the correction details.

1import requests
2import os
3
4url = "https://api.letsdeel.com/rest/time_tracking/shifts"
5
6headers = {
7 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
8 "Content-Type": "application/json"
9}
10
11payload = {
12 "data": {
13 "contract_id": "3j5z2e6",
14 "shifts": [
15 {
16 "external_id": "shift_example47",
17 "description": "Correction shift",
18 "shift_type": "CORRECTION_DELTA",
19 "shift_reference_id": "shift_example45",
20 "corrections": [
21 {
22 "type": "SUBTRACTION",
23 "time_amount": 2.5
24 }
25 ]
26 }
27 ]
28 }
29}
30
31response = requests.post(url, json=payload, headers=headers)
32print(response.json())

In the body:

NameRequiredTypeFormatDescriptionExample
contract_idtruestring-Unique identifier of the contract for which the correction is being submitted3j5z2e6
external_idtruestring-User-defined ID for the correction shiftshift_example47
descriptiontruestring-Description of the correction. Use it to describe the reason for the correction.Correction shift
shift_typetruestring-Type of shift being submitted. Use CORRECTION_DELTA for correctionsCORRECTION_DELTA
shift_reference_idtruestring-External ID of the original shift that is being correctedshift_example45
correctionstruearray-Array containing correction details-
corrections[].typetruestring-Type of correction. Use SUBTRACTION to reduce hours or ADDITION to add hoursSUBTRACTION
corrections[].time_amounttruenumber-Amount of time to add or subtract from the original shift2.5
3

Verify the response

A successful response (200) returns the details of the correction shift created.

1{
2 "data": [
3 {
4 "external_id": "shift_example47",
5 "description": "Correction shift",
6 "date_of_work": "2019-08-24T14:15:22Z",
7 "contract_id": "3j5z2e6",
8 "summary": {
9 "time_amount": -2.5,
10 "total_payable_hours": -2.5
11 },
12 "shift_type": "CORRECTION_DELTA",
13 "shift_reference_id": "shift_example45",
14 "created_at": "2022-05-24T09:38:46.235Z",
15 "updated_at": "2022-05-24T09:38:46.235Z"
16 }
17 ]
18}

Where:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined ID of the correction shiftshift_example47
descriptiontruestring-Description of the correctionCorrection shift
date_of_worktruestringdate-timeDate of the original shift being corrected2019-08-24T14:15:22Z
contract_idtruestring-Unique identifier of the contract3j5z2e6
summarytrueobject-Object containing the delta values for the correction. Negative values indicate reductions, positive values indicate additions.-
summary.time_amounttruenumber-Adjustment amount, negative for reduction and positive for increase-2.5
shift_typetruestring-Type of shift, will be CORRECTION_DELTA for correctionsCORRECTION_DELTA
shift_reference_idtruestring-External ID of the original shift being correctedshift_example45
created_attruestringdate-timeDate on which the correction is created2022-05-24T09:38:46.235Z
updated_attruestringdate-timeDate on which the correction is updated2022-05-24T09:38:46.235Z

Manage shift rates

Shift rates are used in payroll calculations to define the amount of salary to be paid for a specific shift. The shift rate types are:

NameDescriptionFormulaExample
MULTIPLIER_PERCENTAGEDefines the rate of a shift as a percentage of the salary, using the employee’s hourly salary (if it’s an hourly contract) or equivalent hourly salary (for non-hourly contracts).Total amount for shift = (MULTIPLIER_PERCENTAGE/100) * Per_hour_salary * Total_payable_hours10/houristhebasesalary;theusersubmittedashiftwithatotalof5payablehours,andaccordingtotheshiftrateattachedtotheshift,MULTIPLIERPERCENTAGEissetto200/hour is the base salary; the user submitted a shift with a total of 5 payable hours, and according to the shift rate attached to the shift, `MULTIPLIER_PERCENTAGE` is set to 200%, so `Total amount paid for the shift = 2 * 10 * 5 = 100`
PER_HOUR_FLAT_RATEDefines the rate of a shift as a flat rate per hour.Total amount for shift = PER_HOUR_FLAT_RATE * Total_payable_hoursPER_HOUR_FLAT_RATE is set to 100$ and total_payable_hours for the shift are 5 hours. Total amount paid for the shift = 100 * 5 = 500$
PER_UNIT_FLAT_RATEDefines the rate of a shift as a flat rate per unit of work, instead of per hour. Use this for work compensated by output (for example, deliveries, tickets, or items processed) rather than by time.Total amount for shift = PER_UNIT_FLAT_RATE * summary.time_amountPER_UNIT_FLAT_RATE is set to 2.50$ and time_amount for the shift is 40 (units). Total amount paid for the shift = 2.50 * 40 = 100$

PER_HOUR_FLAT_RATE and PER_UNIT_FLAT_RATE apply their value directly to summary.time_amount and do not use summary.time_unit in the calculation.time_amount can represent hours, units, or any other count you choose to submit. MULTIPLIER_PERCENTAGE is the only rate type that reads the worker’s base salary, and it only produces an amount when summary.time_unit is HOUR. Submitting a MULTIPLIER_PERCENTAGE shift with time_unit set to DAY, WEEK, or MONTH will not be compensated, since there is no base-salary conversion for those units.

Create a shift rate

You can create shift rates for your organization, which you can then map to individual shifts when adding them.

1

Make the request

Make a POST request to the time_tracking/shift_rates endpoint.

1import requests
2import os
3
4url = "https://api.letsdeel.com/rest/time_tracking/shift_rates"
5
6headers = {
7 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
8 "Content-Type": "application/json"
9}
10
11payload = {
12 "data": {
13 "external_id": "regular_rate_1",
14 "name": "Regular Shift rate 1",
15 "type": "PER_HOUR_FLAT_RATE",
16 "value": 150
17 }
18}
19
20response = requests.post(url, json=payload, headers=headers)
21print(response.json())

In the body:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User defined unique identifier for the shift rateregular_rate_1
nametruestring-A human readable string to identify the purpose of the shift rateRegular Shift rate 1
typefalsestringENUMDefines the type of rate that must be used. Use any of the available shift rate types.PER_HOUR_FLAT_RATE
valuefalsenumber-Value of the shift rate, to use in combination with the type parameter150

For example, to create a per-unit rate for piecework, such as $2.50 per delivery, use PER_UNIT_FLAT_RATE as the type:

1{
2 "data": {
3 "external_id": "delivery_rate_1",
4 "name": "Delivery rate",
5 "type": "PER_UNIT_FLAT_RATE",
6 "value": 2.50
7 }
8}

Reference this rate’s external_id from a categorized shift’s summary.shift_rate_external_id, and set summary.time_amount to the number of units completed (for example, 40 for 40 deliveries). See Add categorized shifts.

Retrieve a shift rate

You can retrieve a shift rate using the external_id of the shift rate.

1

Make the request

Make a GET request to the Retrieve a single time tracking shift rate endpoint.

1import requests
2import os
3
4external_id = "regular_rate_1"
5url = f"https://api.letsdeel.com/rest/time_tracking/shift_rates/{external_id}"
6
7headers = {
8 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
9 "Content-Type": "application/json"
10}
11
12response = requests.get(url, headers=headers)
13print(response.json())

Where:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User-defined unique identifier for the shift rateshift_123456
2

Review the response

A successful response (200) returns the shift rate of the requested shift.

1{
2 "data": {
3 "organization_id": "string",
4 "external_id": "string",
5 "name": "string",
6 "rate_type": "MULTIPLIER_PERCENTAGE",
7 "value": 0,
8 "created_at": "2022-05-24T09:38:46.235Z",
9 "updated_at": "2022-05-24T09:38:46.235Z"
10 }
11}

List shift rates

You can retrieve the list of shift rates for your organization.

1

Make the request

Make a GET request to the List time tracking shift rates endpoint.

1import requests
2import os
3
4url = "https://api.letsdeel.com/rest/time_tracking/shift_rates"
5
6headers = {
7 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
8 "Content-Type": "application/json"
9}
10
11params = {
12 "limit": 10,
13 "offset": 5
14}
15
16response = requests.get(url, headers=headers, params=params)
17print(response.json())
NameRequiredTypeFormatDescriptionExample
limitfalsenumberNumber of rows that must be returned in one API call100
offsetfalsenumberNumber of rows that must be skipped when returning the results10
2

Review the response

A successful response (200) returns the list of shift rates available in your organization and matching any filters applied.

1{
2 "data": [
3 {
4 "organization_id": "string",
5 "external_id": "string",
6 "name": "string",
7 "rate_type": "MULTIPLIER_PERCENTAGE",
8 "value": 0,
9 "created_at": "2022-05-24T09:38:46.235Z",
10 "updated_at": "2022-05-24T09:38:46.235Z"
11 }
12 ],
13 "page": {
14 "total_rows": 0,
15 "items_per_page": 1,
16 "offset": 999999999
17 }
18}

Where:

NameRequiredTypeFormatDescriptionExample
datatruearray-An array of shift rates[shift_rate_1, shift_rate_2, shift_rate_3]
organization_idtruenumber-The ID of your organization123456
external_idtruestring-User defined unique identifier for the shift rateregular_rate_1
nametruestring-A human readable string to identify the purpose of the shift rateRegular Shift rate 1
rate_typefalsestringENUMDefines the type of rate that must be used. Use any of the available shift ratesPER_HOUR_FLAT_RATE
valuefalsenumber-Value of the shift rate, to use in combination with the type parameter150
created_attruestringdate-timeDate on which the shift rate is created2022-05-24T09:38:46.235Z
updated_attruestringdate-timeDate on which the shift rate is updated2022-05-24T09:38:46.235Z
pagetrueobject-An object containing pagination information. Use it to navigate through sets of-

Update a shift rate

You can update a shift rate if it’s not being used in any shift, by using the external_id of the shift rate.

Only shift rates that are not used in any shift can be updated.

1

Make the request

Make a PATCH request to the Update a time tracking shift rate endpoint.

1import requests
2import os
3
4external_id = "regular_rate_1"
5url = f"https://api.letsdeel.com/rest/time_tracking/shift_rates/{external_id}"
6
7headers = {
8 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
9 "Content-Type": "application/json"
10}
11
12payload = {
13 "data": {
14 "name": "On-call shift rate",
15 "type": "PER_HOUR_FLAT_RATE",
16 "value": 150
17 }
18}
19
20response = requests.patch(url, json=payload, headers=headers)
21print(response.json())

In the path:

NameRequiredTypeFormatDescriptionExample
external_idtruestring-User defined unique identifier for the shift rateregular_rate_1

In the body:

NameRequiredTypeFormatDescriptionExample
nametruestring-A human readable string to identify the purpose of the shift rateRegular Shift rate 1
typetruestringENUMDefines the type of rate that must be used. Use any of the available shift ratesPER_HOUR_FLAT_RATE
valuetruenumber-Value of the shift rate, to use in combination with the type parameter150
2

Review the response

A successful response (200) returns the updated shift rate.

1{
2 "data": {
3 "organization_id": "string",
4 "external_id": "string",
5 "name": "string",
6 "rate_type": "MULTIPLIER_PERCENTAGE",
7 "value": 0,
8 "created_at": "2022-05-24T09:38:46.235Z",
9 "updated_at": "2022-05-24T09:38:46.235Z"
10 }
11}

Delete a shift rate

You can delete a shift rate if it’s not being used in any shift, by using the external_id of the shift rate.

Only shift rates that are not used in any shift can be deleted.

1

Make the request

Make a DELETE request to the Delete a time tracking shift rate endpoint.

1import requests
2import os
3
4external_id = "regular_rate_1"
5url = f"https://api.letsdeel.com/rest/time_tracking/shift_rates/{external_id}"
6
7headers = {
8 "Authorization": f"Bearer {os.getenv('DEEL_API_TOKEN')}",
9 "Content-Type": "application/json"
10}
11
12response = requests.delete(url, headers=headers)
13print(response.status_code)
2

Verify the response

A successful response (204) returns an empty body.

Common errors

Most errors return a 400, 404, or 422 response with a self-explanatory message, for example, a missing organization ID or a shift that does not exist. This section covers the errors whose cause is not obvious from the message alone.

Payroll cycle already closed

A message about the payroll cycle being closed can mean two different things, depending on when you see it:

When it happensStatusMessage
Creating a shift with an explicit payroll_cycle_ref.date whose cutoff has already passed400Delayed submission is not allowed. Payroll cycle is already closed.
Updating or deleting a shift whose payroll cycle has already been processed403You cannot update or delete this shift as payroll cycle is already closed

The first only happens if you explicitly set payroll_cycle_ref.date — see Preventing late submissions. If you omit payroll_cycle_ref, the shift is never rejected for being late; it rolls over silently to the next cycle instead. The second happens any time you try to modify a shift after its cycle has already been compensated. Use a correction shift instead.

Shift rate type mismatch on update

400Cannot update rate associated with the shift: the type of the new rate must match the type of the original rate.

A shift can be repointed to a different shift rate of the same type, for example, swapping one PER_HOUR_FLAT_RATE for another, but not to a shift rate of a different type, such as switching from PER_HOUR_FLAT_RATE to MULTIPLIER_PERCENTAGE.

Shift rate is already in use

409The shiftRate with id: '{id}' is currently in use and cannot be updated/deleted

A shift rate becomes locked as soon as any shift references it. To change its value or delete it, first confirm no shift still references it, or create a new shift rate instead.

Correction shift errors

Corrections carry a few rules that are not obvious from the request shape alone:

  • Cannot apply correction to shift {id}. Correction can only be applied to a regular shift that has been exported. (400): A correction can only target a shift that has already been processed for payroll. If the original shift has not been processed yet, update it directly instead of submitting a correction.
  • Cannot apply correction to shift {id}. Only summary shifts can be corrected. (400): Corrections only work on categorized shifts. Uncategorized (raw) shifts cannot be corrected.
  • Cannot apply {type} correction to shift {id}. {otherType} corrections have already been applied to this shift. (400): Once an ADDITION correction has been applied to a shift, you cannot later apply a SUBTRACTION correction to the same shift, and vice versa. Corrections to a given shift must stay in one direction.
  • Cannot apply correction to shift {id}. Subtraction correction would result in an overall negative value (400): A SUBTRACTION correction cannot reduce the shift’s payable amount below zero.
  • Cannot submit multiple corrections for the same shift reference ID: {ids} (400): A single request cannot contain two corrections for the same original shift. Submit them one at a time, or combine the adjustment into a single correction.

Next steps