192 lines
5.1 KiB
Markdown
192 lines
5.1 KiB
Markdown
# Portal API v2 — current schema
|
||
|
||
The kiosk talks to `https://portal.radiantwavetech.com/api/v2`. This document
|
||
describes the current contract after the move to an **active/inactive license
|
||
model**. There are no subscription levels: a license is either **valid**
|
||
(subscribed) or not, and the kiosk treats a successful `POST /license`
|
||
validation as the device's "subscribed" flag.
|
||
|
||
Base URL: `https://portal.radiantwavetech.com/api/v2`
|
||
|
||
Authentication: `Authorization: Bearer sk_<hex>` on every request (the API key
|
||
is baked into the kiosk binary at build time).
|
||
|
||
---
|
||
|
||
## `POST /license`
|
||
|
||
Validates a license key / email pair.
|
||
|
||
Request:
|
||
|
||
```
|
||
POST /api/v2/license
|
||
Content-Type: application/json
|
||
Authorization: Bearer sk_<hex>
|
||
```
|
||
|
||
```json
|
||
{
|
||
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
||
"email_address": "user@example.com"
|
||
}
|
||
```
|
||
|
||
Response (200):
|
||
|
||
```json
|
||
{
|
||
"key": "XXXX-XXXX-XXXX-XXXX",
|
||
"email_address": "user@example.com"
|
||
}
|
||
```
|
||
|
||
Contract (kiosk `parseLicenseJson`):
|
||
|
||
- `key`: string, **required non-empty** (empty key ⇒ parse failure).
|
||
- `email_address`: string.
|
||
- Any other fields (e.g. the legacy `subscription_level`) are ignored. A 2xx
|
||
response means the license is valid and the subscription is **active**; the
|
||
kiosk stores no subscription state other than the `license_last_verified`
|
||
stamp that drives its 7-day offline grace window.
|
||
|
||
Errors:
|
||
|
||
| Code | Body | Meaning |
|
||
|------|------|---------|
|
||
| 200 | success object | License valid and owned by the email |
|
||
| 400 | `{"message": "license_key is required"}` | Missing `license_key` |
|
||
| 400 | `{"message": "email_address is required"}` | Missing `email_address` |
|
||
| 400 | `{"message": "license key not found"}` | No key with that value |
|
||
| 400 | `{"message": "license key is not active"}` | Key exists but inactive |
|
||
| 400 | `{"message": "user does not exist"}` | No user with that email |
|
||
| 400 | `{"message": "key does not belong to specified user"}` | Key owned by a different user |
|
||
| 401 | `{"error": "..."}` | Missing/invalid API key |
|
||
|
||
---
|
||
|
||
## `GET /proclamations`
|
||
|
||
Lists every proclamation. There is **no `subscriptionLevelId`** — any validated
|
||
license may access any proclamation.
|
||
|
||
Response (200):
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "b40fdcb9-703c-420e-af32-a081964c6f08",
|
||
"name": "Peace & Relaxation",
|
||
"content": "<p>The text content, HTML formatted</p>",
|
||
"categoryId": 5,
|
||
"solfeggioFrequencyId": 5,
|
||
"colorPalette": ["#32CD32", "#3CB371", "#90EE90", "#98FB98", "#ADFFA9", "#C8FFD2", "#F0FFF0"]
|
||
}
|
||
]
|
||
```
|
||
|
||
Contract (kiosk `parseProclamationsJson` + `parseProclamationPalettesJson`):
|
||
|
||
- `id`: string UUID, **required non-empty** (empty ids are skipped).
|
||
- `name`: string, used as the display title. Kiosk strips HTML from it.
|
||
- `content`: string, **HTML** (the kiosk strips tags and unescapes entities
|
||
before rendering).
|
||
- `categoryId`: int, maps to a `/categories` id.
|
||
- `solfeggioFrequencyId`: int, **must be a canonical id (1–9)**.
|
||
- `colorPalette` (optional): array of exactly 7 `"#RRGGBB"` strings. When
|
||
absent, the kiosk falls back to `/color-palettes`, then to its own 528 Hz
|
||
seed.
|
||
- Legacy `subscriptionLevelId` fields in the payload are ignored.
|
||
|
||
Errors:
|
||
|
||
| Code | Body | Meaning |
|
||
|------|------|---------|
|
||
| 200 | array of proclamation records | Success |
|
||
| 400 | `{"message": "not-authorized"}` | Entitlement headers supplied but key or email does not exist |
|
||
| 401 | `{"error": "..."}` | Missing/invalid API key |
|
||
|
||
---
|
||
|
||
## `GET /categories`
|
||
|
||
Lists the proclamation categories.
|
||
|
||
Response (200):
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": 4,
|
||
"name": "Spiritual",
|
||
"parentId": 0,
|
||
"sortOrder": 0
|
||
}
|
||
]
|
||
```
|
||
|
||
Contract (kiosk `parseCategoriesJson`):
|
||
|
||
- `id`: int, **required non-zero** (zero ids are skipped).
|
||
- `name`: string.
|
||
- `parentId`: int, `null` maps to 0 (root).
|
||
- `sortOrder`: int.
|
||
|
||
---
|
||
|
||
## `POST /automations/create/account`
|
||
|
||
Creates (or reuses) a user account and issues a license key. **No
|
||
`subscription_level` is sent** — every created license is a standard active
|
||
license.
|
||
|
||
Request:
|
||
|
||
```
|
||
POST /api/v2/automations/create/account
|
||
Content-Type: application/json
|
||
Authorization: Bearer sk_<hex>
|
||
X-Timestamp: <unix seconds>
|
||
X-Nonce: <random uuid>
|
||
X-Signature: <hex HMAC-SHA256 of canonical request with the onboarding secret>
|
||
```
|
||
|
||
```json
|
||
{
|
||
"email": "user@example.com",
|
||
"name": "Optional Display Name"
|
||
}
|
||
```
|
||
|
||
- `email` (required, string).
|
||
- `name` (optional, string; defaults to the local part of `email`).
|
||
|
||
Response (200):
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Account and license key created",
|
||
"user_id": "uuid"
|
||
}
|
||
```
|
||
|
||
Errors:
|
||
|
||
| Code | Body | Meaning |
|
||
|------|------|---------|
|
||
| 200 | success object | Account (+ license key) created |
|
||
| 400 | `{"message": "email is required"}` | Missing/empty `email` |
|
||
| 401 | `{"error": "invalid signature"}` | Bad/expired/replayed HMAC signature |
|
||
| 429 | `{"message": "rate limit exceeded"}` | Too many requests from this IP/email |
|
||
| 500 | `{"message": "Failed to create account"}` | User creation failed |
|
||
|
||
---
|
||
|
||
## Readiness routes (not yet wired into the app)
|
||
|
||
- `GET /color-palettes` → `[{solfeggioFrequencyId, colors[7]}]`
|
||
- `GET /frequencies` → canonical id 1–9 + `value` in Hz
|
||
|
||
These exist for future sync steps; the kiosk does not call them yet.
|