Invites
Invites let space creators distribute membership tokens without knowing recipients’ DIDs in advance.
Creating an invite
Section titled “Creating an invite”Only the space’s creator or an operator with superadmin can create invites.
const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.createInvite", { 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", access: { read: true, write: true }, maxUses: 10, expiresAt: "2026-06-01T00:00:00Z", }),});interface CreateInviteResponse { inviteId: string; token: string; access: { read: boolean; write: boolean }; maxUses: number; expiresAt: string;}const data: CreateInviteResponse = await response.json();const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.createInvite", { 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", access: { read: true, write: true }, maxUses: 10, expiresAt: "2026-06-01T00:00:00Z", }),});const data = await response.json();let response = client .post("https://happyview.example.com/xrpc/dev.happyview.space.createInvite") .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", "access": { "read": true, "write": true }, "maxUses": 10, "expiresAt": "2026-06-01T00:00:00Z" })) .send() .await?;let data: serde_json::Value = response.json().await?;body := bytes.NewBufferString(`{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "access": { "read": true, "write": true }, "maxUses": 10, "expiresAt": "2026-06-01T00:00:00Z"}`)req, _ := http.NewRequest("POST", "https://happyview.example.com/xrpc/dev.happyview.space.createInvite", 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)curl -X POST 'https://happyview.example.com/xrpc/dev.happyview.space.createInvite' \ -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", "access": { "read": true, "write": true }, "maxUses": 10, "expiresAt": "2026-06-01T00:00:00Z" }'Input:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
space | string | Yes | The space this invite is for | |
access | object | No | { "read": true, "write": false } | Access granted on acceptance: read and write booleans, both required when access is given |
maxUses | integer | No | unlimited | Maximum number of times the invite can be redeemed |
expiresAt | string (datetime) | No | never | When the invite expires |
Response (201):
{ "inviteId": "uuid", "token": "a1b2c3d4e5f6...", "access": { "read": true, "write": true }, "maxUses": 10, "expiresAt": "2026-06-01T00:00:00Z"}Accepting an invite
Section titled “Accepting an invite”Any authenticated user can accept an invite token to join the space.
const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.acceptInvite", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ token: "a1b2c3d4e5f6...", }),});interface AcceptInviteResponse { uri: string; access: { read: boolean; write: boolean };}const data: AcceptInviteResponse = await response.json();const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.acceptInvite", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, "Authorization": `DPoP ${ACCESS_TOKEN}`, "DPoP": DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ token: "a1b2c3d4e5f6...", }),});const data = await response.json();let response = client .post("https://happyview.example.com/xrpc/dev.happyview.space.acceptInvite") .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {}", access_token)) .header("DPoP", &dpop_proof) .json(&serde_json::json!({ "token": "a1b2c3d4e5f6..." })) .send() .await?;let data: serde_json::Value = response.json().await?;body := bytes.NewBufferString(`{"token": "a1b2c3d4e5f6..."}`)req, _ := http.NewRequest("POST", "https://happyview.example.com/xrpc/dev.happyview.space.acceptInvite", 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)curl -X POST 'https://happyview.example.com/xrpc/dev.happyview.space.acceptInvite' \ -H 'X-Client-Key: hvc_...' \ -H 'Authorization: DPoP <token>' \ -H 'DPoP: <proof>' \ -H 'Content-Type: application/json' \ -d '{ "token": "a1b2c3d4e5f6..." }'Response (201):
{ "uri": "at://did:web:happyview.example.com/space/com.example.forum/main", "access": { "read": true, "write": true }}Acceptance fails if:
- The token is invalid (no matching hash found)
- The invite has been revoked
- The invite has reached its
maxUses - The invite has expired
- The user is already a member of the space
Revoking an invite
Section titled “Revoking an invite”const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.revokeInvite", { 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", inviteId: "uuid", }),});const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.revokeInvite", { 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", inviteId: "uuid", }),});let response = client .post("https://happyview.example.com/xrpc/dev.happyview.space.revokeInvite") .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", "inviteId": "uuid" })) .send() .await?;body := bytes.NewBufferString(`{ "space": "at://did:web:happyview.example.com/space/com.example.forum/main", "inviteId": "uuid"}`)req, _ := http.NewRequest("POST", "https://happyview.example.com/xrpc/dev.happyview.space.revokeInvite", 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)curl -X POST 'https://happyview.example.com/xrpc/dev.happyview.space.revokeInvite' \ -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", "inviteId": "uuid" }'Revoking an invite prevents future redemptions but does not remove members who already redeemed it.
Listing invites
Section titled “Listing invites”Only the space’s creator or an operator with superadmin can list invites.
const response = await fetch( "https://happyview.example.com/xrpc/dev.happyview.space.listInvites?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 Invite { id: string; access: { read: boolean; write: boolean }; maxUses: number; uses: number; expiresAt: string; revoked: boolean; createdBy: string; createdAt: string;}const data: { invites: Invite[] } = await response.json();const response = await fetch( "https://happyview.example.com/xrpc/dev.happyview.space.listInvites?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/dev.happyview.space.listInvites") .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?;req, _ := http.NewRequest("GET", "https://happyview.example.com/xrpc/dev.happyview.space.listInvites?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)curl 'https://happyview.example.com/xrpc/dev.happyview.space.listInvites?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 | Description |
|---|---|---|---|
space | string | Yes | The space to list invites for |
Response:
{ "invites": [ { "id": "uuid", "access": { "read": true, "write": true }, "maxUses": 10, "uses": 3, "expiresAt": "2026-06-01T00:00:00Z", "revoked": false, "createdBy": "did:plc:abc123", "createdAt": "2026-05-09T12:00:00Z" } ]}The token itself is never returned in list responses — only the invite metadata.

