Skip to content
Timerfly

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

  1. 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.
  2. Send it on every request: Authorization: Bearer tf_…
  3. 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.

GET /me

The company this key reads, with its timezone and default working hours. Use it to check a key.

GET /members

Everyone in the company: id, name, email, role (owner, admin, employee), status, department, joinedAt.

include=deactivatedAlso people who were deactivated.
GET /departments

Departments: id and name.

GET /days

Attendance and time, one row per person per day.

fromFirst day, YYYY-MM-DD (required).
toLast day (default: from). Up to 92 days.
memberIdOne 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

GET /apps

Apps and websites used in a range, most time first: name, category (productive, neutral, unproductive), seconds, people.

fromRequired.
toOptional.
memberIdOne person only.
GET /projects

Projects: id, name, status (active, archived), color.

GET /time-entries

Time booked to projects (timers and entries added by hand). A running timer has endedAt null.

fromRequired.
toOptional.
memberIdOne person.
projectIdOne project.

Fields: id, member, project, task, startedAt, endedAt, durationSec, note

GET /leave

Leave and holidays overlapping a range (a holiday has member null). Notes are not included: they can be personal.

fromRequired.
toOptional.
memberIdOne person (plus company holidays).

Fields: id, member, type (vacation, sick, remote, holiday, other, unpaid), status (pending, approved, rejected), startDate, endDate

GET /live

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

EventWhendata
day.startedSomeone 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.createdAn 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.joinedSomeone joined. An invitation was accepted.member { id, name, email }, role, departmentId
time_entry.stoppedA timer stopped. Time booked to a project with the timer.id, member, project, task, startedAt, endedAt, durationSec, note
time_entry.createdTime added by hand. An entry added to a project afterwards.id, member, project, task, startedAt, endedAt, durationSec, note
leave.createdLeave recorded. Leave or a holiday was added or requested.id, member (null for a company holiday), type, status, startDate, endDate
leave.updatedLeave decided. A leave request was approved or rejected.id, member, type, status, startDate, endDate
pingThe “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 the Timerfly-Delivery header). 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

400A 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_keyNo key, a mistyped key, or a revoked one.
403 plan_requiredThe company’s plan doesn’t include the API (it’s on Business).
403 workspace_pausedThe trial or plan ended and wasn’t renewed. Nothing is deleted; it answers again once a plan is chosen.
429More 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