Browse the spaces on this instance for moderation. These endpoints give operators access to spaces they are not members of, so they live in the admin API rather than the space XRPC API.
Listing spaces and reading their metadata and members requires spaces:read. Reading records and blobs requires spaces:manage-records, and each read is written to the event log as a space.moderator_read event.
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 keyconst token = "hv_..." // your API key# All examples assume $TOKEN is an API key (hv_...)AUTH="Authorization: Bearer $TOKEN"List spaces
Section titled “List spaces”GET /admin/spacesEvery space on the instance, newest first.
| Param | Type | Required | Description |
|---|---|---|---|
limit | number | no | Max results per page (default 50, max 100) |
cursor | string | no | Pagination cursor from a previous response |
const response = await fetch("http://127.0.0.1:3000/admin/spaces?limit=20", { headers,});const data = await response.json();const response = await fetch("http://127.0.0.1:3000/admin/spaces?limit=20", { headers,});const data = await response.json();let client = reqwest::Client::new();let response = client .get("http://127.0.0.1:3000/admin/spaces?limit=20") .bearer_auth(token) .send() .await?;let data: serde_json::Value = response.json().await?;req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/spaces?limit=20", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl "http://127.0.0.1:3000/admin/spaces?limit=20" -H "$AUTH"Response: 200 OK
{ "spaces": [ { "id": "0b6c1f0e-...", "uri": "at://did:web:happyview.example.com/space/com.example.forum/main", "did": "did:web:happyview.example.com", "authority_did": "did:web:happyview.example.com", "creator_did": "did:plc:creator123", "type": "com.example.forum", "skey": "main", "display_name": "Forum", "description": null, "read_policy": { "$type": "com.atproto.simplespace.defs#memberListPolicy" }, "write_policy": { "$type": "com.atproto.simplespace.defs#memberListPolicy" }, "app_access": { "$type": "com.atproto.simplespace.defs#open" }, "config": { "membership_public": false, "records_public": false }, "revision": "3k...", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ], "cursor": "50"}cursor is an offset. It is returned whenever a page is full, so the last page can be empty. revision is null until the space’s first commit.
Get a space
Section titled “Get a space”GET /admin/spaces/{id}A space’s metadata, its resolved member list (including members added through delegation), and the record count of each collection in it. {id} is the space’s id from List spaces.
const response = await fetch("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-...", { headers,});const data = await response.json();const response = await fetch("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-...", { headers,});const data = await response.json();let client = reqwest::Client::new();let response = client .get("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-...") .bearer_auth(token) .send() .await?;let data: serde_json::Value = response.json().await?;req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/spaces/0b6c1f0e-...", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl http://127.0.0.1:3000/admin/spaces/0b6c1f0e-... -H "$AUTH"Response: 200 OK
{ "space": { "id": "0b6c1f0e-...", "uri": "at://...", "...": "..." }, "members": [ { "did": "did:plc:creator123", "read": true, "write": true } ], "collections": [ { "collection": "com.example.forum.post", "count": 42 } ]}Returns 404 Not Found if no space has that ID.
List records in a space
Section titled “List records in a space”GET /admin/spaces/{id}/recordsRecords in a space, newest first. Requires spaces:manage-records.
| Param | Type | Required | Description |
|---|---|---|---|
repo | string | no | Only records authored by this DID |
collection | string | no | Only records in this collection |
limit | number | no | Max results per page (default 20, max 100) |
cursor | string | no | Pagination cursor from a previous response |
const response = await fetch("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../records?collection=com.example.forum.post", { headers,});const data = await response.json();const response = await fetch("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../records?collection=com.example.forum.post", { headers,});const data = await response.json();let client = reqwest::Client::new();let response = client .get("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../records?collection=com.example.forum.post") .bearer_auth(token) .send() .await?;let data: serde_json::Value = response.json().await?;req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../records?collection=com.example.forum.post", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl "http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../records?collection=com.example.forum.post" -H "$AUTH"Response: 200 OK
{ "records": [ { "uri": "at://did:web:happyview.example.com/space/com.example.forum/main/did:plc:abc/com.example.forum.post/3k...", "did": "did:plc:abc", "collection": "com.example.forum.post", "rkey": "3k...", "cid": "bafyrei...", "indexed_at": "2026-01-01T00:00:00Z", "record": { "...": "..." } } ], "cursor": "..."}cursor is returned whenever a page is full, so the last page can be empty. Each request is logged with the URIs of the records it returned.
Get a blob in a space
Section titled “Get a blob in a space”GET /admin/spaces/{id}/blobA blob referenced by a record in the space, fetched from its author’s PDS. The response body is the blob, with the content type the PDS reports. Requires spaces:manage-records.
| Param | Type | Required | Description |
|---|---|---|---|
cid | string | yes | The blob CID |
const response = await fetch("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../blob?cid=bafkrei...", { headers,});const blob = await response.blob();const response = await fetch("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../blob?cid=bafkrei...", { headers,});const blob = await response.blob();let client = reqwest::Client::new();let response = client .get("http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../blob?cid=bafkrei...") .bearer_auth(token) .send() .await?;let bytes = response.bytes().await?;req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../blob?cid=bafkrei...", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl "http://127.0.0.1:3000/admin/spaces/0b6c1f0e-.../blob?cid=bafkrei..." -H "$AUTH" -o blobReturns 404 Not Found if no record in the space references the blob, and 502 Bad Gateway if the author’s PDS cannot serve it.
Space migration status
Section titled “Space migration status”GET /admin/spaces/migration-statusHow far each space’s repositories have moved from HappyView-hosted copies to their authors’ PDSes, and which PDSes support spaces. Requires spaces:read.
Response: 200 OK
{ "spaces": [ { "spaceId": "0b6c1f0e-…", "type": "com.example.forum", "skey": "main", "polyfill": 12, "migrating": 3, "native": 40 } ], "awaitingAuthorization": 5, "detection": [ { "pds": "https://pds.example.com", "supported": true, "tier": "descriptor", "missing": [], "checkedAt": "2026-10-01T12:00:00Z" } ]}polyfill,migratingandnativecount the space’s repositories by where they’re hosted: still on HappyView, moving, or on the author’s PDS. A space with no repositories is not listed.awaitingAuthorizationcounts accounts whose repositories are still on HappyView and who haven’t signed in with a session grantingspace:access, so they can’t move yet.detectionis what HappyView last found out about each PDS’s support for spaces, and what it’smissing.tiersays how it found out:descriptor(the PDS lists its own methods),probe(inferred from how it answers a method that needs auth) orunreachable(HappyView couldn’t reach the PDS, which says nothing about its support).
© HappyProto

