Domains
Manage the domains a HappyView instance serves. Each domain gets its own atproto OAuth client identity. The primary domain is set from PUBLIC_URL on first boot. All endpoints require the settings:manage permission.
On an instance run by a managed-hosting platform (one with PLATFORM_API_KEY_HASH set, which GET /config reports as platform_managed), the platform decides which hostnames reach the instance and which one is its public URL. There, adding, deleting and making a domain primary accept only the platform’s key, and answer 403 to everyone else, an operator with superadmin included. Listing domains still accepts settings:manage.
const TOKEN = "hv_..."; // your API keyconst headers = { Authorization: `Bearer ${TOKEN}` };const TOKEN = "hv_..."; // your API keyconst headers = { Authorization: `Bearer ${TOKEN}` };let token = "hv_..."; // your API keytoken := "hv_..." // your API key# All examples assume $TOKEN is an API key (hv_...)AUTH="Authorization: Bearer $TOKEN"List domains
Section titled “List domains”GET /admin/domainsinterface Domain { id: string; url: string; is_primary: boolean; created_at: string; updated_at: string;}
const response = await fetch("http://127.0.0.1:3000/admin/domains", { headers,});const data: Domain[] = await response.json();const response = await fetch("http://127.0.0.1:3000/admin/domains", { headers,});const data = await response.json();let client = reqwest::Client::new();let response = client .get("http://127.0.0.1:3000/admin/domains") .bearer_auth(token) .send() .await?;let data: serde_json::Value = response.json().await?;req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/domains", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)curl http://127.0.0.1:3000/admin/domains -H "$AUTH"Response: 200 OK
[ { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://gamesgamesgamesgames.games", "is_primary": true, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" }]Add a domain
Section titled “Add a domain”POST /admin/domainsinterface Domain { id: string; url: string; is_primary: boolean; created_at: string; updated_at: string;}
const response = await fetch("http://127.0.0.1:3000/admin/domains", { method: "POST", headers: { ...headers, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://api.example.com", }),});const data: Domain = await response.json();const response = await fetch("http://127.0.0.1:3000/admin/domains", { method: "POST", headers: { ...headers, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://api.example.com", }),});const data = await response.json();let response = client .post("http://127.0.0.1:3000/admin/domains") .bearer_auth(token) .json(&serde_json::json!({ "url": "https://api.example.com" })) .send() .await?;let data: serde_json::Value = response.json().await?;body := bytes.NewBufferString(`{"url": "https://api.example.com"}`)req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/admin/domains", body)req.Header.Set("Authorization", "Bearer "+token)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)curl -X POST http://127.0.0.1:3000/admin/domains \ -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.example.com" }'| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | Valid origin (scheme + host, no path or trailing slash). Must be https unless PUBLIC_URL is a loopback address. |
Returns 400 Bad Request if the URL is invalid or already registered.
Response: 201 Created
{ "id": "550e8400-e29b-41d4-a716-446655440001", "url": "https://api.example.com", "is_primary": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z"}Also builds an OAuth client for the domain and updates the in-memory cache.
Remove a domain
Section titled “Remove a domain”DELETE /admin/domains/{id}await fetch("http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001", { method: "DELETE", headers,});await fetch("http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001", { method: "DELETE", headers,});client .delete("http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001") .bearer_auth(token) .send() .await?;req, _ := http.NewRequest("DELETE", "http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)curl -X DELETE http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001 \ -H "$AUTH"Returns 400 Bad Request if the domain is the primary domain (make another domain primary first), or if it’s the instance’s only domain. Returns 404 Not Found if the domain doesn’t exist.
Response: 204 No Content
Also removes the domain’s OAuth client and cache entry.
Set primary domain
Section titled “Set primary domain”POST /admin/domains/{id}/primaryawait fetch("http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary", { method: "POST", headers,});await fetch("http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary", { method: "POST", headers,});client .post("http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary") .bearer_auth(token) .send() .await?;req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)curl -X POST http://127.0.0.1:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary \ -H "$AUTH"Sets the target domain as the primary. Unsets the current primary in a single operation. Returns 404 Not Found if the domain doesn’t exist.
Response: 204 No Content
Also updates the in-memory cache and primary client reference.

