# HiveRelay > A Nostr relay (NIP-42, NIP-53) that also fronts LiveKit: it owns the room > registry, decides who may open a room, and mints the LiveKit access tokens. > If you are building a client, everything you need is the JSON API below — > you never talk to LiveKit's server API directly, only to the LiveKit SFU with > the token this relay gives you. Machine-readable description: /openapi.yaml (OpenAPI 3.1) Rendered reference: /docs (light/dark toggle, internal routes hidden) Nostr relay information: / with `Accept: application/nostr+json` (NIP-11) ## The two things to understand first **Rooms are permanent or ephemeral.** A permanent room has a row in the registry: an owner pubkey, an immutable `room_id`, a public name that can be renamed, and a privacy flag. An ephemeral room has no row — it exists only while people are in it, has no owner and no moderation state, and dies after 60 minutes. Permanent rooms run up to 4 hours per session. **Ephemeral rooms are a demo surface of the HiveTalk dashboard and are not part of the public API.** A third-party client can only open permanent rooms, and only when the room's *owner* holds an active subscription. This is why `POST /api/get-token` answers `403 room_not_registered` for a name that has no registry row: there is nobody to charge for an ephemeral room. Entitlement is always the **room owner's**, never the caller's. Guests of a subscribed owner's room join from any client without a subscription. ## Authentication HiveRelay uses **two different signed-event mechanisms** depending on the endpoint. They are not interchangeable — using the wrong one fails. - **Action events** (header-based, with challenge) authenticate a *user* on the billing and registry endpoints: `/api/auth/login`, `/api/subscribe`, `/api/payment/status`, `/api/subscription`, `/api/register-room`, `/api/room/edit`. See "Action events (the challenge flow)" below. - **Body-based signed event** — used by `/api/get-token` **only**. The signed kind-27235 event is passed in the request body as `attributes.signed_event` (a JSON-stringified event string, not base64), and `pubkey` in the body must equal the event's pubkey. No `Authorization` header, no `X-Challenge`, no nonce. The server checks signature, `created_at` within ±5 min, and (only if `NIP98_STRICT=true`) the `u` and `method` tags. See "Joining a room" below. - **Header NIP-98** (`Authorization: Nostr `, no challenge) — used by the lobby endpoints (`/api/room/lobby/request`, `/api/room/lobby/status`) and `/api/room/poll/vote`. These do not require `X-Challenge`. - **Session cookie** — optional convenience. `GET /api/auth/challenge` then `POST /api/auth/login` with a signed nonce; the cookie then stands in for the action event so a user signs once rather than per request. - **Client attestation** (`X-Client-Attestation` + `X-User-Pubkey`) identifies the *calling application*, not the user. Only the HiveTalk dashboard holds a trusted key. You cannot produce one, and you do not need one: it exists to mark the dashboard's traffic, and its only extra privilege is ephemeral rooms. An invalid or replayed attestation is `401` — send none at all. - **LiveKit JWT** — the token from `/api/get-token`. Moderation endpoints authorise from its `owner`/`moderator` claims, so they need nothing else. ## Action events (the challenge flow) Subscribe, register-room, room/edit, login, payment-status and the subscription read all use a kind-27235 event in `Authorization` plus a challenge JWT in `X-Challenge`. The flow is: 1. `GET /api/auth/challenge` → `{challenge, nonce, expires_at, domain}`. 2. Build a kind-27235 event with tags: - `["payload", ]` (the empty string's hash for a bodyless GET) - `["action", "subscribe" | "create-room" | "edit-room" | "login" | "payment-status" | "subscription"]` - `["nonce", ]` - the standard NIP-98 `u` (full URL incl. query) and `method` tags 3. Sign it, base64 the JSON, send: ``` Authorization: Nostr X-Challenge: ``` The nonce is single-use and expires in 5 minutes; the event ID is replay-cached for 10. A fresh challenge is needed per call. ## Building a client, end to end 1. `GET /api/plans` → `{plans: [...], free_quota: 3, free_features: [...]}`. Public, database-free, authoritative dashboard card metadata. Each paid plan keeps `id`, `room_quota`, `days`, `price_sats`, adding `display_name` (`Standard` or `Pro Bulk`) and `features`. Billing still uses `standard_1y`, `standard_2y`, `bulk10_1y`, `bulk10_2y`; never submit display names. Both feature arrays use `{id, label, included, available?, value?, unit?}`. Render `label` verbatim: quantities/scope are already formatted. `value` and `unit` are for machine readers, not text to append. Examples: “3 permanent rooms on dashboard only” (free), “10 permanent rooms on any client” (paid), “24h recording retention”, “180-day recording retention”. Retention uses hours below 48h or when not divisible by 24 (49h stays “49h recording retention”); otherwise it uses days (48h becomes “2-day recording retention”). When excluded, its label is “No recording retention”; numeric values remain. Environment overrides update both labels and values. Stable IDs: `permanent_rooms` (value in `rooms`), `moderation`, `ephemeral_rooms` (dashboard demo rooms), `recording`, `recording_retention` (value in `hours`), `third_party_api`, `nip53_broadcasting`. Free includes three permanent dashboard rooms, moderation, dashboard ephemeral rooms, and NIP-53 announcements. Third-party room API access is a guaranteed paid benefit, not a restriction on Nostr publishing or public read APIs. Free `third_party_api` stays false regardless of client gate state. When `CLIENT_GATE_ENFORCED` is disabled (the default), third-party access is permitted without a subscription check; this bypass is not a guaranteed free benefit. The free room label's “dashboard only” describes guaranteed tier scope, not actual restrictions on a gate-disabled deployment. Feature rows must not be used as authorization results. No white-label feature is advertised. `free_features` describes the free card; there is no free subscription SKU. Recording and retention are always included for paid plans; for free they are included only if trimmed `RECORDING_REQUIRE_SUBSCRIPTION` parses false (unset/invalid requires a subscription). Both advertise `available` using `strconv.ParseBool(EGRESS_ENABLED)`; unset/invalid is false. Availability is independent of inclusion and is only the operator switch, not a health or authorization check. Preserve `included` and optionally suffix “unavailable” when `available` is false. Match by ID, not label or index. “Unlimited recordings” means no total count cap, not unlimited concurrent recordings. Gate responses carrying `plans` contain the same paid metadata. `/api/subscription.can_record` remains a legacy paid-entitlement indicator, not the EGRESS switch or free-recording override; use catalog feature rows for tier cards and recording endpoints for actual owner eligibility. 2. `GET /api/auth/challenge` → `{challenge, nonce}`. `POST /api/subscribe` `{plan}` with `Authorization: Nostr ` + `X-Challenge` → 402 `WWW-Authenticate: L402 macaroon="...", invoice="..."` and `{intent_id, bolt11, amount_sats, ...}`. Pay the invoice, then retry with the L402 macaroon and preimage (use `X-L402` alongside NIP-98) → 200 `{status: "settled", intent_id, subscription}`. At most one pending invoice per pubkey; asking again returns the same one. Purchases are one-time payments at the full selected plan price, without prorating. For fixed-term subscriptions, settlement adds 365 or 730 days to the later of the current expiry and settlement time. Early renewal preserves remaining time; expired subscriptions restart at settlement. Standard-to-Pro (`bulk10_*`) upgrades apply immediately at settlement, including the higher room quota and Pro retention for new recordings; remaining Standard time becomes Pro time. Pro one year followed by Pro two years adds 730 days rather than replacing the remaining term. Standard one year, Pro one year, then Pro two years bought in immediate succession yield approximately four years (1,460 days) from the first activation. Active Pro-to-Standard downgrades are rejected. Perpetual admin/comp grants remain open-ended. 3. `GET /api/payment/status?id=` — poll. Settlement is re-verified with the provider on read, and a background worker reconciles anything unattended within a couple of minutes. There is no payment callback endpoint. `DELETE /api/payment/status?id=` cancels a still-pending intent (re-verifies with the provider first so a landed payment is never discarded), marking it `failed` so the caller can pick a different plan. 4. `GET /api/auth/challenge` again, then `POST /api/register-room` `{roomName}` (optionally with your signed kind-30312 as `event`) with `Authorization: Nostr ` + `X-Challenge` → 201 `{room_id, room_name, ...}`. Names are URL-safe: letters and digits from any script plus `-` and `_`. Your plan's quota caps how many you hold; renaming with `POST /api/room/edit` does not consume quota because `room_id` is immutable. 5. `POST /api/get-token` — the LiveKit JWT. This endpoint uses a **different auth mechanism** from steps 2-4: the signed kind-27235 event goes in the request body, not the `Authorization` header. Send: ``` POST /api/get-token Content-Type: application/json { "roomName": "", "participantName": "", "pubkey": "", "attributes": { "signed_event": "" } } ``` The event has tags `["u", fullUrl]` and `["method", "POST"]`, `content: ""`, and is signed with the user's Nostr key. No `Authorization` header, no `X-Challenge`, no nonce. `pubkey` in the body must equal the event's pubkey. Response: `{token, url}` — connect your LiveKit client SDK to `url` with `token`. 6. Moderate with that token: `/api/room/stage/*`, `/api/room/moderator/*`, `/api/kick-user`, `/api/mute-user`, `/api/room/lobby/*`. 7. Polls (`/api/room/poll/*`) are free and never subscription-gated. ## Other endpoints - `GET /api/subscription` — the caller's current subscription and entitlement state. - `POST /api/auth/logout` — clear the session cookie. - `GET /api/room-info?room_name=` — resolve a name (including aliases) to the canonical room metadata. `404` distinguishes a permanent room from an ephemeral one. - `GET /api/rooms-by-pubkey?pubkey=` — list rooms owned by a pubkey (from the registry, not the 30312 events). - `GET /api/list-rooms` — live LiveKit rooms with metadata. - `GET /api/list-users?roomName=` — participants currently connected to a room. - `GET /api/events` — query stored Nostr events by kind, pubkey, roomName or identifier. Without `kind`, returns the room's metadata instead. - `POST /api/room/delete` (owner JWT) — cascade-delete a permanent room: registry row, Nostr events, recordings, lobby state, stage membership. - `POST /api/room/audience-mode` (owner JWT) — toggle audience mode (publish off for all non-stage participants). - `POST /api/room/mute-on-join` (owner/mod JWT) — toggle mute-on-join, persisted and broadcast as `lk.muteonjoin` metadata. - `POST /api/room/notify-lock` (owner/mod JWT) — toggle room locked state. - `POST /api/usage-total`, `POST /api/room-sessions`, `POST /api/usage-by-pubkey`, `POST /api/owner-room-sessions` — analytics. The per-pubkey and owner-session endpoints require a JWT whose pubkey matches the query. ## Errors worth handling Gate rejections carry a machine-readable body: `{error, reason, plans[], subscribe_api, subscribe_url?}` — enough to render a "subscribe for N sats" prompt inline. - `402 subscription_required` — the room's owner is on the free tier. Free rooms work in the dashboard; opening them elsewhere needs the owner to pay. - `402 subscription_expired` — lapsed, inside the grace period. Renewing restores access immediately. - `403 subscription_expired` — lapsed past grace; the registry row is expired. - `403 room_not_registered` — no registry row (an ephemeral name). - `403 ephemeral_rooms_are_dashboard_only` - `401 client_attestation_failed` — an attestation was sent and did not verify. - `503` — the registry or billing lookup failed. Access checks fail closed: the relay never grants on a lookup it could not perform. Retry. ## Recording `/api/room/recording/{start,stop,status,list,download,delete}`. Starting one requires the owner's entitlement to be active by default; setting `RECORDING_REQUIRE_SUBSCRIPTION=false` permits free owners to record. Egress must be enabled, S3/LiveKit configured, and capacity available: one in-flight recording per room, plus `EGRESS_MAX_CONCURRENT` relay-wide (default 2). A running recording may finish after entitlement lapses. Output is a single audio-only MP4. Retention for new recordings is per-plan: Standard 24h (`RECORDING_RETENTION_STANDARD_HOURS`), Pro Bulk 4320h / 180 days (`RECORDING_RETENTION_PRO_HOURS`). Free and admin/comp grants use `RECORDING_RETENTION_HOURS` (24h default), not a global override of paid plans. Catalog retention values fall back on unset/invalid/non-positive overrides; recording-start configuration separately rejects invalid global `RECORDING_RETENTION_HOURS`. `available` is not a configuration-health probe. Expiry is stamped at start, not rewritten when plans/configuration change. The relay's reaper marks terminal recordings past `expires_at` as `expired` and best-effort deletes the S3 object — no external bucket lifecycle rule is needed. ## Nostr side Rooms are announced as NIP-53 events: kind 30312 (interactive room), 30313 (scheduled). The relay also stores kinds 5, 30000, 30078 and 22242. The registry — not the Nostr event — is authoritative for ownership and privacy; the event is the public announcement of it. ## Source https://github.com/HiveTalk/hiverelay ## Browser clients CORS is a restrictive allowlist from `CORS_ALLOWED_ORIGINS` on payment/registry/session endpoints, not a wildcard — browser-based clients must proxy `/api/subscribe`, `/api/register-room`, `/api/room/edit`, `/api/get-token`, `/api/subscription` etc. through their own server. The relay's `Access-Control-Allow-Headers` includes `Content-Type, Authorization, X-Challenge, X-Client-Attestation`. Non-browser clients are unaffected.