# RALPH API reference

The RALPH API is a REST API described by one OpenAPI 3.1 contract. This page is built from that contract, and the contract itself is at /developers/api/openapi.v1.yaml.

## The contract

[`openapi.v1.yaml`](/developers/api/openapi.v1.yaml) is the whole API: every path, schema and error. The endpoints below are written from it when the site is built, so they can't drift from it. The MCP server and RALPH's own apps are built on the same file. Each endpoint below names its schemas, such as `Booking`. The contract defines them in full.

## Authentication

Send a Bearer credential in `Authorization: Bearer <token>`. It is one of:

- an **access token** from RALPH's OAuth server, for an app or agent acting for a person;
- a **sign-in token** for a person signed in with Google; or
- an **agent key** (`rk_…`), for unattended use. Mint one with [the CLI](/developers/cli).

The exceptions say so in their entry below: the web app's session operations take its session cookie instead, and a calendar feed's URL carries its own token. The session operations are the web app's own: they answer only on the app host, to a same-origin request, and a `POST` or `DELETE` must also send `Ralph-CSRF: 1`, or it gets `403`.

Access tokens last 15 minutes. An access token or an agent key acts for one person in one organisation. A person's sign-in token reaches every organisation they belong to, which `/me` lists. Either way, the audit trail names who acted.

## Conventions

- **Organisations in the path.** Organisation-scoped operations sit under `/orgs/{org}/…`, by the organisation's slug. `/me` and `/orgs` sit outside.
- **UTC times.** Every timestamp is ISO 8601 in UTC. An organisation's home time zone is an IANA name, such as `Europe/London`. The exception is an availability rule's `start_local` and `end_local`: `HH:MM` wall-clock times in that home time zone, so a rule keeps its hours across a clock change.
- **Safe retries.** An operation that lists an `Idempotency-Key` parameter below can be retried within 24 hours, with the same key and the same body, without repeating its effect: you get the first answer again. Creating a booking requires a key. The same key with a different body gets `422`, a retry while the first is still running gets `409` (try again shortly), and after 24 hours the key counts as new. The exceptions are creating an agent key and creating a calendar feed. Their secret is shown only once, so a retry gets `409` (`agent-key-secret-not-replayable` or `calendar-feed-token-not-replayable`) instead: nothing is created twice, but the secret can't be recovered, so revoke that one and create another. Retrying any other `POST`, such as rotating a calendar feed, repeats it.
- **Optimistic updates.** An update that lists an `If-Match` parameter below takes the `ETag` from your last read, and a stale one gets `412`. The others, such as updating a category, are last write wins.
- **Merge patches.** A body sent as `application/merge-patch+json` follows RFC 7396: a field you leave out is unchanged, and `null` clears it.
- **Cursor pages.** A list that takes `limit` (up to 200) and `cursor` returns `next_cursor`: pass it as the next request's `cursor`, until it comes back `null`. A list without them, such as your sessions, returns everything at once.

## Errors and hints

Every error is an RFC 9457 problem (`application/problem+json`) with a stable `type`, a `title`, the HTTP `status` and usually a `detail`. Some carry more:

- **`409` booking conflict:** `conflicts` lists the bookings in the way, with their resources and times.
- **`422` booking blocked:** `checks` lists each failed check, with a hint that resolves it.
- **`412`:** your `If-Match` is stale. Read again, then retry.
- **`401` or `404`:** a missing or expired credential, or an organisation you can't see. An organisation you have no access to answers `404`, not `403`.

## Endpoints

### control-plane

Identities, organizations, agent keys — everything outside an org DB (ADR-0001).

#### `GET /me`

The calling identity, its organizations, and who it acts for in each.

- `200`: Caller context. `Me`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /me/sessions`

Where the caller is signed in to the web app — their live sessions, this one first.

The app host only (ADR-0028): a session operation authenticates by the session cookie,
never a Bearer credential, so the API host answers `401`. Every live session is in the
one page, this one first, then the rest by last use.

Authentication: `sessionCookie`, not a Bearer token.

- `200`: The caller's live sessions. `Page`, with `items` (array of `Session`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /me/sessions/{sessionId}`

End one of the caller's sessions, such as one on a lost phone.

Its next request is refused. Ending this session is signing out, answered as `signOut`
answers. Ending another needs a sign-in in the last 10 minutes (ASVS 5.0 V7.5.2), or it
answers `403` with `type` `…/problems/fresh-sign-in-required`: sign in again, then
retry. Another person's session, or none, is `404`. The app host only.

Authentication: `sessionCookie`, not a Bearer token.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `sessionId` | path | `Uuid` | yes |  |

- `204`: Ended. (header `Clear-Site-Data`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /me/sessions/end-others`

Sign out everywhere else — end every session of the caller's but this one.

Needs a sign-in in the last 10 minutes (ASVS 5.0 V7.5.2), or it answers `403` with
`type` `…/problems/fresh-sign-in-required`. The app host only.

Authentication: `sessionCookie`, not a Bearer token.

- `204`: Every other session ended.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /me/sign-out`

Sign out — end this session at once and clear the site.

Deletes the session, clears its cookie, and answers `Clear-Site-Data: "cache",
"cookies", "storage"`, so the browser also drops what it cached, the offline cache
included (ADR-0028). A session that has already ended answers `401`. The app host only.

Authentication: `sessionCookie`, not a Bearer token.

- `204`: Signed out. (header `Clear-Site-Data`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs`

Organizations the caller can access.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |

- `200`: Page of organizations. `Page`, with `items` (array of `Org`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}`

One organization. Suspended orgs are served read-only; deleted orgs are 404, always.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |

- `200`: The organization. `Org` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}`

Update org settings (admin). The slug is immutable — renames change `name` only.

Changing `home_timezone` re-anchors day-boundary interpretation (availability rules, flight dates) from the moment of change; history is not reinterpreted.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/merge-patch+json`): object, with `name` (string); `home_timezone` (string); `accent_colour` (any): The org's colour, which paints only the primary action (ADR-0034); `null` clears it, and the primary action is white again. A colour too close to a coded colour is saved but falls back to white — `branding.accent_colour.fallback` says why. Preview one with `previewOrgBrand` first.

- `200`: Updated. `Org` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}`

Request deletion of the org (admin). Moves it to pending_delete with a grace window; reversible via undeleteOrg until the deadline.

The org moves to `pending_delete` with a `deleted_after` grace deadline: it stays readable
and exportable, and can be undeleted, until then. Billing is revoked and the org database
destroyed by the service-principal teardown only after the grace window (ADR-0020), so an
org reclaimed during grace keeps its live entitlement. Authorized by the customer's grant
(customer_access). Idempotent during grace.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |

- `202`: Deletion scheduled; the org is now pending_delete. `Org`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/brand-preview`

What a colour would do as the org's colour, without saving it.

The pair the server generates for the primary action in each theme, or the fallback to white
with its reason when the colour is too close to a coded colour (ADR-0034). Persists nothing;
any member may ask. `updateOrg` saves the same colour to the same answer.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |

Request body (required, `application/json`): object, with `accent_colour` (`ColourInput`, required)

- `200`: What the colour comes to. `BrandColour`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PUT /orgs/{org}/logos/{slot}`

Upload one of the org's logos (admin). It is rasterised on upload and served only as a PNG.

A PNG or an SVG, base64 in a JSON body, at most 512 KiB decoded. It is redrawn as a PNG, 128 px
high for a logo (no wider than 6:1, no taller than 1:2) or 256 px square for the icon, and only
that PNG is ever served (ADR-0030). An SVG draws its shapes only: no text, fonts or images.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `slot` | path | `LogoSlot` | yes | For light surfaces, for dark surfaces, or the square icon. |

Request body (required, `application/json`): object, with `content_type` (string: `image/png`, `image/svg+xml`, required); `data` (string, required)

- `200`: The org, with the logo's new URL. `Org` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}/logos/{slot}`

Remove one of the org's logos (admin). Idempotent.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `slot` | path | `LogoSlot` | yes | For light surfaces, for dark surfaces, or the square icon. |

- `200`: The org, without the logo. `Org` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/undelete`

Reverse a pending deletion during the grace window (admin), restoring the org to the state it was deleted from.

Restores a `pending_delete` org to the state it was in when deletion was requested:
`active` for a servable org, or `provisioning` for an org an operator retired before its
provisioning finished — the provisioning sweep then resumes it, and it is not yet servable
(poll `getOrg` until `status` is `active`). Allowed only until the grace deadline and only
with a current active/trialing entitlement, so an entitlement-lost org cannot dodge teardown
by reclaiming it during grace (ADR-0020). Idempotent: an org already restored is returned
as it is.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |

- `200`: The org is restored (`status` is `active`, or `provisioning` for an unfinished org). `Org`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/members`

Identity↔person access links for this org (admin).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |

- `200`: Page of members. `Page`, with `items` (array of `Member`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/members`

Grant a person API access (admin) — the membership workflow.

Links an identity to an existing person by verified email match: if an identity with that
email exists it is linked now; otherwise the link activates on the person's first login
with a matching verified email. People are created first (ADR-0009); the active-email
dedupe guard prevents duplicates. Person merge is deliberately deferred.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): object, with `person_id` (`Uuid`, required); `email` (string, email, required)

- `201`: Access granted (or pending first login). `Member`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}/members/{memberId}`

Revoke an identity's access to this org (admin; effective next request).

Does not archive the person — history stays — but ends the agents acting for them through this membership (ADR-0039): the apps connected through its login, whose access tokens stop at once, and — unless the person still holds another active or pending membership in this org — their agent keys and any other connected apps. Giving access again brings none of them back. Archiving a person separately auto-revokes their agent keys and access.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `memberId` | path | `Uuid` | yes |  |

- `204`: Revoked.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/agent-keys`

Agent keys in this organization (metadata only, never secrets).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |

- `200`: Page of keys. `Page`, with `items` (array of `AgentKey`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/agent-keys`

Mint an agent key acting for a person.

The secret is returned once, in this response only (ADR-0003).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `AgentKeyCreate`

- `201`: Key created; `secret` present only here. `AgentKeyWithSecret`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}/agent-keys/{keyId}`

Revoke an agent key (immediate, irreversible).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `keyId` | path | `Uuid` | yes |  |

- `204`: Revoked.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### people

People the organization knows about; a person is not a login (ADR-0009).

#### `GET /orgs/{org}/people`

People the organization knows about. Members see names only (plus their own full record).

Field visibility follows the authorization matrix (docs/design/architecture.md) — contact details and notes are instructor/staff+.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `status` | query | string: `active`, `archived` | no |  |
| `role` | query | `Role` | no |  |
| `q` | query | string | no | Name/email substring search. |

- `200`: Page of people. `Page`, with `items` (array of `Person`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/people`

Add a person (no login required — ADR-0009).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `PersonCreate`

- `201`: Created. `Person` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/people/{personId}`

One person.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `personId` | path | `Uuid` | yes |  |

- `200`: The person. `Person` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/people/{personId}`

Update a person (contact details, status, bookability).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `personId` | path | `Uuid` | yes |  |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/merge-patch+json`): `PersonUpdate`

- `200`: Updated. `Person` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PUT /orgs/{org}/people/{personId}/roles`

Replace the person's role set (admin; audited; bumps the Person entity version).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `personId` | path | `Uuid` | yes |  |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/json`): object, with `roles` (array of `Role`, required)

- `200`: New role set. `Person` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### aircraft

The fleet, including derived meter readings and audited adjustments.

#### `GET /orgs/{org}/aircraft`

The fleet.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `status` | query | string: `airworthy`, `grounded`, `retired` | no |  |
| `kind` | query | `AircraftKind` | no |  |

- `200`: Page of aircraft. `Page`, with `items` (array of `Aircraft`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/aircraft`

Add an aircraft (creates its resource).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `AircraftCreate`

- `201`: Created. `Aircraft` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/aircraft/{aircraftId}`

One aircraft, including current derived meter readings.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `aircraftId` | path | `Uuid` | yes |  |

- `200`: The aircraft. `Aircraft` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/aircraft/{aircraftId}`

Update aircraft details or status (e.g. ground it).

Correcting `kind` or `type_designator` requires the `admin` role (a `403` otherwise):
policy selectors (`applies_to`) and type-scoped pilot authorizations match on them, so a
correction changes how every later booking of the airframe is judged. Restating the
current value needs nothing more. Every other field needs staff.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `aircraftId` | path | `Uuid` | yes |  |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/merge-patch+json`): `AircraftUpdate`

- `200`: Updated. `Aircraft` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/aircraft/{aircraftId}/meter-events`

The append-only, totally-ordered meter trail (ADR-0010).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `aircraftId` | path | `Uuid` | yes |  |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `meter_id` | query | `Uuid` | no |  |

- `200`: Page of events, trail order (per-meter seq). `Page`, with `items` (array of `MeterEvent`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/aircraft/{aircraftId}/meter-events`

Append an adjustment or meter-replacement event (staff; audited).

Adjustments may set any value (that is what they are for); flight readings are appended via flight logging, never here.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `aircraftId` | path | `Uuid` | yes |  |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `MeterAdjustmentCreate`

- `201`: Appended; the meter's cached reading updated. `MeterEvent`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### resources

The generic bookable abstraction over aircraft and instructors (ADR-0007).

#### `GET /orgs/{org}/resources`

Everything bookable (aircraft, instructors — ADR-0007).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `kind` | query | `ResourceKind` | no |  |

- `200`: Page of resources. `Page`, with `items` (array of `Resource`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/resources/{resourceId}/availability`

Free/busy timeline for a resource over a window.

The primary "find a time" query for agents; busy blocks reference their booking.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `resourceId` | path | `Uuid` | yes |  |
| `from` | query | string, date-time | yes |  |
| `to` | query | string, date-time | yes |  |

- `200`: Ordered busy blocks; gaps are free. `Availability`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/availability-rules`

Org-wide and per-resource availability rules.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `resource_id` | query | `Uuid` | no |  |

- `200`: Page of rules. `Page`, with `items` (array of `AvailabilityRule`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/availability-rules`

Add an availability rule (staff) — opening hours, working days, closures.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `AvailabilityRuleCreate`

- `201`: Created. `AvailabilityRule`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/availability-rules/{ruleId}`

One availability rule, with the ETag its update is conditioned on.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `ruleId` | path | `Uuid` | yes |  |

- `200`: The rule. `AvailabilityRule` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/availability-rules/{ruleId}`

Correct an availability rule in place (staff). Moving a rule to a different resource is not a correction but a different rule, so resource_id is not patchable — create the new rule and delete the old one.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `ruleId` | path | `Uuid` | yes |  |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/merge-patch+json`): `AvailabilityRuleUpdate`

- `200`: Updated. `AvailabilityRule` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}/availability-rules/{ruleId}`

Remove an availability rule (staff).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `ruleId` | path | `Uuid` | yes |  |

- `204`: Removed.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### authorizations

Pilot authorizations — facts that eligibility checks consume (ADR-0008).

#### `GET /orgs/{org}/pilot-authorizations`

Pilot authorizations (facts consumed by eligibility — ADR-0008).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `person_id` | query | `Uuid` | no |  |
| `active` | query | boolean | no | Only authorizations that are unexpired and unrevoked. |

- `200`: Page of authorizations. `Page`, with `items` (array of `PilotAuthorization`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/pilot-authorizations`

Grant a pilot authorization (staff/instructor action).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `PilotAuthorizationCreate`

- `201`: Granted. `PilotAuthorization`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}/pilot-authorizations/{authorizationId}`

Revoke an authorization (kept for history, marked revoked).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `authorizationId` | path | `Uuid` | yes |  |

- `204`: Revoked.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### policies

Org-authored booking policies with per-policy enforcement (ADR-0008).

#### `GET /orgs/{org}/policies`

The organization's booking policies (ADR-0008).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `enabled` | query | boolean | no |  |

- `200`: Page of policies. `Page`, with `items` (array of `Policy`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/policies`

Author a policy (data, not code).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `PolicyCreate`

- `201`: Created. `Policy` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/policies/{policyId}`

One policy.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `policyId` | path | `Uuid` | yes |  |

- `200`: The policy. `Policy` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/policies/{policyId}`

Update a policy (params, enforcement, enabled).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `policyId` | path | `Uuid` | yes |  |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/merge-patch+json`): `PolicyUpdate`

- `200`: Updated. `Policy` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}/policies/{policyId}`

Archive a policy (the row survives — evaluations/overrides reference it forever).

Sets `archived_at`; archived policies stop being evaluated. There is no hard delete.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `policyId` | path | `Uuid` | yes |  |

- `204`: Archived.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### bookings

Scheduling with race-proof conflicts and explained eligibility.

#### `GET /orgs/{org}/categories`

The org's category catalogue (ADR-0013).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `active` | query | boolean | no |  |

- `200`: Page of categories. `Page`, with `items` (array of `Category`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/categories`

Author a category (staff).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `CategoryCreate`

- `201`: Created. `Category`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/categories/{categorySlug}`

Rename, recode, describe, or archive a category (staff). Slug is immutable; archive = active false.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `categorySlug` | path | string | yes |  |

Request body (required, `application/merge-patch+json`): object, with `name` (string); `code` (string, matching `^[A-Za-z0-9]{1,4}$`): A new code, stored in capitals. A code another category has, in any case, is a 409 (ADR-0035).; `description` (string or null); `active` (boolean)

- `200`: Updated. `Category`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/bookings`

Bookings, filterable by window, resource, person, status.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `from` | query | string, date-time | no |  |
| `to` | query | string, date-time | no |  |
| `resource_id` | query | `Uuid` | no |  |
| `person_id` | query | `Uuid` | no | Matches made-for person or any participant. |
| `status` | query | `BookingStatus` | no |  |
| `category` | query | string | no | Category slug filter (ADR-0013). |

- `200`: Page of bookings. `Page`, with `items` (array of `Booking`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/bookings`

Create a booking (evaluated, race-proof).

Runs eligibility (ADR-0008) and reserves the resources atomically (ADR-0007).
Outcomes: `201` confirmed; `201` with status `held` when a `require_approval`
check fired (the slot IS held); `409 booking-conflict` with the conflicting
bookings; `422 booking-blocked` with the blocking checks. `Idempotency-Key`
is REQUIRED — agents retry.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | yes | REQUIRED here — booking creation must be retry-safe (ADR-0005 §5). |

Request body (required, `application/json`): `BookingCreate`

- `201`: Booked (`confirmed` or `held`); evaluation attached. `BookingWithEvaluation` (header `ETag`)
- `409`: A reserved resource is already booked in that window (ADR-0007). `ConflictProblem` (`application/problem+json`)
- `422`: A `block`-enforced policy check failed (ADR-0008). `BlockedProblem` (`application/problem+json`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/bookings/evaluate`

Dry-run eligibility + conflicts without creating anything.

Agents call this before proposing times; persists nothing.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |

Request body (required, `application/json`): `BookingCreate`

- `200`: What WOULD happen — evaluation plus any conflicts. `BookingPreview`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/bookings/{bookingId}`

One booking with its current evaluation.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `bookingId` | path | `Uuid` | yes |  |

- `200`: The booking. `BookingWithEvaluation` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/bookings/{bookingId}`

Amend a booking (reschedule, notes, participants) — re-evaluated.

Only `held` and `confirmed` bookings are amendable (terminal states return a problem).
Changing times or resources re-runs conflicts and eligibility exactly like creation and
VOIDS prior overrides — a `confirmed` booking regresses to `held` if a new
require_approval outcome fires. Note/participant-only changes are not re-evaluated.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `bookingId` | path | `Uuid` | yes |  |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/merge-patch+json`): `BookingUpdate`

- `200`: Updated. `BookingWithEvaluation` (header `ETag`)
- `409`: A reserved resource is already booked in that window (ADR-0007). `ConflictProblem` (`application/problem+json`)
- `422`: A `block`-enforced policy check failed (ADR-0008). `BlockedProblem` (`application/problem+json`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/bookings/{bookingId}/evaluate`

Dry-run an amendment — what updateBooking would do, without changing anything.

Merges the `BookingUpdate` onto the current booking exactly as `updateBooking` would,
then runs eligibility and the conflict probe with this booking excluded (it never
conflicts with itself). Persists nothing. Readable by anyone who can read the booking
(`getBooking` visibility; otherwise 404). A terminal booking returns the same problem
as `updateBooking`. The `ETag` names the version evaluated — send it as the `PATCH`'s
`If-Match` so the amendment applies only to what was previewed.

A note/title/participant-only change is not re-evaluated by `updateBooking`, so the
preview returns the booking's current `evaluation` unchanged (same `id`) and `would`
is what its current status stands for (`held` → `hold`, `confirmed` → `confirm`).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `bookingId` | path | `Uuid` | yes |  |

Request body (required, `application/merge-patch+json`): `BookingUpdate`

- `200`: What the amendment WOULD do — evaluation plus any conflicts. `BookingPreview` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/bookings/{bookingId}/cancel`

Cancel a booking, releasing its slots (the booking's person or staff).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `bookingId` | path | `Uuid` | yes |  |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (`application/json`): object, with `reason` (string)

- `200`: Cancelled. `Booking`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/bookings/{bookingId}/deny`

Deny a held booking (instructor/staff — the authority that approves it).

The approver's counterpart to overrideBookingCheck: the booking is cancelled with the
reason, its slots released, and the denial audited as `booking.denied`. Only a `held`
booking can be denied; any other state is a 409 `booking-not-held`.

Send the `ETag` of the booking as shown in `If-Match` to deny only that version: if the
booking has changed since, the denial is refused with a 412 and nothing is recorded.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `bookingId` | path | `Uuid` | yes |  |
| `Idempotency-Key` | header | string, up to 128 characters | yes | REQUIRED here — booking creation must be retry-safe (ADR-0005 §5). |
| `If-Match` | header | string | no | The booking's `ETag` as the caller saw it; if the booking has changed since, the action is refused with a 412 carrying the current `ETag`. Omit it to act on the current booking. |

Request body (required, `application/json`): object, with `reason` (string, matching `\S`, required): Shown to the booking's person; must not be blank.

- `200`: Denied; the booking is returned cancelled. `Booking` (header `ETag`)
- `412`: The booking has changed since the `If-Match` version; nothing was recorded. `Problem` (`application/problem+json`) (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/bookings/{bookingId}/overrides`

Approve a require_approval check (instructor/staff; audited — ADR-0008).

When every outstanding require_approval check is overridden, a `held` booking becomes
`confirmed`. Overrides are scoped to the evaluation they approve — amending the booking's
times or resources voids them.

Send the `ETag` of the booking as shown in `If-Match` to approve only that version: if
the booking has changed since, the override is refused with a 412 and nothing is
recorded. The `201` carries the booking's new `ETag`, so the next override can name it
without a re-read.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `bookingId` | path | `Uuid` | yes |  |
| `Idempotency-Key` | header | string, up to 128 characters | yes | REQUIRED here — booking creation must be retry-safe (ADR-0005 §5). |
| `If-Match` | header | string | no | The booking's `ETag` as the caller saw it; if the booking has changed since, the action is refused with a 412 carrying the current `ETag`. Omit it to act on the current booking. |

Request body (required, `application/json`): `OverrideCreate`

- `201`: Override recorded; booking returned with new state. `BookingWithEvaluation` (header `ETag`)
- `412`: The booking has changed since the `If-Match` version; nothing was recorded. `Problem` (`application/problem+json`) (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/bookings/{bookingId}/evaluations`

Full evaluation history ("why was this held?" — forever answerable).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `bookingId` | path | `Uuid` | yes |  |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |

- `200`: Page of evaluations, newest first. `Page`, with `items` (array of `Evaluation`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### flights

Tech-log flight records; fields provisional pending FR-sheet reconciliation (ADR-0010).

#### `GET /orgs/{org}/flights`

Flight records (tech-log lines — ADR-0010).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `aircraft_id` | query | `Uuid` | no |  |
| `person_id` | query | `Uuid` | no | Matches any crew role. |
| `from` | query | string, date | no |  |
| `to` | query | string, date | no |  |
| `status` | query | string: `draft`, `logged` | no |  |
| `category` | query | string | no | Category slug filter (ADR-0013). |

- `200`: Page of flights. `Page`, with `items` (array of `Flight`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/flights`

Record a flight (draft until logged).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `FlightCreate`

- `201`: Created (draft). `Flight` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/flights/{flightId}`

One flight.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `flightId` | path | `Uuid` | yes |  |

- `200`: The flight. `Flight` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/flights/{flightId}`

Edit a flight — draft or logged — via audited in-place corrections (ADR-0010).

Logged flights are editable, not frozen: a logged flight is corrected by direct, audited
edits exactly like a draft (ADR-0010 reverses the earlier "logging freezes the flight"
stance), including its `meter_readings`, which are part of the flight record. Editing a
logged flight's readings corrects the recorded readings on the flight only; it does NOT
rewrite the append-only `meter_event` trail — the immutable source of truth behind the
aircraft's current readings — so correcting the meter itself is a separate adjustment event
(POST .../aircraft/{aircraftId}/meter-events), not a side effect of this patch. The
draft-to-logged transition is one-way: reverting a logged flight to draft is refused.
Advance a draft to logged via POST .../log, not this endpoint.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `flightId` | path | `Uuid` | yes |  |
| `If-Match` | header | string | yes | ETag from the last read; mismatch returns 412 (ADR-0005 §6). |

Request body (required, `application/merge-patch+json`): `FlightUpdate`

- `200`: Updated. `Flight` (header `ETag`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/flights/{flightId}/log`

Log a draft flight — advance its aircraft's meter trails atomically (one-way draft-to-logged).

Validates each recorded reading against its meter's trail (flight readings must advance
monotonically; regressions get a problem pointing at the adjustment path), materializes one
`flight`-kind meter event per reading, and advances the flight's status to `logged`. This
transition is one-way — a logged flight cannot revert to draft. Logging does NOT freeze the
record: a logged flight stays editable and is corrected by direct, audited edits, while
meter corrections remain adjustment events (ADR-0010).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `flightId` | path | `Uuid` | yes |  |
| `Idempotency-Key` | header | string, up to 128 characters | yes | REQUIRED here — booking creation must be retry-safe (ADR-0005 §5). |

- `200`: Logged; meter events appended, cached readings updated. `Flight`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### audit

The append-only audit trail with full actor attribution.

#### `GET /orgs/{org}/audit-events`

The append-only audit trail (ADR-0005 §8). Staff/admin only.

Payloads are PII-safe by rule — for person entities, changed field names only, never values.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |
| `entity_type` | query | string | no |  |
| `entity_id` | query | `Uuid` | no |  |
| `actor_identity_id` | query | `Uuid` | no |  |
| `from` | query | string, date-time | no |  |
| `to` | query | string, date-time | no |  |

- `200`: Page of audit events, newest first. `Page`, with `items` (array of `AuditEvent`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### calendar-feeds

Read-only iCalendar subscription feeds of bookings, at secret capability URLs (ADR-0024).

#### `GET /orgs/{org}/calendar-feeds`

List calendar feeds (metadata only, never tokens or URLs).

`scope=person` (the default) lists the acting person's own feeds — self-service, any
role. `scope=org` lists the organization's org-scope feeds, and `scope=resource` lists its
resource-scope feeds (aircraft/instructor schedules) — both staff/admin only (ADR-0024); a
member requesting either is refused.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `scope` | query | string: `person`, `org`, `resource` (default `person`) | no | Which feeds to list — the caller's own (`person`), the org's (`org`), or the organization's resource feeds (`resource`). |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |

- `200`: Page of feeds. `Page`, with `items` (array of `CalendarFeed`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/calendar-feeds`

Create a calendar feed (person-scope by default; org/resource for staff/admin).

`scope: person` (the default) creates a feed of the acting person's own bookings —
self-service, any role. `scope: org` creates a feed of every booking in the
organization; `scope: resource` creates a feed of one bookable resource's bookings (an
aircraft's, or an instructor's) named by `resource_id`. Org and resource feeds are
staff/admin only (ADR-0024).

The feed's secret token and its `https`/`webcal` subscription URLs are returned once, in
this response only; subsequent lists carry metadata only (ADR-0024).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Idempotency-Key` | header | string, up to 128 characters | no | Makes this request safely retryable (ADR-0005 §5). |

Request body (required, `application/json`): `CalendarFeedCreate`

- `201`: Feed created; `token` and URLs present only here. `CalendarFeedWithToken`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `DELETE /orgs/{org}/calendar-feeds/{feedId}`

Revoke a calendar feed (immediate, idempotent).

The feed's URL goes dark at once, answering the same non-disclosing 404 as an unknown
token. A person may revoke their own feeds; staff/admin may revoke any feed in the org
(ADR-0024). Idempotent: revoking an already-revoked feed is a no-op that still answers
204.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `feedId` | path | `Uuid` | yes | The calendar feed's id (from a create response or a directory listing). |

- `204`: Revoked.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `POST /orgs/{org}/calendar-feeds/{feedId}/rotate`

Rotate a calendar feed's secret token, invalidating the old URL.

Swaps the feed's token for a fresh one, keeping the feed's id and name. The old URL
stops working immediately; the new token and its `https`/`webcal` URLs are returned
once, in this response only. A person may rotate their own feeds; staff/admin may rotate
any feed in the org (ADR-0024).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `feedId` | path | `Uuid` | yes | The calendar feed's id (from a create response or a directory listing). |

- `200`: Rotated; the new `token` and URLs are present only here. `CalendarFeedWithToken`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `GET /orgs/{org}/calendar-feeds/{token}/calendar.ics`

The iCalendar feed itself (public; the URL token is the capability, not a Bearer).

Authentication: `feedToken`, not a Bearer token.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `token` | path | string, matching `^cf_[0-9a-f]{64}$` | yes | The feed's secret capability token (`cf_...`). The URL itself is the credential — no Bearer auth (ADR-0024). Unknown, malformed, and revoked tokens all answer 404. |
| `If-None-Match` | header | string | no | A previously served ETag; an unchanged feed answers 304. |

- `200`: The iCalendar feed (RFC 5545/7986). string (`text/calendar`) (header `ETag`)
- `304`: Not modified — the feed body matches the `If-None-Match` validator.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### events

The org's live change stream — server-sent events naming what changed, never its contents (ADR-0031).

#### `GET /orgs/{org}/events`

The org's live change stream, as server-sent events (ADR-0031).

A long-lived `text/event-stream`, kept alive with a comment every 15 seconds. Events are
hints, not data: each names what changed by type and id, never field values, and the
client refetches through this API. Every write publishes once it commits, whichever
client made it — a person, an agent, or RALPH itself.

Each subscriber sees only what its reads would show it (G-125). Someone who can't see
a booking learns only that its resources' availability changed
(`availability.changed`); someone an edit takes a booking away from gets `revoked` for
it.

Event types, each with a JSON `data` line:

- `hello` (`StreamHello`): the stream is open. On a first load it carries the `id`
  to resume from; fetch after it arrives, so nothing that commits in between is missed.
- `change` (`ChangeEvent`): something changed.
- `revoked` (`RevokedEvent`): drop this entity; you can no longer see it.
- `notification` (`NotificationEvent`): a new notification for you, and only you; it is
  already in your inbox (`listNotifications`).
- `reset` (`StreamReset`): what was missed can't be replayed, or what you may read
  changed (a resource linked or unlinked); refetch everything for this org.
- `bye` (`StreamBye`): the server is ending the stream, and says why. A browser
  `EventSource` never sees the status of a refused reconnect, so read the reason:
  `access_revoked` and `roles_changed` mean drop everything held for this org before
  reconnecting; `credential_expired` means sign in or refresh the token first;
  `session_ended` means the app host's session ended, so drop everything held and
  sign in again;
  `server_shutdown` means reconnect.

Each event's SSE `id` is an opaque cursor for this org. To resume, reconnect with
`Last-Event-ID` (a browser `EventSource` sends it by itself) or `?after=`: events still
buffered are replayed, and anything else — a restart, a deploy, a cursor too old —
gets `reset`.

A person may hold at most 5 streams to an org at once, across all their credentials;
another is refused `429` (`too-many-streams`) until one closes. A stream lasts no longer
than the credential that opened it: it says `bye` once a change made through this API
takes its access away or changes its roles, when the credential expires, and when the
session that opened it ends.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `Last-Event-ID` | header | string | no | The id of the last event received, to resume after it. |
| `after` | query | string | no | The same cursor as `Last-Event-ID`, for readers that can't set the header. |

- `200`: The event stream. string, binary (`text/event-stream`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

### notifications

Your in-app inbox — what happened to bookings you're involved in (ADR-0031).

#### `GET /orgs/{org}/notifications`

Your notifications, unread first, then newest first (ADR-0031).

Only your own. Each is written with the change it reports, so none is lost. One whose
booking you can no longer see (your roles changed, or you were taken off it) is left out.
New ones also arrive on the live stream as `notification` events (`streamOrgEvents`).

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `cursor` | query | string | no | Opaque cursor from a previous page (ADR-0005 §4). |
| `limit` | query | integer, 1–200 (default `50`) | no |  |

- `200`: Page of notifications. `Page`, with `items` (array of `Notification`)
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/notifications`

Mark all your notifications read.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |

Request body (required, `application/merge-patch+json`): `NotificationUpdate`

- `204`: All marked read.
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)

#### `PATCH /orgs/{org}/notifications/{notificationId}`

Mark one of your notifications read or unread.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org` | path | string, matching `^[a-z0-9][a-z0-9-]{1,62}$` | yes | Organization slug (e.g. `hq-aviation`). Unknown or inaccessible orgs return 404. |
| `notificationId` | path | `Uuid` | yes |  |

Request body (required, `application/merge-patch+json`): `NotificationUpdate`

- `200`: Updated. `Notification`
- `default`: Any error — RFC 9457 (ADR-0005 §3). `Problem` (`application/problem+json`)
