Files
portal/docs/openapi.yaml
T

803 lines
26 KiB
YAML

openapi: 3.0.3
info:
title: Radiant Wave Portal API (v2)
version: 2.0.0
description: |
Read API used by the Radiant Wave kiosk plus the automation onboarding
webhook.
## Authentication
The v2 API is **not** API-key gated. The former bearer-token middleware in
`src/hooks.server.ts` is present but disabled, so v2 routes are served
without a global API key.
Two endpoints carry their own credential semantics:
- `GET /api/v2/proclamations` optionally accepts the paired
`x-license-key` / `x-email` headers to scope results to an entitled user.
Supplying neither returns the full catalogue; supplying only one, or a
pair that does not match, is rejected.
- `POST /api/v2/automations/create/account` is protected by an HMAC-SHA256
request signature (see the operation description).
servers:
- url: https://portal.radiantwavetech.com
description: Production
- url: http://localhost:5173
description: Local development
tags:
- name: Catalogue
description: Reference data for categories, groups, frequencies and palettes.
- name: Proclamations
description: Proclamation content, optionally scoped by license entitlement.
- name: License
description: License key validation.
- name: Schedule
description: A user's daily listening schedule, consumed by the kiosk.
- name: Automations
description: Signed machine-to-machine webhooks.
paths:
/api/v2/categories:
get:
tags: [Catalogue]
summary: List proclamation categories
operationId: listCategories
responses:
'200':
description: All proclamation categories.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
/api/v2/proclamation-categories:
get:
tags: [Catalogue]
summary: List proclamation categories (dedicated alias)
description: Alias of `/api/v2/categories`; returns the identical payload.
operationId: listProclamationCategories
responses:
'200':
description: All proclamation categories.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
/api/v2/groups:
get:
tags: [Catalogue]
summary: List proclamation groups
description: Groups are ordered by `sortOrder` ascending.
operationId: listGroups
responses:
'200':
description: All proclamation groups.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
/api/v2/proclamation-groups:
get:
tags: [Catalogue]
summary: List proclamation groups (dedicated alias)
description: Alias of `/api/v2/groups`; returns the identical payload.
operationId: listProclamationGroups
responses:
'200':
description: All proclamation groups.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
/api/v2/frequencies:
get:
tags: [Catalogue]
summary: List canonical solfeggio frequencies
description: |
Returns the nine canonical frequencies (ids 1-9) the kiosk expects.
These are static values, not the portal database autoincrement ids.
operationId: listFrequencies
responses:
'200':
description: The nine canonical frequencies.
content:
application/json:
schema:
type: array
minItems: 9
maxItems: 9
items:
$ref: '#/components/schemas/Frequency'
/api/v2/color-palettes:
get:
tags: [Catalogue]
summary: List color palettes per canonical frequency
description: |
Returns one 7-color palette for each canonical frequency id (1-9).
Palette rows stored in the database take precedence; ids without a row
fall back to the built-in canonical palette.
operationId: listColorPalettes
responses:
'200':
description: Nine palettes, one per canonical frequency id.
content:
application/json:
schema:
type: array
minItems: 9
maxItems: 9
items:
$ref: '#/components/schemas/ColorPalette'
/api/v2/proclamations:
get:
tags: [Proclamations]
summary: List proclamations
description: |
Without entitlement headers, returns the full proclamation catalogue.
With both `x-license-key` and `x-email`, returns only the proclamations
assigned to the matching user. Both headers must be present together and
the key must belong to the user.
Proclamations whose frequency has no canonical id (for example 936 Hz)
are omitted from the response.
operationId: listProclamations
parameters:
- in: header
name: x-license-key
schema:
type: string
required: false
description: License key. Must be supplied together with `x-email`.
- in: header
name: x-email
schema:
type: string
format: email
required: false
description: Account email. Must be supplied together with `x-license-key`.
responses:
'200':
description: Proclamation records with canonical frequency ids and embedded palettes.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Proclamation'
'400':
description: |
Missing entitlement or the supplied key/email pair is invalid.
Message is `not-authorized`, or
`this key doesn't appear to belong to you` when the key does not
belong to the specified user.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/license:
post:
tags: [License]
summary: Validate a license key
description: |
Validates that the license key exists, is active, and belongs to the
account matching `email_address`. A successful validation records
`lastValidatedAt` on the key.
operationId: validateLicense
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseRequest'
responses:
'200':
description: The validated key and its owner email.
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseResponse'
'400':
description: |
Validation failure or malformed body. Possible messages:
`content-type must be application/json`, `invalid JSON body`,
`request body is required`, `license_key is required`,
`email_address is required`, `license key not found`,
`license key is not active`, `user does not exist`,
`key does not belong to specified user`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'413':
description: The request body exceeded the 64 KiB limit.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/schedule:
get:
tags: [Schedule]
summary: Get a user's listening schedule (query-param auth)
description: |
Machine-to-app endpoint used by the RadiantWave kiosk. Authenticates via
the paired `email` + `licenseKey` **query parameters** and validates only
that the key belongs to the account for that email. License
active/entitlement is deliberately not checked here: an invalid license
would already lock the kiosk out, so this endpoint only checks the
pairing.
Returns every Block with its start/end times and the ordered list of
selected Proclamation IDs (the legacy daily flattening in `blocks`), and
is extended additively with `version`, `type`, `daily`, and `weekly`.
Overnight blocks (an end clock time before the start) are supported and
flagged with `overnight: true` and a wrapping `durationMinutes`. Each
block also carries `dynamicAudio`; when true, the kiosk enables dynamic
audio for the block's duration, overriding any audio selections made on
the device. Proclamation content is not included; the kiosk fetches it
separately via `/api/v2/proclamations`. For `type: "weekly"`, `blocks` is
an empty array (the server cannot flatten weekly in the kiosk's
timezone). This endpoint emits no ETag and never returns 304.
operationId: getSchedule
parameters:
- in: query
name: email
required: true
schema:
type: string
format: email
description: Account email.
- in: query
name: licenseKey
required: true
schema:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
description: License key. The legacy alias `key` is also accepted.
responses:
'200':
description: The user's schedule (possibly empty).
content:
application/json:
schema:
$ref: '#/components/schemas/ScheduleResponse'
example:
email: jane@example.com
blocks:
- id: 3f1c2b7a-9d4e-4c8a-b2f6-1a2b3c4d5e6f
startTime: '21:00'
startMinutes: 1260
endTime: '09:00'
endMinutes: 540
overnight: true
durationMinutes: 720
dynamicAudio: true
proclamationIds:
- b40fdcb9-703c-420e-af32-a081964c6f08
- 7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b
'400':
description: |
Missing/invalid parameters, unknown key or user, or the key does not
belong to the email's account. Possible messages: `not-authorized`,
`this key doesn't appear to belong to you`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/licenses/{licenseKey}/schedule:
get:
tags: [Schedule]
summary: Get a License Key's schedule (Kiosk-facing, conditional)
description: |
Kiosk-facing, License-Key-only schedule read. Returns the target payload
(`version`, `type`, `daily`, `weekly`, `blocks`). `version` is a
content-derived opaque string and is also the `ETag`; a matching
`If-None-Match` returns `304`. An unknown License Key is `404`; a key
with no Schedule returns `type: null` with empty lists. No
authentication (see the API description) beyond the 10/min/IP limit.
operationId: getLicenseSchedule
parameters:
- in: path
name: licenseKey
required: true
schema:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
- in: header
name: If-None-Match
required: false
schema:
type: string
description: Prior ETag; when it matches, returns 304.
responses:
'200':
description: The target schedule payload.
headers:
ETag:
schema:
type: string
description: The quoted `version`.
content:
application/json:
schema:
$ref: '#/components/schemas/SchedulePayload'
'304':
description: Not modified; the caller keeps its cached payload.
headers:
ETag:
schema:
type: string
'404':
description: 'Unknown license (`unknown_license`).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/licenses/{licenseKey}/system-name:
put:
tags: [License]
summary: Set a License Key's System Name (Kiosk-only)
description: |
Sets the human-readable System Name for a License Key. The Kiosk is the
only writer; the Portal displays the name read-only. The name is unique
per account, compared case-insensitively, when non-null. Idempotent:
re-sending the current name returns 200 without a write.
operationId: setSystemName
parameters:
- in: path
name: licenseKey
required: true
schema:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [systemName]
properties:
systemName:
type: string
minLength: 1
maxLength: 40
responses:
'200':
description: The stored name.
content:
application/json:
schema:
type: object
required: [systemName, systemNameStatus]
properties:
systemName:
type: string
systemNameStatus:
type: string
enum: [ok]
'400':
description: 'Invalid name (`invalid_name`); empty/whitespace is invalid.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'404':
description: 'Unknown license (`unknown_license`).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'409':
description: 'Name already used on the account (`name_taken`).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/automations/create/account:
post:
tags: [Automations]
summary: Create an account and license key (signed webhook)
description: |
Machine-to-machine onboarding webhook. The request must be signed with
HMAC-SHA256 using `API_V2_ONBOARDING_SECRET`.
The canonical string that is signed is the newline-joined concatenation
of:
```
POST
<path>
<x-timestamp>
<x-nonce>
<raw request body>
```
where `<path>` is `/api/v2/automations/create/account`. The signature is
the lowercase hex digest.
On success, creates the user if they do not already exist, generates a
license key, emails it, and returns the user id. Invalid or missing
signatures return `401 { "error": "invalid signature" }` (the same
response is used for every signature failure, including replay and
timestamp skew).
operationId: createAutomationAccount
parameters:
- in: header
name: x-timestamp
required: true
schema:
type: string
description: Unix epoch seconds. Must be within 300 seconds of the server clock.
- in: header
name: x-nonce
required: true
schema:
type: string
description: Unique value per request. Replays within the skew window are rejected.
- in: header
name: x-signature
required: true
schema:
type: string
pattern: '^[0-9a-f]{64}$'
description: Lowercase hex HMAC-SHA256 of the canonical request string.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OnboardingRequest'
responses:
'200':
description: Account and license key created.
content:
application/json:
schema:
$ref: '#/components/schemas/OnboardingResponse'
'400':
description: |
Malformed or invalid request. Possible messages:
`request body is required`, `invalid JSON body`, or
`email is required`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'401':
description: Signature missing, malformed, expired, replayed, or incorrect.
content:
application/json:
schema:
type: object
required: [error]
properties:
error:
type: string
example: invalid signature
'413':
description: The request body exceeded the 64 KiB limit.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: |
Rate limit exceeded. Limits are 5 requests per minute per IP and
3 requests per 24 hours per email address.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'500':
description: The account could not be created.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
components:
schemas:
Category:
type: object
required: [id, name, sortOrder]
properties:
id:
type: integer
name:
type: string
sortOrder:
type: integer
Group:
type: object
required: [id, name, sortOrder]
properties:
id:
type: integer
name:
type: string
sortOrder:
type: integer
Frequency:
type: object
required: [id, name, value]
properties:
id:
type: integer
minimum: 1
maximum: 9
description: Canonical id used by the kiosk.
name:
type: string
value:
type: integer
description: Frequency in Hz.
ColorPalette:
type: object
required: [solfeggioFrequencyId, colors]
properties:
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
colors:
type: array
description: Seven hex colors, ordered light to dark.
minItems: 1
items:
type: string
pattern: '^#[0-9A-Fa-f]{6}$'
Proclamation:
type: object
required: [id, name, content, categoryId, groupId, solfeggioFrequencyId]
properties:
id:
type: string
format: uuid
name:
type: string
content:
type: string
description: HTML content.
categoryId:
type: integer
groupId:
type: integer
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
colorPalette:
type: array
description: Present only when a palette exists for the canonical frequency id.
minItems: 1
items:
type: string
pattern: '^#[0-9A-Fa-f]{6}$'
LicenseRequest:
type: object
required: [license_key, email_address]
properties:
license_key:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
example: ABCD-EFGH-IJKL-MNOP
email_address:
type: string
format: email
maxLength: 254
systemName:
type: string
minLength: 1
maxLength: 40
description: |
Optional. The Kiosk's System Name (trimmed). Omitted means the stored
name is left untouched; empty/whitespace is invalid, never a clear.
LicenseResponse:
type: object
required: [key, email_address, systemNameStatus]
properties:
key:
type: string
example: ABCD-EFGH-IJKL-MNOP
email_address:
type: string
format: email
systemNameStatus:
type: string
enum: [ok, unchanged, conflict, invalid]
description: |
Advisory outcome of the optional `systemName`. Validation itself
always succeeds. `unchanged` is returned when the field is omitted.
ScheduleBlock:
type: object
required:
[
id,
startTime,
startMinutes,
endTime,
endMinutes,
overnight,
durationMinutes,
dynamicAudio,
proclamationIds
]
properties:
id:
type: string
format: uuid
startTime:
type: string
example: '06:00'
description: Wall-clock start time as `HH:MM`.
startMinutes:
type: integer
minimum: 0
maximum: 1440
description: Minutes from midnight.
endTime:
type: string
example: '08:00'
description: Wall-clock end time as `HH:MM`.
endMinutes:
type: integer
minimum: 0
maximum: 1440
description: Minutes from midnight.
overnight:
type: boolean
description: True when the end clock time is at or before the start, i.e. the block crosses midnight.
durationMinutes:
type: integer
minimum: 1
description: Block length in minutes. An overnight block counts the wrap past midnight as the next day.
dynamicAudio:
type: boolean
description: |
When true, dynamic audio is enabled for the block's duration, overriding any
audio selections made on the device while the block is active.
proclamationIds:
type: array
description: IDs of the selected Proclamations, in play order.
items:
type: string
format: uuid
ScheduleResponse:
type: object
required: [email, blocks]
properties:
email:
type: string
format: email
version:
type: string
type:
type: string
nullable: true
enum: [daily, weekly, null]
daily:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
weekly:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
blocks:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
SchedulePayload:
type: object
required: [version, type, daily, weekly, blocks]
properties:
version:
type: string
description: Opaque content hash; doubles as the ETag.
type:
type: string
nullable: true
enum: [daily, weekly, null]
daily:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
weekly:
type: object
description: Present with all seven keys when `type` is weekly.
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
blocks:
type: array
description: Legacy daily flattening; empty for weekly.
items:
$ref: '#/components/schemas/ScheduleBlock'
OnboardingRequest:
type: object
required: [email]
properties:
email:
type: string
format: email
maxLength: 254
name:
type: string
maxLength: 120
description: Optional display name. Defaults to the email local-part.
OnboardingResponse:
type: object
required: [success, message, user_id]
properties:
success:
type: boolean
message:
type: string
example: Account and license key created
user_id:
type: string
ErrorMessage:
type: object
required: [message]
properties:
message:
type: string