Main App API
Internal REST API for the main app (Bun + Elysia). Authenticated routes require a signed did cookie.
For the AT Protocol XRPC endpoints, see XRPC API.
Auth /api/auth/*
Section titled “Auth /api/auth/*”GET /api/auth/login
Section titled “GET /api/auth/login”Redirects to the AT Protocol OAuth authorize URL.
- 302 → OAuth URL
- 302 →
/?error=missing_handleif no handle provided - 302 →
/?error=auth_failedon failure
POST /api/auth/signin
Section titled “POST /api/auth/signin”{ "url": "https://..." }On failure: { "error": "Authentication failed", "details": "..." }
GET /api/auth/callback
Section titled “GET /api/auth/callback”- 302 →
/onboarding(new user) - 302 →
/editor(returning user) - 302 →
/?error=auth_failedon failure
POST /api/auth/logout
Section titled “POST /api/auth/logout”{ "success": true }GET /api/auth/status
Section titled “GET /api/auth/status”{ "authenticated": true, "did": "did:plc:..." }{ "authenticated": false }User /api/user/*
Section titled “User /api/user/*”GET /api/user/status
Section titled “GET /api/user/status”{ "did": "did:plc:...", "hasSites": true, "hasDomain": false, "domain": null, "sitesCount": 3}GET /api/user/info
Section titled “GET /api/user/info”{ "did": "did:plc:...", "handle": "user.bsky.social" }GET /api/user/sites
Section titled “GET /api/user/sites”Each site carries its own domains, so listing sites and their domains is a
single request.
{ "sites": [ { "did": "did:plc:...", "rkey": "site-rkey", "display_name": "site-rkey", "created_at": 0, "updated_at": 0, "domains": [ /* domain rows */ ] } ] }GET /api/user/domains
Section titled “GET /api/user/domains”{ "wispDomains": [{ "domain": "name.wisp.place", "rkey": "site-rkey" }], "customDomains": [ /* custom domain rows */ ]}POST /api/user/sync
Section titled “POST /api/user/sync”Lists the caller’s place.wisp.fs records on their PDS. Sites missing from
site_cache get a placeholder row (no files, cold_synced = false) so they are
listed immediately, and every missing, stale or not-yet-downloaded site is queued
for the firehose service’s revalidation worker. synced counts records found on
the PDS, queued the sites handed to the worker. The dashboard calls this on load and sign-in runs it too.
{ "success": true, "synced": 2, "queued": 1, "errors": [] }GET /api/user/site/:rkey/domains
Section titled “GET /api/user/site/:rkey/domains”Domains for one site. GET /api/user/sites already includes these, so prefer it
when listing.
{ "rkey": "site-rkey", "domains": [ /* domain rows */ ] }Domain /api/domain/*
Section titled “Domain /api/domain/*”GET /api/domain/check
Section titled “GET /api/domain/check”{ "available": true, "domain": "name.wisp.place" }{ "available": false, "reason": "invalid" }GET /api/domain/registered
Section titled “GET /api/domain/registered”{ "registered": true, "type": "wisp", "domain": "name.wisp.place", "did": "did:plc:...", "rkey": "site-rkey" }{ "registered": true, "type": "custom", "domain": "example.com", "did": "did:plc:...", "rkey": "site-rkey", "verified": true }{ "registered": false }Caddy’s on-demand TLS asks this endpoint, so a custom domain answers registered: true only once it is verified; an unverified claim answers 404.
POST /api/domain/claim
Section titled “POST /api/domain/claim”{ "success": true, "domain": "name.wisp.place" }POST /api/domain/update
Section titled “POST /api/domain/update”{ "success": true, "domain": "name.wisp.place" }POST /api/domain/custom/add
Section titled “POST /api/domain/custom/add”{ "success": true, "id": "abcdef1234567890", "domain": "example.com", "verified": false }POST /api/domain/custom/verify
Section titled “POST /api/domain/custom/verify”{ "success": true, "verified": true, "error": null, "found": true }DELETE /api/domain/custom/:id
Section titled “DELETE /api/domain/custom/:id”{ "success": true }POST /api/domain/wisp/map-site
Section titled “POST /api/domain/wisp/map-site”{ "success": true }DELETE /api/domain/wisp/:domain
Section titled “DELETE /api/domain/wisp/:domain”{ "success": true }POST /api/domain/custom/:id/map-site
Section titled “POST /api/domain/custom/:id/map-site”{ "success": true }Site /api/site/*
Section titled “Site /api/site/*”DELETE /api/site/:rkey
Section titled “DELETE /api/site/:rkey”{ "success": true, "message": "Site deleted successfully" }On failure: { "success": false, "error": "..." }
GET /api/site/:rkey/settings
Section titled “GET /api/site/:rkey/settings”Returns the place.wisp.settings record or defaults:
{ "indexFiles": ["index.html"], "cleanUrls": false, "directoryListing": false }POST /api/site/:rkey/settings
Section titled “POST /api/site/:rkey/settings”{ "success": true, "uri": "at://...", "cid": "bafy..." }On failure: { "success": false, "error": "Only one of spaMode, directoryListing, or custom404 can be enabled" }
Uploads /wisp/*
Section titled “Uploads /wisp/*”POST /wisp/upload-files
Section titled “POST /wisp/upload-files”{ "success": true, "jobId": "...", "message": "Upload started. Connect to /wisp/upload-progress/..." }Empty upload: { "success": true, "uri": "at://...", "cid": "bafy...", "fileCount": 0, "siteName": "my-site" }
GET /wisp/upload-progress/:jobId
Section titled “GET /wisp/upload-progress/:jobId”Server-sent events stream:
progress→{ status, progress, result, error }done→resulterror→{ error }