Mysolution staffing and payroll
Connect Mysolution (Salesforce-based staffing software) to TimeTracking: placements in via external-ID upserts, approved hours out via GET /days or export runs and webhooks.
Last updated: · Markdown version
1. Summary
| What syncs | In: candidates/employees, accounts (clients), placements. Out: approved hours per placement per hour type, expenses |
| Direction | Mysolution → TimeTracking (master data), TimeTracking → Mysolution (hours) |
| Mechanism | Your middleware (Salesforce Apex/Flow, iPaaS, or a script) calling the TimeTracking REST API: PUT …/by-external/mysolution/{ref} upserts in, GET /days or export runs out |
| Trigger | Placement changes in Mysolution; approval and export webhooks in TimeTracking |
Status: Recipe — works today with your own middleware. No native Mysolution connector.
2. Mechanism
Mysolution runs on Salesforce. Assumption — placements, candidates and accounts are Salesforce (custom) objects in your org, and hours/timesheets can be written through the Salesforce REST API or a Mysolution-provided API; object and field API names differ per installation. Verify with your Mysolution consultant.
- Master data in. On placement create/update in Mysolution, middleware upserts the client, person and placement in TimeTracking by their Salesforce ids:
PUT /clients/by-external/mysolution/{accountId},PUT /people/by-external/mysolution/{candidateId},PUT /placements/by-external/mysolution/{placementId}. These calls are idempotent; no TimeTracking ids need to be stored in Salesforce. Nightly full syncs usePOST /…/batch(≤ 500 rows per call). - Hours out. Two options:
- Approved day values (for timesheets/invoicing in Mysolution):
GET /days?from=…&to=…&status=approved,exported(read key) returns per-day minutes, start/end and break per placement. Paginate withlimit/cursor; poll incrementally withupdated_since. - Payroll lines with hour types (overtime, ORT, expenses): commit an export (
POST /exports) or react toexport.completedand download the committed file withGET /exports/runs/{runId}/file.
- Approved day values (for timesheets/invoicing in Mysolution):
3. Prerequisites
- TimeTracking API key with
writescope for upserts; a separatereadkey is enough forGET /daysand export-run downloads. - Salesforce integration user and connected app for your middleware.
- A fixed external source name, e.g.
mysolution([a-z0-9_-]{1,40}), used on every upsert. - Mysolution hour codes per TimeTracking hour type (only for the payroll-lines option).
4. Entity mapping
| Mysolution (Salesforce) object / field | TimeTracking | Notes / external ID |
|---|---|---|
| Account Id | clients via PUT /clients/by-external/mysolution/{AccountId} |
Repeat calls keep the stored ref. |
| Account name, approval setting | name, approvalMode (office/client/both) |
|
| Account contact who approves hours | client_contacts (approver) |
Managed in the dashboard; no API to create contacts. |
| Candidate Id | people via PUT /people/by-external/mysolution/{CandidateId} |
|
| Employee number, first/last name, email, mobile | employeeNumber, firstName, lastName, email, phone |
Upsert body is the full object; omitted optional fields become null. |
| Placement Id | placements via PUT /placements/by-external/mysolution/{PlacementId} |
Link with personExternalRef + clientExternalRef (same source). |
| Placement number, start/end, function | ref, startsOn, endsOn, roleTitle |
|
| Placement ended/cancelled | DELETE /placements/{id} → active: false |
Deactivates; hours stay. |
| Timesheet line (date, hours, start/end) | GET /days item: work_date, minutes, start_min, end_min, break_min, status |
Join placement_id to the placement's external_ref (GET /placements/{id}). |
| Timesheet line with hour code | Export file row: placementRef, date, hourType, minutes |
Only after commit. |
5. Step-by-step setup
export TT=https://app.timetracking.tech/api/v1
export KEY=tt_live_… # write scope
- Upsert the client:
curl -s -X PUT "$TT/clients/by-external/mysolution/001Qy00000AbCdEIAV" \
-H "Authorization: Bearer $KEY" -H "content-type: application/json" -H "Idempotency-Key: sf-001Qy00000AbCdEIAV-v3" \
-d '{"name":"Logistiek Parkstad","externalRef":"001Qy00000AbCdEIAV","approvalMode":"client","mailDomains":"logistiekparkstad.example"}'
- Upsert the person:
curl -s -X PUT "$TT/people/by-external/mysolution/003Qy00000XyZ12IAB" \
-H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"employeeNumber":"104577","firstName":"Piotr","lastName":"Nowak","email":"piotr@example.com","locale":"pl"}'
Real response (demo data, source bullhorn; abridged):
{ "data": { "id": "69a47995-bc6a-4bc9-9b9b-070935b974bb", "employee_number": "9910", "first_name": "Eva",
"last_name": "Docs", "email": "eva.docs@example.com", "active": true,
"external_source": "bullhorn", "external_ref": "12345", "created": true } }
A repeat call returns the same id with "created": false. Person upserts emit person.created / person.updated.
- Upsert the placement by external refs:
curl -s -X PUT "$TT/placements/by-external/mysolution/a0X7Q00000PLC103" \
-H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"personExternalRef":"003Qy00000XyZ12IAB","clientExternalRef":"001Qy00000AbCdEIAV","ref":"PLC-103","roleTitle":"Heftruckchauffeur","startsOn":"2026-10-01"}'
Missing person or client → 422 Person and client must exist (by id or external ref) before the placement.
- Nightly full sync with batch (≤ 500 rows, sequential, one bad row never aborts):
curl -s -X POST "$TT/people/batch" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"items":[{"externalSource":"mysolution","externalRef":"003Qy00000XyZ12IAB","employeeNumber":"104577","firstName":"Piotr","lastName":"Nowak"}]}'
Real response shape:
{ "data": { "ok": 1, "failed": 1, "results": [
{ "index": 0, "ok": true, "data": { "id": "02595da9-06ea-45d1-a5e8-927d854a2c46", "external_ref": "12346", "created": true } },
{ "index": 1, "ok": false, "error": { "code": "invalid", "message": "firstName: Too small: expected string to have >=1 characters; lastName: Invalid input: expected string, received undefined" } } ] } }
- Hours out — approved day values, incremental:
curl -s "$TT/days?from=2026-11-30&to=2026-12-13&status=approved,exported&updated_since=2026-12-07T00:00:00Z&limit=500" \
-H "Authorization: Bearer $KEY"
Real item (demo data):
{ "id": "0ce9b17d-40ab-4542-937a-de57896a7358", "placement_id": "00000000-0000-4000-8000-0000000000e4",
"person_id": "00000000-0000-4000-8000-0000000000d4", "work_date": "2026-12-01", "minutes": 480,
"start_min": 420, "end_min": 930, "break_min": 30, "status": "exported", "resolution": "office_choice",
"has_conflict": false, "export_run_id": "239a1898-fab5-486c-a2be-92d939be90cc", "version": 6,
"updated_at": "2026-10-23T19:08:42.871612+00:00" }
Follow next_cursor until it is null. Write each item to the Mysolution timesheet object; use the day id (and version) to update instead of duplicate.
- Hours out — payroll lines: on
export.completed{runId, …}download the committed file:
curl -s "$TT/exports/runs/<runId>/file" -H "Authorization: Bearer $KEY" -o run.csv
preset-generic columns: employee_number;employee_name;client;placement;date;hour_type;hours (per day, decimal point, no amounts). For expenses and Mysolution hour codes, an admin builds a custom mapping in the dashboard (/export/mappings).
6. Sync schedule and triggering events
| Flow | Trigger |
|---|---|
| Placement upsert | Salesforce record-triggered Flow / Apex on placement change; nightly batch as safety net |
| Day values out | timesheet.approved {dayIds, approved} / timesheet.client_approved → GET /days?updated_since=…; or hourly poll |
| Payroll lines out | export.completed → GET /exports/runs/{runId}/file |
| Echo of upserts (optional) | person.created/updated, placement.created/updated |
Webhooks are managed by an admin (dashboard, admin user token or an admin-scope API key). Verify tt-signature (Verify signatures); dedupe on the event id, because a replay (POST /webhook-deliveries/{id}/replay) resends the same id.
7. Corrections, conflicts and locked periods
- Ending a placement early fails with
409when hours exist after the new end date. - A changed value on an approved day moves it back to
submittedand bumpsversion; yourupdated_sincepoll picks it up. - Exported/locked days are immutable (
423); corrections go into an open week and appear as new days/lines. - If Mysolution hours are also pushed into TimeTracking as claims (
source: "import", e.g. viaPOST /claims/batch), differences create conflicts that the office resolves.
8. Testing checklist
- Running the same placement upsert twice returns the same ids with
created: false. - Ending a placement in Mysolution deactivates it in TimeTracking.
-
GET /daysincremental poll picks up a re-approved day exactly once per version. - Webhook receiver rejects a bad signature and ignores a replayed event id it already processed.
9. Troubleshooting
| HTTP / code | Message | Fix |
|---|---|---|
422 invalid |
Person and client must exist (by id or external ref) before the placement | Upsert person and client with the same source first. |
422 invalid |
Invalid cursor | Restart pagination without cursor. |
422 invalid |
Idempotency-Key was already used for a different request | Use a new key per distinct request (8–200 chars). |
404 not_found |
Not found | GET /{resource}/{id} with a wrong or foreign id. |
409 conflict |
N registration(s) exist after | Hours after new end date. |
409 conflict |
Client has placements; deactivate those instead | DELETE /clients/{id} only works without placements. |
403 forbidden |
API key is read-only / Admin role required | write key for upserts; webhooks need an admin user or an admin-scope key. |
429 rate_limited |
Rate limit of 600 requests per minute exceeded | Wait 60 s (Retry-After); prefer batch. |
10. Limits and known gaps
- No API to create client contacts (approvers).
- Upsert and
PATCHbodies are full objects; omitted optional fields becomenull. - Plain
POST /peopleemits no webhook; only by-external and batch-external upserts emitperson.created.
Planned (not in API v1.1) — export-mapping CRUD via the API, OAuth2 for marketplace apps.
11. Build it with Claude
Build a Mysolution (Salesforce) ↔ TimeTracking sync.
- Source: Salesforce placement, candidate and account records (ask me for object/field API names; mark them TODO).
- In: PUT /api/v1/clients/by-external/mysolution/{AccountId},
PUT /api/v1/people/by-external/mysolution/{CandidateId}, PUT /api/v1/placements/by-external/mysolution/{PlacementId}
with personExternalRef/clientExternalRef. Nightly: the same via POST /api/v1/{people|clients|placements}/batch (≤500 rows).
Ended placements → DELETE /api/v1/placements/{id}. Send an Idempotency-Key per write.
- Out: poll GET /api/v1/days?status=approved,exported&updated_since=<last run>&limit=500 (follow next_cursor) and upsert
Mysolution timesheet lines keyed on day id. On export.completed, download GET /api/v1/exports/runs/{runId}/file.
- Respect 429 Retry-After; log per-row batch errors without stopping.