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/
rawBodyfor 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 withv1. 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
adminto create webhooks; awritekey 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
- Create credential Header Auth: name
Authorization, valueBearer tt_live_…. - Webhook node: method POST, path
tt-events, Respond: immediately, options Raw Body = on. - 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) }];
- Switch node on
{{$json.type}}. - Example branch
timesheet.approved: HTTP Request GEThttps://app.timetracking.tech/api/v1/days?from={{$now.minus({days:35}).toFormat('yyyy-MM-dd')}}&to={{$now.toFormat('yyyy-MM-dd')}}&status=approved&limit=500with the Header Auth credential (enable Pagination: next URL addscursor={{$response.body.data.next_cursor}}, stop when it is empty), then Split Out ondata.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.