596 lines
19 KiB
YAML
596 lines
19 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. Overnight blocks (an end clock time before
|
|
the start) are supported and flagged with `overnight: true` and a
|
|
wrapping `durationMinutes`. Proclamation content is not included; the
|
|
kiosk fetches it separately via `/api/v2/proclamations`.
|
|
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'
|
|
'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/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
|
|
LicenseResponse:
|
|
type: object
|
|
required: [key, email_address]
|
|
properties:
|
|
key:
|
|
type: string
|
|
example: ABCD-EFGH-IJKL-MNOP
|
|
email_address:
|
|
type: string
|
|
format: email
|
|
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
|
|
blocks:
|
|
type: array
|
|
maxItems: 4
|
|
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
|