Developers
Timerfly API & webhooks
Read your company’s attendance, hours, apps, projects and leave as JSON, and get an event the moment someone starts work, an alert fires or leave is approved. For CRMs, payroll, BI dashboards and your own systems. On the Business plan and in the 14-day trial.
Getting started
- An owner or admin goes to Settings → API & integrations → API keys, names a key after what will use it, and copies it. It’s shown once; keep it like a password.
- Send it on every request:
Authorization: Bearer tf_… - Base address:
https://api.timerfly.com/api/v1
- Read-only. A key can’t change anything, and reads only its own company.
- Dates are the company’s own days (its timezone), as YYYY-MM-DD. Times are ISO 8601 in UTC. Durations are whole seconds.
- Answers are
{ "data": [...] }, so new fields can be added without breaking your code. Ignore fields you don’t know. - Revoke a key in Settings any time; it stops working at once.
Endpoints
All are GET.
The company this key reads, with its timezone and default working hours. Use it to check a key.
Everyone in the company: id, name, email, role (owner, admin, employee), status, department, joinedAt.
| include=deactivated | Also people who were deactivated. |
|---|
Departments: id and name.
Attendance and time, one row per person per day.
| from | First day, YYYY-MM-DD (required). |
|---|---|
| to | Last day (default: from). Up to 92 days. |
| memberId | One person only. |
Fields: date, member, status (worked, absent, leave, holiday, day_off, not_started, upcoming, not_joined: before they joined), leaveType, arrivalAt, departureAt, trackedSec, productiveSec, neutralSec, unproductiveSec, idleSec, breakSec, offlineSec, expectedSec, late, leftEarly, effectiveness (%), productivity (%), scheduled { start, end }, location (office, remote, both or null), officeSec, remoteSec
Apps and websites used in a range, most time first: name, category (productive, neutral, unproductive), seconds, people.
| from | Required. |
|---|---|
| to | Optional. |
| memberId | One person only. |
Projects: id, name, status (active, archived), color.
Time booked to projects (timers and entries added by hand). A running timer has endedAt null.
| from | Required. |
|---|---|
| to | Optional. |
| memberId | One person. |
| projectId | One project. |
Fields: id, member, project, task, startedAt, endedAt, durationSec, note
Leave and holidays overlapping a range (a holiday has member null). Notes are not included: they can be personal.
| from | Required. |
|---|---|
| to | Optional. |
| memberId | One person (plus company holidays). |
Fields: id, member, type (vacation, sick, remote, holiday, other, unpaid), status (pending, approved, rejected), startDate, endDate
Who’s working right now: status (working, idle, break, private, offline), current app while working, and today’s arrival and totals.
Examples
curl
curl https://api.timerfly.com/api/v1/days?from=2026-09-01&to=2026-09-30 \
-H "Authorization: Bearer tf_your_key"JavaScript (Node 18+)
const res = await fetch('https://api.timerfly.com/api/v1/days?from=2026-09-01&to=2026-09-30', {
headers: { Authorization: `Bearer ${process.env.TIMERFLY_KEY}` },
});
const { data } = await res.json();
const absent = data.filter((d) => d.status === 'absent');Python
import os, requests
r = requests.get("https://api.timerfly.com/api/v1/days",
params={"from": "2026-09-01", "to": "2026-09-30"},
headers={"Authorization": f"Bearer {os.environ['TIMERFLY_KEY']}"})
for d in r.json()["data"]:
print(d["date"], d["member"]["name"], d["status"], d["trackedSec"] // 3600, "h")Google Sheets (Apps Script): yesterday’s attendance, every morning
// Google Sheets: Extensions → Apps Script. Put the key in Project Settings → Script properties (TIMERFLY_KEY).
function loadYesterday() {
const key = PropertiesService.getScriptProperties().getProperty('TIMERFLY_KEY');
const day = Utilities.formatDate(new Date(Date.now() - 864e5), 'Asia/Kolkata', 'yyyy-MM-dd');
const res = UrlFetchApp.fetch('https://api.timerfly.com/api/v1/days?from=' + day, { headers: { Authorization: 'Bearer ' + key } });
const rows = JSON.parse(res.getContentText()).data.map((d) =>
[d.date, d.member.name, d.status, d.arrivalAt, d.departureAt, Math.round(d.trackedSec / 36) / 100, d.late]);
if (rows.length) SpreadsheetApp.getActiveSheet().getRange(SpreadsheetApp.getActiveSheet().getLastRow() + 1, 1, rows.length, 7).setValues(rows);
}
// Then Triggers → Add trigger → loadYesterday, time-driven, every day at 6–7 am.Power BI: Get data → Web → Advanced, URL https://api.timerfly.com/api/v1/days?from=…&to=…, and add the header Authorization with Bearer tf_…. Expand data into rows.
Webhooks
In Settings → API & integrations → Webhooks, add an https address and choose the events (none ticked means all of them, including ones added later). Timerfly POSTs each event as it happens.
Events
| Event | When | data |
|---|---|---|
| day.started | Someone started work. Their first activity of the day, with the arrival time. (Lateness comes as an alert: alert.created, type late_start.) | member { id, name }, date, arrivalAt |
| alert.created | An alert fired. Late start, idle too long, a long break, overtime, the app not reporting… — the same alerts as in Timerfly. | id, type (late_start, long_idle, unproductive, overtime, long_break, break_total, work_start, work_stop, device_silent, multi_device, approval, test), message, member { id, name } or null, createdAt |
| member.joined | Someone joined. An invitation was accepted. | member { id, name, email }, role, departmentId |
| time_entry.stopped | A timer stopped. Time booked to a project with the timer. | id, member, project, task, startedAt, endedAt, durationSec, note |
| time_entry.created | Time added by hand. An entry added to a project afterwards. | id, member, project, task, startedAt, endedAt, durationSec, note |
| leave.created | Leave recorded. Leave or a holiday was added or requested. | id, member (null for a company holiday), type, status, startDate, endDate |
| leave.updated | Leave decided. A leave request was approved or rejected. | id, member, type, status, startDate, endDate |
| ping | The “Send test” button in Settings. | message |
What arrives
POST https://your-endpoint.example.com/timerfly
Content-Type: application/json
Timerfly-Event: leave.updated
Timerfly-Delivery: 3f6c…
Timerfly-Signature: t=1790661600,v1=5d41402abc4b2a76b9719d911017c592…
{
"id": "3f6c…",
"event": "leave.updated",
"createdAt": "2026-09-28T09:20:00.000Z",
"organization": { "id": "ada9…" },
"data": {
"id": "91b2…",
"member": { "id": "7554…", "name": "Aditya Kulkarni" },
"type": "vacation",
"status": "approved",
"startDate": "2026-10-06",
"endDate": "2026-10-08"
}
}- Answer with any 2xx within 10 seconds. Do slow work afterwards.
- Retries: if your endpoint is down or answers anything else, Timerfly tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Redirects aren’t followed.
- Idempotency: a retry has the same
id(also in theTimerfly-Deliveryheader). Skip ids you’ve already handled. - Health: each webhook’s log in Settings shows every delivery and its result. After 50 failures in a row it’s switched off and your owners and admins get an email.
- Addresses: public https only — never private or internal network addresses.
Checking the signature
Timerfly-Signature is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + the raw body with the webhook’s signing secret (whsec_…, under Secret in Settings). Compare in constant time and reject old timestamps.
Node.js
import crypto from 'node:crypto';
// req.rawBody: the request body exactly as received (before JSON.parse).
function isFromTimerfly(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes: replay
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Python
import hmac, hashlib, time
def is_from_timerfly(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])PHP
function is_from_timerfly(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int) $p['t']) > 300) return false;
$expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $p['v1']);
}
// $raw = file_get_contents('php://input'); $sig = $_SERVER['HTTP_TIMERFLY_SIGNATURE'];Zapier, Make and n8n
No code needed: point a webhook at their “catch hook” address and use the fields in any of their apps. Step-by-step guides: Zapier, Make, n8n. To read reports from them, call the API with an HTTP / Webhooks GET step and the Authorization header.
Errors & limits
| 400 | A parameter is missing or wrong (for example a date that isn’t YYYY-MM-DD, `to` before `from`, or more than 92 days). |
|---|---|
| 401 invalid_api_key | No key, a mistyped key, or a revoked one. |
| 403 plan_required | The company’s plan doesn’t include the API (it’s on Business). |
| 403 workspace_paused | The trial or plan ended and wasn’t renewed. Nothing is deleted; it answers again once a plan is chosen. |
| 429 | More than 120 requests a minute with one key. Wait a minute; the Retry-After header says how long. |
Errors are JSON: { "statusCode": 400, "message": "…" }, with a code where there’s one to act on.
Versions
v1 — 28 September 2026. New fields and events may be added to v1; anything that would break existing code gets a new version.
Build on Timerfly
The API and webhooks are in the 14-day trial. Make a key and try them on your own team’s data.
Start free trial