Main App API
These endpoints power the main wisp.place backend (Bun + Elysia). Responses below are the shapes returned by the handlers in apps/main-app/src/routes/*.
Notes:
- Authenticated routes rely on the signed
didcookie. If authentication fails, the handler throws and Elysia returns an error response. - Admin routes rely on the signed
admin_sessioncookie. Unauthorized requests return401 { error: 'Unauthorized' }.
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/PDS 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”Redirects after OAuth completes.
- 302 →
/onboarding(no sites or domain) - 302 →
/editor(existing user) - 302 →
/?error=auth_failedon failure
POST /api/auth/logout
Section titled “POST /api/auth/logout”{ "success": true }On failure:
{ "error": "Logout failed" }GET /api/auth/status
Section titled “GET /api/auth/status”Authenticated:
{ "authenticated": true, "did": "did:plc:..." }Not authenticated:
{ "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”{ "sites": [/* site 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”{ "success": true, "synced": 2, "errors": [] }GET /api/user/site/:rkey/domains
Section titled “GET /api/user/site/:rkey/domains”{ "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" }Invalid handle:
{ "available": false, "reason": "invalid" }GET /api/domain/registered
Section titled “GET /api/domain/registered”Registered:
{ "registered": true, "type": "wisp", "domain": "name.wisp.place", "did": "did:plc:...", "rkey": "site-rkey" }Custom domain:
{ "registered": true, "type": "custom", "domain": "example.com", "did": "did:plc:...", "rkey": "site-rkey", "verified": true }Unregistered:
{ "registered": false }Missing domain:
{ "error": "Domain parameter required" }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 when present, otherwise defaults:
{ "indexFiles": ["index.html"], "cleanUrls": false, "directoryListing": false }On failure:
{ "success": false, "error": "..." }POST /api/site/:rkey/settings
Section titled “POST /api/site/:rkey/settings”{ "success": true, "uri": "at://...", "cid": "bafy..." }On validation failure:
{ "success": false, "error": "Only one of spaMode, directoryListing, or custom404 can be enabled" }Wisp Uploads (/wisp/*)
Section titled “Wisp Uploads (/wisp/*)”GET /wisp/upload-progress/:jobId
Section titled “GET /wisp/upload-progress/:jobId”Server-sent events stream for upload progress.
- event:
progress→{ status, progress, result, error } - event:
done→result - event:
error→{ error }
Errors:
{ "error": "Job not found" }{ "error": "Unauthorized" }POST /wisp/upload-files
Section titled “POST /wisp/upload-files”Empty upload (no files):
{ "success": true, "uri": "at://...", "cid": "bafy...", "fileCount": 0, "siteName": "my-site" }Async upload:
{ "success": true, "jobId": "...", "message": "Upload started. Connect to /wisp/upload-progress/..." }Admin (/api/admin/*)
Section titled “Admin (/api/admin/*)”POST /api/admin/login
Section titled “POST /api/admin/login”{ "success": true }Invalid credentials (401):
{ "error": "Invalid credentials" }POST /api/admin/logout
Section titled “POST /api/admin/logout”{ "success": true }GET /api/admin/status
Section titled “GET /api/admin/status”Authenticated:
{ "authenticated": true, "username": "admin" }Not authenticated:
{ "authenticated": false }GET /api/admin/logs
Section titled “GET /api/admin/logs”{ "logs": [/* combined logs */] }GET /api/admin/errors
Section titled “GET /api/admin/errors”{ "errors": [/* combined errors */] }GET /api/admin/metrics
Section titled “GET /api/admin/metrics”{ "overall": {}, "mainApp": {}, "hostingService": {}, "timeWindow": 3600000 }GET /api/admin/database
Section titled “GET /api/admin/database”{ "stats": {}, "recentSites": [], "recentDomains": [] }GET /api/admin/cache
Section titled “GET /api/admin/cache”Returns the hosting service cache stats payload or:
{ "error": "Failed to fetch cache stats from hosting service", "message": "Hosting service unavailable" }GET /api/admin/sites
Section titled “GET /api/admin/sites”{ "sites": [/* sites */], "customDomains": [/* domains */] }GET /api/admin/health
Section titled “GET /api/admin/health”{ "uptime": 12345, "memory": { "heapUsed": 123, "heapTotal": 456, "rss": 789 }, "timestamp": "2026-01-22T00:00:00.000Z"}