Build on LoggerIQ
A REST API with scoped keys, signed webhooks and a read-only connector for AI assistants, so payroll, HR and reporting systems stay in step with the hours your team actually worked.
On this page
Quickstart
- A workspace admin turns on API and webhooks in Settings, Modules. It is included in Pro.
- In Settings, API and webhooks, they create a key with the scopes your integration needs and copy it. It is shown once.
- Call the API with the key as a bearer token.
curl https://app.loggeriq.io/api/v1/me \
-H "Authorization: Bearer liq_sk_..."
curl "https://app.loggeriq.io/api/v1/clock-entries?from=2026-10-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer liq_sk_..."Every endpoint lives under https://app.loggeriq.io/api/v1. The full description is an OpenAPI 3.1 document at /api/v1/openapi.json, ready for code generators and API clients.
Authentication and scopes
Send the key in the Authorization header as Bearer liq_sk_.... A key belongs to one workspace and can only ever see that workspace. Keys start with liq_sk_ so a leaked one is easy to spot in a scan. We store only a hash of each key, so a lost key cannot be shown again: revoke it and make a new one.
Each key holds one or more scopes. An endpoint needs exactly one of them.
| Scope | Grants |
|---|---|
people:read | People in the workspace and its sites. |
time:read | Clock entries, approved timesheet weeks and published shifts. |
clock_entries:write | Create clock entries. They arrive for review, as they carry no location proof. |
leave:read | Leave requests and leave balances. |
pay:read | Pay run totals. Never tax file numbers or bank details. |
webhooks:manage | Create, list and remove webhook endpoints. |
Pages, limits and errors
Pagination
Lists return data and nextCursor. Pass the cursor back as cursor for the next page; it is null on the last one. limit is 1 to 100 and defaults to 50. Cursors are stable: rows added while you page never shift a later page.
{
"data": [ { "id": "...", "name": "Sam Lee", "status": "active" } ],
"nextCursor": "eyJsIjoicGVvcGxlIi..."
}Rate limits
Each key may make 120 requests a minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Over the limit you get 429 with Retry-After in seconds.
Errors
Every error is JSON with a sentence for people and a stable code for your code. Validation errors add fields.
{
"error": "This key lacks the people:read scope.",
"code": "insufficient_scope",
"requiredScope": "people:read"
}Idempotent writes
Send an Idempotency-Key header on every write (it is required when creating a clock entry). A retry with the same key and body within 24 hours returns the first response with Idempotent-Replayed: true. The same key with a different body is refused with 422.
Shapes
Fields are only ever added, never renamed or removed. Money is integer cents, instants are ISO 8601 in UTC and calendar days are YYYY-MM-DD in the workspace time zone.
Endpoints
| Request | What it does | Scope |
|---|---|---|
get /me | Check a key. The workspace a key belongs to and the scopes it holds. Any valid key may call it. | Any key |
get /people | List people. | people:read |
get /people/{id} | Get a person. | people:read |
get /sites | List sites. | people:read |
get /clock-entries | List clock entries. Newest first. Locations are never included. | time:read |
post /clock-entries | Create a clock entry. Records a clock in or out for a person at a site. Entries from the API carry no location proof, so they arrive for review. Send an `Idempotency-Key` header: a retry with the same key and body returns the first result. | clock_entries:write |
get /timesheets | List approved timesheet weeks. Approved weeks only, newest first, with the week’s totals in minutes. | time:read |
get /shifts | List published shifts. | time:read |
get /leave-requests | List leave requests. Newest first. Reasons and attached documents are never included. | leave:read |
get /pay-runs | List pay run summaries. Finalised and reversed runs, newest first. Totals only: no payslips, tax file numbers or bank details. | pay:read |
get /webhook-endpoints | List webhook endpoints. | webhooks:manage |
post /webhook-endpoints | Create a webhook endpoint. The response carries the signing secret once. Store it then. | webhooks:manage |
delete /webhook-endpoints/{id} | Delete a webhook endpoint. | webhooks:manage |
Creating a clock entry
Entries made through the API carry no location proof, so they are recorded for a manager to review. externalId is your own id: sending it again returns the first entry instead of a second one. recordedAt may be up to seven days in the past.
curl -X POST https://app.loggeriq.io/api/v1/clock-entries \
-H "Authorization: Bearer liq_sk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2b7e-door-17-0830" \
-d '{
"personId": "usr_...",
"siteId": "3f2a...",
"type": "in",
"recordedAt": "2026-10-03T08:30:00Z",
"externalId": "door-17-0830"
}'Pay run summaries carry totals only. No tax file numbers, bank details or payslips ever leave through the API.
Webhooks
Add an endpoint in Settings, API and webhooks (or with the webhook-endpoints API) and choose its events. We send a POST with a JSON body to your https address when one happens. Answer with any 2xx within ten seconds.
| Event | Sent when |
|---|---|
clock_entry.created | Someone clocked in or out, from any device or the API. |
timesheet.approved | A person’s week was approved by a manager or approved itself. |
leave.decided | A leave request was approved, declined or cancelled. |
expense.decided | An expense claim was approved or declined. |
pay_run.finalised | A pay run was finalised. Totals only. |
person.joined | Someone joined the workspace. |
person.left | Someone left the workspace. |
{
"id": "0b5f9f8e-6c1d-4f8e-9a51-2d1f0c7e9b11",
"type": "leave.decided",
"createdAt": "2026-10-03T09:12:44.120Z",
"workspaceId": "11111111-...",
"data": {
"id": "...", "personId": "...", "type": "annual",
"startDate": "2026-10-20", "endDate": "2026-10-24",
"status": "approved", "decidedAt": "2026-10-03T09:12:44.000Z"
}
}data has the same shape the matching API endpoint returns. Each delivery also carries LoggerIQ-Event, LoggerIQ-Event-Id and LoggerIQ-Delivery headers.
Retries
A delivery that fails or times out is retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: eight attempts over about a day and a half. Redirects are not followed. The same event can arrive more than once, so dedupe on the event id. Each endpoint keeps a delivery log you can read in settings, where you can also send a test event.
Verifying signatures
Every delivery is signed with the endpoint’s signing secret, shown once when the endpoint is made. The LoggerIQ-Signature header looks like t=1759482764,v1=5d41..., where v1 is the hex HMAC-SHA256 of the timestamp, a full stop and the raw body. Check it against the raw bytes before you parse the JSON, compare in constant time and refuse a timestamp more than 5 minutes from your clock.
Node.js
import crypto from 'node:crypto';
// rawBody: the request body exactly as received, before any JSON parsing.
export function verifyLoggerIQ(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const given = Buffer.from(parts.v1 ?? '', 'hex');
const want = Buffer.from(expected, 'hex');
return given.length === want.length && crypto.timingSafeEqual(given, want);
}Python
import hashlib, hmac, time
def verify_loggeriq(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))If a secret leaks, replace it from settings. The old one stops working at once.
AI assistants
AI assistants that support the Model Context Protocol can read a workspace through https://app.loggeriq.io/api/mcp, authenticated with an API key. The connection is read-only and an assistant sees only the tools its key’s scopes allow.
| Tool | What it answers | Scope |
|---|---|---|
list_people | List people in the workspace with their role, status and sites. | people:read |
get_timesheet_week | One person’s week: minutes worked each day, overtime, shortfall and whether it is approved. | time:read |
list_needs_you | What is waiting on a manager: timesheet weeks with exceptions, leave and expense claims to decide. | time:read |
clock_summary | Who is clocked in now and how many clock entries a day had, by site. | time:read |
leave_balances | A person’s leave balances: entitlement, used and still bookable, in days. | leave:read |
Most assistants take a configuration like this:
{
"mcpServers": {
"loggeriq": {
"type": "http",
"url": "https://app.loggeriq.io/api/mcp",
"headers": { "Authorization": "Bearer liq_sk_..." }
}
}
}Questions about the API? Talk to us.