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 · ~10 min read · Updated 15 Sept 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 leave operations and for creating or updating employees, generating documents, and opening roles and adding candidates. 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.

MCP over HTTP

The stdio servers above have one structural limit: they run on the machine of whoever configured them, so they suit a developer's laptop and not a person who simply wants to ask Claude or ChatGPT about their leave balance. Those assistants add a "custom connector" by URL, and they expect three things from it — a Streamable-HTTP MCP endpoint, OAuth 2.1 discovery so they can find the sign-in, and dynamic client registration so nobody has to hand them a client id. The work to provide all three is tracked as one program, and this section says what is done and what is not, because a document that describes the finished state as present would be the kind of lie the honesty guards exist to catch.

Done. The discovery documents are served at /.well-known/oauth-authorization-server (RFC 8414) and /.well-known/oauth-protected-resource (RFC 9728), both built from one source so they cannot disagree about the issuer. The authorization server is live: authorization code with PKCE (S256 only), a consent screen that asks the person to sign in, pick one workspace and allow named permission groups, dynamic client registration with exact-match https or loopback redirect URIs, token issuance and rotating refresh, revocation, and a Connected assistants page under Settings where a person sees and disconnects what they allowed. Tokens minted through consent are bound to the person and the workspace they chose, so a write made through one names that person in the audit log with no actor_employee_id in the call — and a header naming a different workspace is refused rather than ignored.

Consent-minted tokens carry a directory audience: an explicit allow-list of scopes a public connector may ever hold. Reads across the product, plus the writes the stdio write server already exposes — leave, employees, documents, recruiting — plus helpdesk. Running payroll, grievances, data-subject requests, platform administration, credentials and billing writes are absent from that list, and widening it is a recorded decision rather than a code change.

Being built. The endpoint itself, /api/mcp. It will speak JSON-RPC 2.0 over POST (single requests and batches), answer initialize, tools/list, tools/call, prompts/list and prompts/get, accept either an API key or a consent-minted token as a bearer, and answer an unauthenticated request with 401 and a WWW-Authenticate header whose resource_metadata points at the protected-resource document — which is how a client discovers the sign-in without being told. Its tools come from a single registry that calls the same core functions the REST routes call; the two stdio servers become thin bridges that forward every message to it, so there is one tool surface, not three. A compact mode (ashr_discover, ashr_describe, ashr_dispatch) keeps the advertised tool list inside a small token budget however many tools the registry grows to.

Until that endpoint is on a release, the connector URL in the help article answers 404, and the consent flow — which works today — has nothing to hand its token to. The daily production smoke checks the discovery documents and the endpoint's unauthenticated 401, so the day it ships is measured rather than announced.

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.
Can Claude or ChatGPT connect without an API key?
That is what the OAuth 2.1 surface is for. The assistant registers itself, sends the person to a consent screen, and receives a token bound to that person and one workspace. The hosted MCP endpoint that token unlocks is the last piece and is described below with its current status.