The public API and agent surfaces

Why the API is anonymous-first, how keys and scopes are shaped, what writes are allowed today, and which surfaces exist specifically so AI agents and crawlers can read the product.

Explanation · ~8 min read · Updated 18 Aug 2026

Most HRMS APIs start authenticated. This one deliberately does not, because two different audiences need two different things: an agent or a crawler needs to understand what the product does, and an integration needs access to what a specific organisation has. Only the second one needs a credential.

Anonymous first

Three things are readable with no credentials: a health check, a capability manifest describing every module and user journey, and the module catalogue with its honest availability status. That is enough for a crawler, a research agent or a prospective customer's automation to answer "what can this product do?" without anyone issuing a key.

Everything tied to an organisation requires one.

Keys, and what they are allowed to touch

Keys come in live and test flavours, distinguished by their prefix. The full secret is displayed exactly once at creation and never stored — only a hash of it is kept, so a key cannot be recovered, only rotated. Rotation mints a new secret immediately and the old one stops working at once, with no overlap window, so plan a rotation as a coordinated change rather than a gradual one. Keys can carry an optional expiry, and revoking one is a soft action that keeps the record for audit.

Machine clients that prefer short-lived credentials can use OAuth2 client credentials instead, exchanging a client id and secret for an access token with a configurable lifetime and a refresh token that rotates on every use.

Scopes follow a module:read / module:write shape, with a few distinct high-privilege actions — approving leave and running payroll are separate grants, not implied by write access. A key can only be granted scopes for modules your organisation actually has enabled.

One scope deliberately does not exist. Grievance and POSH data is not reachable through the API at any scope. That is a product decision rather than an omission: confidential reports handled by a committee should not be readable by an integration, however well-intentioned.

The scope vocabulary is broader than the endpoint surface today. Reads are live for employees, leave requests, leave types, holidays, assets, job openings, helpdesk tickets and surveys. The remaining scopes are grantable but do not yet have endpoints behind them.

Reading

Every list follows the same shape:

  • limit defaults to 50 and is capped at 100.
  • cursor is an opaque token you echo back from the previous response; you never construct one.
  • Responses are wrapped as data, next_cursor and has_more. When next_cursor is null there are no more pages.

Two things happen on every read that are worth knowing about. Each resource declares an explicit list of fields it returns, so a column added to the underlying model never starts appearing in API responses by accident. And every query is filtered to the key's own organisation, independently of the database's own isolation — the same belt-and-braces approach described in how tenancy and roles work.

Writing

Writes are narrow on purpose. Today there are four, all on leave: create a request, and approve, reject or cancel one. There is no update or delete verb anywhere in the API.

The interesting design point is the acting employee. An API key belongs to an organisation, not a person — but "approved by" has to mean somebody. So write calls name the employee performing the action, and that identity is resolved strictly within the key's own organisation. From there the request goes through the same core functions the web app and the Slack app use. Balance checks, overlap detection, and the rule about who may approve whose request are enforced identically whichever door you came in through. The API is not a bypass.

Idempotency is supported on writes through an Idempotency-Key header. Send the same key with the same request body and the original response is replayed verbatim, marked with a replay header, rather than the action happening twice. Send the same key with a different body and you get a conflict — which is the useful behaviour, because it means a retry loop with a mutated payload fails loudly instead of silently doing something new.

Errors and limits

Errors are RFC 9457 problem documents, served as application/problem+json, carrying a stable machine-readable code alongside the human title and detail, plus a request id you can quote in a support conversation.

Rate limiting is 120 requests per minute per IP address. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; exceeding the limit returns 429 with Retry-After. Write calls are additionally counted per key.

Webhooks

Four events fire today, all on leave: created, approved, rejected and cancelled. An endpoint can subscribe to specific events or to everything.

Each delivery is signed. The signature header carries a timestamp and an HMAC-SHA256 digest in the form t=TIMESTAMP,v1=HEXDIGEST, computed over the timestamp and the raw request body joined by a period, keyed with the endpoint secret shown to you once at creation. A second header names the event. Verify the digest against the raw body before parsing it, and choose a timestamp tolerance appropriate to your own clock skew to reject replays.

Be clear-eyed about the delivery guarantee: each event is attempted once, with a short timeout, and the result is recorded. There is no automatic retry and no backoff queue. Webhooks are a low-latency nudge, not a durable message bus — the correct pattern is to treat one as a hint to go and read the current state, and to reconcile periodically so a dropped delivery is self-healing.

The spec, and how it stays true

The OpenAPI 3.1 document served from the API is partly generated: every read resource's list and get operation is emitted from the same registry the routes themselves are built from, so an endpoint and its specification cannot describe different things.

That is backed by a test rather than by discipline. Continuous integration asserts that every path the spec declares has a real route file on disk, that every declared route appears in the spec, and that every error response declares the problem+json content type. Adding a route without specifying it, or specifying one that does not exist, fails the build.

An interactive reference renders that spec directly, and a Postman collection generated from it ships as a static file.

Surfaces built for agents

Beyond the API there are surfaces whose entire purpose is machine readability:

  • A capability manifest — a single versioned document describing the product: every module with its status, the user journeys inside each one, and which AI crawlers are welcome.
  • llms.txt and llms-full.txt — the compact and complete text corpora, so an assistant can ingest what the product does without crawling the site.
  • A read-only MCP server with seven tools for discovering modules, searching them, and describing user journeys. It holds no credentials and can reach no organisation's data — it reads the same public manifest anyone else can.
  • A separate write MCP server with tools for the four leave operations. It is a thin client over the public API rather than a second way into the database, so scopes, organisation isolation, idempotency and audit all still apply. Its write tools are annotated as destructive so an agent framework can prompt before using them.

Keeping read and write in two separate servers is deliberate: the read server can be pointed at safely by anyone, and adopting it never implies granting write access.

About the status page

The status page is a liveness check, and it is worth saying exactly what it does: your browser calls the API health endpoint every 30 seconds and reports what it gets back, along with the round-trip time. It tells you whether the web tier is answering right now. It is not an uptime history, an incident feed, or an independent probe of each subsystem.

Frequently asked questions

Do I need an API key to read anything?
Not for product metadata. Health, the capability manifest and the module catalogue are readable with no credentials at all. A key is required the moment you want your own organisation's data.
Can the API write to payroll?
No. Writes today are limited to leave — creating, approving, rejecting and cancelling a request. Everything else in the API is read-only.
Why do write calls need an employee id?
Because an API key belongs to an organisation, not a person, and approvals need an approver. Naming the acting employee lets the same permission rules the web app uses decide whether that person may act.
Are webhook deliveries retried?
No. Delivery is attempted once, with a short timeout, and the outcome is recorded. Treat webhooks as a fast notification and reconcile with a read call rather than relying on them as a guaranteed queue.