# 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_` 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_ ``` ```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": "

The text content, HTML formatted

", "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_ X-Timestamp: X-Nonce: X-Signature: ``` ```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.