prop/docs/api/README.md
Graham McIntire ff92979b50
docs(api): point users at /users/settings for token creation
The settings UI is the preferred path for interactive token minting
now that it exists; POST /api/v1/auth/tokens stays for scripts/CLIs.
2026-05-09 10:15:23 -05:00

276 lines
9.8 KiB
Markdown

# Microwaveprop REST API
The Microwaveprop public REST API exposes the read-and-write surface a
**regular user** of the website has access to: contact (QSO) submission,
beacon submission, beacon-monitor management, propagation queries, and
profile management. Admin-only operations (user management, beacon
approval, contact moderation) are deliberately excluded from this API.
* **Base URL:** `https://prop.w5isp.com/api/v1`
* **Versioning:** path-based (`/api/v1`). Breaking changes will ship a
`/api/v2` rather than mutate `/api/v1`.
* **Auth:** opaque bearer tokens (`Authorization: Bearer mwp_...`).
The easiest way to mint a token is from your
[account settings page](/users/settings#api-tokens) — log in, scroll
to **API tokens**, name it, and copy the value (shown once). You can
also mint one programmatically at `POST /api/v1/auth/tokens`. Either
way, your password is never sent to API clients.
* **Format:** `application/json` for requests and successful responses;
errors use [RFC 9457 problem+json](https://www.rfc-editor.org/rfc/rfc9457).
* **OpenAPI 3.1 spec:** [`openapi.yaml`](/docs/api/openapi.yaml).
## Quickstart
```bash
# 1. Mint a long-lived bearer token (one-time, requires your password).
curl -sS -X POST https://prop.w5isp.com/api/v1/auth/tokens \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"...","name":"laptop"}'
# => {"data":{...},"token":"mwp_..."} <-- copy the token; it's shown once
# 2. Use the token for everything else.
TOKEN=mwp_...
curl -sS -H "Authorization: Bearer $TOKEN" https://prop.w5isp.com/api/v1/me
```
## Authentication
| Endpoint | Auth | Notes |
|-------------------------|----------|---------------------------------------------|
| `POST /auth/tokens` | password | The only endpoint that accepts a password. |
| `GET /me`, `PATCH /me` | bearer | The user behind the bearer token. |
| `POST /contacts` | bearer | Regular user QSO submission. |
| `GET /contacts` | optional | Public; bearer reveals viewer-private rows. |
| `GET /beacons` | none | Approved beacons only. |
| `POST /beacons` | bearer | New beacons start unapproved. |
| `GET /scores` | none | Public read of propagation scores. |
| `GET /profiles/:call` | none | Public per-callsign profile. |
### Token format
Tokens are 32 random bytes URL-base64-encoded, prefixed with `mwp_`. The
prefix lets static-analysis tools (`gitleaks`, `truffleHog`, etc.) match
leaked tokens. Only the SHA-256 of the token is stored server-side.
### Token lifecycle
* Tokens may carry an optional `expires_at` (ISO 8601 UTC). Without one
they live until revoked.
* `GET /me/api-tokens` lists every non-revoked token belonging to the
authenticated user (without plaintext).
* `DELETE /me/api-tokens/:id` revokes a token (soft delete — the row
remains for audit).
* Revoking the token used by the current request immediately invalidates
every subsequent request from that token.
## Errors
All error responses follow [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457):
```json
{
"type": "about:blank",
"title": "validation_failed",
"status": 422,
"detail": "One or more fields are invalid.",
"errors": {
"callsign": ["must be 3-10 letters and digits"]
}
}
```
| Status | When |
|--------|-------------------------------------------------------------------|
| 400 | Missing/malformed query or body parameter. |
| 401 | No / invalid / revoked / expired bearer token. |
| 403 | Authenticated, but the action is forbidden for the caller. |
| 404 | Resource not found, or hidden by privacy (private contact, etc.). |
| 409 | Duplicate — an equivalent resource already exists. |
| 422 | Validation failed; `errors` carries per-field messages. |
| 429 | Rate limit exceeded; check `RateLimit-*` headers + `Retry-After`. |
| 5xx | Bug. Please open an issue. |
## Rate limiting
Each response carries the [RFC 9651](https://datatracker.ietf.org/doc/rfc9651/)
`RateLimit-*` headers:
| Header | Meaning |
|-----------------------|----------------------------------------------------|
| `RateLimit-Limit` | Requests permitted in the current window. |
| `RateLimit-Remaining` | Requests remaining in the current window. |
| `RateLimit-Reset` | Seconds until the window resets. |
| `Retry-After` | Sent on 429s; retry no sooner than this many secs. |
Defaults:
| Caller | Limit |
|-----------------------------|----------------------------|
| Anonymous (per IP) | 60 req / minute |
| Authenticated (per token) | 600 req / minute |
| `POST /auth/tokens` (per IP)| 30 req / minute |
## Endpoint reference
### `POST /auth/tokens` — issue an API token
Prefer the [account settings page](/users/settings#api-tokens) for
interactive use; this endpoint is here for scripts and CLI tools that
need to mint a token without leaving the terminal.
Request:
```json
{
"email": "you@example.com",
"password": "your password",
"name": "iPad",
"expires_at": "2027-01-01T00:00:00Z"
}
```
`expires_at` is optional; omit it for a token that lives until revoked.
Response (201):
```json
{
"data": {
"id": "01HX...",
"name": "iPad",
"inserted_at": "2026-05-09T12:34:00Z",
"last_used_at": null,
"expires_at": "2027-01-01T00:00:00Z",
"revoked_at": null
},
"token": "mwp_AbCdEf..."
}
```
### `GET /me` — current user
Returns the authenticated user's profile (callsign, name, email, home
QTH, is_admin flag).
### `PATCH /me` — update home QTH
Accepts any subset of `home_grid` (Maidenhead), `home_lat`, `home_lon`,
`home_elevation_m`. The grid is auto-derived from lat/lon and vice
versa.
### `GET /me/contacts`
Every QSO submitted under the authenticated user's account, newest first.
### `GET /me/beacons`
Every beacon (approved or pending) submitted by the user.
### `GET /me/api-tokens`
List the user's non-revoked API tokens.
### `DELETE /me/api-tokens/:id`
Revoke a token. Returns the updated record (with `revoked_at` set).
### `GET /me/beacon-monitors`, `POST /me/beacon-monitors`, `DELETE /me/beacon-monitors/:id`
CRUD for the user's distributed beacon monitor stations. Each monitor
has a `token` field — the credential the monitor program uses to
identify itself when reporting.
### `GET /contacts`
Paginated public list of QSOs.
| Query | Default | Notes |
|-----------|---------|-----------------------------------------------------|
| `page` | `1` | 1-based. |
| `per_page`| `50` | Capped at 200. |
| `search` | — | One or two callsigns; matches station1 / station2. |
When called with a bearer token, the user's own private contacts are
included in addition to the public set.
### `GET /contacts/:id`
A single QSO. Private QSOs return 404 to non-owners.
### `POST /contacts`
Submit a new QSO. Required fields:
```json
{
"station1": "W5XD",
"station2": "K5XD",
"qso_timestamp": "2026-05-08T12:34:00Z",
"band": "10000",
"grid1": "EM12",
"grid2": "EM13",
"mode": "CW"
}
```
Optional: `user_declared_prop_mode`, `height1_ft`, `height2_ft`,
`private`, `notes`. The QSO is automatically attributed to the
authenticated user; their email is recorded as `submitter_email`.
A duplicate (same stations + same hour + same band) returns `409 Conflict`
with the existing record in the `existing` field.
### `GET /beacons`, `GET /beacons/:id`, `POST /beacons`
Approved beacons listing, single-beacon read, and unauthenticated submit
(the new beacon starts in `approved=false` state until an admin approves
it via the website).
### `GET /scores/bands`
Lists every band the propagation engine scores for, with their humidity-
effect classification.
### `GET /scores`
Returns the propagation score + factor breakdown at a grid point.
| Query | Notes |
|--------------|----------------------------------------------------|
| `band` | MHz integer. Required. |
| `lat`, `lon` | Decimal degrees. Required. |
| `valid_time` | ISO 8601 UTC. Optional; defaults to latest hour. |
### `GET /forecast`
The 18-hour score timeline at a grid point. Same `band` + `lat` + `lon`
parameters; returns an array of `{valid_time, score}` tuples.
### `GET /profiles/:callsign`
Public per-user profile (callsign + name + home QTH) plus all public
contacts involving the callsign and all approved beacons submitted by
the user. Email is **not** exposed.
## Conventions
* All timestamps are ISO 8601 UTC (`...Z`).
* All identifiers are UUIDv7 (binary_id) strings.
* Bands are integer MHz strings (`"10000"`, `"24000"`).
* Latitude / longitude are decimal degrees.
* Maidenhead grid squares are 4- or 6-character (`EM12`, `EM12kx`).
## Stability promise
* Adding new fields to existing responses is non-breaking.
* Removing or renaming fields will only happen in a new `/api/vN`.
* Adding new endpoints to `/api/v1` is non-breaking.
* Tightening validation may produce new 422 responses; these are not
considered breaking either, but will be called out in the changelog.
## See also
* [`openapi.yaml`](/docs/api/openapi.yaml) — the machine-readable spec.
* `/.well-known/api-catalog` — RFC 9727 service descriptor (public).
* `/algo` — the scoring algorithm in detail.