# 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.

URL: https://timetracking.tech/developers/integrations/ipaas/zapier
Last updated: 2026-10-24

## 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 verify `tt-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 header `Authorization: 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 `admin` to create webhooks. With an admin key the Zap itself can call `POST /webhooks` and `POST /webhooks/{id}/test`.
- TimeTracking API key (`write` for actions, `read` is enough for `GET /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

1. *Webhooks by Zapier → Catch Raw Hook*. Copy the hook URL.
2. In TimeTracking (admin) create a webhook with that URL and the events you need. Copy the `whsec_…` secret.
3. *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).

```js
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])) };
```

4. *Filter by Zapier*: continue only if `type` equals the event you want.
5. 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 header `idempotent-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](https://timetracking.tech/developers/webhooks/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 /claims` for 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-Key` header, and for claims also a client-generated `id` (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 `id` does 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

```text
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.
```
