Sync time and attendance to payroll with your own IDs
Send pay items from your time and attendance system with partner_external_id, so resends, changes and corrections never create duplicates.
Your time and attendance (T&A) system sends hours worked, overtime and leave to Remote Payroll as pay items. The same timesheet line often has to be sent more than once: the job retries after a timeout, a manager fixes the hours, or the line is deleted. This guide shows how to handle each of those cases with partner_external_id, so Remote always holds one pay item per T&A record.
New to partner_external_id?Your own IDs on Remote records explains the field itself: who can see your values, how long they can be, and how to find a record by one. This guide shows how to use it with pay items.
This is part of your payroll implementationWhich pay codes you use, how you date them and your payroll cutoffs are agreed with Remote during your payroll implementation. Wherever this guide says confirm with your Remote contact, check the detail with your payroll implementation or payroll support contact.
Who this is for
- Global Payroll customers who track time in another system and send it to Remote through the API.
- Engineers and integration partners building that connection for a customer.
Before you start
- Global Payroll employments. Pay items can only be created for employees on Remote Global Payroll.
- A company-level access token with the
pay_item:writescope (or a broader one such aspayrollorall:write). Integration partners: use a token the customer has authorised. Your own client-credentials token can list pay codes but can't create pay items. - The pay codes for each legal entity. List them with
GET /v1/companies/{company_id}/legal-entities/{legal_entity_id}/pay-codes. Sendamountas a whole number in the unit of the code'stype, for example seconds for adurationcode (7.5 hours →27000). Always convert from the code'stype: if you send8(meaning hours) to adurationcode, it is stored as 8 seconds and the request still succeeds. - Your payroll cutoffs. Pay items need to arrive before the input cutoff of their payroll run. Read it from
GET /v1/payroll-calendars/{cycle}(input_cutoff_date). partner_external_idturned on for pay items. It is being rolled out. Until it is on for you, requests that send it fail with "partner_external_id is not available". Ask your Remote contact.- A Sandbox to test in:
https://gateway.remote-sandbox.com.
What's in scope
This guide covers one-off pay items: a pay item for one day or one period, such as a shift's hours, an overtime entry or a leave period. That is what a T&A system sends.
Recurring pay items are out of scopeRecurring pay items, such as a fixed monthly allowance set up in the Remote app, don't work with this flow:
GET /v1/pay-itemsdoesn't return them. It returns the monthly payments generated from them instead.PATCHandDELETErefuse them with422"Recurring pay items cannot be updated or deleted through this API."- A monthly payment generated from a recurring pay item can be regenerated with a new
idwhen the recurring item changes. Its oldidthen returns404.Manage recurring pay items in the Remote app, and use
partner_external_idonly for the one-off pay items your T&A system sends.
Pick your ID
Use the ID of the T&A record a pay item comes from: a timesheet line, a shift or a leave request. It must be:
- Stable. The same record always has the same ID, every time you send it.
- Unique per record. Two records never share an ID. An employee ID doesn't work: all of that employee's rows would share it.
- Up to 64 characters, and never
nullor empty.
If more than one of your systems sends pay items, add a prefix per system, for example ta:TS-0042 and expenses:EX-17.
How Remote handles each row
You send every row through POST /v1/pay-items/bulk, up to 500 rows per request. Remote looks at each row's partner_external_id on its own:
| The row sends | What happens |
|---|---|
| A value no live pay item holds | Creates a new pay item that holds it |
| A value a live pay item holds | Updates that pay item's amount, effective_date, end_date and provider_data |
No partner_external_id | Creates a new pay item. Sending it again creates a second one. |
null or an empty string | The row fails with partner_external_id_blank |
Each scenario below builds on this.
Scenario 1: Send a new timesheet
An employee worked 7.5 hours on 12 October. Your timesheet line is TS-0042.
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"
}
]
}'No pay item holds TS-0042 yet, so Remote creates one:
{
"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": {},
"replaced_ids": []
}
}
],
"failures": []
}
}
A200 OKdoes not mean every row was savedAlways read
failures[]. A row that breaks a business rule is listed there, and the other rows are still saved. A format problem in any row (a decimalamount, a value over 64 characters, more than 500 rows) returns422instead, and nothing in the request is saved.
You don't need to store Remote's id. Your partner_external_id is enough to find, change and reconcile the pay item later.
Scenario 2: Your job times out, and you send it again
Your job sent 300 rows, then the connection dropped. You don't know which rows landed.
Send the same request again, with the same IDs. Rows that landed the first time are updated with the same values. Rows that didn't land are created. Either way, each timesheet line ends up as one pay item.
This is the main reason to use partner_external_idWithout it, a retry creates a second pay item for every row that already landed, and the employee is paid twice.
If you retry while the first request is still running, both requests may try to create the same pay item. Only one of them can. The other request's row fails with {"errors": {"partner_external_id": ["has already been taken"]}}. That means the row already landed: read it back with GET /v1/pay-items?partner_external_id=... and don't treat it as broken. To avoid this, wait for a request to finish or time out before you retry it.
Scenario 3: A manager changes the hours before payroll cutoff
The manager changes TS-0042 from 7.5 to 7 hours. Send the line again with the same ID and the new amount:
{
"employment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "worked_hours",
"effective_date": "2026-10-12",
"amount": 25200,
"partner_external_id": "TS-0042"
}Remote finds the pay item that holds TS-0042 and updates it. What you get back depends on what changed:
amountoreffective_datechanged: Remote replaces the pay item with a new version. The response has a newid, and the old one is listed inreplaced_ids.TS-0042moves to the new version, so you keep sending the same value.- Only
provider_dataorend_datechanged: the pay item keeps itsid. - Nothing changed: nothing happens, and the pay item keeps its
id.
provider_data you send is merged into what's stored: keys you leave out keep their old values.
Old Remote IDs keep working: PATCH and DELETE on any id Remote returned act on the current version.
The employee and pay code can't change this way. If a row sends TS-0042 with another employee or another pay code, it fails with partner_external_id_held_by_other_pay_item. That usually means two of your records share an ID. To move hours to another employee or pay code, delete the pay item first (Scenario 4). That frees the ID, so you can then send the row again with the same ID.
Scenario 4: A timesheet line is deleted before payroll cutoff
Find the pay item by your ID, then delete it:
curl "https://gateway.remote-sandbox.com/v1/pay-items?partner_external_id=TS-0042" \
-H "Authorization: Bearer $REMOTE_API_TOKEN"
curl -X DELETE "https://gateway.remote-sandbox.com/v1/pay-items/$PAY_ITEM_ID" \
-H "Authorization: Bearer $REMOTE_API_TOKEN"A successful delete returns {"data": {"status": "ok"}} and frees TS-0042. If you send TS-0042 again later, Remote creates a new pay item. PATCH or DELETE on the deleted id returns 404 "Pay item not found because it was deleted".
Scenario 5: Hours change after payroll has locked
The run for 14 October has locked, and the manager finds the employee worked 5 hours, not 8. If you resend TS-0050 with the new amount, the row fails with unprocessable_entity and 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: a new pay item, with a new ID of its own.
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 row later instead of sending a correction.
{
"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" }
}effective_dateis the day you send the correction.provider_data.correction_effective_dateis the original day. Set it only on corrections: it's what marks a pay item as one.amountis the difference.partner_external_idis new, becauseTS-0050still belongs to the locked pay item. Keep the correction's ID stable too, so a retry of the correction doesn't duplicate it.
Whether a negative amount is the right way to send a correction for your pay codes depends on your setup. Confirm with your Remote contact.
Scenario 6: Leave that spans several days
Send one pay item for the whole period: effective_date is the first day and end_date the last (inclusive). Use the leave request's 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"
}If the leave is shortened before cutoff, send LEAVE-0007 again with the new end_date and amount. Only set end_date on leave-of-absence pay codes.
Scenario 7: Reconcile before each cutoff
Read back what Remote holds for the period:
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"Match each pay item to your T&A record on partner_external_id, then compare employee, pay code, effective date and amount. Look for anything missing, anything extra and any difference.
- Page through until
current_pageequalstotal_pages. - Pay items without your ID came from somewhere else: the Remote app or another system. Agree with your Remote contact which system owns which pay codes.
When a row fails
Each entry in failures[] has a row_number and an error. row_number is the row's position in the pay_items array you sent, starting at 0. The error comes in three shapes, so check which one you got before you compare it:
{
"failures": [
{ "row_number": 0, "error": "pay_item_code_not_allowed" },
{ "row_number": 1, "error": { "code": "partner_external_id_blank", "message": "partner_external_id must not be null or blank when creating pay items" } },
{ "row_number": 2, "error": { "errors": { "partner_external_id": ["has already been taken"] } } }
]
}error | What it means | What to do |
|---|---|---|
{code: "partner_external_id_held_by_other_pay_item"} | Your ID is on a pay item for another employee or another pay code | Check your IDs are unique per record |
{code: "partner_external_id_repeated_in_request"} | The same ID appears twice in one request. The message names the first row that used it. | Send each record once per request |
{code: "partner_external_id_ambiguous"} | More than one live pay item holds your ID | Contact Remote support |
{code: "partner_external_id_blank"} | The value was null, empty or only spaces | Leave the field out, or send a real ID |
{code: "partner_external_id_not_available"} | The field isn't turned on for you yet | Ask your Remote contact |
{errors: {partner_external_id: ["has already been taken"]}} | Another request created a pay item with this ID at the same time | It already landed: read it back (Scenario 2) |
{code: "unprocessable_entity"}, payroll run no longer in preparation or past cutoff | The pay item's run has locked | Send a correction (Scenario 5) |
{code: "unprocessable_entity"}, figures being updated | The run is being recalculated | Retry in a few minutes |
{code: "unprocessable_entity"}, recurring pay item | Your ID is held by a recurring pay item | Out of scope: see What's in scope |
{code: "unprocessable_entity"}, other message | The resend broke a rule, for example a date after the employee's termination date | Read the message |
"pay_item_code_not_allowed" | The pay code isn't accepted on that employee's legal entity | Check the legal entity's pay codes |
"employment_not_found" / "employment_not_global_payroll" | The employee doesn't exist for your token, or isn't on Global Payroll | Check the employment ID |
For AI agents
- Send a
partner_external_idon every row: the source record's own ID, stable, unique per record, at most 64 characters, nevernullor empty. - A bulk request whose rows all carry
partner_external_idis safe to retry. Without it, read back first: a retry creates duplicates. - To change a record before cutoff, resend it with the same ID. To delete it, find it with
GET /v1/pay-items?partner_external_id=...andDELETEit. - A failure saying the payroll run is no longer in preparation, or that its cutoff has passed, means: send a correction with a new
partner_external_idandprovider_data.correction_effective_date. - Never use this flow for recurring pay items.
- Treat any non-empty
failures[]as a failed send, and any422as "nothing saved". - Don't decide effective-date conventions, negative values or pay code substitutions yourself. Ask the user to confirm them with their Remote contact.
Next steps
- Your own IDs on Remote records, for how
partner_external_idworks across Remote. - API reference: Bulk Create Pay Items, List Pay Items, Update a Pay Item, Delete a Pay Item, List Pay Codes.
Updated about 20 hours ago