# Agent authentication on Omra

How an agent gets access to Omra's surfaces, and exactly which credentials exist.
Much of what an agent needs is open and keyless; the REST API is not. This file
follows the auth.md convention (WorkOS spec: https://workos.com/auth-md) and
documents what Omra implements and what it deliberately does not.

## Discover

- Keyless and public: the two trust registers, `GET https://api.clubomra.com/trust/subprocessors`
  and `GET https://api.clubomra.com/trust/ai-systems`, plus every published document on
  https://clubomra.com — `/llms.txt`, `/llms-full.txt`, `/openapi.json`, `/pricing.md`,
  the `.md` twin of any page, and `/.well-known/ard.json`.
- Keyed: the REST API at `https://api.clubomra.com/api/v1`. Its full
  surface is described by https://clubomra.com/openapi.json, which declares the `api-key`
  security scheme.
- There is no `/.well-known/oauth-protected-resource` (RFC 9728), no
  `/.well-known/oauth-authorization-server` (RFC 8414) and no `agent_auth`
  metadata block, because no Omra endpoint is OAuth-protected. Nothing here
  issues an OAuth access token.

## Pick a method

- **No credential** — the trust registers and every published document above.
  Start here; it answers what Omra is, what it costs, who processes club data,
  and what the API can do.
- **API key** — the only credential Omra issues. It is a per-club, scoped key
  sent in the `api-key` header, and it is required for every `/api/v1` route.

## Register

**Omra does not support agentic self-registration.** There is no `register_uri`,
no anonymous registration shape and no `verified_email` flow. An agent cannot
obtain a credential on its own.

Keys are created by club staff inside the Omra app, under **Settings →
Developer**, and are scoped to the club that issued them. A club has to exist and
be onboarded first — there is no self-serve signup for API access. An agent
acting for a club should ask that club's staff to mint a key, or write to
hello@clubomra.com.

## Claim

There is no claim ceremony and no `claim_uri`. A key is shown once at creation
and is ready to use immediately.

## Use the credential

Send the key in the `api-key` header:

```
curl https://api.clubomra.com/api/v1/members?club_id=YOUR_CLUB_ID \
  -H "api-key: YOUR_API_KEY"
```

The key is never accepted in a query string, and there is no
`Authorization: Bearer` form. A key grants only the scopes chosen when it was
created:

| Scope | Grants |
| --- | --- |
| `members:read` | Read member and household records |
| `applications:read` | Read membership applications |
| `applications:write` | Submit and update applications |
| `billing:read` | Read invoices, payments and line items |
| `orders:read` | Read point-of-sale orders |
| `feedback:read` | Read feedback tied to orders, locations and staff |
| `portal:sessions:create` | Mint a member-portal session |
| `tee_times:read` | Read the tee sheet |
| `scores:write` | Write regatta race scores |
| `wristbands:read` | Read regatta wristband checks |
| `wristbands:write` | Record wristband checks |
| `banquets:read` | Read event orders and their line items |
| `camps:read` | Read camps, sessions and registrations |

Scopes are additive and least-privilege: a key holding `members:read` alone can
read members and nothing else.

Two request headers change behaviour. `idempotency-key` on a mutation replays
the first response instead of applying the write twice, scoped per key and per
route — retry a failed mutation with the *same* value, which is what makes the
retry safe. List routes accept `cursor` and `limit` (1–100, default 25) and
return `has_more` alongside the collection; page by the last item's cursor
rather than assuming offset paging.

Successful responses use the envelope
`{ request_id, code, status, data }`. The API is versioned in the path, so a
client pinned to `/api/v1` keeps working; breaking changes ship as a new path
segment, while new fields and routes land in `v1` without notice. Parse
defensively and ignore unknown fields.

## Errors

| HTTP | code | Meaning |
| --- | --- | --- |
| 401 | `INVALID_API_KEY` | Missing, unknown, revoked or expired key |
| 403 | `API_KEY_SCOPE_DENIED` | Valid key, but it lacks the scope this route needs |
| 429 | `RATE_LIMITED` | Too many requests for this key |

Omra returns `INVALID_API_KEY` for every unusable key rather than distinguishing
"unknown" from "revoked" — that distinction only helps someone guessing keys.

No route emits a `WWW-Authenticate` challenge, because there is no authorization
server to point at. A 401 means the key is wrong, not that a token can be
negotiated.

## Revocation

Club staff revoke a key in **Settings → Developer**. Revocation takes effect
immediately, and the next call with that key returns
`401 INVALID_API_KEY`. There is no `revocation_uri` an agent can call: an
agent cannot revoke its own credential, and should ask club staff to.

Report a leaked key to hello@clubomra.com.
