Public REST API

Tikklok REST API v1

A stateless, API-key-authenticated REST API for third-party integrations. Read your company's employees, locations, and performance data. Every response is JSON.

Base URL

https://api.tikklok.appproduction
http://localhost:8084local development

All endpoints are versioned under /v1. Every request must be made over HTTPS in production.

Authentication

HTTP Basic Auth over HTTPS. Send an Authorization header with your key ID and secret, base64-encoded:

Authorization: Basic base64(KEY_ID:SECRET)
CredentialFormatNotes
KEY_IDTK_PUB_LIVE_… (28)Public identifier, uppercase, 16-hex tail. Safe to log.
SECRETTK_SEC_LIVE_… (60)Private. Shown exactly once at creation; only a SHA-256 hash is stored — unrecoverable if lost.

Auth failure returns 401 unauthorized. The API never distinguishes a bad key from a bad secret, to prevent enumeration.

Example

curl -u TK_PUB_LIVE_A1B2C3D4E5F60718:TK_SEC_LIVE_… \
     https://api.tikklok.app/v1/me

Rate limits

ScopeLimit
Per key60 requests / minute · 1000 / hour (fixed windows)
Per IP, pre-auth20 requests / minute (before Basic Auth is checked)
Per IP, auth failures10 failures / minute → temporary lockout

Exceeding a limit returns 429 rate_limited. Wait a minute and retry.

Responses & errors

Every response is JSON with a success boolean. Errors also carry a short machine-readable error string. Reads never emit success: false for missing data — a 404 body means the resource itself was missing. Every response carries Cache-Control: no-store.

StatuserrorMeaning
400invalid_roleUnknown role filter value.
400invalid_dateMissing/malformed date, or min_date > max_date.
400range_too_largePerformance span exceeds 366 days.
401unauthorizedMissing or invalid credentials.
402subscription_requiredCompany has no active (paid) subscription — the free trial does not include API access (every endpoint except /v1/me).
404not_foundResource missing — or belongs to another company (indistinguishable, by design).
413payload_too_largeRequest body over 1 MB.
429rate_limitedRate limit exceeded.

Scoping & IDs

Every request is scoped to the company that owns the API key. Resources are addressed by their global IDs:

IDMeaning
company_user_idA person in your company (the "employee" identity). Stable, globally unique.
location_idOne of your company's locations. Stable, globally unique.
Each employee's locations[] includes its location_user_id — the internal per-assignment key — used to target clock events for that employee at that location. Path parameters still address an assignment by the (company_user_id, location_id) pair. Any ID in a path that doesn't belong to your company returns 404 not_found — identical to a non-existent ID, so foreign IDs can't be probed. Request bodies must be ≤ 1 MB (413 payload_too_large otherwise).

Endpoints

All endpoints are GET. Every endpoint except /v1/health requires authentication.

GET/v1/healthno auth

Uptime probe. No authentication.

Response 200

{
  "success": true,
  "status": "ok",
  "version": "v1",
  "time": "2026-08-08T14:23:00Z"
}
GET/v1/meauth

Confirms your credentials work and shows which company they map to.

Response 200

{
  "success": true,
  "company_id": 1,
  "company_name": "Acme Corp",
  "key_id": "TK_PUB_LIVE_A1B2C3D4E5F60718",
  "key_name": "My Slack integration"
}
GET/v1/employeesauth

Paginated roster of your company's people (company_users), each with a lean list of the locations they're actively assigned to. Owners/admins appear too, with an empty locations array if they have no assignments.

Query parameters

ParamDefaultNotes
limit100Clamped to 1–500.
offset0For paging.
location_id—Only employees actively assigned to this location.
role—Company role: owner, admin, employee. Invalid → 400 invalid_role.
include_inactive01 to include deactivated employees.

Response 200

{
  "success": true,
  "total": 25,
  "limit": 100,
  "offset": 0,
  "data": [
    {
      "company_user_id": 42,
      "first_name": "Jane",
      "last_name": "Doe",
      "is_active": true,
      "locations": [
        { "location_user_id": 15, "location_id": 7, "location_name": "Main Store" }
      ]
    }
  ]
}
GET/v1/employees/{company_user_id}auth

Full detail for one employee plus every location assignment (active and historical — each carries its own is_active). Each assignment includes its location_user_id (the per-assignment key, used for API clock events). 404 not_found if the employee isn't in your company.

Response 200

{
  "success": true,
  "data": {
    "company_user_id": 42,
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@example.com",
    "tel": "+32 470 00 00 00",
    "role": "employee",
    "status": "active",
    "is_active": true,
    "created": "2025-01-15 10:30:00",
    "updated": "2025-08-27 14:22:15",
    "locations": [
      {
        "location_user_id": 15,
        "location_id": 7,
        "location_name": "Main Store",
        "role": "staff",
        "function_id": 3,
        "function_name": "Sales",
        "is_user_primary": true,
        "clocking_allowed": true,
        "valid_from": null,
        "valid_to": null,
        "is_active": true
      }
    ]
  }
}
GET/v1/employees/{company_user_id}/locations/{location_id}auth

One employee's assignment at one location. 404 not_found if the employee isn't in your company, the location isn't in your company, or there's no assignment linking the two.

Response 200

{
  "success": true,
  "data": {
    "company_user_id": 42,
    "location_user_id": 15,
    "location_id": 7,
    "location_name": "Main Store",
    "role": "staff",
    "function_id": 3,
    "function_name": "Sales",
    "is_user_primary": true,
    "clocking_allowed": true,
    "valid_from": null,
    "valid_to": null,
    "is_active": true,
    "created": "2025-01-15 10:30:00",
    "updated": "2025-08-27 14:22:15"
  }
}
GET/v1/employees/{company_user_id}/performanceauth

The same performance data as the dashboard's downloadable report, as JSON, grouped by location. For each location the employee is (or was) assigned to, a per-day breakdown plus summed totals. All figures are hours (2 decimals); overtime_hours can be negative (undertime).

expected_hours = planned − closed − location_holiday − user_holiday − sick · overtime_hours = worked − expected

Query parameters (both required)

ParamNotes
min_dateYYYY-MM-DD
max_dateYYYY-MM-DD, ≥ min_date

Bad/missing date or min_date > max_date → 400 invalid_date. A span over 366 days → 400 range_too_large. 404 not_found if the employee isn't in your company. An employee with no data in range returns 200 with an empty locations array.

Response 200

{
  "success": true,
  "data": {
    "company_user_id": 42,
    "employee_name": "Jane Doe",
    "period": { "min_date": "2025-01-01", "max_date": "2025-01-31" },
    "locations": [
      {
        "location_id": 7,
        "location_name": "Main Store",
        "days": [
          {
            "date": "2025-01-06",
            "planned_hours": 8,
            "worked_hours": 7.75,
            "closed_hours": 0,
            "location_holiday_hours": 0,
            "user_holiday_hours": 0,
            "sick_hours": 0,
            "expected_hours": 8,
            "overtime_hours": -0.25
          }
        ],
        "totals": {
          "planned_hours": 40, "worked_hours": 39, "closed_hours": 0,
          "location_holiday_hours": 0, "user_holiday_hours": 0, "sick_hours": 0,
          "expected_hours": 40, "overtime_hours": -1
        }
      }
    ]
  }
}
GET/v1/locationsauth

Your company's locations. qr_base64 is intentionally omitted (large, and clocking is handled app-side).

Query parameters

ParamDefaultNotes
include_inactive01 to include deactivated locations.

Response 200

{
  "success": true,
  "data": [
    {
      "location_id": 7,
      "name": "Main Store",
      "street": "Rue de la Paix",
      "house_nr": "42",
      "postal_code": "1000",
      "city": "Brussels",
      "country": "BE",
      "timezone": "Europe/Brussels",
      "is_company_primary": true,
      "latitude": 50.8503,
      "longitude": 4.3517,
      "geo_radius_meters": 100,
      "is_active": true,
      "created": "2024-06-01 09:00:00",
      "updated": "2025-08-27 14:22:15"
    }
  ]
}
GET/v1/locations/{location_id}auth

One location, same object shape as the list. 404 not_found if it isn't in your company.

GET/v1/locations/{location_id}/employeesauth

The active people working at one location — each active assignment (location_users row) joined with the person's name. Only rows where the assignment, its company_user, and the user are all active are returned. 404 not_found if the location isn't in your company; an in-company location with no active staff returns 200 with an empty data.

Response 200

{
  "success": true,
  "location_id": 7,
  "data": [
    {
      "location_user_id": 15,
      "first_name": "Jane",
      "last_name": "Doe",
      "role": "staff",
      "function_id": 3,
      "function_name": "Sales"
    }
  ]
}
GET/v1/employees/{company_user_id}/locations/{location_id}/clock-statusauth

Current clock status for one employee at one location, resolved from their most recent clock event there. clocked_in is true when the latest event is an IN, false when it's an OUT or the employee has never clocked there. 404 not_found if the employee or location isn't in your company, or the employee has no assignment at that location. The last_event_* fields are null if they've never clocked at that location.

Response 200

{
  "success": true,
  "data": {
    "company_user_id": 42,
    "location_id": 7,
    "location_user_id": 15,
    "clocked_in": true,
    "last_event_type": "IN",
    "last_event_time_utc": "2026-09-03 08:00:00",
    "last_event_time_local": "2026-09-03 10:00:00"
  }
}
GET/v1/locations/{location_id}/clock-statusauth

Clock status for multiple employees at one location in a single call — built for an admin overview. Each row is one employee's current status, resolved from their latest clock event at that location. 404 not_found if the location isn't in your company; a location with no matching staff returns 200 with an empty data.

Query parameters

ParamNotes
company_user_idsOptional. Comma-separated (e.g. 42,43,44), max 500. Omit to get every active employee at the location. Any id that isn't an active employee at this location is silently skipped (a bad id doesn't fail the request).
Best practice: send only the employees your integration actually tracks — e.g. the staff configured in the end user's own program — rather than omitting the filter and pulling the whole location.

Response 200

{
  "success": true,
  "location_id": 7,
  "data": [
    {
      "company_user_id": 42,
      "location_user_id": 15,
      "clocked_in": true,
      "last_event_type": "IN",
      "last_event_time_utc": "2026-09-03 08:00:00",
      "last_event_time_local": "2026-09-03 10:00:00"
    }
  ]
}

API key management

API keys are created, listed, and revoked from the Tikklok dashboard (Settings → API keys, owner/admin only). The secret is shown once at creation — copy it immediately. Revoking a key is immediate.

OpenAPI spec

A machine-readable OpenAPI 3.0 description is available for import into Postman, Insomnia, or client-SDK generators.

Download openapi.yaml