The settings UI is the preferred path for interactive token minting now that it exists; POST /api/v1/auth/tokens stays for scripts/CLIs.
276 lines
9.8 KiB
Markdown
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.
|