# 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` — plan ids, sat prices, room quotas. 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. 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. 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; a recording already running is allowed to finish even if the entitlement lapses mid-session. Output is a single audio-only MP4. Retention is per-plan: Standard 24h, Pro 6 months (`RECORDING_RETENTION_HOURS` overrides). 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.