Skip to content

Request an integration

Tell us which system you want to connect. We review every request weekly and publish a step-by-step guide or build the connection.

n8n recipes

Use TimeTracking with n8n today: Webhook node with raw body, Crypto node HMAC verification, and HTTP Request nodes with header auth.

Last updated: · Markdown version

1. Summary

Status: Recipe. A community/native TimeTracking node is Planned.

Direction: Both.

n8n is a good fit for self-hosted agencies: the workflow can run in your own infrastructure next to your ATS or payroll export folder.

2. Mechanism

  • Trigger: Webhook node (POST), option Raw Body on, so the exact bytes are available as binary/rawBody for verification.
  • Verify: Crypto node, action Hmac, type SHA256, encoding hex, secret = webhook secret, value = {{$json.headers["tt-signature"].split(",")[0].slice(2)}}.{{raw body}}; then an IF node comparing with v1. A Code node is simpler (example below).
  • Action: HTTP Request node with Header Auth credential (Authorization = Bearer tt_live_…).

3. Prerequisites

  • n8n 1.x (cloud or self-hosted) reachable over public HTTPS for webhooks.
  • An admin user (dashboard Regels & koppelingen) or an API key with scope admin to create webhooks; a write key for the workflow.

4. Entity mapping

Workflow Source TimeTracking call
ATS → placements ATS node / HTTP PUT /{clients,people,placements}/by-external/{source}/{ref} or POST /…/batch
Client spreadsheet in a folder → claims Spreadsheet File node POST /claims {source:"import", sourceRef, id}
timesheet.approved → billing data.dayIds GET /days?from&to&status=approved&updated_since=…
export.completed → SFTP to payroll data.runId GET /exports/runs/{runId}/file (exact committed CSV), then the SFTP node

Upserts by external ID are full replaces. Add header Idempotency-Key on writes so Retry On Fail is safe.

5. Step-by-step setup

  1. Create credential Header Auth: name Authorization, value Bearer tt_live_….
  2. Webhook node: method POST, path tt-events, Respond: immediately, options Raw Body = on.
  3. Code node (JavaScript) right after it:
const crypto = require("crypto"); // self-hosted: set NODE_FUNCTION_ALLOW_BUILTIN=crypto
const item = $input.first();
const raw = Buffer.from(item.binary.data.data, "base64").toString("utf8"); // raw body
const parts = Object.fromEntries(String(item.json.headers["tt-signature"] || "").split(",").map((p) => p.trim().split("=", 2)));
const expected = crypto.createHmac("sha256", $env.TT_WEBHOOK_SECRET).update(`${parts.t}.${raw}`).digest("hex");
if (!parts.v1 || expected !== parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
  throw new Error("Invalid TimeTracking signature");
}
return [{ json: JSON.parse(raw) }];
  1. Switch node on {{$json.type}}.
  2. Example branch timesheet.approved: HTTP Request GET https://app.timetracking.tech/api/v1/days?from={{$now.minus({days:35}).toFormat('yyyy-MM-dd')}}&to={{$now.toFormat('yyyy-MM-dd')}}&status=approved&limit=500 with the Header Auth credential (enable Pagination: next URL adds cursor={{$response.body.data.next_cursor}}, stop when it is empty), then Split Out on data.items.

Where the raw body lives (binary.data vs json.body) depends on the n8n version; inspect one execution and adjust.

Example action branch, record hours:

{
  "method": "POST",
  "url": "https://app.timetracking.tech/api/v1/claims",
  "authentication": "genericCredentialType",
  "genericAuthType": "httpHeaderAuth",
  "sendBody": true,
  "contentType": "json",
  "jsonBody": "={{ JSON.stringify({ id: $json.claimId, placementId: $json.placementId, workDate: $json.date, minutes: Math.round($json.hours * 60), source: 'import', sourceRef: 'n8n-' + $json.row }) }}"
}

Generate claimId once per source row (for example a UUIDv5 of the row key) so re-runs are idempotent.

6. Sync schedule and triggering events

Webhook-driven (5-minute delivery cycle) plus Schedule Trigger polls with updated_since for reconciliation.

7. Corrections, conflicts and locked periods

Treat 409 and 423 as business errors (route to an Error Trigger workflow or a notification). Conflicting sources produce conflict.created; resolve in TimeTracking.

8. Testing checklist

  • Wrong secret throws in the Code node.
  • Re-running an execution creates no duplicate claim.
  • Error workflow fires on 423.

9. Troubleshooting

Problem Fix
Cannot find module 'crypto' Self-hosted: NODE_FUNCTION_ALLOW_BUILTIN=crypto, or use the Crypto node.
Signature mismatch Raw Body option off, or body re-serialised.
403 Read-only key on a write endpoint, or webhook management with a non-admin key.
429 Add Wait 60 s; enable Retry On Fail with 60 s interval.

10. Limits and known gaps

No native node (Planned); ID-only payloads; webhook management needs an admin user or admin-scope key.

11. Build it with Claude

Generate an importable n8n workflow JSON: Webhook (POST, raw body) → Code node verifying TimeTracking
tt-signature (v1 = hex HMAC-SHA256(secret, `${t}.${raw}`), 300 s tolerance) → Switch on type →
export.completed: email the accountant with startsOn/endsOn/rows; conflict.created: GET
https://app.timetracking.tech/api/v1/placements/{placementId} (Header Auth "Authorization: Bearer …"), post to Slack;
export.completed: GET /exports/runs/{runId}/file and upload via SFTP. Second workflow: Schedule → read new rows from Google Sheets (placement_id, date, hours) → POST /claims
with source "import", sourceRef "sheet-<row>", id = UUIDv5(row key). Explain where to put credentials.