Reference
The API
One contract, defined once and generated outward: an OpenAPI 3.1 document, a typed client and an MCP tool list all come from the same source as the handlers. This page is generated from that document too.
Status, before anything else
The API is not deployed. There is no public base URL yet. Everything below describes the contract as it is built and tested, and you can run it locally, but you cannot call it over the internet today.
Organization identity is derived from the authenticated session. Better Auth owns identities and HTTP-only sessions. PEOPLE records organization memberships and company access as append-only statements, and PostgreSQL rechecks every link for each company transaction.
If you are planning an integration against this, the shape of the contract is stable enough to build to and the deployment is not. Talk to us before you start —hello@flightworkly.com.
The document
GET /openapi.json, unauthenticateddocs/openapi.jsonPayroll-first HR platform. Amounts are decimal strings, never JSON numbers. A pay item states its type and amount; which statutory bases it reaches is the country pack's answer.
The document is generated at startup from the same route objects the handlers are bound to, so it cannot describe an endpoint nobody serves or omit one that is served. A separate check regenerates it and compares byte for byte, so a contract change that forgets to regenerate fails rather than shipping a document that lies.
Authentication and tenancy
Every route under /v1 requires a Better Auth session cookie. /health and/openapi.json sit outside that prefix and are public.
- Credential. An HTTP-only Better Auth session cookie. Browser clients send it through the same origin; Node clients may pass the cookie explicitly.
- Organization. The session selects one organization through
POST /v1/session/organization. The database accepts it only while the user has a current membership. No request header may claim an organization. - Company. A path parameter, not a header. An organization can act for many companies, so the company belongs in the URL where it is visible, linkable and logged. The database checks the company directly and requires active access for the acting organization.
Two conventions that will bite you otherwise
Money is a string
Amounts are decimal strings — "5000.00" — never JSON numbers. A documented number is a float in every client, and a float is the wrong type for money in a way that shows up as a one-sen discrepancy months later. Inside the engine the same amounts are integer minor units. A test asserts the schema type, so this cannot regress into a number.
A statement is complete, never a difference
Recording facts states every fact; recording pay states every line. A later statement that omits something drops it. Nothing merges a statement over what came before, because that would make "not mentioned" and "not true" the same thing on the wire. Send the whole picture each time.
Every route
Read from docs/openapi.json when this page was built, so it is the same list the server serves.
| Method | Path | What it does | Auth |
|---|---|---|---|
| GET | /health | Liveness, and which versions are running. | Public |
| GET | /v1/companies | Companies the selected organization may currently act for. | Bearer |
| POST | /v1/companies | Create a company owned by this organization. | Bearer |
| GET | /v1/companies/{companyId}/employees | Everyone on this company's payroll. | Bearer |
| POST | /v1/companies/{companyId}/employees | Add someone to this company's payroll. | Bearer |
| POST | /v1/companies/{companyId}/employees/{employeeId}/compensation | Record what this person is standingly paid, commencing in a month. | Bearer |
| GET | /v1/companies/{companyId}/employees/{employeeId}/compensation | What this person is standingly paid in a given month. | Bearer |
| GET | /v1/companies/{companyId}/employees/{employeeId}/employment | When this person was employed here. | Bearer |
| POST | /v1/companies/{companyId}/employees/{employeeId}/employment-events | Record that this employment began or ended. | Bearer |
| POST | /v1/companies/{companyId}/employees/{employeeId}/facts | Record what is known about this person, commencing in a month. | Bearer |
| GET | /v1/companies/{companyId}/employees/{employeeId}/facts | What is known about this person in a given month. | Bearer |
| POST | /v1/companies/{companyId}/employees/{employeeId}/opening-balance | Import what a previous provider paid this person before the changeover. | Bearer |
| GET | /v1/companies/{companyId}/employees/{employeeId}/opening-balance | What was imported for this person, if anything. | Bearer |
| POST | /v1/companies/{companyId}/facts | Record the employer's own answers, commencing in a month. | Bearer |
| GET | /v1/companies/{companyId}/facts | The employer's own answers in force for a month. | Bearer |
| GET | /v1/companies/{companyId}/item-type-overrides | The company's own readings of the item types its employer decides. | Bearer |
| POST | /v1/companies/{companyId}/item-type-overrides/{code} | Record this company's reading of an item type the statute leaves to its employer. | Bearer |
| POST | /v1/companies/{companyId}/payroll/preview | Compute a payroll run without storing anything. | Bearer |
| GET | /v1/companies/{companyId}/payroll/readiness | Whether this company can run a payroll for a period, and what is missing. | Bearer |
| GET | /v1/companies/{companyId}/payroll/runs | Every run this company has, newest first. | Bearer |
| POST | /v1/companies/{companyId}/payroll/runs | Compute a payroll run and store it. | Bearer |
| GET | /v1/companies/{companyId}/payroll/runs/{runId} | Read a stored run, recomputed from what was stored. | Bearer |
| POST | /v1/companies/{companyId}/payroll/runs/{runId}/reversal | Reverse a stored run in full, as a contra run. | Bearer |
| GET | /v1/jurisdictions/{jurisdiction} | Every fact a jurisdiction reads and every pay line it recognises. | Bearer |
| GET | /v1/organizations | Organizations this session may currently act for. | Bearer |
| POST | /v1/session/organization | Select the organization this session acts for. | Bearer |
Generated clients
The contract emits more than a document. A typed client and an MCP tool list are generated from the same route registry, so an agent and an application reach the same API through the same names.
- Client. One method per route, named identically to the registry key. Bodies and responses are typed as unknown on purpose: the response types already exist in the contract as inferences of the schemas the API validates with, and generating a second set beside them would be the parallel copy the one-contract-source rule exists to prevent.
- MCP tools. The same routes as a tool list, so a model can drive a payroll through the same contract a client does.
- Errors. A failed call raises with the HTTP status and the parsed problem body. Problems have their own content type rather than being JSON that happens to describe a failure.
Where the API refuses
Validation happens where somebody types, not where the payroll runs. An unknown fact key, a fact on the wrong subject, an answer a choice does not offer, money as a bare number, an item type the country pack refuses — all are rejected at the moment they are recorded.
The reason is practical rather than purist: a wrong value accepted into the record blocks a payroll weeks later, under a description that sends whoever typed it looking for something else entirely. Rejecting it at the keystroke costs one request and saves that afternoon.