How to add TimeTracking to your own CRM
Connect any CRM, ATS or back office you built or run yourself: push workers, clients and placements in with external IDs, and pull approved hours back, with or without code.
Last updated: · Markdown version
This guide is for teams with a CRM, ATS or back office that is not on our integrations list, or that is built in-house. At the end of it:
- every worker, client and placement in your CRM exists in TimeTracking, kept in sync automatically;
- approved hours flow back to your CRM, per placement, without anyone retyping them.
Time needed: about an hour with code, or an afternoon with a no-code tool. Everything below works today on the public API; nothing has to be built on our side.
How it fits together
| What | Lives in | Direction | How |
|---|---|---|---|
| Workers, clients, placements | Your CRM (source of truth) | CRM → TimeTracking | PUT /…/by-external/{source}/{ref} |
| Hours, approvals, conflicts | TimeTracking (source of truth) | TimeTracking → CRM | Webhooks + GET /days |
| Payroll file | TimeTracking | TimeTracking → payroll | Export mappings |
The key idea: your CRM's own record IDs are the link. You never have to store TimeTracking IDs in your CRM.
Step 1. Create an API key
- In the TimeTracking dashboard, open Regels & koppelingen (Rules & integrations) → API-sleutels (API keys).
- Create a key with the
writescope. Name it after your CRM, for examplecrm-sync. - Store it as a secret in your CRM or integration server. It starts with
tt_live_.
Every call below sends Authorization: Bearer <key> to https://app.timetracking.tech/api/v1. See Authentication.
Step 2. Pick a source name
Choose one short, lowercase name for your system, for example mycrm. It goes in every external-ID path and keeps your IDs separate from other systems (pattern ^[a-z0-9_-]+$).
Step 3. Push clients, workers and placements
Call these whenever a record is created or changed in your CRM. They are upserts: the first call creates, the next calls update. Safe to repeat.
Client (the company the worker is placed at):
curl -X PUT "https://app.timetracking.tech/api/v1/clients/by-external/mycrm/CLIENT-77" \
-H "Authorization: Bearer $TT_KEY" \
-H "content-type: application/json" \
-d '{"name": "Kade Logistics BV", "approvalMode": "office"}'
Worker:
curl -X PUT "https://app.timetracking.tech/api/v1/people/by-external/mycrm/W-12345" \
-H "Authorization: Bearer $TT_KEY" \
-H "content-type: application/json" \
-d '{"employeeNumber": "1005", "firstName": "Piotr", "lastName": "Nowak", "phone": "+31612345678", "locale": "pl"}'
Placement (worker × client × period). Refer to the worker and client by your IDs:
curl -X PUT "https://app.timetracking.tech/api/v1/placements/by-external/mycrm/PL-555" \
-H "Authorization: Bearer $TT_KEY" \
-H "content-type: application/json" \
-d '{"personExternalRef": "W-12345", "clientExternalRef": "CLIENT-77", "ref": "PL-555", "roleTitle": "Order picker", "startsOn": "2026-11-02"}'
Each response contains "created": true (new) or false (updated). To end a placement, send the same call with endsOn. To stop a worker, send "active": false.
Importing many records at once? Use the batch endpoints (POST /people/batch, /clients/batch, /placements/batch) or a one-off CSV import first, then switch to upserts for changes.
Tip: add an Idempotency-Key header with your CRM's change ID. A retried request then never creates a second record. See Idempotency.
Step 4. Get hours back into your CRM
Pick one of two patterns, or combine them.
A. Push: webhooks (recommended). Add your CRM's endpoint once under Regels & koppelingen → Webhooks in the dashboard, or by API with an admin key:
curl -X POST "https://app.timetracking.tech/api/v1/webhooks" \
-H "Authorization: Bearer $TT_KEY" \
-H "content-type: application/json" \
-d '{"url": "https://crm.example.com/hooks/timetracking", "events": ["timesheet.approved", "timesheet.client_approved", "export.completed", "conflict.created"]}'
TimeTracking then sends a signed POST for each event. Always check the tt-signature header first; see Verify webhook signatures. The full list of events is in Webhook events.
B. Pull: a nightly sync. Fetch everything that changed since your last run:
curl "https://app.timetracking.tech/api/v1/days?from=2026-11-01&to=2026-11-30&status=approved,exported&updated_since=2026-11-20T02:00:00Z" \
-H "Authorization: Bearer $TT_KEY"
Each row has placement_id, person_id, work_date, minutes and status. Follow next_cursor until it is empty (Pagination). To map a row to your CRM, read the placement once (GET /placements/{id}): it carries your external_ref.
What agencies usually write back:
| CRM field | From |
|---|---|
| Approved hours this week / month | Sum of minutes for status=approved,exported ÷ 60 |
| Last approved by client | approved_by_client, approved_by_client_at |
| Open conflicts | GET /conflicts or the conflict.created webhook |
| Exported to payroll | export.completed webhook |
Step 5. Test it end to end
- Create a test placement in your CRM and check that it appears in the TimeTracking dashboard under Plaatsingen (Placements).
- Log a day of hours for that worker in the app, submit and approve it.
- Send a test event with
POST /webhooks/{id}/testand check what arrived withGET /webhooks/{id}/deliveries. - Confirm the hours landed in your CRM. Failed deliveries can be replayed with
POST /webhook-deliveries/{id}/replay.
No code? Use Zapier, Make or n8n
The same five steps work in an automation tool: a "record created/updated" trigger in your CRM → an HTTP PUT module for step 3, and a webhook trigger → "update record" for step 4. Step-by-step: Zapier, Make, n8n.
Let Claude build it for you
Install the Claude Skill or the MCP server, then ask, for example: "Write a sync that upserts our CRM placements into TimeTracking every hour and writes approved hours back to the placement record." Claude knows the endpoints, the external-ID pattern and the safe weekly-close workflow. Paste /llms.txt or the OpenAPI spec into any other AI tool for the same effect.
Common questions
What if a worker exists in both systems already? An upsert only finds records it created itself (by source + ref). Linking a record that already exists in TimeTracking to your CRM ID is not available through the API yet. Start the sync before anyone enters master data by hand, or load everything once through the sync and archive the hand-made duplicates.
Can the CRM change hours? It can add hours as a source (POST /claims), but approval stays in TimeTracking. Exported and locked days never change; corrections go into an open period. See Status lifecycle.
Rate limits? 600 requests per minute per key. Use batch endpoints for bulk loads. See Rate limits.
Using a known system? There are ready-made recipes for HubSpot, Salesforce, Pipedrive, Bullhorn, Carerix, Recruitee and OTYS.