peoplBook a walkthroughTalk to us

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

SpecificationOpenAPI 3.1.0
TitlePEOPLE by SPEED
Contract version0.0.1
Operations26 across 19 paths
Served atGET /openapi.json, unauthenticated
Checked in atdocs/openapi.json

Payroll-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 throughPOST /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.

MethodPathWhat it doesAuth
GET/healthLiveness, and which versions are running.Public
GET/v1/companiesCompanies the selected organization may currently act for.Bearer
POST/v1/companiesCreate a company owned by this organization.Bearer
GET/v1/companies/{companyId}/employeesEveryone on this company's payroll.Bearer
POST/v1/companies/{companyId}/employeesAdd someone to this company's payroll.Bearer
POST/v1/companies/{companyId}/employees/{employeeId}/compensationRecord what this person is standingly paid, commencing in a month.Bearer
GET/v1/companies/{companyId}/employees/{employeeId}/compensationWhat this person is standingly paid in a given month.Bearer
GET/v1/companies/{companyId}/employees/{employeeId}/employmentWhen this person was employed here.Bearer
POST/v1/companies/{companyId}/employees/{employeeId}/employment-eventsRecord that this employment began or ended.Bearer
POST/v1/companies/{companyId}/employees/{employeeId}/factsRecord what is known about this person, commencing in a month.Bearer
GET/v1/companies/{companyId}/employees/{employeeId}/factsWhat is known about this person in a given month.Bearer
POST/v1/companies/{companyId}/employees/{employeeId}/opening-balanceImport what a previous provider paid this person before the changeover.Bearer
GET/v1/companies/{companyId}/employees/{employeeId}/opening-balanceWhat was imported for this person, if anything.Bearer
POST/v1/companies/{companyId}/factsRecord the employer's own answers, commencing in a month.Bearer
GET/v1/companies/{companyId}/factsThe employer's own answers in force for a month.Bearer
GET/v1/companies/{companyId}/item-type-overridesThe 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/previewCompute a payroll run without storing anything.Bearer
GET/v1/companies/{companyId}/payroll/readinessWhether this company can run a payroll for a period, and what is missing.Bearer
GET/v1/companies/{companyId}/payroll/runsEvery run this company has, newest first.Bearer
POST/v1/companies/{companyId}/payroll/runsCompute 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}/reversalReverse 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/organizationsOrganizations this session may currently act for.Bearer
POST/v1/session/organizationSelect the organization this session acts for.Bearer
26 operations. Grouped in the document as platform, tenancy, employment, configuration, payroll.

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.