Timesheet Integration
What this integration is for
This integration is for Worksome clients whose workers register their time in an external system — custom internal tooling, a time-tracking product, or similar — and who want to avoid duplicating that data by hand into Worksome.
The client captures timesheet data externally and pushes it to Worksome via the Custom API. Worksome then:
- Creates Worksome timesheet registrations for the workers on their hires.
- Automatically creates payment requests for the workers based on those registrations.
- Invoices the client for those payment requests through the normal Worksome billing flow.
It is assumed that timesheets have been approved on the client side before being pushed to Worksome. Payment requests created by this integration are auto-approved by default. The worker gets paid and the client gets invoiced without any further manual review inside Worksome.
The mutation for submitting timesheet data is intended for clients with large volumes of externally-captured timesheet data who want to automate both the capture of that data in Worksome and the subsequent approval and payout.
Not a complete standalone integration
This integration covers pushing timesheet data into Worksome. For it to be useful, the client’s external system also needs the hire and contract context from Worksome — specifically, the Global ID of each hire and its contract period — so timesheet data sent over can be matched to the correct worker and contract.
An additional layer of integration is therefore expected, using the Worksome GraphQL API and/or webhooks, to capture hire and contract data from Worksome into the external system. See Hire Global ID below for how to obtain the IDs you will need.
Working with Worksome
This part of the API is expected to be built upon in collaboration with Worksome. Your Worksome customer success manager can help with sandbox access, payment and billing configuration for your use case, and clarifying how timesheets and payment requests are managed inside Worksome once data starts flowing.
Authentication and API access
For authentication and general GraphQL API usage, see:
Testing
The timesheet endpoint and data processing can be tested in the sandbox environment. Submitting registrations via the API will create timesheets, but processing those timesheets into payment requests requires coordination with your Worksome customer success manager, as this trigger is not currently available automatically in the sandbox.
GraphQL mutation
mutation CreateCustomTimesheet($input: CreateCustomTimesheetInput!) {
createCustomTimesheet(input: $input) {
providedRegistrations
successfulRegistrations
rejectedRegistrations {
externalId
reason
message
}
}
}
Input
| Field | Type | Description |
|---|---|---|
schema |
String! |
Must be "default-json" |
data |
String! |
JSON string containing the registration payload (see Data format) |
Response
| Field | Type | Description |
|---|---|---|
providedRegistrations |
Int! |
Total number of registrations detected in the payload. |
successfulRegistrations |
Int! |
Number of registrations that passed all validation and were queued for processing. |
rejectedRegistrations |
[RejectedCustomTimesheetRegistration!]! |
One entry per registration that was not accepted, with the reason and a human-readable message. Empty when everything was accepted. |
Each RejectedCustomTimesheetRegistration has:
| Field | Type | Description |
|---|---|---|
externalId |
String |
The externalId from the submitted registration (may be null if that field was the one missing). |
reason |
CustomTimesheetRejectionReason! |
Machine-readable rejection reason (see below). |
message |
String! |
Human-readable explanation, suitable for your own logs or UI. |
Rejection reasons
reason |
When it fires | How to fix |
|---|---|---|
MISSING_REQUIRED_FIELD |
One or more required fields (hireId, reportedDate, hours, externalId) are missing, empty, or invalid. Includes unparseable dates. |
Fix the payload on your side and resubmit. The message names the offending field. |
HIRE_NOT_FOUND |
The submitted hireId does not resolve to a hire the authenticated account can submit timesheets for. This covers both “hire does not exist” and “hire exists but your account has no access” — the two cases are surfaced identically so that access scopes are not leaked. |
Verify the hireId is correct and belongs to one of your Worksome-connected companies. See Hire Global ID. |
DATE_OUTSIDE_CONTRACT_PERIOD |
reportedDate falls before the hire’s contract startDate or after its endDate. |
Check the contract dates on the hire. Fix the date or stop submitting for the hire once it has ended. See the US-payroll exception below. |
Registrations that pass all validation are accepted and counted in successfulRegistrations. Registrations that fail stop at the first failure — you only get one rejection reason per registration.
Partial-success semantics. A single payload may produce both accepted and rejected registrations. This is always a
200response with structured errors in the body; there is no top-level GraphQL error. Process therejectedRegistrationsarray the same way you would any business-level response.
US-payroll exception
For hires on a US payroll scheme, DATE_OUTSIDE_CONTRACT_PERIOD is not raised. Contract dates can change after a registration has already been submitted, so for US-payroll hires Worksome accepts the registration and handles any date-vs-contract discrepancy on its side — you do not need to filter these registrations out client-side. Out-of-contract dates on non-US-payroll hires are rejected as above.
Recommended: submit in large batches
We strongly recommend submitting timesheet data in large, combined payloads — many registrations per mutation call — rather than making one mutation call per registration or per worker.
Large batches are easier to debug, monitor, re-send, and reprocess if something needs to be corrected. Sending thousands of small individual mutations makes it much harder to reason about failures, retries, and state.
Data format
The data field is a JSON string containing either a single registration object or an array of registration objects. A formal JSON Schema is published at:
You can use the schema to validate outgoing payloads with any JSON Schema validator, generate type definitions for your integration code, or build transformation pipelines from non-JSON sources (CSV exports, spreadsheets, legacy systems) into the expected format. Business rules — for example whether the hireId resolves to a real hire — are validated by Worksome after submission.
Registration object
| Field | Type | Required | Description |
|---|---|---|---|
hireId |
String |
Yes | Worksome Global ID of the hire (e.g. SGlyZToxMjM0). See Hire Global ID. |
reportedDate |
String |
Yes | Date of the registration in YYYY-MM-DD format. Must fall within the hire’s contract start/end dates. |
hours |
Number |
Yes | Number of hours worked. |
externalId |
String |
Yes | Unique identifier from the external system for this registration (see Updating registrations). |
reference |
String |
No | Free-text reference field (e.g. project code, cost centre, purchase order number). |
isPayable |
Boolean |
No | Whether this registration is billable. Defaults to true. |
meta |
Object |
No | Arbitrary key-value metadata to store with the registration. |
Hire Global ID
The hireId must be a Worksome Global ID — a base64-encoded identifier (e.g. SGlyZToxMjM0) that uniquely references a hire across the Worksome platform. This is different from the numeric internal ID visible in some URLs.
Two ways to obtain hire Global IDs for your integration:
- Webhooks — subscribe to
hireAcceptedto capture thehireId(atdata.contract.hireId) as soon as a hire is accepted and persist it alongside your internal worker/job records. Additional hire lifecycle events exist for keeping your records in sync; see the Webhooks event reference for the full list. - GraphQL query — poll the
hiresquery to list hires on demand.
Pick whichever matches your integration style — webhooks require no polling and are lower-latency, while the GraphQL API is simpler for ad-hoc lookups or scheduled syncs.
Example: list recent hires
query {
hires(first: 25, orderBy: [{ field: CREATED_AT, order: DESC }]) {
data {
id
number
startDate
endDate
worker { name }
job { name }
}
paginatorInfo { total currentPage lastPage }
}
}
The id returned is the Global ID you need for the hireId field when submitting timesheets. The hires query supports pagination, free-text search, and a range of filters — see the hires query reference for all arguments. Notably useful for this integration are activeStatus, startDateRange/endDateRange, and externalIdentifiers.
Look up a single hire by Global ID
query {
hire(id: "SGlyZToxMjM0") {
id
number
startDate
endDate
worker { name }
job { name }
}
}
Example: single registration
The data field is a JSON string — the actual registration payload — wrapped inside the outer GraphQL variables object. You have to escape the inner double quotes so the whole thing is still valid JSON:
{
"input": {
"schema": "default-json",
"data": "[{\"hireId\": \"SGlyZToxMjM0\", \"reportedDate\": \"2026-04-07\", \"hours\": 8, \"externalId\": \"TS-001\"}]"
}
}
When the outer wrapping is parsed, the data value decodes to this valid JSON payload:
[
{
"hireId": "SGlyZToxMjM0",
"reportedDate": "2026-04-07",
"hours": 8,
"externalId": "TS-001"
}
]
JSON requires double quotes for property names and string values. Most HTTP clients and GraphQL tooling (Postman, Insomnia, etc.) can serialize the inner payload automatically if you pass it as an object; if you’re writing the request by hand, escape with backslashes as shown above.
Example: a work week for one worker
[
{ "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-07", "hours": 8, "externalId": "TS-001", "reference": "PROJ-42" },
{ "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-08", "hours": 7.5, "externalId": "TS-002" },
{ "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-09", "hours": 8, "externalId": "TS-003", "isPayable": true, "meta": { "department": "Engineering" } }
]
Example: non-billable registration
[
{
"hireId": "SGlyZToxMjM0",
"reportedDate": "2026-04-09",
"hours": 4,
"externalId": "TS-004",
"isPayable": false,
"meta": { "reason": "Training day" }
}
]
Processing behaviour
Asynchronous processing
Registrations are processed asynchronously. The mutation returns immediately with validation counts; actual timesheet creation happens in the background.
Timesheet grouping
Registrations are grouped into weekly timesheets (Monday-Sunday) per hire. If a timesheet already exists for the hire and week, registrations are added to it.
Registration types
Each registration creates two records:
- A line registration preserving the original payload (for audit/timeline).
- A day registration aggregating hours for the date (used for billing calculations).
Updating registrations (External ID)
The externalId field is the key for updates. When you send a registration with an externalId that already exists for the same date and timesheet, the previous line registration is replaced with the new one. The day registration is then recalculated from all current line registrations for that date.
This means you can correct a registration by sending the same externalId with updated hours:
{ "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-07", "hours": 6, "externalId": "TS-001" }
If TS-001 was previously submitted with 8 hours, it is now replaced with 6 hours.
Deleting registrations
Deletion of individual registrations is not currently supported via the API. To effectively zero out a registration, send an update with 0 hours or "isPayable": false using the same externalId.
Custom fields
If the hire’s company has custom fields defined on timesheets, payload keys matching custom field slugs are automatically applied to registrations.
Validation
All validation happens at submission time and is surfaced immediately in the response via rejectedRegistrations. See Rejection reasons for the full list of rejection types and how to address each one.
The top-level data string must be valid JSON. If it is not, the mutation responds with a top-level GraphQL error instead of per-registration rejections.
Registrations that pass submission-time validation are queued for processing and become visible as Worksome timesheets shortly after. Contracts can change after submission — if a hire’s end date is shortened after you submitted a registration for a now-out-of-contract date, that registration will be handled correctly at payment-request-creation time (accepted + isolated for US-payroll hires; otherwise ignored for billing). You do not need to manage this on the client side.
Payment request creation
Payment requests are automatically created from completed timesheets on a schedule. A timesheet is eligible for processing when:
- It has a resolved hire and worker.
- The timesheet week has ended (end date is on or before the previous Sunday).
- It has not been previously processed (or its duration has changed since the last payment request was created).
- It has not been held back by additional validation by the Worksome team.
The Worksome team may hold back individual timesheets from automatic processing when additional review is required. This is an internal mechanism that is not surfaced in the API and not something you need to configure from your side. If you see that a submitted timesheet has not produced a payment request and you have verified hire and date resolution are correct, contact your Worksome customer success manager.
Schedule
Automatic payment request creation runs every Wednesday at 13:00 UTC. Timesheets for weeks ending on or before the most recent Sunday are processed at that time.
Auto-approval
All payment requests created from API-submitted timesheets are automatically approved. The integration assumes timesheets have been reviewed and approved on the client side before being pushed to Worksome.
If auto-approval is not compatible with your requirements — for example if you need a second review inside Worksome before payouts run — contact your Worksome customer success manager to discuss options.
Rate calculation
Payment request amounts are calculated based on the hire’s rate type:
- Hourly: Total payable hours × hourly rate.
- Daily: Number of unique payable days × daily rate.
- Weekly: Worked-day proportion of the week × weekly rate (see Pro-rating).
Pro-rating (weekly rate hires)
For hires on a weekly rate, a full week is paid when 5 or more payable weekdays are worked. The week is pro-rated only when the contract starts or ends mid-week:
- Contract starts mid-week: the week is pro-rated based on the number of weekdays from the contract start date to the end of the timesheet week. If the worker logged more days than the entitled weekdays, the higher count is used.
- Contract ends mid-week: the week is pro-rated based on the number of weekdays from the start of the timesheet week to the contract end date. Again, the higher of entitled weekdays and worked days is used.
- Full week within contract: up to 5 worked days count; additional days do not increase the amount.
The week’s payable proportion is calculated as worked-or-entitled-days ÷ 5 and multiplied by the weekly rate. If the timesheet has no payable hours, the proportion is 0 and no payment request is created.
Billing period
The billing period is constrained to the hire’s contract dates. If a timesheet spans beyond the contract start or end, only the portion within the contract period is billed.
Manual processing
Payment request creation can also be manually triggered by the Worksome team for an individual timesheet, which is useful during sandbox testing or when correcting an issue.
Current limitations
- Deletion of individual registrations via API is not supported (use the update mechanism to zero out).
- Overtime is currently calculated only for hires where Worksome is the Employer of Record in the United States. Non-US / non-EoR hires are billed at the contract rate without overtime adjustments.