SDKmembers

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

CodeStatusWhen
invalid_api_key401API key missing, malformed, or revoked. Thrown.

404 not_found is converted to null; it is not thrown.

See also

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

CodeStatusWhen
invalid_api_key401API key missing, malformed, or revoked. Thrown.

See also

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

FieldTypeDefaultNotes
limitnumber501-100 inclusive.
cursorstringundefinedThe nextCursor from a previous list call.

Returns

Page<Member>:

FieldTypeNotes
itemsMember[]Up to limit members, ordered by joinedAt desc with id desc as a tiebreaker.
nextCursorstring | nullThe id of the last item, or null if this is the last page.

Errors

CodeStatusWhen
bad_request400limit out of range, or cursor does not point at a member of this group.
not_found404No group with that id in the calling game (soft-deleted and cross-game collapse here).
invalid_api_key401API key missing, malformed, or revoked.

See also

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

FieldTypeDefaultNotes
gameIdGameIdthe calling gameOptional. 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

CodeStatusWhen
bad_request400gameId does not match the calling game.
invalid_api_key401API key missing, malformed, or revoked.

See also

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

CodeStatusWhen
not_found404Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group. Thrown.
invalid_api_key401API key missing, malformed, or revoked. Thrown.

See also

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

CodeStatusWhen
bad_request400grant missing or non-boolean, or permission is empty / over 128 characters.
not_found404Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group.
invalid_api_key401API key missing, malformed, or revoked.

See also

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

CodeStatusWhen
not_found404Group 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_key401API key missing, malformed, or revoked.

See also

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

CodeStatusWhen
not_found404Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group.
invalid_api_key401API key missing, malformed, or revoked.

See also

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

CodeStatusWhen
role_group_mismatch400The role exists but belongs to a different group than the member.
not_found404Group 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_key401API key missing, malformed, or revoked.

See also

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

CodeStatusWhen
not_found404Group 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_key401API key missing, malformed, or revoked.

See also

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

FieldTypeNotes
notesPublicstring | null | undefinedUp to 5000 chars. null clears. Omit to leave alone.
notesPrivatestring | null | undefinedSame rules as notesPublic.

Returns

Member (the post-update wire format).

Errors

CodeStatusWhen
bad_request400Input has neither notesPublic nor notesPrivate, or one of them is over 5000 characters.
not_found404Group missing / soft-deleted / cross-game, or no ExternalIdentity for the user, or no GroupMember row in this group.
invalid_api_key401API key missing, malformed, or revoked.

See also