Space records are stored separately from public atproto records. They use the at:// scheme with a space path segment to distinguish them from public records:
at:// did:plc:abcdefghijklmnop1234567890 / space / com.example.forum / main / did:plc:author / com.example.forum.post / abcdefghijklmnop1234567890 └── space DID ───────────────────┘ └── space type ─┘ └── skey ─┘ └── author ──┘ └── collection ──────┘ └── rkey ────────────────┘Creating a record
Section titled “Creating a record”The caller must be a member with write access. HappyView generates the rkey as a TID.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.createRecord", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", record: { $type: "com.example.forum.post", text: "Hello from the forum!", createdAt: "2026-05-09T12:00:00Z", }, }),});interface CreateRecordResponse { uri: string; cid: string;}const data: CreateRecordResponse = await response.json();const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.createRecord", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", record: { $type: "com.example.forum.post", text: "Hello from the forum!", createdAt: "2026-05-09T12:00:00Z", }, }),});const data = await response.json();let response = client .post("https://happyview.example.com/xrpc/com.atproto.space.createRecord") .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .json(&serde_json::json!({ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "record": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" } })) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
body := bytes.NewBufferString(`{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "record": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" }}`)req, _ := http.NewRequest("POST", "https://happyview.example.com/xrpc/com.atproto.space.createRecord", body)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl -X POST 'https://happyview.example.com/xrpc/com.atproto.space.createRecord' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>' \ -H 'Content-Type: application/json' \ -d '{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "record": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" } }'Input:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space to write into |
collection | string (NSID) | Yes | The record collection |
record | object | Yes | The record data |
Response (201):
{ "uri": "at://did:web:happyview.example.com/space/com.example.forum/main/did:plc:author/com.example.forum.post/3l2tkbx7225co", "cid": "bafyrei..."}createRecord always inserts a new record. If a record with the generated URI already exists, it returns 409 Conflict.
Updating a record
Section titled “Updating a record”The caller must be a member with write access.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.putRecord", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", rkey: "3k2abc", record: { $type: "com.example.forum.post", text: "Hello from the forum!", createdAt: "2026-05-09T12:00:00Z", }, }),});interface PutRecordResponse { uri: string; cid: string;}const data: PutRecordResponse = await response.json();const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.putRecord", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", rkey: "3k2abc", record: { $type: "com.example.forum.post", text: "Hello from the forum!", createdAt: "2026-05-09T12:00:00Z", }, }),});const data = await response.json();let response = client .post("https://happyview.example.com/xrpc/com.atproto.space.putRecord") .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .json(&serde_json::json!({ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "rkey": "3k2abc", "record": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" } })) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
body := bytes.NewBufferString(`{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "rkey": "3k2abc", "record": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" }}`)req, _ := http.NewRequest("POST", "https://happyview.example.com/xrpc/com.atproto.space.putRecord", body)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl -X POST 'https://happyview.example.com/xrpc/com.atproto.space.putRecord' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>' \ -H 'Content-Type: application/json' \ -d '{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "rkey": "3k2abc", "record": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" } }'Input:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space to write into |
collection | string (NSID) | Yes | The record collection |
rkey | string | Yes | The record key |
record | object | Yes | The record data |
swapRecord | string | No | Expected CID of the existing record (for optimistic concurrency) |
Response (201):
{ "uri": "at://did:web:happyview.example.com/space/com.example.forum/main/did:plc:author/com.example.forum.post/3k2abc", "cid": "bafyrei..."}HappyView takes the author DID from the authenticated caller, so you can write records only as yourself and the URI’s author component is always your DID.
putRecord performs an upsert: if a record with the same collection + rkey already exists for this author in this space, it’s overwritten. Use swapRecord to prevent unintended overwrites (see Optimistic concurrency below).
Getting a record
Section titled “Getting a record”The caller must be a member with read access, or present a valid space credential.
Members with read_self access can read only their own records. Reading another author’s record returns 403 Forbidden.
const params = new URLSearchParams({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", rkey: "3k2abc",});const response = await fetch( `https://happyview.example.com/xrpc/com.atproto.space.getRecord?${params}`, { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);interface GetRecordResponse { uri: string; cid: string; value: Record<string, unknown>;}const data: GetRecordResponse = await response.json();const params = new URLSearchParams({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", rkey: "3k2abc",});const response = await fetch( `https://happyview.example.com/xrpc/com.atproto.space.getRecord?${params}`, { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.getRecord") .query(&[ ("space", "at://did:web:happyview.example.com/space/com.example.forum/main"), ("collection", "com.example.forum.post"), ("rkey", "3k2abc"), ]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.getRecord?space=at://did:web:happyview.example.com/space/com.example.forum/main&collection=com.example.forum.post&rkey=3k2abc", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.getRecord?space=at://did:web:happyview.example.com/space/com.example.forum/main&collection=com.example.forum.post&rkey=3k2abc' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>'Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space containing the record |
repo | string | No | The author’s DID |
collection | string (NSID) | Yes | The record collection |
rkey | string | Yes | The record key |
Without repo, HappyView returns the caller’s own record when one exists, and otherwise the record whose author DID sorts first. A caller with a space credential reads the repo its request is addressed to, and a read_self member reads only their own. Pass repo when several authors may use the same rkey.
Response:
{ "uri": "at://did:web:happyview.example.com/space/com.example.forum/main/did:plc:author/com.example.forum.post/3k2abc", "cid": "bafyrei...", "value": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" }}Listing records
Section titled “Listing records”The caller must be a member with read access, or present a valid space credential. Members with read_self access list only their own records.
const params = new URLSearchParams({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", limit: "20",});const response = await fetch( `https://happyview.example.com/xrpc/com.atproto.space.listRecords?${params}`, { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);interface RecordEntry { collection: string; rkey: string; cid: string; // Only present when `includeValues` is `true`. value?: Record<string, unknown>;}interface ListRecordsResponse { records: RecordEntry[]; cursor?: string;}const data: ListRecordsResponse = await response.json();const params = new URLSearchParams({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", limit: "20",});const response = await fetch( `https://happyview.example.com/xrpc/com.atproto.space.listRecords?${params}`, { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.listRecords") .query(&[ ("space", "at://did:web:happyview.example.com/space/com.example.forum/main"), ("collection", "com.example.forum.post"), ("limit", "20"), ]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.listRecords?space=at://did:web:happyview.example.com/space/com.example.forum/main&collection=com.example.forum.post&limit=20", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.listRecords?space=at://did:web:happyview.example.com/space/com.example.forum/main&collection=com.example.forum.post&limit=20' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>'Parameters:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
space | string | Yes | The space to list from | |
repo | string | No | Filter by author DID. Without it, an OAuth caller lists their own records and a space credential lists every repo. | |
collection | string | No | Filter by collection NSID | |
limit | integer | No | 50 | Max records to return (1-100) |
cursor | string | No | Pagination cursor | |
reverse | boolean | No | false | List newest first. By default the oldest record comes first. |
includeValues | boolean | No | false | Include each record’s value in the response |
Response:
{ "records": [ { "collection": "com.example.forum.post", "rkey": "3k2abc", "cid": "bafyrei...", "value": { "$type": "com.example.forum.post", "text": "Hello from the forum!", "createdAt": "2026-05-09T12:00:00Z" } } ], "cursor": "MjAyNi0wNS0wOVQxMjowMDowMCswMDowMHxhdDovL2RpZDp3ZWI6aGFwcHl2aWV3LmV4YW1wbGUuY29tL3NwYWNlL2NvbS5leGFtcGxlLmZvcnVtL21haW4vZGlkOnBsYzphdXRob3IvY29tLmV4YW1wbGUuZm9ydW0ucG9zdC8zazJhYmM="}value is only present when includeValues is true. By default each entry carries only collection, rkey, and cid.
Deleting a record
Section titled “Deleting a record”You can delete only your own records, and must be a member with write access.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.deleteRecord", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", rkey: "3k2abc", }),});const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.deleteRecord", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", collection: "com.example.forum.post", rkey: "3k2abc", }),});let response = client .post("https://happyview.example.com/xrpc/com.atproto.space.deleteRecord") .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .json(&serde_json::json!({ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "rkey": "3k2abc" })) .send() .await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
body := bytes.NewBufferString(`{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "rkey": "3k2abc"}`)req, _ := http.NewRequest("POST", "https://happyview.example.com/xrpc/com.atproto.space.deleteRecord", body)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl -X POST 'https://happyview.example.com/xrpc/com.atproto.space.deleteRecord' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>' \ -H 'Content-Type: application/json' \ -d '{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "rkey": "3k2abc" }'Input:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space containing the record |
collection | string (NSID) | Yes | The record collection |
rkey | string | Yes | The record key |
swapRecord | string | No | Expected CID of the existing record (for optimistic concurrency) |
HappyView builds the record’s URI from your DID, so a collection and rkey that only another author uses return 404 Not Found.
Batch writes (applyWrites)
Section titled “Batch writes (applyWrites)”applyWrites performs multiple create, update, and delete operations in a single request. The caller must be a member with write access.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.applyWrites", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", writes: [ { action: "create", collection: "com.example.forum.post", value: { $type: "com.example.forum.post", text: "First post" }, }, { action: "update", collection: "com.example.forum.post", rkey: "3k2abc", value: { $type: "com.example.forum.post", text: "Edited post" }, swapRecord: "bafyrei...", }, { action: "delete", collection: "com.example.forum.post", rkey: "old-post", }, ], }),});interface ApplyWritesResult { uri?: string; cid?: string;}const data: { results: ApplyWritesResult[] } = await response.json();const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.applyWrites", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ space: "at://did:web:happyview.example.com/space/com.example.forum/main", writes: [ { action: "create", collection: "com.example.forum.post", value: { $type: "com.example.forum.post", text: "First post" }, }, { action: "update", collection: "com.example.forum.post", rkey: "3k2abc", value: { $type: "com.example.forum.post", text: "Edited post" }, swapRecord: "bafyrei...", }, { action: "delete", collection: "com.example.forum.post", rkey: "old-post", }, ], }),});const data = await response.json();let response = client .post("https://happyview.example.com/xrpc/com.atproto.space.applyWrites") .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .json(&serde_json::json!({ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "writes": [ { "action": "create", "collection": "com.example.forum.post", "value": { "$type": "com.example.forum.post", "text": "First post" } }, { "action": "update", "collection": "com.example.forum.post", "rkey": "3k2abc", "value": { "$type": "com.example.forum.post", "text": "Edited post" }, "swapRecord": "bafyrei..." }, { "action": "delete", "collection": "com.example.forum.post", "rkey": "old-post" } ] })) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
body := bytes.NewBufferString(`{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "writes": [ { "action": "create", "collection": "com.example.forum.post", "value": { "$type": "com.example.forum.post", "text": "First post" } }, { "action": "update", "collection": "com.example.forum.post", "rkey": "3k2abc", "value": { "$type": "com.example.forum.post", "text": "Edited post" }, "swapRecord": "bafyrei..." }, { "action": "delete", "collection": "com.example.forum.post", "rkey": "old-post" } ]}`)req, _ := http.NewRequest("POST", "https://happyview.example.com/xrpc/com.atproto.space.applyWrites", body)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl -X POST 'https://happyview.example.com/xrpc/com.atproto.space.applyWrites' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>' \ -H 'Content-Type: application/json' \ -d '{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "writes": [ { "action": "create", "collection": "com.example.forum.post", "value": { "$type": "com.example.forum.post", "text": "First post" } }, { "action": "update", "collection": "com.example.forum.post", "rkey": "3k2abc", "value": { "$type": "com.example.forum.post", "text": "Edited post" }, "swapRecord": "bafyrei..." }, { "action": "delete", "collection": "com.example.forum.post", "rkey": "old-post" } ] }'Input:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space to write into |
swapCommit | string | No | Expected space revision (for optimistic concurrency) |
writes | array | Yes | List of write operations |
Each write operation has an action field:
| Action | Fields | Description |
|---|---|---|
create | collection, value, rkey? | Insert a new record. Auto-generates rkey if omitted. |
update | collection, rkey, value, swapRecord? | Upsert a record. |
delete | collection, rkey, swapRecord? | Delete a record. |
Response:
{ "results": [ { "uri": "at://...", "cid": "bafyrei..." }, { "uri": "at://...", "cid": "bafyrei..." }, {} ]}Each entry in results corresponds to the write at the same index. Create and update operations return uri and cid; delete operations return an empty object.
Optimistic concurrency
Section titled “Optimistic concurrency”swapRecord and swapCommit provide optimistic concurrency control to prevent lost updates when several writers change the same space.
swapRecord
Section titled “swapRecord”Pass the swapRecord field on putRecord, deleteRecord, or individual operations within applyWrites. The value is the CID of the record you expect to be replacing. If the record’s current CID doesn’t match, the operation fails with 409 Conflict, and if no such record exists, with 404 Not Found.
{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "collection": "com.example.forum.post", "rkey": "3k2abc", "record": { "text": "updated safely" }, "swapRecord": "bafyrei_old_cid"}swapCommit
Section titled “swapCommit”Pass the swapCommit field on applyWrites to assert the space’s current revision. If another write has reached the space since you last read its state, the operation fails with 409 Conflict before any writes are applied.
The space’s current revision is revision in the space object returned by com.atproto.simplespace.getSpace. Only the space’s creator, an operator with superadmin, or a caller with a space credential can read it there.
{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "swapCommit": "3l2tkbx7225co", "writes": [...]}Latest commit
Section titled “Latest commit”getLatestCommit returns the per-user signed commit for a space, including the current revision and deniable commit data.
const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.getLatestCommit?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);interface LatestCommitResponse { rev: string | null; commit: { ver: number; hash: { $bytes: string }; ikm: { $bytes: string }; sig: { $bytes: string }; mac: { $bytes: string }; rev: string; } | null;}const data: LatestCommitResponse = await response.json();const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.getLatestCommit?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.getLatestCommit") .query(&[ ("space", "at://did:web:happyview.example.com/space/com.example.forum/main"), ("did", "did:plc:author"), ]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.getLatestCommit?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.getLatestCommit?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>'Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space URI |
did | string | Yes | The DID of the user to get state for |
Response:
| Field | Type | Description |
|---|---|---|
rev | string/null | Current revision for this user’s repo in the space |
commit | object/null | Deniable commit data: ver and rev, plus hash, ikm, sig and mac, each a bytes object {"$bytes": "<base64>"} |
Record operation log
Section titled “Record operation log”listRepoOps returns the operation log for a user in a space. HappyView records each write (create, update, delete) as an oplog entry.
const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.listRepoOps?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();// data.ops — array of oplog entriesconst response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.listRepoOps?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.listRepoOps") .query(&[ ("space", "at://did:web:happyview.example.com/space/com.example.forum/main"), ("did", "did:plc:author"), ]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.listRepoOps?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.listRepoOps?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>'Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space URI |
did | string | Yes | The DID of the user whose ops to list |
limit | integer | No | Max number of entries to return (default 100, max 1000) |
cursor | string | No | Position to start after, as {rev}:{idx} from a previous response. A bare rev skips that whole revision |
excludeValues | boolean | No | If true, omit record values from response (default false) |
Response:
{ "ops": [ { "id": "...", "space_id": "...", "author_did": "did:plc:author", "rev": "3l2tkbx7225co", "idx": 0, "action": "create", "collection": "com.example.forum.post", "rkey": "3k2abc", "cid": "bafyrei...", "prev": null, "value": { "text": "hello world" }, "created_at": "2026-05-09T12:00:00.482913+00:00" } ], "cursor": "3l2tkbx7225co:0", "commit": { "ver": 1, "rev": "3l2tkbx7225co", "hash": { "$bytes": "..." }, "ikm": { "$bytes": "..." }, "sig": { "$bytes": "..." }, "mac": { "$bytes": "..." } }}Each entry records a single write operation. The action is one of create, update, or delete. The prev field contains the CID of the record before the operation (for updates and deletes). The value field contains the record value the operation wrote, and is omitted for deletes, when excludeValues=true, and once a later write has replaced or deleted that record. Entry fields are snake_case.
cursor is the position of the last entry returned, as {rev}:{idx}; pass it back to read the next page. It is null when no more entries remain. commit is the repo’s current commit, so a syncer can fetch the ops and verify them in one round trip.
Repo export
Section titled “Repo export”getRepo exports a user’s full repo within a space as a CAR v1 file. The file contains two roots: the signed commit and a DRISL index (flat DAG-CBOR map of collection/rkey to CID). Record blocks follow in lexicographic order.
const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.getRepo?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const car = await response.arrayBuffer();const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.getRepo?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const car = await response.arrayBuffer();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.getRepo") .query(&[ ("space", "at://did:web:happyview.example.com/space/com.example.forum/main"), ("did", "did:plc:author"), ]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let bytes = response.bytes().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.getRepo?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.getRepo?space=at://did:web:happyview.example.com/space/com.example.forum/main&did=did:plc:author' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>' \ --output repo.carParameters:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space URI |
did | string | Yes | The DID of the user whose repo to export |
The response body is a CAR v1 file with content type application/vnd.ipld.car.
Listing repos
Section titled “Listing repos”listRepos returns the space’s writer set: every repo whose writes the space has accepted, with its current revision and LtHash digest. A repo joins the writer set when it commits to the space and passes the space’s write policy.
Syncers call this to find every repo in the space, and to catch up after missing a write notification. The caller must be a member with read access to the whole space, or present a space credential whose audience is the space authority’s DID.
const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.listRepos?space=at://did:web:happyview.example.com/space/com.example.forum/main", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);interface Repo { did: string; repoRev: string; hash: { $bytes: string }; spaceRev: string;}interface ListReposResponse { repos: Repo[]; cursor?: string;}const data: ListReposResponse = await response.json();const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.listRepos?space=at://did:web:happyview.example.com/space/com.example.forum/main", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.listRepos") .query(&[("space", "at://did:web:happyview.example.com/space/com.example.forum/main")]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.listRepos?space=at://did:web:happyview.example.com/space/com.example.forum/main", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.listRepos?space=at://did:web:happyview.example.com/space/com.example.forum/main' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>'Parameters:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
space | string | Yes | The space URI | |
limit | integer | No | 100 | Max repos to return (1-1000) |
cursor | string | No | A space revision. Lists only repos updated after it. Pass a previous response’s cursor, or the last spaceRev already processed. |
Response:
{ "repos": [ { "did": "did:plc:author1", "repoRev": "3l2tkbx7225co", "rev": "3l2tkbx7225co", "hash": { "$bytes": "q83vEjRWeJC..." }, "spaceRev": "3l2tkbx7a2v4s" }, { "did": "did:plc:author2", "repoRev": "3l2tkbwz4fa2k", "rev": "3l2tkbwz4fa2k", "hash": { "$bytes": "3q2+7wABAgM..." }, "spaceRev": "3l2tkbx7a3k2s" } ], "cursor": "3l2tkbx7a3k2s"}| Field | Type | Description |
|---|---|---|
repos | array | The repos on this page, in the order their updates were sequenced. A repo updated while paging can appear again on a later page. |
cursor | string? | The spaceRev of the last repo on the page. Absent when the page is empty. |
Each repo carries did, repoRev (its revision as last reported to HappyView), hash, and spaceRev (the space revision of its last update). rev repeats repoRev under the alpha lexicon’s name. It is deprecated, still served in v3 alpha, and removal is not yet scheduled.
To stay in sync, a syncer keeps the last spaceRev it processed and passes it as cursor on the next call. See Receiving notifications.
Getting a blob
Section titled “Getting a blob”getBlob returns a blob from a space. The blob must be referenced by a record in the space, or the call returns 404 Not Found. HappyView fetches it from the author’s PDS and passes it through with access control.
const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.getBlob?space=at://did:web:happyview.example.com/space/com.example.forum/main&cid=bafyrei...", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const blob = await response.blob();const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.getBlob?space=at://did:web:happyview.example.com/space/com.example.forum/main&cid=bafyrei...", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const blob = await response.blob();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.getBlob") .query(&[ ("space", "at://did:web:happyview.example.com/space/com.example.forum/main"), ("cid", "bafyrei..."), ]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let bytes = response.bytes().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.getBlob?space=at://did:web:happyview.example.com/space/com.example.forum/main&cid=bafyrei...", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.getBlob?space=at://did:web:happyview.example.com/space/com.example.forum/main&cid=bafyrei...' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>' \ --output image.jpgParameters:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space URI |
cid | string | Yes | The CID of the blob |
The response body is the raw blob data with the original Content-Type header preserved.
Listing blobs
Section titled “Listing blobs”listBlobs lists the CIDs of the blobs a repo’s records reference.
const response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.listBlobs?space=at://did:web:happyview.example.com/space/com.example.forum/main&repo=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();// data.cids — array of blob CIDsconst response = await fetch( "https://happyview.example.com/xrpc/com.atproto.space.listBlobs?space=at://did:web:happyview.example.com/space/com.example.forum/main&repo=did:plc:author", { headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, }, },);const data = await response.json();let response = client .get("https://happyview.example.com/xrpc/com.atproto.space.listBlobs") .query(&[ ("space", "at://did:web:happyview.example.com/space/com.example.forum/main"), ("repo", "did:plc:author"), ]) .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .send() .await?;let data: serde_json::Value = response.json().await?;clientKey := "hvc_..." // your API client keyaccessToken := "..." // DPoP access tokendpopProof := "..." // DPoP proof JWT for this request
req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/com.atproto.space.listBlobs?space=at://did:web:happyview.example.com/space/com.example.forum/main&repo=did:plc:author", nil)req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl 'https://happyview.example.com/xrpc/com.atproto.space.listBlobs?space=at://did:web:happyview.example.com/space/com.example.forum/main&repo=did:plc:author' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>'Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
space | string | Yes | The space URI |
repo | string | Yes | The DID of the repo whose blobs to list |
limit | integer | No | Max number of CIDs to return (default 500, max 1000) |
cursor | string | No | Cursor from a previous response |
Response:
{ "cids": ["bafkrei..."], "cursor": "..."}cursor is present when there are more CIDs to fetch.
Cross-service access
Section titled “Cross-service access”A service that is not a member, such as a syncer or feed generator, reads records with a space credential in place of an OAuth session. The credential goes in an Atproto-Space authorization header, alongside an Atproto-Space-Audience header and an HTTP Message Signature by the credential’s key:
curl 'https://happyview.example.com/xrpc/com.atproto.space.getRecord?space=at%3A%2F%2Fdid%3Aweb%3Ahappyview.example.com%2Fspace%2Fcom.example.forum%2Fmain&collection=com.example.forum.post&rkey=3k2abc' \ -H 'Authorization: Atproto-Space <credential>' \ -H 'Atproto-Space-Audience: did:plc:author' \ -H 'Signature-Input: atproto-space=("authorization" "atproto-space-audience")' \ -H 'Signature: atproto-space=:<base64 signature>:'A credential grants read access to every repo in the space, and never grants write access. See Using a credential for the audience each method expects and full examples.
© HappyProto

