Main App API
Internal REST API for the main app (Bun + Elysia). Authenticated routes require a signed did cookie. Admin routes require a signed admin_session cookie and return 401 { error: 'Unauthorized' } otherwise.
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”{ "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" }{ "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 }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 }
Admin /api/admin/*
Section titled “Admin /api/admin/*”POST /api/admin/login
Section titled “POST /api/admin/login”{ "success": true }On failure (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": true, "username": "admin" }{ "authenticated": false }GET /api/admin/database
Section titled “GET /api/admin/database”{ "stats": {}, "recentSites": [], "recentDomains": [] }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"}