Player data
Send fields and events about your players to CEEJAY: how a batch works, what each result means, where the data is stored, and how a player's data is deleted.
Once a player is connected to a Discord member, your server can send CEEJAY what the player does in the game. CEEJAY stores it on the member: it appears on their profile in the community's dashboard, it can be used in segments and filters, and it can earn a reward. This page is for the studio's developers. It explains the two things you can send, how a batch is checked, and what is deleted when a player asks.
Members only
Player data is stored on members of the community's server, for the players who connected. For a player who has not connected, or whose Discord account is not in the server, the response has a status that says nothing was stored. Players who have not connected explains what can be held.
What you need
- A connected player. Connect joins a player of your game to a Discord member. Nothing here creates a member or a connection.
- A secret key with
players:writeto send data, andplayers:readto read it back. Both are included in a Full Access key. To flag a player as under age, or to disconnect a player, the key needsplayers:connect, and to delete oneplayers:delete: a Full Access key includes neither. Authentication has the scopes. - A game that shares the right categories. A game shares
progressandactivity, which are part of connecting, and may sharepurchasesandlocation, which a member selects one by one. A value or an event in a category the game does not share, or the member did not consent to, is refused. - For events: the member history. It has to be switched on for the community's instance. Where it is off, Log player events returns
404withfeature: "member_history".
Fields and events
| A field | An event | |
|---|---|---|
| What it is | A value about a player that changes: a league position, a club, the day they last played | Something a player did once: won a match, entered the cup |
| Sent with | Set player fields | Log player events |
| Stored as | One current value on the member, with the older ones as history | One line in the member history |
| Safe to retry | Yes: sending the same value changes nothing | Yes: an event with the same eventId is logged only once, however often it is sent |
| Where it shows | The member's profile, the Members table and filter, segments, exports | The member's profile and timeline, segments ("did something in a game"), rewards, automations |
| Needs | A field in the field map | An event in the field map |
Send money and other private values as fields, not as events: the member history has no sensitive switch, and an event takes no free text.
Sending fields
A call carries 1 to 500 players, each named by playerId or by discordId, with up to 40 fields. The body can be up to 512 KB.
{
"players": [
{
"playerId": "p_19",
"fields": {
"league_position": 3,
"club": "Harbour Town",
"last_played": "2026-10-07T21:10:00+02:00",
"old_trophy": null
}
}
],
"dryRun": false
}- Set, not added. A value that equals the current one is not a new line of history.
nullclears a field's value. A field that is not in the object is left unchanged. - The name is matched to the name your game uses for the field first, then to the field's key, case-insensitively. A name that matches nothing is added to the field map as a proposal, and the result is
not_approved. Its value is written once the community's team approves it. - Numbers are multiplied by the field's scale and rounded to 6 places, so a scale of
0.01turns cents into dollars. A number beyond 1e15 either way is refused. - Dates are
YYYY-MM-DD, or a full time with an offset, which becomes its UTC day. - Dropdowns take your value through the field's value map, and the result has to be one of the choices. A value that is not is offered to the team, and the result is
not_a_choice. - Text is up to 500 characters. An object or a list is refused.
The player ID and name are filled from the connection itself (std.player_id and std.player_name): a call that names them gets the result managed.
Logging events
A call carries 1 to 500 events, each with an eventId of your own, an event, a player and optionally occurredAt and quantity.
{
"events": [
{ "eventId": "m-88213", "playerId": "p_19", "event": "match_won", "occurredAt": "2026-10-07T19:02:00Z", "quantity": 1 }
],
"dryRun": false
}- The
eventIdmakes a retry safe. Use an ID that names the thing that happened, such as a match number. Sending it again returns the resultduplicate. One event ID belongs to one player. - An event has to be approved in the field map before it is stored. A new name is proposed with the result
not_approved, and can be resent after approval, within 7 days. - Time.
occurredAtcan be at most 5 minutes ahead and at most 7 days back. Without it, the event is stamped when CEEJAY receives the call. - Quantity is stored only where the event's entry stores one, and is off by default.
- Rewards. An event with a reward list returns
reward: "queued", and CEEJAY's bot grants the reward shortly afterwards. See Game rewards. - A ceiling. A game can send 3,000 events a minute unless another number was set for it. Above it the whole call returns
429with the codeevent_rate_exceeded.
Play sessions. Send one event named play_session at the end of each session, with the minutes played as its quantity. A community's programs count them: sessions and hours per tester on a playtest's Results.
Batches, results and dry runs
A batch with problems still returns 200, with a result for each player or event in the order you sent them. A body that is not shaped right is refused as a whole with 400. An entry that cannot be read gets the result bad_item with a message, and the rest are processed.
Read the result of each item and act on its status. A result of Set player fields names the player by the name you used, playerId or discordId, and only by that one; a result of Log player events returns its eventId. The list of statuses is on each request's page. The ones you will meet most:
| Status | What to do |
|---|---|
written, logged, unchanged, duplicate | Nothing: it is already stored |
not_approved | Wait for the community's team to approve the field or event, then resend it |
not_connected | The player has not connected, so there is no member to store it on. Send it after they connect, or turn on holding for the game |
not_member | The player is connected, but their Discord account is not in the server. Nothing is stored |
not_consented | An event only: the member did not consent to share activity. Nothing is stored |
held | Stored separately until the player connects |
under_age | You flagged the player as under age. Nothing is stored and the connection is ended |
dryRun: true runs every check and reports what would happen to each value or event, and writes nothing: no value, no proposed field, no counted event, no held player. Use it to try a new mapping.
A batch of 500 is one request, so at the key's 60 requests a minute you can send 30,000 players a minute.
Where the data is stored
Values and events are stored for members only. A connected player whose Discord account is not in the server (never joined, or left) gets the result not_member. Nothing here creates a member profile.
A connection a member made through Connect stores the categories the member consented to. A field of another category gets the result not_consented. An event gets not_consented when the member consented to share without the activity category, because they connected before the game shared it and have not consented since on their connections page. A connection your own server made with Link an account has no stored consent and is not checked: your server is responsible for consent.
Proof levels
A connection is at the verified level when the game verified who the player is, or when your own server linked the player. It is at the basic level when the game supplied the player ID without verification: Connect explains the difference. Anyone with the game's public key can supply any ID, so a basic connection does not verify who the player really is. For that reason a player connected at the basic level does not accept fields in the purchases or location category: Set player fields reports each such field as a problem with the code needs_verified_proof, and still writes the player's other fields. progress fields are not affected, and neither are players your own server linked.
The level also decides what a device may do with a member's coins in the game loop. A basic device can read, do quests and check in, but it cannot buy or claim unless the community's team allows a basic connection to spend, and a new device has a hold period before it can spend at all.
The older value of a field stays as history. A daily job retains the 5 newest values of each field for each member, and deletes game events from the member history after 13 months.
Players who have not connected
By default a value for a player who has not connected gets the result not_connected and is not stored, because consent covers only the members of the server. Holding is a setting of the game, off by default. Where it is on:
- The cleaned values (the latest value of each field) and the approved events of a player who has not connected are held separately, up to 50,000 players for a game and 50 events for a player. Above that the result is
held_limit. - They are held for 90 days from the first time they were seen. The 90 days never restart.
- When the player connects at the verified level, the held values are written to the member through the normal checks, and the events are added to the member history with their own times. They grant no reward and start no automation. At the basic level nothing is attached: anyone could claim the ID and receive another player's spend and country on their profile. The held rows stay until their 90 days end.
- A player who was erased is never held.
Sensitive data
A field the community's team marks sensitive is stored like any other, with these restrictions:
- It is never returned to a key: Get a player leaves it out, and no scope adds it.
- It cannot be used in a segment or a filter, is not a Members column, and is not in an export.
- Team members who are not admins do not see it.
Your server may still write a sensitive field: it already has the value. A field proposed by your game is not sensitive until the team marks it.
The player ID
A game's own ID for a player is 1 to 128 characters: letters, digits and . _ : @ | + = -, starting with a letter or a digit. Always send it as a string, also when your IDs are numbers. It is compared case-insensitively and stored as sent. It travels in URLs, so an ID that contains another character is encoded the same way every time: base64url is a good choice.
Any other ID returns 400 with the code bad_player_id.
Under age
Send underAge: true on a player or an event when your game knows the player is below the age it may connect. Nothing is stored, the connection of the player is ended with the reason under_age, and anything held for the player is removed. Because it ends a connection, it needs the key to have players:connect as well as players:write: without it the whole call returns 403 with the code insufficient_scope and required: ["players:connect"], and nothing in the call is written. A call can carry at most 50 players or events with underAge: true, or it returns 400 with the code bad_request. After that Link an account refuses that member for the game with 403 and the code under_age, until the member connects again through Connect.
Ending a connection, and deleting a player
Two requests remove a player. They differ in what they take away.
| Disconnect a player | Delete a player | |
|---|---|---|
| Scope | players:connect | players:delete |
| The connection | Ended | Ended, and then deleted with its tokens and stored consent |
| The member's role for being connected | Removed | Removed |
| The values for the game's fields | Deleted from the member | Deleted, current and older |
| The player's events in the member history | Stay | Deleted, with the lines that name the player's ID |
| Rewards that were granted | Stay | Stay |
| What the member can do after | Connect again | Connect again. The ID is marked as erased, so data your game sends for it is not held again |
Neither scope is part of *: a key has them only when they are ticked by name. Both work for a game directed at children too: a studio can always disconnect and delete, while the other requests return audience_not_allowed for such a game. A member can end their own connection at any time, in the game, on their connections page, or through the community's team. If the member asks to be erased from the community, CEEJAY ends their game connections first and then deletes their data.
Limits
| What | The limit |
|---|---|
| Players in one call | 500 |
| Fields for one player in one call | 40 |
| Events in one call | 500 |
| The size of a body | 512 KB |
| Events a minute for a game | 3,000, unless another number was set for the game |
| Approved fields of the game's own | 25, besides the 14 standard fields |
| Approved events | 25 |
| Proposed fields, and proposed events | 50 each, at a time |
| Players held for a game | 50,000, and 50 events for each |
| Requests | The key's own number a minute: 60 unless set higher, at most 600 |
Related
- Set player fields and Log player events
- The field map and Get the field map
- Get a player and List players
- Game rewards
- The TypeScript SDK has a function for each of these requests.
Quests, shop and rank in your game
Show the community's quests, shop, locker, rank, daily reward and surveys in your game, and sell from its shop, with rules that keep the player's coins safe.
The field map
How the names your game sends become fields on the members of a community: the three states, the standard fields, and who approves what.

