openapi: 3.1.0 info: title: Microwaveprop Public API version: "1.0.0" summary: REST API for QSO submission, beacon management, and propagation queries. description: | Public REST API for the NTMS microwave propagation prediction service. Implements every action a regular signed-in user can take on the website (submit contacts and beacons, manage beacon monitors, manage their own API tokens, query propagation scores), plus public read-only endpoints for contacts, beacons, profiles, and scores. Admin-only operations (user management, beacon approval, contact moderation) are intentionally **not** exposed here. See `docs/api/README.md` for prose, examples, and the stability promise. contact: name: NTMS url: https://w5hn.org/ servers: - url: https://prop.w5isp.com/api/v1 description: Production - url: http://localhost:4000/api/v1 description: Local development security: - bearerAuth: [] tags: - name: auth - name: me - name: contacts - name: beacons - name: monitors - name: scores - name: profiles paths: /auth/tokens: post: tags: [auth] summary: Issue a long-lived bearer token security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AuthRequest" } responses: "201": description: Token created content: application/json: schema: { $ref: "#/components/schemas/AuthResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/ValidationFailed" } "429": { $ref: "#/components/responses/RateLimited" } /me: get: tags: [me] summary: Current user profile responses: "200": description: Profile content: application/json: schema: { $ref: "#/components/schemas/MeResponse" } "401": { $ref: "#/components/responses/Unauthorized" } patch: tags: [me] summary: Update home QTH requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/HomeQthRequest" } responses: "200": description: Updated profile content: application/json: schema: { $ref: "#/components/schemas/MeResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/ValidationFailed" } /me/contacts: get: tags: [me] summary: My QSOs responses: "200": description: List content: application/json: schema: { $ref: "#/components/schemas/ContactList" } "401": { $ref: "#/components/responses/Unauthorized" } /me/beacons: get: tags: [me] summary: My beacons responses: "200": description: List content: application/json: schema: { $ref: "#/components/schemas/BeaconList" } "401": { $ref: "#/components/responses/Unauthorized" } /me/api-tokens: get: tags: [me] summary: List my API tokens responses: "200": description: List content: application/json: schema: { $ref: "#/components/schemas/TokenList" } "401": { $ref: "#/components/responses/Unauthorized" } /me/api-tokens/{id}: delete: tags: [me] summary: Revoke an API token parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "200": description: Revoked record content: application/json: schema: { $ref: "#/components/schemas/TokenResponse" } "404": { $ref: "#/components/responses/NotFound" } /me/beacon-monitors: get: tags: [monitors] summary: List my beacon monitors responses: "200": description: List content: application/json: schema: { $ref: "#/components/schemas/BeaconMonitorList" } post: tags: [monitors] summary: Create a beacon monitor requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/BeaconMonitorCreate" } responses: "201": description: Created content: application/json: schema: { $ref: "#/components/schemas/BeaconMonitorResponse" } "422": { $ref: "#/components/responses/ValidationFailed" } /me/beacon-monitors/{id}: delete: tags: [monitors] summary: Delete a beacon monitor parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "204": description: No content "404": { $ref: "#/components/responses/NotFound" } /contacts: get: tags: [contacts] summary: List public QSOs security: [] parameters: - in: query name: page schema: { type: integer, minimum: 1, default: 1 } - in: query name: per_page schema: { type: integer, minimum: 1, maximum: 200, default: 50 } - in: query name: search schema: { type: string } responses: "200": description: Paginated list content: application/json: schema: { $ref: "#/components/schemas/PaginatedContactList" } post: tags: [contacts] summary: Submit a QSO requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ContactCreate" } responses: "201": description: Created content: application/json: schema: { $ref: "#/components/schemas/ContactResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/ValidationFailed" } /contacts/{id}: get: tags: [contacts] summary: Show a single QSO security: [] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "200": description: QSO content: application/json: schema: { $ref: "#/components/schemas/ContactResponse" } "404": { $ref: "#/components/responses/NotFound" } /beacons: get: tags: [beacons] summary: List approved beacons security: [] responses: "200": description: List content: application/json: schema: { $ref: "#/components/schemas/BeaconList" } post: tags: [beacons] summary: Submit a beacon requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/BeaconCreate" } responses: "201": description: Created (pending approval) content: application/json: schema: { $ref: "#/components/schemas/BeaconResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/ValidationFailed" } /beacons/{id}: get: tags: [beacons] summary: Show a single beacon security: [] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "200": description: Beacon content: application/json: schema: { $ref: "#/components/schemas/BeaconResponse" } "404": { $ref: "#/components/responses/NotFound" } /profiles/{callsign}: get: tags: [profiles] summary: Public per-callsign profile security: [] parameters: - in: path name: callsign required: true schema: { type: string } responses: "200": description: Profile + contacts + approved beacons content: application/json: schema: { $ref: "#/components/schemas/ProfileResponse" } "404": { $ref: "#/components/responses/NotFound" } /scores/bands: get: tags: [scores] summary: Bands with propagation scores security: [] responses: "200": description: List of bands content: application/json: schema: type: object properties: data: type: array items: type: object properties: mhz: { type: integer } label: { type: string } humidity_effect: type: string enum: [beneficial, harmful, neutral] /scores: get: tags: [scores] summary: Score + factor breakdown at a grid point security: [] parameters: - in: query name: band required: true schema: { type: integer } - in: query name: lat required: true schema: { type: number, format: double } - in: query name: lon required: true schema: { type: number, format: double } - in: query name: valid_time schema: { type: string, format: date-time } responses: "200": description: Score content: application/json: schema: { $ref: "#/components/schemas/ScoreResponse" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /forecast: get: tags: [scores] summary: 18-hour score timeline at a grid point security: [] parameters: - in: query name: band required: true schema: { type: integer } - in: query name: lat required: true schema: { type: number, format: double } - in: query name: lon required: true schema: { type: number, format: double } responses: "200": description: Timeline content: application/json: schema: type: object properties: data: type: array items: type: object properties: valid_time: { type: string, format: date-time } score: { type: integer } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: opaque responses: BadRequest: description: Bad request content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Unauthorized: description: Authentication required content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } NotFound: description: Resource not found content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Conflict: description: Duplicate content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } ValidationFailed: description: Validation failed content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } RateLimited: description: Rate limit exceeded headers: RateLimit-Limit: { schema: { type: integer } } RateLimit-Remaining: { schema: { type: integer } } RateLimit-Reset: { schema: { type: integer } } Retry-After: { schema: { type: integer } } content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } schemas: Problem: type: object properties: type: { type: string, default: "about:blank" } title: { type: string } status: { type: integer } detail: { type: string } errors: type: object additionalProperties: type: array items: { type: string } AuthRequest: type: object required: [email, password, name] properties: email: { type: string, format: email } password: { type: string } name: { type: string, maxLength: 100 } expires_at: { type: string, format: date-time, nullable: true } AuthResponse: type: object properties: token: { type: string } data: { $ref: "#/components/schemas/Token" } Token: type: object properties: id: { type: string, format: uuid } name: { type: string } inserted_at: { type: string, format: date-time } last_used_at: { type: string, format: date-time, nullable: true } expires_at: { type: string, format: date-time, nullable: true } revoked_at: { type: string, format: date-time, nullable: true } TokenList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Token" } TokenResponse: type: object properties: data: { $ref: "#/components/schemas/Token" } Me: type: object properties: id: { type: string, format: uuid } callsign: { type: string } name: { type: string } email: { type: string, format: email } is_admin: { type: boolean } confirmed_at: { type: string, format: date-time, nullable: true } home_grid: { type: string, nullable: true } home_lat: { type: number, format: double, nullable: true } home_lon: { type: number, format: double, nullable: true } home_elevation_m: { type: integer, nullable: true } MeResponse: type: object properties: data: { $ref: "#/components/schemas/Me" } HomeQthRequest: type: object properties: home_grid: { type: string } home_lat: { type: number, format: double } home_lon: { type: number, format: double } home_elevation_m: { type: integer } Contact: type: object properties: id: { type: string, format: uuid } station1: { type: string } station2: { type: string } qso_timestamp: { type: string, format: date-time } grid1: { type: string } grid2: { type: string } pos1: type: object properties: lat: { type: number } lon: { type: number } pos2: type: object properties: lat: { type: number } lon: { type: number } mode: { type: string } band_mhz: { type: string } distance_km: { type: string, nullable: true } private: { type: boolean } user_declared_prop_mode: { type: string, nullable: true } propagation_mechanism: { type: string, nullable: true } propagation_mechanism_confidence: type: string nullable: true enum: [high, medium, low] notes: { type: string, nullable: true } "mine?": { type: boolean } ContactCreate: type: object required: [station1, station2, qso_timestamp, band, grid1, grid2] properties: station1: { type: string } station2: { type: string } qso_timestamp: { type: string, format: date-time } band: { type: string } grid1: { type: string } grid2: { type: string } mode: { type: string, enum: [CW, SSB, FM, FT8, FT4, Q65] } user_declared_prop_mode: { type: string } height1_ft: { type: integer } height2_ft: { type: integer } private: { type: boolean } notes: { type: string, maxLength: 2000 } ContactResponse: type: object properties: data: { $ref: "#/components/schemas/Contact" } ContactList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Contact" } PaginatedContactList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Contact" } meta: type: object properties: page: { type: integer } per_page: { type: integer } total_entries: { type: integer } total_pages: { type: integer } Beacon: type: object properties: id: { type: string, format: uuid } callsign: { type: string } frequency_mhz: { type: number } lat: { type: number } lon: { type: number } grid: { type: string } power_mw: { type: number } height_ft: { type: integer } on_the_air: { type: boolean } approved: { type: boolean } keying: { type: string } bearing: { type: string } beamwidth_deg: { type: number, nullable: true } notes: { type: string, nullable: true } inserted_at: { type: string, format: date-time } BeaconCreate: type: object required: [frequency_mhz, callsign, lat, lon, power_mw, height_ft, keying] properties: frequency_mhz: { type: number } callsign: { type: string } grid: { type: string } lat: { type: number } lon: { type: number } power_mw: { type: number } height_ft: { type: integer } on_the_air: { type: boolean } keying: { type: string } bearing: { type: string } beamwidth_deg: { type: number } notes: { type: string } BeaconResponse: type: object properties: data: { $ref: "#/components/schemas/Beacon" } BeaconList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Beacon" } BeaconMonitor: type: object properties: id: { type: string, format: uuid } name: { type: string } token: { type: string, nullable: true } last_seen_at: { type: string, format: date-time, nullable: true } inserted_at: { type: string, format: date-time } BeaconMonitorCreate: type: object required: [name] properties: name: { type: string, maxLength: 100 } BeaconMonitorResponse: type: object properties: data: { $ref: "#/components/schemas/BeaconMonitor" } BeaconMonitorList: type: object properties: data: type: array items: { $ref: "#/components/schemas/BeaconMonitor" } ProfileResponse: type: object properties: user: type: object properties: id: { type: string, format: uuid } callsign: { type: string } name: { type: string } home_grid: { type: string, nullable: true } home_lat: { type: number, nullable: true } home_lon: { type: number, nullable: true } contacts: type: array items: { $ref: "#/components/schemas/Contact" } beacons: type: array items: { $ref: "#/components/schemas/Beacon" } ScoreResponse: type: object properties: data: type: object properties: lat: { type: number } lon: { type: number } score: { type: integer } factors: type: object additionalProperties: true profile_source: oneOf: - { type: string, enum: [exact, unavailable] } - { type: array } valid_time: { type: string, format: date-time }