CEEJAYDocs

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:write to send data, and players:read to 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 needs players:connect, and to delete one players:delete: a Full Access key includes neither. Authentication has the scopes.
  • A game that shares the right categories. A game shares progress and activity, which are part of connecting, and may share purchases and location, 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 404 with feature: "member_history".

Fields and events

A fieldAn event
What it isA value about a player that changes: a league position, a club, the day they last playedSomething a player did once: won a match, entered the cup
Sent withSet player fieldsLog player events
Stored asOne current value on the member, with the older ones as historyOne line in the member history
Safe to retryYes: sending the same value changes nothingYes: an event with the same eventId is logged only once, however often it is sent
Where it showsThe member's profile, the Members table and filter, segments, exportsThe member's profile and timeline, segments ("did something in a game"), rewards, automations
NeedsA field in the field mapAn 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.

Set player fields (body)
{
  "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. null clears 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.01 turns 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.

Log player events (body)
{
  "events": [
    { "eventId": "m-88213", "playerId": "p_19", "event": "match_won", "occurredAt": "2026-10-07T19:02:00Z", "quantity": 1 }
  ],
  "dryRun": false
}
  • The eventId makes a retry safe. Use an ID that names the thing that happened, such as a match number. Sending it again returns the result duplicate. 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. occurredAt can 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 429 with the code event_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:

StatusWhat to do
written, logged, unchanged, duplicateNothing: it is already stored
not_approvedWait for the community's team to approve the field or event, then resend it
not_connectedThe 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_memberThe player is connected, but their Discord account is not in the server. Nothing is stored
not_consentedAn event only: the member did not consent to share activity. Nothing is stored
heldStored separately until the player connects
under_ageYou 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 playerDelete a player
Scopeplayers:connectplayers:delete
The connectionEndedEnded, and then deleted with its tokens and stored consent
The member's role for being connectedRemovedRemoved
The values for the game's fieldsDeleted from the memberDeleted, current and older
The player's events in the member historyStayDeleted, with the lines that name the player's ID
Rewards that were grantedStayStay
What the member can do afterConnect againConnect 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

WhatThe limit
Players in one call500
Fields for one player in one call40
Events in one call500
The size of a body512 KB
Events a minute for a game3,000, unless another number was set for the game
Approved fields of the game's own25, besides the 14 standard fields
Approved events25
Proposed fields, and proposed events50 each, at a time
Players held for a game50,000, and 50 events for each
RequestsThe key's own number a minute: 60 unless set higher, at most 600

On this page

Community StudiosPrivacyTerms