Send time & attendance to payroll with pay items
Send hours worked, overtime, leave and other pay inputs from your time and attendance system into Remote Payroll as pay items.
Send hours worked, overtime, leave and other pay inputs from your time and attendance (T&A) system into Remote Payroll as pay items, automatically and at any volume.
This is part of your payroll implementationWhether you'll send time data from another system is agreed with Remote during your payroll implementation. That's also when the details that differ from customer to customer are set up and confirmed: which pay codes you use, how you date them, and your cutoffs. This guide explains how the Pay Items API works in general. Wherever it says confirm with your Remote contact, check the detail with your payroll implementation or payroll support contact before you rely on it.
Who this is for
- Global Payroll customers who track time in another system (a T&A, scheduling or workforce management tool) and want to send it to Remote without uploading files by hand.
- Engineers and integration partners building that connection for a customer.
What you'll do
Look up which pay codes each legal entity accepts, convert your T&A values into the right units, send them in batches, check what was rejected, then read back what landed and reconcile it against your T&A system.
How this is different
Pay items are the same records whether they arrive through the bulk uploader in the Remote app or through the API. The same pay codes are accepted and the same rules apply. The API is the route for sending them on a schedule, without anyone preparing or uploading a file.
Not only time and attendanceYou can send any pay code that is set up on the legal entity to accept input, including allowances, deductions and other payments. This guide uses time and attendance as the main example.
Send your own ID on every pay itemAdd
partner_external_idwith the ID of the record each pay item comes from, such as a timesheet line. Then a retry or a resend updates the same pay item instead of creating a second one, and you can find any pay item by your own ID. This guide shows where the field goes. Sync time and attendance to payroll with your own IDs covers each case: retries, changes, deletes and corrections.
Before you start
- Global Payroll employments. Pay items can only be created for employees on Remote Global Payroll.
- Pay codes set up on each legal entity. Your pay codes are configured on each legal entity with Remote during payroll implementation.
- A company-level access token with the
pay_item:writescope (or a broader one such aspayrollorall:write). Use a company API token or an OAuth token that the company has authorised. Integration partners: your own client-credentials token can list pay codes but can't create pay items, so use a token the customer has authorised. See Authentication and Scopes. - Your company ID, legal entity IDs and employment IDs. Employments are identified by their Remote employment ID (a UUID). You can find these with
GET /v1/employments. If you've stored your own employee ID on the employment aspartner_external_id, filter by it:GET /v1/employments?partner_external_id=EMP-17. See Your own IDs on Remote records. partner_external_idturned on for pay items (recommended). It's being rolled out. Until it's on for you, requests that send it fail with "partner_external_id is not available". Ask your Remote contact.- A Sandbox environment to test in. Build and test against
https://gateway.remote-sandbox.combefore you go live onhttps://gateway.remote.com. See Sandbox Quickstart.
Confirm with your Remote contact
These details depend on your country, your legal entities and what was agreed during your payroll implementation. The API doesn't check them, so confirm each one before you go live:
- Which pay code to use for each kind of time or pay. If a pay code configured for you doesn't fit your data (for example, you need part-hours on a code that only takes whole numbers), ask your Remote contact. Don't substitute a different code.
- How to send deductions and downward corrections. The API accepts a negative
amounton every pay code. Whether that's the right way to send them for your pay codes depends on your setup. - Your effective-date convention. For most customers,
effective_dateis the day the work happened, but some pay cycles use a different convention. A date that doesn't follow the convention agreed for your legal entity can put a pay item in the wrong payroll run, or get it processed as a retroactive correction. - Your input cutoffs. Pay items need to arrive before the payroll input cutoff for the run they belong to. You can read cutoff dates from
GET /v1/payroll-calendars/{cycle}(input_cutoff_date). See List Company Payroll Calendar. - How corrections are handled once a payroll run has locked.
How it works
- List the pay codes each legal entity accepts, and how each one must be encoded.
- Convert each T&A value into the unit its pay code requires.
- Send pay items in batches of up to 500, each with your own ID.
- Handle failures: rejected rows in a successful response, and whole requests rejected for format errors.
- Read back and reconcile what landed against your T&A system.
- Change, delete or correct pay items when something is wrong.
Step 1: List the pay codes for each legal entity
curl "https://gateway.remote-sandbox.com/v1/companies/$COMPANY_ID/legal-entities/$LEGAL_ENTITY_ID/pay-codes?page_size=100" \
-H "Authorization: Bearer $REMOTE_API_TOKEN"{
"data": {
"pay_codes": [
{
"slug": "e8dc8652-ccbf-456e-a42c-2f977e24dbfc",
"code": "worked_hours",
"name": "Worked hours",
"type": "duration",
"category": "time_attendance",
"external_import_code": null,
"description": null
}
],
"current_page": 1,
"total_pages": 1,
"total_count": 48
}
}This list is the source of truth for what you can send. It returns exactly the codes the legal entity accepts, and nothing else.
- Run it per legal entity. Each legal entity has its own list, and the lists differ. Key your mapping on legal entity and code, never on code alone.
- Run it before every send. Codes can be archived or changed. You'll find out here instead of from rejected rows.
- Treat
codeas an opaque string. Codes can contain spaces, accents and symbols. Don't change their case or strip characters. external_import_codeis your own code for that pay code, if one has been set up. You can send it instead ofcode.custom_configurationappears only on pay codes that need extra details on each pay item, such as an employee's company car. It gives thekeyto send, whether the details arerequired, and the format of the values (schema). See Step 3.page_sizecan be up to 100.
Step 2: Convert values to the unit the pay code requires
amount is always a whole number, and its unit depends on the pay code's type:
Pay code type | Send amount as | Example |
|---|---|---|
duration | seconds | 7.5 hours → 27000 |
hours | whole hours | 8 hours → 8 |
unit | a count | 3 days → 3 |
amount | cents (minor currency units) | €150.00 → 15000 |
percentage | basis points | 1.5% → 150 |
Always convert from the code'stype. Never assume a unit.If you send 8 (meaning hours) to a
durationcode, it's stored as 8 seconds, and the request still succeeds. Nothing flags it until the payslip is wrong. The same kind of code can have a differenttypein different countries, so check per legal entity.
- No decimals.
hoursandunitcodes can't hold part-hours. If that doesn't fit your data, confirm with your Remote contact. - Negative values are accepted on every pay code, for example to take away hours reported in error. Whether that's the right way to send deductions and corrections for your pay codes depends on your setup. Confirm with your Remote contact.
Step 3: Send pay items in batches
curl -X POST "https://gateway.remote-sandbox.com/v1/pay-items/bulk" \
-H "Authorization: Bearer $REMOTE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pay_items": [
{
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "worked_hours",
"effective_date": "2026-10-12",
"amount": 27000,
"partner_external_id": "TS-0042",
"provider_data": {
"shift_code": "DAY",
"work_day_duration": 28800
}
},
{
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "overtime",
"effective_date": "2026-10-12",
"amount": 7200,
"partner_external_id": "TS-0043",
"provider_data": {
"pay_rate": 1.5,
"hourly_rate": 2550,
"hourly_rate_currency_code": "EUR"
}
},
{
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "meal_allowance",
"effective_date": "2026-10-12",
"amount": 1500,
"currency": "EUR",
"partner_external_id": "TS-0044"
}
]
}'The pay codes above are examples. Use the codes from your own legal entity's list.
| Field | Required | What it is |
|---|---|---|
employment_id | Yes | The employee's Remote employment ID (UUID). |
code or external_import_code | Exactly one | The pay code, from Step 1. Send one or the other, never both. |
effective_date | Yes | YYYY-MM-DD. Follow the convention agreed for your legal entity. |
amount | Yes | A non-zero whole number in the pay code's unit (Step 2). |
partner_external_id | Recommended | Your own ID for the record this pay item comes from, up to 64 characters. Sending the same ID again updates that pay item instead of creating a new one. See Your own IDs on Remote records. |
currency | No | ISO currency code for money codes. Defaults to the legal entity's currency. |
end_date | No | Last day of a leave period. Leave-of-absence codes only (see Leave periods). |
provider_data.shift_code | No | The shift identifier from your T&A system. |
provider_data.pay_rate | No | Rate multiplier, for example 1.5 for overtime. Not enforced, so validate it on your side. |
provider_data.hourly_rate | No | Hourly rate in cents, with hourly_rate_currency_code. |
provider_data.work_day_duration | No | Expected working day, in seconds. |
provider_data.correction_effective_date | Corrections only | The original day being corrected (see Corrections). |
custom_configuration | Only for codes that use one | Extra details some pay codes need, such as a company car. Send the pay code's key and the values in the format from Step 1. |
Up to 500 pay items per request. Send larger volumes as several requests.
Step 4: Handle both kinds of failure
A200 OKdoes not mean every row was savedAlways read
failures[]. If your integration only checks the HTTP status, it will silently drop rows.
| What happened | Response | Was anything saved? |
|---|---|---|
| A row broke a business rule, for example a pay code not accepted on that legal entity | 200, with the row in failures[] | Yes. Every valid row was saved. |
Any row had a format problem: a zero or decimal amount, both or neither of code / external_import_code, a partner_external_id over 64 characters, an unknown field, or more than 500 rows | 422, listing each problem and the row it's in | No. Nothing in the request was saved. |
A 200 with one rejected row:
{
"data": {
"successes": [
{
"row_number": 0,
"pay_item": {
"id": "65cb9aeb-54dd-409e-924f-a8ae1d9d1397",
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "worked_hours",
"effective_date": "2026-10-12",
"end_date": null,
"amount": 27000,
"partner_external_id": "TS-0042",
"provider_data": { "shift_code": "DAY", "work_day_duration": 28800 },
"replaced_ids": []
}
}
],
"failures": [
{ "row_number": 1, "error": "pay_item_code_not_allowed" }
]
}
}row_number is the position of the row in the pay_items array you sent, starting at 0. If you send partner_external_id, you don't need to store Remote's id: your own ID is enough to find, change and delete the pay item later. If you don't, store the returned id against the record it came from (see Reconcile with your T&A system).
A 422 can come back in either of two shapes. Handle both:
{ "errors": { "pay_items/1/amount": ["Invalid integer. Got: number"] } }{ "message": [ { "code": "parameter_value_invalid", "message": "Parameter `amount` must be a non-zero integer", "param": "pay_items/1/amount" } ] }Row-level errors you may see in failures[]. The error is either a string, such as "pay_item_code_not_allowed", or an object with a code and a message. Check which one you got before you compare it.
| Error | What it means |
|---|---|
pay_item_code_not_allowed / pay_item_external_import_code_not_allowed | The code isn't accepted on that employee's legal entity. If it's missing from the Step 1 list for that legal entity, that's why. Confirm with your Remote contact if you expected it to be there. |
employment_not_found | The employment ID doesn't exist in your company, or your token can't see it. |
employment_not_global_payroll | The employee isn't on Global Payroll. |
effective_date_after_termination_date | A time and attendance item is dated after the employee's termination date. Comes with a message that gives the date. A date on the termination day itself is accepted. |
custom_configuration_required / custom_configuration_not_used / invalid_custom_configuration | The pay item's custom_configuration doesn't match what its pay code needs. Comes with a message and the param at fault. |
partner_external_id_... errors | A problem with your own ID, for example the same ID twice in one request. Sync time and attendance to payroll with your own IDs lists each one and what to do. |
Step 5: Read back what landed
curl "https://gateway.remote-sandbox.com/v1/pay-items?legal_entity_id=$LEGAL_ENTITY_ID&effective_from=2026-10-01&effective_to=2026-10-31&page_size=100&page=1" \
-H "Authorization: Bearer $REMOTE_API_TOKEN"{
"data": {
"pay_items": [
{
"id": "65cb9aeb-54dd-409e-924f-a8ae1d9d1397",
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "worked_hours",
"effective_date": "2026-10-12",
"end_date": null,
"amount": 27000,
"partner_external_id": "TS-0042",
"provider_data": { "shift_code": "DAY", "work_day_duration": 28800 },
"replaced_ids": []
}
],
"current_page": 1,
"total_pages": 1,
"total_count": 1
}
}- Filters:
legal_entity_id,employment_slug(the employee's employment ID),effective_from,effective_to, andpartner_external_idto find one pay item by your own ID. Filter names must be exact: an unrecognised parameter returns a422. - Page through until
current_pageequalstotal_pages.page_sizecan be up to 100. - This returns all of the company's current pay items in that range, including ones created in the Remote app or by other systems, not only the ones you sent.
- Recurring pay items aren't returned. A pay item set up in the Remote app to repeat every month doesn't appear. The monthly payments made from it do.
hourly_rateis returned as a string (for example"2550").
Reconcile with your T&A system
Make reconciliation part of every sendYour T&A system and Remote should agree before each payroll cutoff. After each send, read back (Step 5) and compare against what your T&A system holds for the same employees and dates.
- Match on your own ID first. If you send
partner_external_id, match each pay item to your T&A record on it. Then compare employee, pay code, effective date and amount. Look for anything missing, anything extra, and any difference in amount or date. - Without your own ID, keep the Remote
id. You need eitherpartner_external_idor Remote'sidto change or delete a pay item later. - Retries without your own ID create duplicates. Sending the same row twice without
partner_external_idcreates two pay items, so a retry after a timeout can double-count. Withpartner_external_id, a retry updates the pay item that already landed. Without it, read back before you resend and check whether the row already landed. - Pay items in Remote may come from more than one place. Someone may also add or change pay items in the Remote app. Pay items without your
partner_external_idcame from somewhere else. Agree with your Remote contact which system is the source of truth for which pay codes.
Step 6: Change, delete or correct pay items
Change or delete before the run locks
# Change the amount
curl -X PATCH "https://gateway.remote-sandbox.com/v1/pay-items/$PAY_ITEM_ID" \
-H "Authorization: Bearer $REMOTE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "amount": 25200 }'
# Remove it
curl -X DELETE "https://gateway.remote-sandbox.com/v1/pay-items/$PAY_ITEM_ID" \
-H "Authorization: Bearer $REMOTE_API_TOKEN"- Changing
amountoreffective_datereplaces the pay item. The response has a newid, with the old one listed inreplaced_ids. The oldidkeeps working:PATCHandDELETEon anyidRemote returned act on the current version. Storing the newidis still a good idea. - Changing only
provider_dataorend_datekeeps the sameid. - Resending a row with the same
partner_external_idthroughPOST /v1/pay-items/bulkworks like aPATCH, without needing Remote'sid. See Sync time and attendance to payroll with your own IDs. - A successful delete returns
{"data": {"status": "ok"}}and frees the pay item'spartner_external_id. APATCHorDELETEon a deleted pay item returns404"Pay item not found because it was deleted". - Some pay items can't be changed through the API: salary items, items linked to a payroll adjustment, items created automatically by Remote from other sources, and recurring pay items. These return a
422that explains why. Manage recurring pay items in the Remote app.
How you know a correction is needed
Once a pay item's payroll run has locked, PATCH and DELETE return a 422 with one of these messages:
Associated payroll run is no longer in preparationAssociated payroll run has a cut off date which is in the past.
That's your signal to leave the original pay item alone and send a correction instead.
A third message, This payroll run's figures are being updated. Try again in a few minutes., is different: the run hasn't locked, so retry the same request later instead of sending a correction.
Corrections
A correction is a new pay item:
effective_date: the day you submit the correctionprovider_data.correction_effective_date: the original day being correctedamount: the differencepartner_external_id: a new ID of its own, because the original ID still belongs to the locked pay item
Set correction_effective_date only on corrections. Its presence is what marks a pay item as a correction.
For example, 8 hours were reported for 14 October but the employee worked 5. On 21 October, after the run locked:
{
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "worked_hours",
"effective_date": "2026-10-21",
"amount": -10800,
"partner_external_id": "TS-0050-correction-1",
"provider_data": { "correction_effective_date": "2026-10-14" }
}How corrections are processed, and whether a negative amount is the right way to send one for your pay codes, depends on your setup. Confirm with your Remote contact.
Leave periods
For leave-of-absence pay codes (parental, sick, unpaid leave and similar), set effective_date to the first day and end_date to the last day (inclusive). Don't set end_date on anything else. amount is in the unit of the pay code's type, like any other pay item (Step 2). Use the leave request's ID as partner_external_id.
{
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "unpaid_leave",
"effective_date": "2026-10-05",
"end_date": "2026-10-30",
"amount": 576000,
"partner_external_id": "LEAVE-0007"
}Make your workflow reliable
Sending pay items from a T&A system is usually a scheduled job that runs every day or every pay period. A few habits make it much easier to run:
- Refresh pay codes at the start of each run (Step 1), and map from them rather than from a hard-coded list.
- Send your own ID on every row, so a retry never pays anyone twice.
- Check every row before sending. Catch the problems that would reject a whole request: decimals, zero amounts, both or neither code fields, IDs over 64 characters, more than 500 rows, and codes missing from the legal entity's list.
- Send well before the cutoff, so there's time to fix rejected rows.
- Log every response. Keep
failures[]and every422, and alert on them. - Read back and reconcile after every send (see Reconcile with your T&A system).
Use the Remote CLI
The Remote CLI isn't a separate way of sending pay items: it calls the same API with the same API key, so everything on this page still applies. What it adds is the plumbing, which makes it a good base for a scheduled job, or for an AI assistant to build one with you:
remotecli pay-items codes --legal-entity-id $LEGAL_ENTITY_ID # list pay codes (Step 1)
remotecli pay-items create --file pay-items.csv # send (Steps 3–4)
remotecli pay-items list --effective-from 2026-10-01 --effective-to 2026-10-31 --all # read back (Step 5)
remotecli pay-items update $PAY_ITEM_ID --amount 25200 # change (Step 6)
remotecli pay-items delete $PAY_ITEM_ID # delete (Step 6)create --file takes CSV or JSON with the same field names as the API. It checks every row before sending, reports all problems together, and exits with an error if any row fails. See Introduction to the Remote CLI.
Good to know
- Up to 500 pay items per request.
- Global Payroll only.
- Codes are per legal entity. The same code name can have a different
typein another country. - Retired fields.
provider_data.payout_amount,payout_currency_codeandis_deductionare no longer used. If you send them they are ignored, not rejected, and the response carriesDeprecationandSunsetheaders. Send a payout as its own pay item with its own pay code.adjustment_effective_datehas been renamedcorrection_effective_date. - Recurring pay items aren't covered. The API doesn't return them and can't change them. Manage them in the Remote app.
For AI agents
- Always call the pay-codes endpoint for the employee's legal entity first. Only send codes it returns, and encode
amountfrom that code'stype. amountmust be a non-zero integer. Never send decimals.- Treat any non-empty
failures[]as a failed send, and treat any422as "nothing saved". Handle both 422 shapes (errorsandmessage). - Send a
partner_external_idon every row: the source record's own stable ID, unique per record, at most 64 characters. A bulk request whose rows all carry one is safe to retry. Without it, read back first: a retry creates duplicates. - Don't decide effective-date conventions, negative values or pay code substitutions yourself. These are agreed during payroll implementation: ask the user to confirm them with their Remote contact.
- After a
PATCHthat changesamountoreffective_date, the response has a newid. Old ids still work onPATCHandDELETE. - Never use this API to change recurring pay items.
- A
422saying the payroll run is no longer in preparation, or that its cutoff has passed, means: send a correction instead, with a newpartner_external_id. A 422 saying the run's figures are being updated means: retry later. - See llms.txt for the machine-readable docs.
Next steps
- Sync time and attendance to payroll with your own IDs, to resend, change and correct pay items without duplicates.
- Your own IDs on Remote records, for how
partner_external_idworks across Remote. - Sandbox Quickstart, to set up a place to test.
- API reference: List Pay Codes, Bulk Create Pay Items, List Pay Items, Update a Pay Item, Delete a Pay Item.
- List Company Payroll Calendar, for input cutoff dates.
- Authentication and Scopes, to set up your token.
Updated about 16 hours ago