Zapier recipes
Use TimeTracking with Zapier today: Catch Raw Hook for signed webhooks, Code by Zapier for signature checks, and Webhooks by Zapier for API calls.
Last updated: · Markdown version
1. Summary
Status: Recipe. A native TimeTracking app in the Zapier directory is Planned.
Direction: Both. Triggers: TimeTracking webhooks. Actions: TimeTracking REST API calls.
2. Mechanism
- Trigger: Webhooks by Zapier → Catch Raw Hook. It exposes the raw body (
raw_body) and request headers, which you need to verifytt-signature. - Verify: Code by Zapier → Run JavaScript computes the HMAC and stops the Zap when it does not match.
- Action: Webhooks by Zapier → Custom Request (or POST) to
https://app.timetracking.tech/api/v1/...with headerAuthorization: Bearer tt_live_….
Catch Hook (non-raw) parses JSON for you but loses the exact bytes; the signature then cannot be verified reliably. Use Catch Raw Hook.
3. Prerequisites
- Zapier plan with multi-step Zaps, Webhooks by Zapier and Code by Zapier.
- An admin user (dashboard Regels & koppelingen) or an API key with scope
adminto create webhooks. With an admin key the Zap itself can callPOST /webhooksandPOST /webhooks/{id}/test. - TimeTracking API key (
writefor actions,readis enough forGET /org/week,/people,/clients,/placements).
4. Entity mapping
Zapier itself holds no entities; you map fields between the trigger app and TimeTracking. Common mappings:
| Zap | Source fields | TimeTracking call |
|---|---|---|
| New row in Google Sheets → record hours | placement id, date, hours | POST /claims {placementId, workDate, minutes, source:"import", sourceRef:"sheet-row-<n>"} |
| New hire in ATS → placement | name, email, client, start date | PUT /{clients,people,placements}/by-external/{source}/{ref} |
export.completed → email accountant with the file |
data.runId, data.startsOn, data.endsOn, data.rows |
GET /exports/runs/{runId}/file (CSV, read key OK) |
conflict.created → Slack/Teams/task |
data.workDate, data.candidates |
GET /placements/{placementId} for names |
External IDs: upsert with PUT /{people|clients|placements}/by-external/{source}/{ref} (for example source recruitee), so no lookup step is needed. Placements reference personExternalRef / clientExternalRef of the same source. Upserts are full replaces. Add an Idempotency-Key header so Zapier auto-replays are safe.
5. Step-by-step setup
Trigger Zap: TimeTracking event → anything
- Webhooks by Zapier → Catch Raw Hook. Copy the hook URL.
- In TimeTracking (admin) create a webhook with that URL and the events you need. Copy the
whsec_…secret. - Code by Zapier → Run JavaScript, input data:
raw=Raw Body,sig=Headers Tt Signature(header names appear in the trigger sample),secret= your secret (or store it in Zapier's Storage).
const crypto = require("crypto");
const parts = Object.fromEntries(String(inputData.sig || "").split(",").map((p) => p.trim().split("=", 2)));
const expected = crypto.createHmac("sha256", inputData.secret).update(`${parts.t}.${inputData.raw}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
if (!fresh || !parts.v1 || expected !== parts.v1) throw new Error("Invalid TimeTracking signature");
const evt = JSON.parse(inputData.raw);
return { type: evt.type, id: evt.id, ...Object.fromEntries(Object.entries(evt.data).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : v])) };
- Filter by Zapier: continue only if
typeequals the event you want. - Add your action steps.
Action Zap: anything → TimeTracking
Webhooks by Zapier → Custom Request:
- Method:
POST - URL:
https://app.timetracking.tech/api/v1/claims - Data:
{"placementId":"{{placement_id}}","workDate":"{{date}}","minutes":{{minutes}},"source":"import","sourceRef":"zap-{{row_id}}"} - Headers:
Authorization=Bearer tt_live_…,Content-Type=application/json,Idempotency-Key=zap-{{zap_meta_id}}-{{row_id}}(8–200 chars; replays return the stored response with headeridempotent-replayed: true)
Use Formatter → Numbers to convert hours to minutes (hours * 60, rounded). Dates must be YYYY-MM-DD.
6. Sync schedule and triggering events
Trigger Zaps run when TimeTracking delivers (every 5 minutes; see Webhook events). Polling is also practical now: Schedule by Zapier + GET /days?from&to&updated_since=<last run> or GET /people?updated_since=…&limit=100 (follow next_cursor).
7. Corrections, conflicts and locked periods
- A second
POST /claimsfor the same placement/day from the same source replaces the earlier one (newest claim per source counts). From a different source that disagrees, it creates a conflict. - Posting to an exported/locked day returns
423 locked; route it to a human. - Zapier auto-replay can resend an action: send an
Idempotency-Keyheader, and for claims also a client-generatedid(UUID) in the body.
8. Testing checklist
- Trigger Zap: tampering with the secret makes the code step fail.
- Action Zap: one row produces one claim; replaying the Zap run with the same
iddoes not create a second claim. - Errors (
422,423) are visible in Zap history and alert someone.
9. Troubleshooting
| Problem | Fix |
|---|---|
| Signature never matches | You used Catch Hook instead of Catch Raw Hook, or mapped a parsed field instead of Raw Body. |
401 |
Header must be Authorization: Bearer tt_live_…. |
403 "API key is read-only" |
Use a write key for POSTs. |
422 "Provide either minutes or start and end time" |
Send minutes (integer) or startMin + endMin. |
429 |
Over 600 req/min per key: add Delay steps. |
10. Limits and known gaps
- No native app, no OAuth: keys are pasted into Zap steps. A native Zapier app is Planned.
- Webhook payloads have IDs only; enrich with
GET /{resource}/{id}. - Webhook management (create, test, replay, rotate secret) needs an admin user or an
admin-scope key; prefer a separate admin key used only for setup. - Rotating the secret (
POST /webhooks/{id}/rotate-secret) invalidates the old one immediately; update the Code step at the same time.
11. Build it with Claude
Design a Zapier Zap that receives TimeTracking webhooks via Catch Raw Hook, verifies tt-signature in a Code by
Zapier JavaScript step (v1 = hex HMAC-SHA256(secret, `${t}.${rawBody}`), 300 s tolerance), and for
conflict.created looks up names with GET https://app.timetracking.tech/api/v1/placements (Bearer key) and posts
a message to Slack. Give me every step with exact field mappings and the code, plus a second Zap that turns new
Google Sheets rows (placement_id, date, hours) into POST /claims with source "import" and a deterministic UUID id.