ceejay.players
Read the players of a game who are connected to a Discord member, send data about them, end a connection and delete the data CEEJAY stores for a player.
Read the players of a game who are connected to a Discord member, send data about them, end a connection and delete the data CEEJAY stores for a player.
This page is for a studio's server, once its players are connected: see Connect. The data is stored on the member the player is connected to, and only on members of the community's server. The community's team decides which of your field names are stored, in the field map: see The field map.
Every function takes the same last argument, options: signal, timeoutMs and
maxRetries, for that one call. Every function can also raise the errors any request can
raise, such as a bad key or the rate limit. See
Errors and retries.
Before you start
- The game is set up for the community first, and gets a slug. Every function here
takes it as its first argument,
game. - The key needs the permission each function names.
players:readandplayers:writeare part of a key with every permission (*).players:connectandplayers:deleteare not: tick Act for a player and Delete a player's data when you create the key.ceejay.players.setFieldsandceejay.players.logEventsalso needplayers:connectfor an entry withunderAge: true. - The limit is the key's own number of requests per minute. A call with up to 500 players or events counts as one request.
- No idempotency key. None of the six functions accepts one, so the SDK never retries
any of the four writes automatically. Retrying one yourself does no harm: setting a field
again changes nothing, events are deduplicated by
eventId, and ending a connection or deleting a player that is already gone has no further effect. - A player ID is a string of 1 to 128 letters, digits and
. _ : @ | + = -, starting with a letter or a digit. A number is never sent: the SDK throws aTypeError.
ceejay.players.get
Reads one player: the Discord member they are connected to, the member profile summary, and the game's approved fields that are not sensitive.
GET /v1/games/{game}/players/{playerId} · requires players:read · API reference: Get a player
Retried by the SDK: Yes. It is a read
Parameters: the game, the player, then options.
Prop
Type
Returns the player, typed as Schemas['PlayerDetail'].
Prop
Type
sharing has agreed, the share categories the member has consented to, and needed, the
categories the game has added since. Both are empty for a connection made by your own server.
A sensitive field is never returned, whatever permissions the key has.
Errors
| Status | code | When |
|---|---|---|
400 | bad_player_id | The player ID contains a character that is not allowed, or is too long |
403 | community_not_active | The community removed CEEJAY |
403 | game_not_allowed | The key is limited to other games |
403 | audience_not_allowed | The game is made for children |
404 | player_not_found | No active connection exists for this player. The body of the error includes held: true when CEEJAY stores data for the ID pending a connection |
404 | game_not_found | The community has no game with this slug |
404 | not_available | Connect is not enabled on this instance |
A paused game can still be read.
Example
const read = await ceejay.players.get('rugby_manager', 'p_19')
console.log(read.player.discordId, read.player.proof, read.member?.level)
console.log(read.fields['std.level'], read.fields['league_position'])ceejay.players.list
Lists the players of a game who are connected to a Discord member, newest connection first, a page at a time. A player who has not connected is not in the list.
GET /v1/games/{game}/players · requires players:read · API reference: List players
Retried by the SDK: Yes. It is a read
Parameters: the game, an optional object, then options.
Prop
Type
Returns the full response, typed as Schemas['ListPlayersResponse']. The players are in
data, and the pagination details are in page.
Prop
Type
Errors
| Status | code | When |
|---|---|---|
403 | community_not_active | The community removed CEEJAY |
403 | game_not_allowed | The key is limited to other games |
403 | audience_not_allowed | The game is made for children |
404 | game_not_found | The community has no game with this slug |
404 | not_available | Connect is not enabled on this instance |
A limit that is not a whole number, or a cursor that cannot be read, is a bad_request.
Example
let cursor: string | null = null
do {
const page = await ceejay.players.list('rugby_manager', { limit: 100, ...(cursor ? { cursor } : {}) })
for (const player of page.data) console.log(player.playerId, player.discordId, player.proof)
cursor = page.page.nextCursor
} while (cursor)ceejay.players.disconnect
Ends the connection of a player, when your server requests it. The member's connected role
is removed, the player's devices are signed out, the values CEEJAY stores for the player
are cleared from the member, and player.disconnected is sent. The member can connect
again.
POST /v1/games/{game}/players/{playerId}/disconnect · requires players:connect · API reference: Disconnect a player
Retried by the SDK: No. Calling it again yourself has no further effect: there is no connection left to end
Parameters: the game, the player, then options. It sends no body.
Prop
Type
Returns the response, typed as Schemas['DisconnectPlayerResult'].
Prop
Type
Errors
| Status | code | When |
|---|---|---|
400 | bad_player_id | The player ID contains a character that is not allowed, or is too long |
403 | community_not_active | The community removed CEEJAY |
403 | game_not_allowed | The key is limited to other games |
404 | player_not_found | No active connection exists for this player |
404 | game_not_found | The community has no game with this slug |
404 | not_available | Connect is not enabled on this instance |
A key with * does not include players:connect. This function also works for a paused
game and for a game made for children: a studio can always disconnect.
Example
import { CeejayError } from '@communitystudios/ceejay'
try {
await ceejay.players.disconnect('rugby_manager', 'p_19')
} catch (error) {
if (error instanceof CeejayError && error.code === 'player_not_found') {
console.log('This player is not connected')
} else {
throw error
}
}ceejay.players.delete
Deletes all data CEEJAY stores for one player ID in one game. Use it when a player requests deletion of their data.
POST /v1/games/{game}/players/{playerId}/delete · requires players:delete · API reference: Delete a player
Retried by the SDK: No. Calling it again yourself is safe: when there is nothing to delete, the response contains zeros
Parameters: the game, the player, then options. It sends no body.
Prop
Type
Returns the response, typed as Schemas['DeletePlayerResult'].
Prop
Type
First the connection is ended, and the bot has up to 5 seconds to remove the role and send
player.disconnected. Then the following are removed: every connection of the player in this
game, with its tokens and consent history; the member's values for the game's fields, current
and previous; the player's game events in the member history, and the history entries
that show the player connected or disconnected; the player's sessions and passes;
queued jobs for the player; and all data stored for the player if they had not connected.
Rewards that were already granted are kept, along with any block the community set on the
player and an erasure marker.
Errors
| Status | code | When |
|---|---|---|
400 | bad_player_id | The player ID contains a character that is not allowed, or is too long |
403 | community_not_active | The community removed CEEJAY |
403 | game_not_allowed | The key is limited to other games |
404 | game_not_found | The community has no game with this slug |
404 | not_available | Connect is not enabled on this instance |
A key with * does not include players:delete. This function also works for a paused
game and for a game made for children: a studio can always delete.
Example
const done = await ceejay.players.delete('rugby_manager', 'p_19')
console.log(done.removed.connection, done.removed.values, done.removed.events, done.removed.pending)ceejay.players.setFields
Sends values about your players, to be stored on the members they are connected to. Up to 500 players in one call, each with up to 40 fields.
POST /v1/games/{game}/player-fields · requires players:write · API reference: Set player fields
Retried by the SDK: No. Calling it again yourself changes nothing: a field is set, not added
Parameters: the game, one object, then options.
Prop
Type
A value is text (up to 500 characters), a number, true, false, or null to clear the
field. A field that is not in fields is left as it is. A name is matched first against the name
your game uses for the field, then against the field's key. A name that matches no field is
submitted for approval by the community's team and returns not_approved. underAge: true
on a player stores nothing and ends their connection. It also needs the players:connect
permission, and a call can contain at most 50 of them.
Returns the response, typed as Schemas['SetPlayerFieldsResult'].
Prop
Type
A batch with problems still returns a response. The status of a player is written, unchanged,
held, held_limit, not_connected, not_member, erased, under_age or bad_item.
Each item of problems has the field you sent and a code: unknown_field,
not_approved, unused, bad_value, not_a_choice, not_consented,
needs_verified_proof or managed. A player connected at the basic level does not accept
fields in the purchases or location category: each is a problem with the code
needs_verified_proof, and the player's other fields are still written. Treat the lists as
open-ended.
Errors
| Status | code | When |
|---|---|---|
400 | too_many_players | More than 500 players in one call |
403 | community_not_active | The community removed CEEJAY |
403 | game_not_allowed | The key is limited to other games |
403 | audience_not_allowed | The game is made for children |
404 | game_not_found | The community has no game with this slug |
404 | not_available | Connect is not enabled on this instance |
409 | game_paused | The game is paused |
413 | body_too_large | The body is larger than 512 KB. Send fewer players in one call |
A body with the wrong shape, such as players that is not a list, or more than 50 players
with underAge: true, returns bad_request. A call with underAge: true and a key without
players:connect returns 403 with the code insufficient_scope. A player ID that is a
number is never sent: the SDK throws a TypeError.
Example
const result = await ceejay.players.setFields('rugby_manager', {
players: [
{ playerId: 'p_19', fields: { league_position: 3, club: 'Harbour Town', last_played: '2026-10-07T21:10:00+02:00' } },
{ discordId: '409876543210987654', fields: { club: null } },
],
})
for (const entry of result.results) {
console.log(entry.index, entry.status, entry.written, entry.problems.map((problem) => `${problem.field}: ${problem.code}`))
}
console.log(result.summary.written, result.summary.refused)ceejay.players.logEvents
Logs actions your players took in the game, to be shown on the profile of the member they are connected to, counted in segments and rewarded. Up to 500 events in one call.
POST /v1/games/{game}/player-events · requires players:write · API reference: Log player events
Retried by the SDK: No. Calling it again yourself is safe: events are deduplicated by eventId
Parameters: the game, one object, then options.
Prop
Type
occurredAt can be at most 5 minutes ahead and at most 7 days back. An event name that is not
approved in the field map is not stored: it is submitted for approval and returns not_approved. The game
has to share the activity category. underAge: true on an event stores nothing and ends the
player's connection. It also needs the players:connect permission, and a call can contain at
most 50 of them.
Returns the response, typed as Schemas['LogPlayerEventsResult'].
Prop
Type
The status of an event is logged, duplicate, held, held_limit, not_connected,
not_member, not_consented, erased, under_age, not_approved, unknown_event,
too_old, in_future or bad_item. reward is queued when the event earns a reward that
CEEJAY's bot grants shortly afterwards, and none otherwise. not_consented means the member
consented to sharing but not the activity category.
Errors
| Status | code | When |
|---|---|---|
400 | too_many_events | More than 500 events in one call |
403 | community_not_active | The community removed CEEJAY |
403 | game_not_allowed | The key is limited to other games |
403 | audience_not_allowed | The game is made for children |
404 | game_not_found | The community has no game with this slug |
404 | not_available | Connect is not enabled on this instance, or member history is switched off. The body of the error includes feature: players or member_history |
409 | game_paused | The game is paused |
409 | activity_not_shared | The game does not share the activity category. The whole call is rejected |
413 | body_too_large | The body is larger than 512 KB. Send fewer events in one call |
429 | event_rate_exceeded | The game exceeded its event limit for this minute. retryAfter is the number of seconds to wait |
A call with underAge: true and a key without players:connect is 403 with the code
insufficient_scope.
Example
import { CeejayError } from '@communitystudios/ceejay'
try {
const result = await ceejay.players.logEvents('rugby_manager', {
events: [
{ eventId: 'm-88213', playerId: 'p_19', event: 'match_won', occurredAt: '2026-10-07T19:02:00Z', quantity: 1 },
{ eventId: 'm-88214', playerId: 'p_77', event: 'match_won' },
],
})
console.log(result.summary.logged, result.summary.rewardsQueued)
} catch (error) {
if (error instanceof CeejayError && error.code === 'event_rate_exceeded') {
console.log('Send these again in', error.retryAfter, 'seconds')
} else {
throw error
}
}
