# 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_...`). Tokens are issued at `POST /api/v1/auth/tokens` and never leak the user's password 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`](./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 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`](./openapi.yaml) — the machine-readable spec. * `/.well-known/api-catalog` — RFC 9727 service descriptor (public). * `/algo` — the scoring algorithm in detail.