CEEJAYDocs

Authentication

Every request is authenticated with a scoped secret key sent as a bearer token.

Authorization header
Authorization: Bearer ceejay_sk_4f8a2c...

Write Bearer, with a capital B, then a space, then the key. The word is case-sensitive: bearer and BEARER are refused. So is a key sent in the query string, such as ?api_key=…, which CEEJAY does not read. Each of these returns 401, with a code that says what was wrong: see Authentication errors.

For example, to read a member's XP with your key. The base URL is your dashboard's URL followed by /api/v1.

Read a member's XP (cURL)
export CEEJAY_BASE_URL="https://ceejay.communitystudios.xyz/api/v1"
export CEEJAY_API_KEY="ceejay_sk_..."

curl "$CEEJAY_BASE_URL/xp/balance?discordId=409876543210987654" \
  -H "Authorization: Bearer $CEEJAY_API_KEY"

On a dedicated instance, set CEEJAY_BASE_URL to your dashboard's URL followed by /api/v1.

To see which community a key belongs to and what it may do, call Community details. It needs no scope: any valid key can call it.

Keep the key on your server

Secret keys are for servers only. Never put one in browser code, a mobile app or a game client: anyone who opens it can read the key and use everything its scopes allow. A browser on another website cannot call these requests anyway: its preflight request carries no key and is refused with 401. A game has a key of its own kind for the few requests it makes itself: see Two kinds of key.

Create a key

Open Settings → Developers → API Keys and press Generate Key.

Give the key a Name that says where it will run, such as Roblox Game Server.

Under Scopes, Full Access is ticked when the dialog opens. Tick only what the key needs instead: ticking a scope clears Full Access. A key needs at least one scope: with none ticked, Generate is switched off. Press Generate.

The dialog closes. Copy the key from the banner above the list. It is shown only once.

The list on that tab shows each active key with its name, the start of the key, its scopes and when it was last used (or Never used).

Key format

Keys are prefixed ceejay_sk_ followed by 64 hexadecimal characters. The prefix makes keys stand out in logs and gives a secret scanner a pattern to look for.

We store only a SHA-256 hash of the key. The dashboard shows the first characters (ceejay_sk_4f8a2c1...) so you can tell keys apart.

Two kinds of key

Everything on this page is about the secret key, which starts with ceejay_sk_ and stays on your server.

A game that uses Connect also has a public key, which starts with ceejay_pk_. It ships inside the game and is not a secret.

Secret keyPublic key
Starts withceejay_sk_ceejay_pk_
Where it runsOn your serverInside the game
Belongs toOne Discord serverOne game of one Discord server
Allowed requestsEvery request its scopes allowOnly the requests under "Connect: from the game" in the reference
ScopesYesNo: the allowed requests are a fixed list
Rate limitPer keyPer IP address

The two are never mixed. A request that takes a secret key returns 401 with the code invalid_api_key for a public key, and a request a game makes returns 401 with the code wrong_key_kind for a secret key.

A public key is made when a game is set up for your community, which we do for you for now. Send it the same way, as Authorization: Bearer ceejay_pk_…. What a public key can call, and what else a game stores, is on the Connect page.

Server binding

A key belongs to exactly one Discord server. You never pass a server ID: every request is automatically scoped to the key's server, and a key cannot read or write another server's data. A server can have as many keys as it needs.

A key also belongs to one CEEJAY instance, the one it was created on. It does not work on another instance.

An organisation's key

A studio that runs several communities in one organisation can use one key for all of them. The organisation's owner or an admin creates the key on the API keys tab of the organisation's settings and chooses which communities it covers: selected communities, or all communities, including any added later. Each covered community sees the key on its own API Keys page and can switch the organisation's keys off there.

Send the community each request is for in the CEEJAY-Community header, as its Discord server ID. To copy a server ID, turn on Developer Mode in Discord (User Settings → Advanced), then right-click the server and choose Copy Server ID.

Read a member's XP with an organisation's key (cURL)
curl "$CEEJAY_BASE_URL/xp/balance?discordId=409876543210987654" \
  -H "Authorization: Bearer $CEEJAY_API_KEY" \
  -H "CEEJAY-Community: 123456789012345678"

Once the community is named, the request works as that community's own key would: it is limited to that community's data, its rate limit is counted for each community separately, and it is in that community's request log. A community's own key ignores the header.

StatuscodeCause
400community_requiredAn organisation's key was sent without the CEEJAY-Community header, or with a value that is not a server ID
403community_not_coveredThe community is not in the organisation, is not covered by the key, or has turned off the organisation's keys. The message is the same in all three cases

Both count toward the key's rate limit. Revoking the key on the organisation's page revokes it in every community.

Scopes

Each key has a list of scopes. A request that requires a scope the key does not have returns 403 with the code insufficient_scope, along with the required and granted scopes. The request still counts toward the key's rate limit.

One request needs no scope at all: Community details accepts any valid key, and lists the scopes the key has.

ScopeCheckbox in the dialogAllows
members:readMembers ReadRead member profiles, XP, balances, linked accounts
members:linkMembers LinkLink an external platform account to a Discord user
xp:readXP ReadRead a member's XP and level
xp:grantXP GrantAward XP
xp:deductXP DeductRemove XP
xp:program:grantProgram XP GrantAward XP inside a community program
xp:program:deductProgram XP DeductRemove XP inside a community program
programs:readRead programsList community programs
economy:readEconomy ReadRead currency balances
economy:grantEconomy GrantCredit currency
economy:deductEconomy DeductDebit currency
quests:readQuests ReadList quests
quests:completeQuests CompleteMark a quest complete and grant its rewards, or report that a member finished a quest task
roles:manageRoles ManageList your server's roles, and grant and revoke them
messages:sendMessages SendList your channels, and send messages and embeds to them
events:emitEvents EmitEmit custom events to your webhook subscribers
shop:fulfilConfirm shop deliveriesList the shop deliveries awaiting fulfilment by your server, and mark one as delivered or failed
players:readPlayers ReadRead the players of a game and its field map: Get a player, List players and Get the field map. It also decides whether Get a member lists the game accounts a member connected through Connect
players:writePlayers WriteSend player data: Set player fields, Log player events and Propose to the field map
players:connectAct for a playerMake a connect pass, start, read and confirm connect sessions, disconnect a player, and flag a player as under age in Set player fields and Log player events. Never part of *
players:deleteDelete a player's dataDelete a player: delete the data CEEJAY stores for a player. Never part of *
*Full AccessAll of the above except players:connect and players:delete. Avoid in production.
403 Forbidden
{
  "error": "Insufficient scopes",
  "code": "insufficient_scope",
  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
  "required": ["economy:grant"],
  "granted": ["xp:grant", "xp:read"]
}

Recommended

Issue one key per integration with the narrowest scope set that works. A game server that only awards XP should have xp:grant and nothing else, so a leaked key cannot post messages or move currency.

Two scopes Full Access never gives

players:connect and players:delete are never part of *. A key has them only when they are ticked by name, as Act for a player and Delete a player's data. A Full Access key that calls a request which needs one of them returns 403 with the code insufficient_scope, like any other key without it.

The reason is what they allow. A server with a key that has players:connect can authenticate any player of a game: with a pass, a player who is already connected at the verified level is signed in without further verification. It can also end any connection, and flag a player as under age, which ends their connection. A server with a key that has players:delete can delete all stored data for a player. Most keys in use are Full Access keys, and none of them should gain these scopes automatically.

In the Generate API Key dialog, ticking a scope clears Full Access. So a key for Connect is made by ticking Act for a player and each other scope the key needs.

Authentication errors

A key that cannot be used returns 401. The code says why:

codeerrorCause
missing_api_keyMissing or invalid Authorization header. Expected: Bearer ceejay_sk_xxxNo Authorization header, or it does not start with Bearer
invalid_api_keyInvalid API key format. Expected: ceejay_sk_xxxThe key does not start with ceejay_sk_
invalid_api_keyInvalid API keyNo such key
api_key_revokedAPI key has been revokedThe key was revoked in the dashboard
api_key_expiredAPI key has expiredThe key had an expiry date and it has passed

A 401 is not counted against any key, and carries no rate limit headers.

Revoking a key

Press the delete icon at the end of the key's row in Settings → Developers → API Keys. The delete icon asks for no confirmation, and a revoked key cannot be restored. Revocation takes effect on the next request; there is no caching delay. A revoked key returns 401 with the code api_key_revoked, and it leaves the list.

Expiry

Keys made in the dashboard do not expire. A key that has been given an expiry date returns 401 with the code api_key_expired once the date has passed. Community details reports a key's expiry as key.expiresAt. For long-running integrations, rotate by creating the new key first, deploying it, then revoking the old one. The two can run side by side.

Not the same as an agent token

Tokens for AI agents start with ceejay_mcp_. They only work on the MCP endpoint, and an API key does not work there.

On this page

Community StudiosPrivacyTerms