Skip to content

REST API · v1

Microwaveprop 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 are deliberately excluded.

Base URL
https://prop.w5isp.com/api/v1
Versioning
Path-based (/api/v1). Breaking changes ship as /api/v2.
Auth
Opaque bearer tokens (Authorization: Bearer mwp_…). Mint one from your account settings or programmatically at POST /api/v1/auth/tokens.
Format
application/json requests and responses. Errors use RFC 9457 problem+json .
Spec
openapi.yaml (OpenAPI 3.1)

Quickstart

Mint a long-lived bearer token once, then use it for every subsequent call. Tokens are 32 random bytes, URL-base64 encoded with an mwp_ prefix so leak scanners can spot them.

The interactive way to mint a token is the account settings page — the value is shown once. Keep it like a password.

Mint a token POST /auth/tokens
curl -sS -X POST https://prop.w5isp.com/api/v1/auth/tokens \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"...","name":"laptop"}'
# => {"data": {...}, "token": "mwp_..."}

TOKEN=mwp_...
curl -sS -H "Authorization: Bearer $TOKEN" \
  https://prop.w5isp.com/api/v1/me

Authentication

Every endpoint that mutates state, or returns the authenticated user's own data, requires a bearer token. Public read endpoints accept an optional bearer that unlocks the caller's private rows.

Endpoint auth matrix

Endpoint Auth Notes
POST /auth/tokens password The only endpoint that accepts a password.
GET / 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 /weather none Public read of surface conditions.
GET /profiles/:call none Public per-callsign profile.

Token lifecycle

  • Tokens carry an expires_at (ISO 8601 UTC). Omit it and you get the default 90-day lifetime; a requested expiry more than 365 days out is rejected. Tokens also stop authenticating after 90 days of disuse.
  • 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.
Authorization header
Authorization: Bearer mwp_AbCdEf123…
Token format
# 32 random bytes, URL-base64
mwp_<43 chars>

# Server stores SHA-256(token) only.

Errors

All error responses follow RFC 9457 — a structured application/problem+json body with a stable title field and per-field errors on validation failures.

Status When
400 Missing or malformed query / 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.
409 Duplicate — an equivalent resource already exists.
422 Validation failed; errors carries per-field messages.
429 Rate limit exceeded; check RateLimit-* + Retry-After.
5xx Bug. Please open an issue.
422 validation failure
{
  "type": "about:blank",
  "title": "validation_failed",
  "status": 422,
  "detail": "One or more fields are invalid.",
  "errors": {
    "callsign": ["must be 3-10 letters and digits"]
  }
}

Rate limiting

Every response carries the RFC 9651 RateLimit-* headers so clients can self-throttle before hitting a 429.

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 seconds.

Defaults

Caller Limit
Anonymous (per IP) 60 req / minute
Authenticated (per token) 600 req / minute
POST /auth/tokens (per IP) 30 req / minute
Response headers
RateLimit-Limit: 600
RateLimit-Remaining: 597
RateLimit-Reset: 42

Endpoint reference

POST /auth/tokens

Issue an API token

Prefer the account settings page for interactive use; this endpoint is here for scripts that need to mint a token without leaving the terminal.

Request body

email string required
Account email address.
password string required
Account password.
name string required
Human-readable label so you can identify the token later.
expires_at datetime
ISO 8601 UTC. Optional — omitted, the token expires 90 days from now. Must be in the future and at most 365 days out.
Request POST /auth/tokens
curl -X POST https://prop.w5isp.com/api/v1/auth/tokens \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "your password",
    "name": "iPad",
    "expires_at": "2027-01-01T00:00:00Z"
  }'
Response · 201
{
  "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

The authenticated user

Returns the user behind the bearer token: callsign, name, email, home QTH, and admin flag.

PATCH /me accepts any subset of home_grid, home_lat, home_lon, home_elevation_m. Grid is auto-derived from lat/lon and vice versa.

Request GET /me
curl -H "Authorization: Bearer $TOKEN" \
  https://prop.w5isp.com/api/v1/me
Update home QTH PATCH /me
curl -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"home_grid":"EM12kx"}' \
  https://prop.w5isp.com/api/v1/me
GET /me/contacts

User-scoped collections

Two sibling endpoints return everything submitted under the authenticated user's account, newest first:

  • GET /me/contacts — every QSO submitted by the user.
  • GET /me/beacons — every beacon (approved or pending) submitted by the user.

Both honour standard page + per_page pagination.

List my contacts GET /me/contacts
curl -H "Authorization: Bearer $TOKEN" \
  "https://prop.w5isp.com/api/v1/me/contacts?per_page=20"
GET /me/api-tokens

Manage your API tokens

GET /me/api-tokens lists every non-revoked token belonging to the authenticated user. The plaintext value is never returned — only the metadata (id, name, timestamps).

DELETE /me/api-tokens/:id revokes a token. The row is soft-deleted (its revoked_at is set) and any in-flight request using it is invalidated on the next call.

List tokens GET /me/api-tokens
curl -H "Authorization: Bearer $TOKEN" \
  https://prop.w5isp.com/api/v1/me/api-tokens
Revoke a token DELETE /me/api-tokens/:id
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  https://prop.w5isp.com/api/v1/me/api-tokens/01HX...
GET /me/beacon-monitors

Beacon monitor stations

Distributed beacon monitor stations that report SNR samples back to Microwaveprop. Creating a monitor returns a token once — the credential the monitor program uses to identify itself when reporting. Only a hash is stored, so it cannot be read back.

Kinds

  • self_service — the Python client. You edit beacon_config.py on your own machine. This is the default.
  • managed — the propmonitor daemon. Its SDR configuration lives on this site and is synced both ways with the node, which also exposes its own LAN web UI.

Operations

  • GET /me/beacon-monitors — list.
  • POST /me/beacon-monitors — create. Accepts name, optional kind and gridsquare. Returns the new monitor's plaintext token (shown once).
  • DELETE /me/beacon-monitors/:id — revoke and remove.

The endpoints a monitor itself calls — measurement upload, config sync, and the managed-monitor sync WebSocket — authenticate with the monitor token rather than a user API token and are specified in the OpenAPI document.

Create a monitor POST /me/beacon-monitors
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"shack-pi","kind":"self_service","gridsquare":"EM12il"}' \
  https://prop.w5isp.com/api/v1/me/beacon-monitors
GET /contacts

Public contact (QSO) feed

Paginated list of QSOs.

Query parameters

page integer
1-based. Defaults to 1.
per_page integer
Defaults to 50; capped at 200.
search string
One or two callsigns; matches station1 or station2.

When called with a bearer token, the user's own private contacts are included in addition to the public set.

GET /contacts/:id returns a single QSO. Private QSOs return 404 to non-owners.

Search by callsign GET /contacts
curl "https://prop.w5isp.com/api/v1/contacts?search=W5XD&per_page=25"
POST /contacts

Submit a QSO

Create a new QSO under the authenticated user's account; their email is recorded as submitter_email.

Required fields

station1 string required
Submitter callsign.
station2 string required
Other operator's callsign.
qso_timestamp datetime required
ISO 8601 UTC.
band string required
MHz integer string ("10000", "24000").
grid1 string required
4- or 6-character Maidenhead.
grid2 string required
Other end's grid.
mode string required
Mode (e.g. CW, SSB).

Optional fields

user_declared_prop_mode string
e.g. tropo, aircraft_scatter.
height1_ft number
Antenna height above ground.
height2_ft number
Other end's antenna height.
private boolean
Hide from the public feed.
notes string
Free-form text.

A duplicate (same stations + same hour + same band) returns 409 Conflict with the existing record in the existing field.

Submit a QSO POST /contacts
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "station1": "W5XD",
    "station2": "K5XD",
    "qso_timestamp": "2026-05-08T12:34:00Z",
    "band": "10000",
    "grid1": "EM12",
    "grid2": "EM13",
    "mode": "CW"
  }' \
  https://prop.w5isp.com/api/v1/contacts
GET /beacons

Beacons

GET /beacons lists approved beacons; GET /beacons/:id reads a single one.

POST /beacons is unauthenticated — the new beacon is created in approved=false state until an admin approves it via the website.

Submit a beacon POST /beacons
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "callsign": "WB5LUA/B",
    "frequency_mhz": 10368.025,
    "grid": "EM13qe",
    "power_w": 5
  }' \
  https://prop.w5isp.com/api/v1/beacons
GET /scores

Propagation score at a grid point

Returns the composite propagation score plus the per-factor breakdown (rain, humidity, refractivity, …) for one grid point at one band.

Query parameters

band integer required
MHz.
lat number required
Decimal degrees.
lon number required
Decimal degrees.
valid_time datetime
ISO 8601 UTC. Defaults to the latest scored hour.

GET /scores/bands lists every band the engine scores for, with each band's humidity-effect classification (beneficial / harmful).

Score for Dallas, 10 GHz GET /scores
curl "https://prop.w5isp.com/api/v1/scores?band=10000&lat=32.78&lon=-96.80"
GET /forecast

48-hour score timeline

Returns the score at one grid point across the available forecast horizon (currently 48 hours). Same band + lat + lon parameters as /scores.

18 h timeline GET /forecast
curl "https://prop.w5isp.com/api/v1/forecast?band=10000&lat=32.78&lon=-96.80"
GET /weather

Surface weather at a point

Current surface conditions from the same HRRR grid that backs /map and /weather, snapped to the 0.125° cell containing the point.

Query parameters

lat number
Decimal degrees. Required unless grid is given.
lon number
Decimal degrees. Required unless grid is given.
grid string
Maidenhead square, 4–10 characters. An alternative to lat/lon; wins if both are supplied. Resolves to the centre of the square, so a longer grid picks a finer cell.
valid_time datetime
ISO 8601 UTC. Defaults to the forecast hour nearest now.

The response reports grid_lat/grid_lon — the cell the values were actually read from — which is why two nearby requests can return identical numbers. wind_direction_deg follows the meteorological convention (the direction the wind blows from) and is null for dead calm. Every value under conditions may be null.

Conditions for Dallas GET /weather
curl "https://prop.w5isp.com/api/v1/weather?lat=32.78&lon=-96.80"
curl "https://prop.w5isp.com/api/v1/weather?grid=EM12kx"
GET /profiles/:callsign

Public per-user profile

Public per-user profile: callsign, name, coarse 4-character home grid, all public contacts involving the callsign, plus all approved beacons submitted by the user. Email is not exposed, and neither are precise home coordinates — the owner reads those from /me.

Read a profile GET /profiles/W5XD
curl https://prop.w5isp.com/api/v1/profiles/W5XD

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 grids 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 422s; called out in the changelog.

See also

  • openapi.yaml — the machine-readable spec.
  • /.well-known/api-catalog — RFC 9727 service descriptor (public).
  • /algo — the scoring algorithm in detail.