MenuCalling group methods

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

  • aud is the host’s service DID with the #opensocial fragment.
  • lxm is 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.