junjo.members
The members namespace operates on Member rows: a member is one user’s relationship to one group. The same external user id can be a member of many groups within the same game; each membership is a separate Member row.
get(groupId, userId)
Fetches a single member by the dev’s external user id within a group. Returns Member | null: the SDK turns the server’s 404 not_found response into null so callers can branch on if (member) without a try/catch. Other errors throw JunjoError.
const member = await junjo.members.get("grp_xyz" as GroupId, "user_alice" as UserId);
if (!member) {
// not in this group (or the group is soft-deleted, or cross-game)
return;
}
member.status; // "active" | "left" | "kicked" | "invited"The response includes members in any status; historical rows (left, kicked) are returned as well as active members.
Errors
| Code | Status | When |
|---|---|---|
invalid_api_key | 401 | API key missing, malformed, or revoked. Thrown. |
404 not_found is converted to null; it is not thrown.
See also
GET /v1/groups/:id/members/:userId- the underlying HTTP route.
getById(id)
Fetches a single member by the Member.id directly. Returns Member | null (404 -> null, other errors throw).
const member = await junjo.members.getById("mem_xyz" as MemberId);Useful when you have already cached a member id (e.g. from a webhook event payload) and want to refresh the row.
Errors
| Code | Status | When |
|---|---|---|
invalid_api_key | 401 | API key missing, malformed, or revoked. Thrown. |
See also
GET /v1/members/:id- the underlying HTTP route.
list(groupId, opts?)
Returns a single page of members in a group, sorted by joinedAt descending. Pagination is cursor-based; pass the nextCursor from the previous page to fetch the next.
let cursor: string | null | undefined;
do {
const page = await junjo.members.list("grp_xyz" as GroupId, {
limit: 50,
cursor: cursor ?? undefined,
});
for (const member of page.items) {
console.log(member.userId, member.status);
}
cursor = page.nextCursor;
} while (cursor);V1 returns members in every status (active, left, kicked, invited); the caller filters client-side. A ?status= filter can land later as an additive change.
Options
| Field | Type | Default | Notes |
|---|---|---|---|
limit | number | 50 | 1-100 inclusive. |
cursor | string | undefined | The nextCursor from a previous list call. |
Returns
Page<Member>:
| Field | Type | Notes |
|---|---|---|
items | Member[] | Up to limit members, ordered by joinedAt desc with id desc as a tiebreaker. |
nextCursor | string | null | The id of the last item, or null if this is the last page. |
Errors
| Code | Status | When |
|---|---|---|
bad_request | 400 | limit out of range, or cursor does not point at a member of this group. |
not_found | 404 | No group with that id in the calling game (soft-deleted and cross-game collapse here). |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
GET /v1/groups/:id/members- the underlying HTTP route.
listForUser(userId, opts?)
Returns every membership a user has within the calling game. Returns a bare Member[] (no pagination wrapper) capped at 1000 rows. A user with no ExternalIdentity for this game returns [].
const memberships = await junjo.members.listForUser("user_alice" as UserId);
for (const m of memberships) {
console.log(m.groupId, m.status);
}Options
| Field | Type | Default | Notes |
|---|---|---|---|
gameId | GameId | the calling game | Optional. Must equal the calling key’s game; mismatches return 400. Provided for forward-compatibility with cloud-only admin tooling. |
Returns
Member[]. All rows carry the same userId (the value supplied to listForUser). Soft-deleted groups are excluded.
Errors
| Code | Status | When |
|---|---|---|
bad_request | 400 | gameId does not match the calling game. |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
GET /v1/users/:userId/members- the underlying HTTP route.
setMetadata(groupId, userId, metadata)
Replaces a member’s metadata wholesale. The supplied object replaces whatever was stored; there is no deep merge. Returns the updated Member.
const updated = await junjo.members.setMetadata(
"grp_xyz" as GroupId,
"user_alice" as UserId,
{ rank: "officer", joinedRaid: "2026-04-01" },
);The server treats every supplied metadata as a change (it does not deep-equal against the stored row, since jsonb does not reliably preserve key order). A PATCH that supplies the same metadata as the stored row still writes a member.metadata.updated audit entry. Pass an empty object ({}) to clear all keys.
Errors
| Code | Status | When |
|---|---|---|
not_found | 404 | Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group. Thrown. |
invalid_api_key | 401 | API key missing, malformed, or revoked. Thrown. |
See also
PATCH /v1/groups/:id/members/:userId- the underlying HTTP route.
overridePermission(groupId, userId, permission, grant)
Sets or updates a member-level permission override. Returns the MemberPermissionOverride row.
// Allow Alice to kick others, regardless of her roles' permissions.
await junjo.members.overridePermission(
"grp_xyz" as GroupId,
"user_alice" as UserId,
"guild.kick" as PermissionKey,
true,
);
// Block Bob from inviting members, even if his role allows it.
await junjo.members.overridePermission(
"grp_xyz" as GroupId,
"user_bob" as UserId,
"guild.invite_member" as PermissionKey,
false,
);The override wins over any role-derived grant during permission resolution (see Permissions). Idempotent: setting the same grant value twice returns the existing override unchanged.
The permission key is auto-registered into the per-game catalog on first sight (the same PermissionDef table populated by roles.grantPermission). Permission keys are 1-128 characters.
Errors
| Code | Status | When |
|---|---|---|
bad_request | 400 | grant missing or non-boolean, or permission is empty / over 128 characters. |
not_found | 404 | Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group. |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
POST /v1/groups/:id/members/:userId/permissions/:permission- the underlying HTTP route.
clearPermissionOverride(groupId, userId, permission)
Clears a member-level permission override. Returns void. Idempotent: clearing a permission the member has no override for is a no-op.
await junjo.members.clearPermissionOverride(
"grp_xyz" as GroupId,
"user_alice" as UserId,
"guild.kick" as PermissionKey,
);After clearing, the member’s effective permission for this key falls back to whatever their roles dictate.
Errors
| Code | Status | When |
|---|---|---|
not_found | 404 | Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group. The override’s existence is not validated; an unknown permission key collapses to a no-op. |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
DELETE /v1/groups/:id/members/:userId/permissions/:permission- the underlying HTTP route.
listPermissionOverrides(groupId, userId)
Returns every permission override on a member. Bare array (no pagination wrapper), sorted by permission ascending.
const overrides = await junjo.members.listPermissionOverrides(
"grp_xyz" as GroupId,
"user_alice" as UserId,
);
for (const o of overrides) {
console.log(o.permission, o.grant ? "granted" : "revoked");
}A member with no overrides returns [].
Returns
MemberPermissionOverride[]. Each entry carries groupId, userId, permission, grant, setAt, and setBy (always null).
Errors
| Code | Status | When |
|---|---|---|
not_found | 404 | Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group. |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
GET /v1/groups/:id/members/:userId/permissions- the underlying HTTP route.
assignRole(groupId, userId, roleId)
Assigns a role to a member. Returns the updated Member. Idempotent: assigning a role the member already has returns the unchanged member.
const updated = await junjo.members.assignRole(
"grp_xyz" as GroupId,
"user_alice" as UserId,
"role_officer" as RoleId,
);
updated.roles; // includes "role_officer"The role must belong to the same group as the member. Passing a role id that exists in a different group throws JunjoError with code role_group_mismatch (status 400). Passing a role id that does not exist at all throws not_found (status 404).
A member in any status (active, left, kicked, invited) can have roles assigned; the SDK does not gate on status. Whether to assign roles to non-active members is a product call left to the caller.
Errors
| Code | Status | When |
|---|---|---|
role_group_mismatch | 400 | The role exists but belongs to a different group than the member. |
not_found | 404 | Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group, or no Role row at all. |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
POST /v1/groups/:id/members/:userId/roles/:roleId- the underlying HTTP route.
removeRole(groupId, userId, roleId)
Removes a role from a member. Returns the updated Member. Idempotent: if the member does not have the role assigned (whether the role exists in another group, exists but is unassigned, or does not exist at all) returns the unchanged member.
const updated = await junjo.members.removeRole(
"grp_xyz" as GroupId,
"user_alice" as UserId,
"role_officer" as RoleId,
);
updated.roles; // does not include "role_officer"Other role assignments on the same member are preserved.
Errors
| Code | Status | When |
|---|---|---|
not_found | 404 | Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group. The role’s existence is not validated; an unknown role id is a no-op. |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
DELETE /v1/groups/:id/members/:userId/roles/:roleId- the underlying HTTP route.
setNotes(groupId, userId, input)
Updates the officer notes attached to a member. Both fields are diffed per-field against the stored row: supplying the same value as stored is a no-op for that field (no DB write, no audit entry, member returned unchanged).
await junjo.members.setNotes("grp_xyz" as GroupId, "user_alice" as UserId, {
notesPublic: "great healer",
notesPrivate: "do not promote yet",
});Notes are scoped per-member (not per-game). notesPublic is conventionally visible to other group members in the dev’s UI; notesPrivate is officer-only (“don’t promote, has been late on raids”). Junjo stores them verbatim and never branches on them.
Pass null to either field to clear it. Omitting a field leaves it untouched.
// clear public notes; leave private notes alone
await junjo.members.setNotes("grp_xyz" as GroupId, "user_alice" as UserId, {
notesPublic: null,
});Input
| Field | Type | Notes |
|---|---|---|
notesPublic | string | null | undefined | Up to 5000 chars. null clears. Omit to leave alone. |
notesPrivate | string | null | undefined | Same rules as notesPublic. |
Returns
Member (the post-update wire format).
Errors
| Code | Status | When |
|---|---|---|
bad_request | 400 | Input has neither notesPublic nor notesPrivate, or one of them is over 5000 characters. |
not_found | 404 | Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group. |
invalid_api_key | 401 | API key missing, malformed, or revoked. |
See also
PATCH /v1/groups/:id/members/:userId- the underlying HTTP route.