Calling group methods
Get service auth from the user's own PDS, then call the group host directly. The host checks the caller's roles in that group.
Every fyi.opensocial.* method (join, invite, admit, assign roles, moderate) is served by the group’s host and authorized against the caller’s roles in the group named by the group parameter. The caller proves who they are with service auth minted by their own PDS. The host never holds their credentials.
1. Service auth, from the user’s own PDS
Through the user’s OAuth session:
GET <user's PDS>/xrpc/com.atproto.server.getServiceAuth
?aud=did:web:host.opensocial.fyi%23opensocial
&lxm=fyi.opensocial.requestJoin
The response is { "token": "<jwt>" }.
audis the host’s service DID with the#opensocialfragment.lxmis the exact method you’re about to call. A token for one method is refused for another.- The PDS only mints it if the session’s scope includes
rpc:<lxm>?aud=<that audience>.
2. Call the host
Send the token as a bearer token, straight to the host (no proxy header):
POST https://host.opensocial.fyi/xrpc/fyi.opensocial.requestJoin
Authorization: Bearer <service auth token>
Content-Type: application/json
{ "group": "did:plc:idmhyhx3335jt2vin45xauu5", "message": "Hi! I ride on weekends." }
{ "status": "pending" }
Queries are GET with the parameters in the query string:
GET https://host.opensocial.fyi/xrpc/fyi.opensocial.listSubjects?group=did:plc:…&status=open
Authorization: Bearer <service auth token for fyi.opensocial.listSubjects>
A helper the reference console uses, trimmed:
async function callHost(session: OAuthSession, lxm: string, body?: unknown, params?: Record<string, string>) {
const { token } = await new Client(session).call(com.atproto.server.getServiceAuth, {
aud: "did:web:host.opensocial.fyi#opensocial",
lxm,
});
const url = new URL(`https://host.opensocial.fyi/xrpc/${lxm}`);
for (const [k, v] of Object.entries(params ?? {})) url.searchParams.set(k, v);
const res = await fetch(url, {
method: body ? "POST" : "GET",
headers: { authorization: `Bearer ${token}`, ...(body ? { "content-type": "application/json" } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
const out = await res.json().catch(() => ({}));
if (!res.ok) throw Object.assign(new Error(out.message ?? out.error), { name: out.error });
return out;
}
Common calls
| Call | Body or params | Returns |
|---|---|---|
requestJoin |
{ group, message? } |
{ status: "admitted" | "pending" }. An outstanding invite for the caller is found and redeemed automatically. |
getJoinRequest |
?group= |
{ request? }: the caller’s own pending request, absent once decided |
cancelJoinRequest |
{ group } |
{} |
listJoinRequests |
?group= (needs admit) |
{ requests, cursor? } |
admitMember |
{ group, did, decision: "admit" | "deny", roles? } |
{}; roles defaults to ["member"] |
createInvite |
{ group, invitee, message? } (needs invite) |
{ uri }, or pending:<did> if the invitee has no invites space yet. The invite still redeems when they ask to join. |
leaveGroup |
{ group } |
{} |
listSubjects |
?group=&status= (needs mod.read) |
{ subjects } |
Every method’s full input and output is in the reference and in llms-full.txt.
Errors
Errors come back as { "error": "<Name>", "message": "…" }. The host answers 400 for a refused call and 401 for bad service auth. Scope problems come earlier, from the user’s own PDS when you ask for service auth.
| Error | When |
|---|---|
AlreadyMember |
requestJoin by a member. |
InviteRequired |
requestJoin on an invite-only group without an invite. |
InviteNotFound |
Revoking an invite that doesn’t exist. |
RequestNotFound |
Admitting or cancelling a request that isn’t pending. |
NotAMember |
leaveGroup by a non-member. |
NotAssignable |
Granting, revoking or ejecting a role outside the caller’s assignable. |
LastConfigurer |
The only member able to configure the group tries to leave or give that up. |
Forbidden |
The caller’s roles lack the action. |
NotHosted |
The group isn’t on this host. |
SpaceNotFound, WellKnownSpace |
updateSpace/deleteSpace on a missing space, or on meta/members. |
SubjectNotFound |
Moderation calls on an unknown subject. |
DpopRequired, InvalidDpop |
getGroupAuth without a valid DPoP proof. |
InvalidRequest |
Malformed input, including an unknown group (message: “unknown group”). |
401 from the host |
Missing, expired or wrong-audience service auth, or a token for a different lxm. |
403 ScopeMissingError from the user’s PDS |
The session’s scope lacks rpc:<method>?aud=…. The message names the exact scope to add. See Signing in. |
Show people a plain-language message; keep error and message for your logs.