Skip to content

Records

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 ────────────────┘

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();

Input:

FieldTypeRequiredDescription
spacestringYesThe space to write into
collectionstring (NSID)YesThe record collection
recordobjectYesThe 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.

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();

Input:

FieldTypeRequiredDescription
spacestringYesThe space to write into
collectionstring (NSID)YesThe record collection
rkeystringYesThe record key
recordobjectYesThe record data
swapRecordstringNoExpected 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).

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();

Parameters:

FieldTypeRequiredDescription
spacestringYesThe space containing the record
repostringNoThe author’s DID
collectionstring (NSID)YesThe record collection
rkeystringYesThe 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"
}
}

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();

Parameters:

FieldTypeRequiredDefaultDescription
spacestringYesThe space to list from
repostringNoFilter by author DID. Without it, an OAuth caller lists their own records and a space credential lists every repo.
collectionstringNoFilter by collection NSID
limitintegerNo50Max records to return (1-100)
cursorstringNoPagination cursor
reversebooleanNofalseList newest first. By default the oldest record comes first.
includeValuesbooleanNofalseInclude 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.

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",
}),
});

Input:

FieldTypeRequiredDescription
spacestringYesThe space containing the record
collectionstring (NSID)YesThe record collection
rkeystringYesThe record key
swapRecordstringNoExpected 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.

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();

Input:

FieldTypeRequiredDescription
spacestringYesThe space to write into
swapCommitstringNoExpected space revision (for optimistic concurrency)
writesarrayYesList of write operations

Each write operation has an action field:

ActionFieldsDescription
createcollection, value, rkey?Insert a new record. Auto-generates rkey if omitted.
updatecollection, rkey, value, swapRecord?Upsert a record.
deletecollection, 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.

swapRecord and swapCommit provide optimistic concurrency control to prevent lost updates when several writers change the same space.

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"
}

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": [...]
}

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();

Parameters:

FieldTypeRequiredDescription
spacestringYesThe space URI
didstringYesThe DID of the user to get state for

Response:

FieldTypeDescription
revstring/nullCurrent revision for this user’s repo in the space
commitobject/nullDeniable commit data: ver and rev, plus hash, ikm, sig and mac, each a bytes object {"$bytes": "<base64>"}

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 entries

Parameters:

FieldTypeRequiredDescription
spacestringYesThe space URI
didstringYesThe DID of the user whose ops to list
limitintegerNoMax number of entries to return (default 100, max 1000)
cursorstringNoPosition to start after, as {rev}:{idx} from a previous response. A bare rev skips that whole revision
excludeValuesbooleanNoIf 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.

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();

Parameters:

FieldTypeRequiredDescription
spacestringYesThe space URI
didstringYesThe DID of the user whose repo to export

The response body is a CAR v1 file with content type application/vnd.ipld.car.

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();

Parameters:

FieldTypeRequiredDefaultDescription
spacestringYesThe space URI
limitintegerNo100Max repos to return (1-1000)
cursorstringNoA 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"
}
FieldTypeDescription
reposarrayThe repos on this page, in the order their updates were sequenced. A repo updated while paging can appear again on a later page.
cursorstring?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.

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();

Parameters:

FieldTypeRequiredDescription
spacestringYesThe space URI
cidstringYesThe CID of the blob

The response body is the raw blob data with the original Content-Type header preserved.

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 CIDs

Parameters:

FieldTypeRequiredDescription
spacestringYesThe space URI
repostringYesThe DID of the repo whose blobs to list
limitintegerNoMax number of CIDs to return (default 500, max 1000)
cursorstringNoCursor from a previous response

Response:

{
"cids": ["bafkrei..."],
"cursor": "..."
}

cursor is present when there are more CIDs to fetch.

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:

Terminal window
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.