Skip to content

This document was written by AI and has been manually reviewed.

API Reference

Base path: /api

All endpoints return JSON. Authenticated endpoints accept either a session JWT (Authorization: Bearer <token>) issued at login, an OAuth access token from the standard authorization code flow, or a personal access token prefixed prism_pat_. Endpoints that take an OAuth token are usually exposed under /api/oauth/me/*.

CORS is locked to APP_URL for /api/*. The /api/proxy/image/*, /.well-known/*, and /api/users/:username (public profile) endpoints are served without Access-Control-Allow-Credentials so they're safely embeddable.

Init

GET /api/init/status

Returns whether the instance has been set up.

Response{ "initialized": false }

POST /api/init

Creates the first admin account. Only works when initialized = false.

json
{
  "email": "admin@example.com",
  "username": "admin",
  "password": "s3cur3",
  "display_name": "Admin",
  "site_name": "My Prism"
}

Response{ "token": "...", "user": { ... } }

Site

GET /api/site

Public site configuration for the frontend. No authentication required. The endpoint reads only fields safe to expose; secrets are never included.

json
{
  "site_name": "Prism",
  "site_description": "...",
  "site_icon_url": null,
  "allow_registration": true,
  "invite_only": false,
  "captcha_provider": "none",
  "captcha_site_key": "",
  "pow_difficulty": 20,
  "accent_color": "#0078d4",
  "custom_css": "",
  "initialized": true,
  "require_email_verification": false,
  "email_verify_methods": "both",
  "enable_public_profiles": true,
  "disable_user_create_team": false,
  "disable_user_create_app": false,
  "enable_sub_teams": true,
  "max_team_depth": 5,
  "inherit_team_membership": true,
  "inherit_team_domains": true,
  "default_team_profile_show_sub_teams": true,
  "enabled_sources": [
    { "slug": "github", "provider": "github", "name": "GitHub" },
    { "slug": "google", "provider": "google", "name": "Google" }
  ]
}

Auth

POST /api/auth/register

json
{
  "email": "user@example.com",
  "username": "alice",
  "password": "hunter2",
  "display_name": "Alice",
  "captcha_token": "...",
  "pow_challenge": "...",
  "pow_nonce": 12345,
  "invite_token": "..."
}

Include whichever bot-protection fields match the active captcha provider. invite_token is required when the site is in invite-only mode.

Response{ "token": "...", "user": { ... } }

POST /api/auth/login

json
{
  "identifier": "alice",
  "password": "hunter2",
  "totp_code": "123456",
  "captcha_token": "..."
}

identifier accepts username, primary email, or any verified secondary email (when allow_alt_email_login is true). totp_code is required only if TOTP is enrolled — for passkey authenticators, use the dedicated passkey endpoints.

Response{ "token": "...", "user": { ... } }

If TOTP is enrolled but no code was provided:

json
{ "totp_required": true, "available_methods": ["totp", "passkey", "backup"] }

POST /api/auth/logout

Revokes the current session. Requires auth.

GET /api/auth/verify-email?token=<token>

Verifies an email using the token sent by email.

POST /api/auth/email-verify-code

Returns a verification address the user can send an email to. Format: verify-<code>@<domain> (Cloudflare Email Workers) or the configured IMAP mailbox with the code as the subject. Requires auth.

json
{ "address": "verify-abc123@example.com", "code": "abc123" }

POST /api/auth/check-email-verification

Long-poll-friendly: returns { "verified": boolean } for the user's primary email. Useful while the user is sending the verify-by-email message.

POST /api/auth/resend-verify-email

Re-sends the verification link. Requires auth. Accepts optional captcha fields.

GET /api/auth/pow-challenge

Returns a PoW challenge for the proof-of-work provider.

json
{ "challenge": "...", "difficulty": 20, "expires_at": 1741568400 }

TOTP (multiple authenticators)

All endpoints require authentication.

GET /api/auth/totp/list

Lists the user's enrolled TOTP authenticators.

POST /api/auth/totp/setup

Generates a new TOTP secret. Returns the secret and otpauth:// URI for QR codes. Pass name to label the new authenticator (e.g. "Pixel 9").

json
{ "name": "Pixel 9", "secret": "...", "uri": "otpauth://totp/..." }

POST /api/auth/totp/verify

Confirms TOTP setup by verifying the first code. Returns backup codes the first time any authenticator is enrolled.

DELETE /api/auth/totp/:id

Removes a single authenticator by ID. Requires either a current TOTP code, a backup code, or a passkey verification — the dialog in Profile → Security walks the user through whichever the account has enrolled.

POST /api/auth/totp/backup-codes

Regenerates backup codes. Requires a valid TOTP code.

Passkeys (WebAuthn)

POST /api/auth/passkey/register/begin / /finish

Adds a passkey for the authenticated user.

POST /api/auth/passkey/auth/begin / /finish

Sign-in with a passkey. Pass username to begin to scope the allowed credentials, or omit it for discoverable credentials.

POST /api/auth/passkey/verify/begin / /finish

Authenticated re-verification with a passkey — used by step-up confirmation flows (e.g. removing the last TOTP authenticator).

GET /api/auth/passkeys

Lists the authenticated user's registered passkeys.

DELETE /api/auth/passkeys/:id

Removes a passkey.

GPG keys

POST /api/auth/gpg-challenge

Request a sign-in challenge. Rate-limited to 30 req/min per IP.

json
{ "identifier": "alice" }

Response{ "challenge": "...", "text": "Prism login\n..." }

The gpg_challenge_prefix config is inserted between the site header and the random challenge so users can verify the text they're signing belongs to your site.

POST /api/auth/gpg-login

Submit a gpg --clearsign-ed challenge. Rate-limited to 10 req/min per IP. The challenge is single-use and expires after 5 minutes.

json
{ "identifier": "alice", "signed_message": "-----BEGIN PGP SIGNED MESSAGE-----\n..." }

Response{ "token": "...", "user": { ... } }

GET /api/user/gpg / POST /api/user/gpg / DELETE /api/user/gpg/:id

Session-auth GPG key management. POST accepts ASCII-armored or binary public_key plus optional name; classical RSA/EdDSA and ML-DSA keys are both supported.

GET /users/:username.gpg

Public, federated lookup. Returns the user's registered GPG keys as ASCII armor blocks separated by blank lines, with Content-Type: application/pgp-keys.

OAuth-scoped GPG endpoints

MethodPathScope required
GET/api/oauth/me/gpg-keysgpg:read
POST/api/oauth/me/gpg-keysgpg:write
DELETE/api/oauth/me/gpg-keys/:idgpg:write

Request/response shapes match the session-auth equivalents.

Sessions

GET /api/auth/sessions / DELETE /api/auth/sessions/:id

List and revoke active sessions for the authenticated user.

User

All endpoints require authentication.

GET /api/user/me / PATCH /api/user/me

Read and partial-update the current user (display name, avatar, profile visibility flags, notification preferences). Some sub-resources have dedicated endpoints below.

POST /api/user/me/change-password

json
{ "current_password": "...", "new_password": "..." }

POST /api/user/me/avatar

multipart/form-data with field avatar. Max 2 MB. Accepted types: JPEG, PNG, WebP, GIF. Stored in R2 (when bound) or inline in D1.

POST /api/user/me/readme / POST /api/user/me/readme/sync

Manually save a markdown README, or sync it from the user's GitHub user-repo README (github.com/<login>/<login>). The sync endpoint respects the github_readme_cache_ttl_seconds cache and github_readme_token PAT.

GET /api/user/me/emails / POST / DELETE /api/user/me/emails/:id

Manage secondary emails. POST /:id/resend re-sends the verification link; POST /:id/set-primary swaps the primary email after verification.

GET /api/user/me/notifications / PUT

Read or replace the user's notification preferences (events × delivery channel × brief|full level). See Notifications.

GET /api/user/me/notification-rulesets / POST / PUT /:id / DELETE /:id

Named rulesets — ordered match/action rules with optional account-key filtering and stop semantics. Same data shape, more expressive than the flat preferences map. See Notifications.

GET /api/user/tokens / POST / DELETE /:id

Personal access tokens. The full plaintext is shown only in the create response. See Personal Access Tokens.

DELETE /api/user/me

Deletes the account permanently. { "password": "...", "confirm": "DELETE" }.

OAuth Apps

All endpoints require authentication. See OAuth / OIDC Guide and Cross-App Permissions for the full integration story.

MethodPathNotes
GET/api/appsList apps owned by the current user
POST/api/appsCreate app
GET/api/apps/:idRead app
PATCH/api/apps/:idUpdate fields including oidc_fields, optional_scopes, use_jwt_tokens, allow_self_manage_exported_permissions
POST/api/apps/:id/rotate-secretRotate client_secret
DELETE/api/apps/:idDelete app
GET/api/apps/:id/scope-definitionsList exported scopes
POST / PATCH / DELETE/api/apps/:id/scope-definitions[/:scope]Manage exported scopes (HTTP Basic from the app itself works when allow_self_manage_exported_permissions is on)
GET / POST / DELETE/api/apps/:id/scope-access-rules[/:ruleId]Owner-allow / owner-deny / app-allow / app-deny rules
GET / POST / PATCH / DELETE/api/apps/:appId/webhooks[/:id]App notification webhooks; see App Notifications

App-event streaming (SSE / WebSocket) is also under /api/apps/:appId/events/* — see App Notifications.

Teams

See Teams for the full guide. Endpoint summary:

MethodPathNotes
GET/api/teamsList teams the user can reach (direct + inherited via sub-team nesting; each entry carries parent_team_id + inherited_from)
POST/api/teamsCreate team. Optional parent_team_id makes it a sub-team — caller must be admin+ (direct or inherited) of the parent, depth ≤ max_team_depth
GET/api/teams/:idTeam details + my_role (effective), inherited_from, ancestors[] (parent → root), sub_teams[] (immediate children with member counts), direct members
PATCH/api/teams/:idUpdate name, description, avatar, public-profile flags (incl. profile_show_sub_teams), parent_team_id (owner-only, cycle/depth-checked), require_2fa, require_verified_email
DELETE/api/teams/:idDisband (owner — direct or inherited). Cascades to every sub-team; each level's apps fall back to that level's own owner
GET/api/teams/:id/sub-teamsList immediate sub-teams. Members of an ancestor team (direct or inherited) may list
POST/api/teams/:id/sub-teamsCreate a sub-team under :id — convenience alias for POST /api/teams with parent_team_id
POST/api/teams/:id/membersAdd member by username/id (admins+)
PATCH/api/teams/:id/members/:userIdChange role
DELETE/api/teams/:id/members/:userIdRemove member (or leave the team if :userId = self)
PATCH/api/teams/:id/membership/show-on-profilePer-member opt-in to appear in the team's public member list
POST/api/teams/:id/transfer-ownershipTransfer ownership to another member
GET/api/teams/:id/invitesList active invite tokens
POST/api/teams/:id/invitesMint an invite token (optional email lock + max uses + expiry)
DELETE/api/teams/:id/invites/:tokenRevoke an invite
GET/api/teams/join/:token (auth optional)Inspect an invite — returns the team, requirements, unmet flags
POST/api/teams/join/:tokenAccept an invite
GET / POST / DELETE/api/teams/:id/domains[/:domainId]Team-owned domains. GET also returns ancestor-owned domains as read-only entries tagged inherited_from (subject to inherit_team_domains)
POST/api/teams/:id/domains/:domainId/verifyTrigger re-verification
POST/api/teams/:id/domains/:domainId/to-personalMove a verified domain to the user's personal namespace
POST/api/teams/:id/domains/:domainId/share-to-teamShare a personal domain with the team
POST/api/teams/:id/domains/:domainId/share-to-personalReverse the above
GET / POST/api/teams/:id/appsTeam-owned OAuth apps
POST/api/teams/:id/apps/transferTransfer a personal app into the team
DELETE/api/teams/:id/apps/:appId/transferMove a team-owned app back to the original owner

Domains

MethodPathNotes
GET/api/domainsList the current user's domains
POST/api/domainsAdd domain. Returns verification_method options + the per-method instructions (DNS TXT, HTML meta, .well-known)
POST/api/domains/:id/verifyTrigger a re-verification check using the chosen method
DELETE/api/domains/:idRemove

Social Connections

MethodPathNotes
GET/api/connectionsList the user's linked accounts
GET/api/connections/:slug/beginRedirect to the source's authorization URL. ?mode=login (default) or ?mode=connect
GET/api/connections/:slug/callbackOAuth callback (auto-handled by the provider redirect)
GET/api/connections/telegram/callbackTelegram widget callback (no :slug because Telegram uses a different flow)
POST/api/connections/:id/refreshRefresh display name / avatar from the provider
DELETE/api/connections/:idDisconnect

OAuth-scoped equivalents:

MethodPathScope
GET/api/oauth/me/social-connectionssocial:read
DELETE/api/oauth/me/social-connections/:idsocial:write

OAuth 2.0 / OIDC

See the OAuth / OIDC Guide for the full walkthrough.

MethodPathNotes
GET/api/oauth/authorizeReturns app info + requested scopes for the consent screen
POST/api/oauth/authorizeApprove / deny
POST/api/oauth/tokenauthorization_code and refresh_token grants
GET/api/oauth/userinfoOIDC UserInfo
POST/api/oauth/introspectRFC 7662
POST/api/oauth/revokeRFC 7009
GET/.well-known/openid-configurationDiscovery
GET/.well-known/jwks.jsonRSA public keys for ID token + JWT access tokens

Step-up 2FA

MethodPathAuth
POST/api/oauth/2fa/challengesApp credentials (HTTP Basic) or PKCE
GET/api/oauth/2fa/infoOptional user session — drives the SPA
POST/api/oauth/2fa/authorizeUser session — submit TOTP/passkey/backup or sudo bypass
POST/api/oauth/2fa/sudo/revokeUser session — drop a sudo grace window
POST/api/oauth/2fa/verifyApp credentials — exchange the redirect code for the verification result

/api/oauth/me/* (token-authenticated user APIs)

These endpoints accept either an OAuth access token from the standard flow or a PAT. The required scopes are listed in OAuth → Scopes and Admin → OAuth Scope Reference.

PathScope
GET /me/profileprofile
PATCH /me/profileprofile:write
GET /me/apps / POST /me/apps / PATCH /me/apps/:id / DELETE /me/apps/:idapps:read / apps:write
GET /me/team-appsapps:read
GET /me/teams / POST / PATCH /me/teams/:id / DELETEteams:read / teams:write / teams:create / teams:delete — listing includes inherited sub-teams (inherited_from). Effective-role auth (inherited admin/owner counts) on PATCH/DELETE
POST /me/teams/:id/members / DELETEteams:write — effective-role auth (inherited admin/owner counts)
GET /me/domains / POST / POST :domain/verify / DELETEdomains:read / domains:write
GET /me/gpg-keys / POST / DELETEgpg:read / gpg:write
GET /me/social-connections / DELETEsocial:read / social:write
GET /me/webhooks / POST / PATCH / DELETE / GET …/deliverieswebhooks:read / webhooks:write
GET /me/admin/users / PATCH / DELETEadmin:users:read / admin:users:write / admin:users:delete
GET /me/admin/config / PATCHadmin:config:read / admin:config:write
POST /me/invites / GET / DELETEadmin:invites:create / admin:invites:read / admin:invites:delete
GET /me/admin/webhooks and friendsadmin:webhooks:read / admin:webhooks:write / admin:webhooks:delete
GET /me/site/users[/:id]admin:users:read
GET /me/team/:teamId/info / PATCHteams:read / teams:write
GET /me/team/:teamId/members / POST / DELETE / PATCH …/roleteams:read / teams:write
GET /me/team/:teamId/members/:userId/profileteams:read

GET /api/oauth/consents / DELETE /api/oauth/consents/:client_id

Manage which apps the current user has authorized. DELETE revokes the consent and all outstanding tokens for that app.

Public profiles

GET /api/users/:username

Returns the user profile filtered by visibility flags, or 404 if the username is unknown, private, or enable_public_profiles is off. The 404 body is identical for all three to avoid leaking which usernames exist. Accepts an optional Bearer — a token belonging to the profile's owner returns the data even when private. See Public Profiles.

GET /api/public/teams/:id

Returns the team profile. Same 404 semantics. A token from any member of the team returns the data even when private.

When sub-teams are enabled and the team owner opted into the section (profile_show_sub_teams, or the site default default_team_profile_show_sub_teams), the response includes a sub_teams[] array with only those children that have themselves opted into a public profile — privacy-preserving (a private sub-team's name isn't leaked just because the parent is public). If the team's parent is itself public, the response also includes a parent_team breadcrumb {id, name, avatar_url}.

Image proxy

GET /api/proxy/image/:id

Streams an image registered in image_proxy_mappings. SVG bodies are sanitized. :id is the opaque ID returned by POST /api/proxy/image/register (authenticated) — there is no URL passthrough, so the proxy cannot be used as an open SSRF relay. Cross-origin headers are set so the response is safely embeddable.

POST /api/proxy/image/register

Register a new mapping for a remote image URL the SPA needs to load (markdown preview, ImageUrlInput preview). Requires auth. Returns { "id": "...", "url": "/api/proxy/image/<id>" }.

Admin

All admin endpoints require auth with role = admin.

Config

MethodPathNotes
GET/api/admin/configAll config keys (sensitive values redacted)
PATCH/api/admin/configUpdate one or more keys; sensitive keys are auto-encrypted with SECRETS_KEY if bound

Stats / dashboard

GET /api/admin/stats{ users, apps, verified_domains, active_tokens }.

Users

MethodPathNotes
GET/api/admin/users?page=…&search=…Paginated user list
GET/api/admin/users/:idDetail (sessions, apps, connections)
PATCH/api/admin/users/:idrole, is_active, email_verified, per-user TTL overrides
DELETE/api/admin/users/:idPermanently delete
DELETE/api/admin/users/:id/sessionsRevoke all sessions

Apps / OAuth Sources / Invites / Webhooks / Teams

PathNotes
GET / PATCH /api/admin/apps[/:id]Verify or deactivate
GET / POST / PATCH / DELETE /api/admin/oauth-sources[/:id]Source CRUD
GET /api/admin/oauth-sources/discoverAuto-fetch OIDC discovery for a candidate issuer
POST /api/admin/oauth-sources/migrateOne-time: import the legacy site_config social keys
GET / POST / DELETE /api/admin/invites[/:id]Site-invite tokens
GET /api/admin/teams / DELETE /:idList / disband teams
POST /api/admin/test-emailSend a test outbound email
POST /api/admin/test-email-receivingGenerate a test verify-by-email code
GET / POST / PATCH / DELETE /api/admin/webhooks[/:id]Site-wide audit-event webhooks

Audit / request logs / login errors

MethodPathNotes
GET/api/admin/audit-log?page=…Audit events
GET/api/admin/login-errorsFailed-login table
GET/api/admin/request-logsFilterable per-request log
GET/api/admin/request-logs/exportCSV export of the current filter
GET/api/admin/request-logs/:id/detailsSingle request detail
DELETE/api/admin/request-logsPurge all
DELETE/api/admin/request-logs/spectateClear the live spectate buffer

Secrets migration / Danger Zone

MethodPathNotes
GET/api/admin/secrets/statusWhether the SECRETS_KEY binding is wired and how many config rows are still plaintext
POST/api/admin/secrets/migrateEncrypt remaining site_config / oauth source / oauth app secrets
GET/api/admin/d1-secrets/statusSame for bearer-style D1 fields
POST/api/admin/d1-secrets/migrateHash remaining tokens / codes
GET / POST/api/admin/teams-as-users-status & /migrate-teams-as-usersBackfill kind = 'team' user rows for every team
GET / POST/api/admin/image-proxy-status & /migrate-image-proxyBackfill image-proxy mappings for legacy avatars/icons
POST/api/admin/sweep-image-proxyDrop orphan mappings now (also runs on cron)
GET / DELETE/api/admin/image-proxy[/:id]Browse / clear proxy entries
POST/api/admin/migrate-recovery-codesRe-hash legacy plaintext backup codes
GET / POST/api/admin/reset/status & /request & /cancel & /confirmSite-reset workflow (email-signed)
GET / POST/api/admin/debugInternal toggles for diagnosing deploys

Health

GET /api/health

Always returns { "ok": true }. No authentication.

Released under the GPL-3.0 License.