Skip to content

Public API

Every Keanan workspace exposes the same REST API the dashboard and the app use. With an API key you can read and write your workspace from scripts, payroll providers, HR systems and AI assistants, and webhooks push events out as they happen.

The API is included in Managed, Self hosted and Reseller plans, free during a pilot, and available as an add on for Starter.

Open Settings, Developers (admins) or Time portal, API and assistants (everyone) and create a key.

  • Personal keys act as you, narrowed to the scopes you pick. An employee can only grant scopes their role could use anyway.
  • Service keys (admins only) act for the workspace, for example a nightly sync into your payroll provider.

The secret is shown once and stored hashed. Send it as a bearer token:

Terminal window
curl https://acme.keanan.app/api/v1/pto/balance/ \
-H "Authorization: Bearer kn_live_..."
Scope Grants
people:read My own profile, contracts and benefits
team:read Other people’s records: team directory and every admin or manager listing (managers and admins only)
people:write Create, update and deactivate people (admins)
time:read / time:write Time entries, clock, my time report; track time
absences:read / absences:write / absences:approve Balances and requests; request time off; review requests
expenses:read / expenses:write / expenses:approve Expense claims; submit; review
payroll:read Payslips, invoices, wallet
reports:read Team reports, presences, overtime, dashboard summaries
webhooks:manage Create and manage webhooks

A key can never do more than its owner: the owner’s role is checked as well, so an employee’s key cannot reach manager endpoints even if the scope name matches. Endpoints that have no place in an integration (login, licensing, pilots, workspace export, theme, key management) reject API keys outright. The interactive reference at /api/docs/public/ on your workspace lists exactly what a key can call and which scope each call needs; the raw schema is at /api/schema/public/.

Responses arrive in a stable envelope:

{ "status": 200, "message": "Success", "data": { } }

Errors carry a human readable message and field errors in errors. Dates are ISO YYYY-MM-DD, timestamps ISO 8601 with a time zone, money as strings with two decimals plus a three letter currency.

Rate limits are per key: 120 requests a minute on Starter and pilots, 600 on Managed, 1200 on Self hosted. A 429 tells you how long to wait.

Every write made with a key is recorded in the audit trail with the key’s prefix and source, so the log reads created employee · api:kn_live_ab12.

Admins register webhooks under Settings, Developers. Keanan POSTs a JSON body to your URL for the events you pick:

Event When
absence.requested, absence.approved, absence.denied, absence.cancelled Time off lifecycle
expense.submitted, expense.approved, expense.denied, expense.deleted Expense claims
time_entry.created, time_entry.updated, time_entry.deleted Manual time entries
clock.started, clock.stopped Live clock
payment.completed, invoice.issued Payroll
employee.created, employee.deactivated People
{
"event": "absence.approved",
"occurred_at": "2026-08-21T10:12:44+00:00",
"actor": { "id": "", "email": "maria@acme.example", "name": "Maria Schmidt" },
"data": { "id": "", "employee": { "…": "" }, "start_date": "2026-09-07", "end_date": "2026-09-08", "status": "approved" }
}

Each delivery carries X-Keanan-Event, X-Keanan-Delivery and X-Keanan-Signature: sha256=<hmac>; the HMAC is computed over the raw body with the webhook secret shown when you created it. Verify it before you trust the payload:

import hashlib, hmac
def verify(secret: str, body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

Answer with any 2xx. Anything else (or a timeout after ten seconds) is retried five times with backoff from one minute to twelve hours. Redirects are not followed. The Developers page shows every delivery with its status, response code and a Resend button (three resends per delivery), Ping sends a webhook.ping to test the wiring, and Rotate secret issues a new signing secret when the old one may have leaked.

Webhook URLs must be public https addresses; private networks, loopback, link local and cloud metadata addresses are refused at creation and again at every delivery. Delivery records are kept for 30 days.

Sync approved absences into another system:

import requests
API = "https://acme.keanan.app/api/v1"
headers = {"Authorization": "Bearer kn_live_..."}
absences = requests.get(f"{API}/pto/admin/requests/", params={"status": "approved"}, headers=headers).json()["data"]

Push a time entry from a ticket system:

Terminal window
curl -X POST https://acme.keanan.app/api/v1/tracking/entries/ \
-H "Authorization: Bearer kn_live_..." -H "Content-Type: application/json" \
-d '{"started_at":"2026-08-21T09:00:00+02:00","ended_at":"2026-08-21T11:30:00+02:00","description":"TICKET-42","billable":true}'

Connect an AI assistant instead of writing code: see the MCP server.