# PAIC Public API Guide Build a website using this guide and the PAIC Public API OpenAPI specification. No repository, server access, or private API key is needed. The documentation is readable by anyone; using account data or processing requires an authorized account. There is no anonymous processing or unrestricted self-registration. All hostnames, usernames and identifiers in examples are placeholders. Obtain the actual HTTPS address and an account or invitation from the service operator. Never put real passwords, tokens, invitation links, private prompts, or media credential URLs in public documentation, source code, screenshots, or logs. ## 1. Address and API reference For an example installation at `https://paic.joshuahall.be`: | Resource | Address | | --- | --- | | Website API base | `https://paic.joshuahall.be/api` | | Public OpenAPI JSON | `https://paic.joshuahall.be/api/openapi.json` | | This guide | `https://paic.joshuahall.be/api/PAIC-PUBLIC-API-GUIDE.md` | | Browser reference | `https://paic.joshuahall.be/openapi` | | Schema shortcut | `https://paic.joshuahall.be/api/docs` | The OpenAPI `info.description` links here, and `externalDocs.url` supplies the same link for tools. Use the schema from the installation you connect to. Resolve its relative `servers` URL against that installation's origin. Paths below are relative to the API base: `/config` means `/api/config`. Returned URLs such as `whip_url`, `whep_url`, and `download_url` already include `/api`; do not add it a second time. Do not use private or administrative APIs. Anonymous `GET /health` returns: ```json {"status":"ok","service":"public-api"} ``` This checks the API process, not GPU readiness or media availability. Always verify HTTPS certificates; do not disable certificate validation. ## 2. Choose your website architecture | Website | Authentication and requests | | --- | --- | | Served on the PAIC origin | Browser calls relative `/api/...` URLs with its HttpOnly session cookie. | | Your own domain, including localhost | Browser calls your own backend. That backend calls PAIC with a separate PAIC session for each signed-in user. | | Unattended server integration | A scoped bearer token, held only on the server. | A standalone page on a different origin cannot rely on PAIC session cookies. PAIC uses `SameSite=Strict`, and credentialed cross-origin browser access is not supported. `credentials: "include"` does not solve this. Do not embed an operator's bearer token in a public frontend or share one PAIC login between visitors. ### Same-origin starter Use this code in a page served on the PAIC HTTPS origin. Bind calls to explicit form submissions/buttons, and display errors with `textContent`, never `innerHTML`. The browser stores the PAIC cookie; JavaScript must not read or copy it. ```js async function paic(path, { method = "GET", json, body } = {}) { const response = await fetch(`/api${path}`, { method, credentials: "same-origin", cache: "no-store", headers: json === undefined ? {} : { "Content-Type": "application/json" }, body: json === undefined ? body : JSON.stringify(json), }); if (!response.ok) { const error = new Error(`PAIC request failed (${response.status})`); error.status = response.status; throw error; } if (response.status === 204) return null; return response.json(); } // Call on the login form's submit event; clear its password field afterward. async function signIn(username, password) { await paic("/auth/login", { method: "POST", json: { username, password } }); return (await paic("/auth/me")).user; } async function loadEditor() { const [config, { presets }, { loras }] = await Promise.all([ paic("/config"), paic("/config/presets"), paic("/config/loras"), ]); return { config, presets, loras }; } async function savePrompt(prompt) { const result = await paic("/config", { method: "POST", json: { prompt } }); return result.config; // Use the server's accepted values in the editor. } async function uploadReference(file) { const body = new FormData(); body.append("file", file); return paic("/uploadImage", { method: "POST", body }); // Do not set Content-Type manually for FormData: fetch supplies the boundary. } async function signOut() { await paic("/auth/logout", { method: "POST" }); // Then clear the application's account data and close any media sessions. } ``` Use this JSON helper only for JSON endpoints. Images, MP4 files and SDP responses require `blob()`, streaming, or `text()` instead. On `401`, clear local account state and show login. On `403`, explain the permission restriction rather than repeatedly attempting login. ### Website on your own domain Your backend must implement this sequence: 1. Accept login only through your own HTTPS login form. Check the request's origin and apply your framework's CSRF protection and login throttling. 2. Send the submitted username/password to PAIC `POST /auth/login`. Capture its `Set-Cookie` with a server-side cookie jar; do not send that cookie to your browser. Cookie names can differ by installation: do not hard-code one. 3. Issue your own random, Secure, HttpOnly website session cookie. Map it to that user's PAIC cookie jar in an access-controlled server-side session store. Discard the submitted password after the login attempt. 4. For subsequent requests, check your own session and CSRF controls before sending that user's PAIC cookie to the configured PAIC origin. Add an `Origin` header equal to the PAIC origin on state-changing requests. 5. Forward only the public operations your website needs. Keep the PAIC origin fixed in server configuration; never accept an arbitrary upstream URL from a browser. Do not relay the browser's Cookie, Authorization, Origin, Host, or forwarded-client headers as trusted PAIC credentials. 6. Preserve relevant status codes and response types. Stream file downloads; limit request bodies and timeouts. Mark user responses `Cache-Control: no-store`. 7. On logout, close media sessions and revoke PAIC's session with `POST /auth/logout`, then expire your own session. If PAIC is temporarily unreachable, clear local access and arrange a bounded revocation retry. On upstream `401`, invalidate the matching local session. WebRTC needs additional backend work: proxy SDP `POST`, ICE `PATCH`, and session `DELETE`; preserve `Content-Type`, `ETag`, `Accept-Patch`, and ICE-server `Link` headers. Rewrite PAIC `Location` and returned public media/download URLs into your own authenticated routes. Validate every rewritten URL against the fixed PAIC origin and expected route prefix before forwarding. Never return PAIC's session cookie or let the browser choose the backend destination. All users behind this backend may share PAIC's source-IP rate budget. WebRTC media uses ICE connectivity negotiated with the browser; forwarding HTTP signaling alone does not relay the video. Test from the networks your users will use. If TURN is needed, obtain supported ICE configuration from the service operator and honor the signaling `Link` configuration. ## 3. Accounts, sessions and invitations `POST /auth/login` takes `{"username":"YOUR_USERNAME","password":"YOUR_PASSWORD"}` and returns `{"ok":true}` with a session cookie. This is not HTTP Basic auth. Login does not return a bearer token. The default cookie name is `paic_admin_session`, but applications should use the cookie the server issues. Sessions expire and can be revoked; do not assume a fixed lifetime. `GET /auth/me` requires a session and returns: ```json {"user":{"id":"USER_ID","username":"YOUR_USERNAME","role":"operator"}} ``` The schema may include optional user timestamps. Code should ignore additional response fields. `POST /auth/logout` returns `{"ok":true}` and clears/revokes the session. Cookie mutations require a matching `Origin`; same-origin browser fetch sets this automatically. Server-side clients must supply it explicitly. `POST /auth/password` requires a session and: ```json {"current_password":"CURRENT_PASSWORD","new_password":"NEW_PASSWORD"} ``` The new password must be 15–128 characters. Success returns `ok` and `user_id` and revokes browser sessions, including the current one. Log in again. Password change does **not** revoke independent API tokens or SRT credentials; revoke those separately when appropriate. If you have an invitation, redeem it without a session: ```text POST /auth/invitations/accept Content-Type: application/json {"token":"INVITATION_TOKEN","username":"CHOSEN_USERNAME","password":"NEW_PASSWORD"} ``` Usernames are 1–100 characters; new passwords are 15–128 characters. The invitation fixes the granted role. Success returns `{"ok":true}`; then log in. An invitation is a secret, not an open registration link for public distribution. Do not send it to analytics or retain it in URLs after redemption. ### Roles and bearer scopes | Role | Public capabilities | | --- | --- | | `viewer` | Read permitted configuration, presets, assets, bookings, recordings and processed streams. No configuration, preset or image writes, booking creation/cancellation, or publishing. Can manage own password, read-scoped tokens and permitted SRT read credentials. | | `operator` | Read plus change own configuration/assets, manage own presets/bookings, and publish own input. | | `admin`, `owner` | Operator capabilities; broader preset visibility/editing and booking-limit exemptions. Account administration is outside this public API. | A bearer token also needs the appropriate scope. Possessing a scope never overrides the account's current role or resource permissions. | Operation family | Read scope | Mutation scope | | --- | --- | --- | | Configuration and LoRA list | `config:read` | `config:write` | | Presets | `presets:read` | `presets:write` (including apply) | | Reference image | `assets:read` | `assets:write` | | Bookings and availability | `bookings:read` | `bookings:write` | | Recordings | `recordings:read` | None | | Processed preview / camera input | `stream:read` | `stream:publish` | Login is required for `/auth/me`, password changes and token management; a bearer token alone cannot use these session-only operations. On endpoints accepting either credential, a Bearer header takes precedence over a cookie. Do not combine both authentication methods. Create a token from a session with `POST /auth/tokens`: ```json {"name":"scheduled integration","scopes":["config:read"],"expires_in_days":30} ``` Names must contain 1–100 characters after trimming. At least one valid scope is required. Expiry is 1–365 days (default 90). The response is `{"token":{"id":"TOKEN_ID","token":"SECRET_SHOWN_ONCE", "...":"metadata"}}`; `token.token` is the secret. The ellipsis is explanatory, not a real field. Store the secret only on your server and send `Authorization: Bearer `. `GET /auth/tokens` returns `{"tokens":[...]}` containing `id`, `user_id`, `name`, `token_prefix`, `scopes`, `expires_at`, `revoked_at`, `last_used_at`, and `created_at`, without the secret. `DELETE /auth/tokens/{token_id}` revokes an owned token and returns `{"ok":true,"kicked":0}`; `kicked` is a count, not a constant. Expired/revoked tokens must be replaced, not retried indefinitely. ## 4. Configuration and LoRA selection `GET /config` returns the configuration object directly. `POST /config` accepts a partial object without a `config` or `value` wrapper and returns: ```json {"message":"User config saved.","config":{"prompt":"a watercolor portrait","seed":42}} ``` The example abbreviates the full returned configuration. Omitted recognized fields retain their values. Unknown keys, startup-only fields and null values are ignored. Read the response back instead of assuming every submitted value was accepted. Requests and preset values use hyphenated keys. Common controls: | Keys | Intended values / behavior | | --- | --- | | `processing-mode` | `gpu` (default) or `passthrough`; see **Choose GPU or passthrough processing** below. | | `prompt`, `negative-prompt` | Text descriptions. | | `seed`, `denoising-steps` | Integer controls; model support varies. | | `guidance`, `strength`, `alpha`, `step-size`, `temporal-blend-value`, `ipadapter-scale` | Numeric controls; preserve working values unless the model supports your change. | | `mode`, `easing`, `scheduler` | Model-dependent strings; preserve current values unless supported. | | `controlnet-scale` | Ordered numeric array; position meanings depend on the model. | | `mask-detection`, `latch-new-target`, `reset`, `klein-reference-image`, `klein-temporal-frame`, `temporal-feedback`, `spatial-cache` | Boolean controls. | | `klein-reference-image-size` | String `"256"` or `"512"`. | | `klein-temporal-frame-lookback-seconds` | `0.5`, `1`, `2`, `5`, or `10`. | | `lora-weights` | A permitted `path` from `/config/loras`, or `""` to disable. | | `lora-scale` | Number from 0 to 4. | | `spatial-cache-mode` | `off`, `image`, `output`, or `trt-crop`. | | `spatial-cache-threshold`, `spatial-cache-dilate`, `spatial-cache-reset-period`, `spatial-cache-crop-halo`, `spatial-cache-snap`, `spatial-cache-crop-grid`, `spatial-cache-max-crop-area`, `compositor-mask-feather` | Advanced numeric controls, dependent on the active model. | | `spatial-cache-crop-sizes` | Array of size strings, for example `["512x512"]`. | The service does not publish a complete per-model capability/range catalog. Do not present every field as supported by every model. A basic website can start with prompt editing and leave advanced values unchanged. GPU dimensions and model installation are not public configuration controls. ### Choose GPU or passthrough processing `processing-mode` belongs to the account, so each publishing user can select a mode without changing another user's setting. A session user must have a write-capable role (operator, admin, or owner); a bearer token also needs `config:write`. ```http POST /config Content-Type: application/json {"processing-mode":"passthrough"} ``` - `gpu` uses the normal GPU pipeline. When booking enforcement is enabled, the account needs an active booking before it receives a publishing URL. - `passthrough` uses the CPU relay, requires no booking, and can run while another account uses booked GPU processing. It decodes H.264, H.265/HEVC, or AV1 input, optionally adds deployment-configured text, and publishes H.264 output for the processed preview. The deployment must have passthrough enabled and enough passthrough capacity. After changing modes, stop any existing publishing session and fetch `GET /streams/routes` again. A change does not move a live session. Confirm the accepted value in the `POST /config` response, then use these route fields as the authoritative runtime state: | `active_stream` field | Client behavior | | --- | --- | | `processing_mode` | Expect `gpu` or `passthrough`; show the selected runtime mode. | | `passthrough_available` | If false, do not start a passthrough publisher; tell the user the deployment has not enabled it. | | `booking_required` | If true, obtain an active booking before publishing. If false, skip booking. | | `booking` | Current booking details, or null. Do not require this in passthrough mode. | The account page exposes the same setting for users who are not integrating the API directly. `GET /config/loras` returns `{"loras":[{"name":"DISPLAY_NAME","path":"OPAQUE_SELECTION"}]}`. Only permitted entries are listed. Show `name`; send `path` unchanged as `lora-weights`. The path is not a URL and does not grant file access. Do not construct paths, expose a server file picker, or assume one installation's values work elsewhere. An empty list means no available selection for this user. ## 5. Presets | Request | Body | Response | | --- | --- | --- | | `GET /config/presets` | None | `{"presets":[...]}` | | `POST /config/presets` | `{"name":"Warm light","value":{"prompt":"warm light"},"is_shared":false}` | `{"preset":{...}}` | | `PATCH /config/presets/{preset_id}` | Any of `name`, `value`, `is_shared` | `{"preset":{...}}` | | `DELETE /config/presets/{preset_id}` | None | `{"ok":true}` | | `POST /config/presets/{preset_id}/apply` | None | `{"message":"...","config":{...}}` | Preset fields are `id`, `owner_user_id`, nullable `owner_username`, `name`, `value`, `is_shared`, `created_at`, and `updated_at`. Ordinary users see their own presets and shared presets; admins/owners have broader visibility. LoRA permissions may hide otherwise visible presets. Only the owner or an admin/owner can edit/delete a preset, and write permissions still apply. Creating a preset does not apply it. Creation fills omitted configuration fields from defaults. **Sending `value` in a preset PATCH replaces that configuration with defaults plus the submitted fields; it does not merge with the old preset value.** Send the full desired value to preserve settings. Applying replaces your current configuration with the preset's configuration. A shared preset exposes its name, values and ownership metadata to other permitted users. Keep private content in private presets (`is_shared: false`). ## 6. Reference images `POST /uploadImage` takes multipart form field `file`. Send PNG, JPEG or WebP bytes with the appropriate image content type. SVG and other image formats are not supported. File bytes must be nonempty and no more than 10 MiB; the multipart request also has a body limit. Acceptance is not a guarantee that every image will decode or suit the processing model. Success is `{"status":"updated"}` or `{"status":"unchanged"}`. `GET /assets/image` returns the user's image bytes or `404` if absent. `DELETE /assets/image` returns `{"status":"deleted"}` or `{"status":"absent"}`. For a same-origin image element, use `/api/assets/image`; refresh it after a successful upload. Do not store authenticated images in a service-worker cache. ## 7. Book processing time `GET /bookings/availability` requires authentication, even though other users' slot ownership is anonymized. Optional query parameters `from` and `to` are RFC 3339 timestamps with a timezone. Omit them to obtain the default window. The response contains `timezone`, `slot_minutes`, `booking_window_days`, `daily_limit_minutes`, `booking_limit_exempt`, and `slots`. Each slot has `start_at`, `end_at`, and state `available`, `mine`, or `unavailable`. Use the returned settings; do not hard-code installation limits. ```js async function reserveFirstAvailableSlot() { const availability = await paic("/bookings/availability"); const slot = availability.slots.find((s) => s.state === "available"); if (!slot) throw new Error("No available slot"); return paic("/bookings", { method: "POST", json: { start_at: slot.start_at, end_at: slot.end_at }, }); } ``` Run booking creation only after the user confirms the displayed slot. Dates must follow the returned slot boundaries and booking window. The current slot can be joined before it ends. A slot may be taken between reading and creating: on `409`, refresh availability and let the user choose again. `POST /bookings` returns `{"booking":{...}}`. `GET /bookings` returns `{"bookings":[...]}` for the user's current listing window. A booking contains `id`, `user_id`, `start_at`, `end_at`, `status`, and `can_cancel`. Status is `upcoming`, `active`, `completed`, or `cancelled`. `DELETE /bookings/{booking_id}` cancels an upcoming owned booking or stops an active one; success is `{"ok":true}`. Stopping an active booking releases its remaining time and triggers media disconnection. Completed bookings cannot be cancelled. An active booking may be required before publishing. ## 8. Browser camera and processed preview `GET /streams/routes` returns `tenant_id`, `capture_defaults`, `active_stream`, and the permitted subset of `input_path`, `output_path`, `whip_url`, `whep_url`, `webrtc_proxy_url`. Treat optional URLs as absent unless they are usable strings. Do not derive another user's paths or use stream URLs as authorization. A read-only user receives processed-preview information, not input publishing. `capture_defaults` provides `width`, `height`, `frame_rate`, `video_bitrate`, and `audio`. `active_stream` provides `active`, `owned_by_current_user`, `processing_mode`, `passthrough_available`, `booking_required`, and `booking` (which can be null). When bookings are enforced for GPU mode, `whip_url` can be null until a booking is active. Passthrough does not require a booking, but clients must check `passthrough_available`. Refresh routes after changing mode or when a booking starts; do not manufacture a publishing URL to bypass that state. For a minimal editor: 1. Log in, load configuration, then fetch routes. Obtain a booking only when `active_stream.booking_required` is true. 2. After an explicit **Start camera** click, request `getUserMedia()` permission over HTTPS and create a publishing peer connection. 3. Prefer an H.264 video transceiver for broad compatibility. Passthrough can also decode negotiated H.265/HEVC or AV1 input; GPU codec support depends on the active pipeline. Use `capture_defaults` when available. 4. POST the SDP offer to `whip_url` with `Content-Type: application/sdp` and the user's authentication. Success is normally `201` with an SDP answer. 5. Resolve the response's `Location` against its response URL, retain the `ETag`, and set the answer as the peer connection's remote description. 6. Create a second peer connection with a `recvonly` video transceiver; repeat offer/answer negotiation against `whep_url`. Attach its incoming tracks to your preview element. Handle browser autoplay restrictions with a Play button. 7. Either wait for complete ICE gathering before posting the offer, or implement trickle ICE with SDP fragments. For trickle, PATCH the session `Location` with `application/trickle-ice-sdpfrag` and `If-Match` as required by the session. Raw `RTCIceCandidate` JSON is not the PATCH format. Honor advertised ICE servers. 8. On Stop, device change, logout, or failure, DELETE each created session, close both peer connections and stop every local camera track. Cleanup on page unload is best effort; also stop explicitly while the page is active. H.264 selection before the WHIP offer: ```js const transceiver = pc.addTransceiver(cameraStream.getVideoTracks()[0], { direction: "sendonly", streams: [cameraStream], }); const codecs = RTCRtpSender.getCapabilities("video")?.codecs ?? []; const h264 = codecs.filter((codec) => codec.mimeType.toLowerCase() === "video/h264"); if (!transceiver.setCodecPreferences || h264.length === 0) { throw new Error("This browser cannot publish H.264 to PAIC"); } transceiver.setCodecPreferences(h264); ``` For non-trickle negotiation, use a bounded wait for `iceGatheringState === "complete"`, then send `pc.localDescription.sdp` (not the original offer, which may lack gathered candidates). Negotiate media directions before creating the offer. Read the response with `text()`, not `json()`. Session URL handling for a same-origin client: ```js const location = response.headers.get("Location"); if (!location) throw new Error("Missing media session URL"); const sessionUrl = new URL(location, response.url); if (sessionUrl.origin !== window.location.origin || !sessionUrl.pathname.startsWith("/api/webrtc/")) { throw new Error("Unexpected media session URL"); } ``` The service controls publishing admission and preview-reader limits. Handle `409` for conflicting admission and `429` for a rate/reader limit. Retry a WHEP `404` only when you are waiting for your processed stream to become available, with bounded backoff and a visible Stop action. Never loop indefinitely or retry session-creating POSTs blindly: a timed-out request may have succeeded. ## 9. OBS / FFmpeg and recordings To obtain an SRT credential, POST `/streams/srt-credentials`: ```json {"name":"My encoder","action":"publish"} ``` Use `action: "read"` for the processed output. Publishing requires an appropriate role and `stream:publish` for a bearer client; reading requires `stream:read`. The response is `{"credential":{...}}` with `id`, `name`, `action`, `created_at`, `expires_at`, `revoked_at`, and one-time `url`. Credentials expire within 30 days. An active booking may still be needed before the encoder can publish in GPU mode. Passthrough mode skips the booking, but the deployment must report `active_stream.passthrough_available: true` from `/streams/routes`. Treat the complete `url` as a password. Never embed it in a public site or log it. `GET /streams/srt-credentials` returns `{"credentials":[...]}` without URLs, filtered by the caller's stream scopes. `DELETE /streams/srt-credentials/{credential_id}` revokes an owned, permitted credential and returns `ok` and a `kicked` connection count. Revocation prevents new authentication and requests active disconnection; do not interpret a zero count as proof that an external player has stopped. For OBS, select a Custom streaming service, put the issued publish URL in Server, and leave the separate stream-key field empty. Configure H.264 video. FFmpeg/FFplay accept the issued URL, but command-line arguments can expose it to local process inspection; use a trusted machine and keep logs private. SRT reachability depends on the installation; a credential is not a connectivity guarantee. WebRTC and SRT are distinct transports. `GET /recordings` returns `{"recordings":[...]}`: up to the latest three completed recordings within the service's retention window. The default retention is seven days; installations may differ. Each entry has `id`, `started_at`, `ended_at`, `duration_seconds`, `size_bytes`, `status`, and `download_url`. Download an owned recording with `GET /recordings/{recording_id}/download` or its returned URL. The response is MP4 bytes with a suggested filename, not JSON. Missing, expired, or inaccessible recordings return `404`. A website backend must authenticate and stream downloads per user. There is no public endpoint to browse all users' recordings or choose an arbitrary server file. ## 10. Complete route checklist All paths use the base from section 1. Authentication follows sections 2–3. | Methods | Path | | --- | --- | | GET | `/health` | | POST | `/auth/login`, `/auth/invitations/accept` (no existing session) | | GET | `/auth/me` | | POST | `/auth/logout`, `/auth/password` | | GET, POST | `/auth/tokens` | | DELETE | `/auth/tokens/{token_id}` | | GET, POST | `/config` | | GET | `/config/loras` | | GET, POST | `/config/presets` | | PATCH, DELETE | `/config/presets/{preset_id}` | | POST | `/config/presets/{preset_id}/apply` | | GET, DELETE | `/assets/image` | | POST | `/uploadImage` | | GET | `/bookings/availability` | | GET, POST | `/bookings` | | DELETE | `/bookings/{booking_id}` | | GET | `/streams/routes` | | GET, POST | `/streams/srt-credentials` | | DELETE | `/streams/srt-credentials/{credential_id}` | | OPTIONS, POST, PATCH, DELETE | `/webrtc/{proxied_path}` (use discovered protocol/session URLs) | | GET | `/recordings`, `/recordings/{recording_id}/download` | The documentation endpoints in section 1 are also anonymous. There is no public account-list, role-management, arbitrary proxy, diagnostic, or worker-control API. ## 11. Errors, limits and release checks Normal API errors usually have `{"detail":"message"}`. Validation errors can have an array in `detail`; signaling errors may be plain text. Check status and content type before parsing, and render any response text as untrusted text. | Status | Client action | | --- | --- | | 400 | Correct an invalid value, body, date or interval. | | 401 | Credentials are missing, invalid, expired or revoked; request login again. | | 403 | Check role/scope, resource permissions, or cookie Origin. | | 404 | Resource is absent, inaccessible or expired; see the limited WHEP retry case above. | | 409 | Refresh conflicting state before offering another attempt. | | 413 | Reduce the request/file size. | | 415 | Use the required signaling content type. | | 422 | Correct request fields/types. | | 429 | Honor `Retry-After` when present; otherwise use bounded exponential backoff with jitter. | | 502, 503 | Show temporary unavailability. Retry reads with bounded backoff; reconcile state before repeating mutations. | Default request limits are 16 KiB for login, 256 KiB for ordinary JSON, 1 MiB for signaling and 12 MiB for multipart requests, with a 10 MiB file-byte limit. Deployments may lower limits. Rate limits also apply to login, invitation redemption, password changes, and media session operations. Before releasing a replacement website, verify login/logout and session expiry, viewer read-only behavior, a missing scope, cross-account resource denial, reference upload/removal, preset replacement semantics, booking conflicts, media Start/Stop cleanup, and owned recording download. Check that your bundle, logs, analytics and caches contain no credentials or private user content. Test media from an external network as well as your development machine. Public documentation is designed to be shareable; it is not a certification of a particular deployment's network configuration or operational security.