Start connect
Start a connect session from the game for one of its players, or attach that player to a code they created in Discord.
POST /v1/connect/start · for public key
Call it when a player presses Connect in your game. The response includes a link, a code and the URL of a QR image; the player approves with their Discord account on CEEJAY's connect page, and the game learns the result with Get the result. Connect explains the whole flow.
Not from your own server
This request is for the game itself, with its public key. It is limited per IP address, and a server has one IP address. From your own server, use the requests under Connect from your server with a secret key.
Body parameters
Prop
Type
Returns
Prop
Type
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/connect/start \
-H "Authorization: Bearer $CEEJAY_PUBLIC_KEY" \
-H "Content-Type: application/json" \
-d '{
"playerId": "p_19",
"playerName": "Nova77",
"proof": {
"type": "pass",
"token": "ceejay_pass_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e"
},
"returnTo": "game"
}'The TypeScript tab calls connect.start of the Connect client in the TypeScript SDK. It stores the session secret in its own storage and returns the session without it. For a code the player made in Discord, call connect.startWithCode instead.
{
"success": true,
"data": {
"sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
"status": "pending",
"url": "https://ceejay.communitystudios.xyz/connect/s/kT3v9QmZ1xY7bLw2RfA8cN",
"qrUrl": "https://ceejay.communitystudios.xyz/connect/s/kT3v9QmZ1xY7bLw2RfA8cN/qr",
"code": "K7M-2QX",
"sessionSecret": "ceejay_cs_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
"expiresAt": "2026-10-08T18:14:11.000Z",
"pollIntervalSeconds": 3,
"proof": "verified",
"alreadyConnected": false,
"member": null
}
}{
"success": true,
"data": {
"sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
"status": "needs_game_confirmation",
"url": null,
"qrUrl": null,
"code": null,
"sessionSecret": "ceejay_cs_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
"expiresAt": "2026-10-08T18:14:11.000Z",
"pollIntervalSeconds": 3,
"proof": "verified",
"alreadyConnected": false,
"member": {
"displayName": "Nova",
"avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
}
}
}What to do with the response
Store sessionId, sessionSecret and expiresAt immediately, in the same place you keep the refresh token. A phone often closes a game while its player is in the browser, and a secret kept only in memory would be lost.
Then open url in a browser, or show code with the URL to enter it at and the image at qrUrl. Never open the link in an embedded web view: Discord refuses to sign in there. Poll for the result every pollIntervalSeconds.
Proof
proof.type | proof.token | Level |
|---|---|---|
pass | A pass your own server made with Create a connect pass | verified |
firebase | The ID token Firebase Authentication gave the game for this player | verified, or basic for an anonymous sign-in |
unity | The access token Unity Player Authentication gave the game for this player | verified, or basic for an anonymous sign-in |
| not sent | basic: the game supplies an unverified ID |
For firebase and unity the token has to be for the project saved for your game, not expired, and for the playerId you send. A basic connection shows the member in the game on the device that connected. It does not verify who owns the player, so a verified connection for the same player always replaces it. A game can be set up to connect verified players only: a basic start then returns 403 with the code proof_required.
With a code
A member can start in Discord, where they get a code to enter in the game. Send it as code with the player. No new session is created: the player is attached to the member's session, the code is cleared, and member identifies whose code it was. Show that name to the player with the option to confirm or cancel before you send Confirm connect. A player who was tricked into entering someone else's code sees a stranger's name there.
The code comes from the game's button in the community's Discord server: the member presses it and gets a code that works once and lasts 10 minutes, and then enters it in the game. Where the community has not posted the button, or the code has been used or has run out, no open request has it and code returns 404 with the code code_not_found.
A player who is already connected
When the proof is verified and was issued in the last 10 minutes, and this player is already connected at the verified level with the member's own approval, nobody is asked again: status is approved and alreadyConnected is true. Get the result then returns the tokens for this device. In every other case the member is asked, also when the player is already connected: a basic claim never signs a device in without Discord's approval.
Earlier sessions
A start with verified proof ends every earlier open session for the same player. A basic start ends only earlier basic sessions for that player that came from the same IP address.
Age
A game set up for a mixed audience must send underAge with every start. true returns 403 with the code under_age and stores nothing. When the proof sent with it is verified, a connection the player has is ended. The public key alone ends nothing.
Limits
| What is counted | Limit |
|---|---|
| Starts from one IP address | 600 in 10 minutes |
| Starts with verified proof for one player | 10 in 10 minutes |
| Attempts with a code that is not taken, from one IP address | 20 in 10 minutes |
Over one of them the response is 429 with the code too_many_attempts and retryAfter. When more than 200 unknown codes are sent with one public key in 10 minutes, requests with a code for that key are throttled until the window ends: each response is delayed 2 seconds and the limit drops to 2 attempts per IP address instead of 20.
Errors
| Status | code | error | When |
|---|---|---|---|
400 | bad_request | The request body must be a JSON object | The body is null, text, a number or a list. A request with no Content-Type header is read as text, so it gets this response too |
400 | bad_player_id | playerId must be 1 to 128 characters: letters, digits and . _ : @ | + = -, starting with a letter or a digit | playerId is missing, is not text, is longer than 128 characters, or contains a character that is not allowed. A number is refused |
400 | bad_request | playerName must be text of at most 80 characters | playerName was sent and is not text, is longer than 80 characters, or contains a null character (U+0000) |
400 | bad_proof | proof must be an object with a type and a token | proof is not an object with a type of pass, firebase or unity and a token of 1 to 4,096 characters. Also when the game has no project saved for that sign-in service |
400 | bad_request | code must be text | code was sent and is not text |
400 | unknown_return | returnTo is not one of the return addresses saved for this game | returnTo is not the key of a return URL saved for the game |
400 | bad_request | locale must be a language tag such as en or pt-BR | locale was sent and is not a language tag of at most 12 characters |
400 | bad_request | underAge must be true or false | underAge was sent and is not true or false |
400 | age_answer_required | This game is for a mixed audience, so underAge must be sent as true or false | The game is set up for a mixed audience and underAge was not sent |
403 | under_age | This player cannot be connected | underAge is true. Nothing is stored, and a connection the player has is ended when the player is verified |
401 | proof_rejected | The proof of the player was not accepted | The pass or the token was not accepted. The body adds reason: signature, expired, wrong_player, wrong_project, or used for a pass or Steam ticket that was used before |
503 | proof_service_unavailable | The proof could not be checked right now. Try again | The keys of Firebase or Unity could not be fetched to check the token. Retry |
403 | proof_required | This game only connects players it has proven | The game is set up to connect only verified players, and the proof is missing or only reaches the basic level |
429 | too_many_attempts | Too many attempts. Wait and try again | More than 600 starts from one IP address in 10 minutes, more than 10 verified starts for one player in 10 minutes, or more than 20 attempts with a code that is not taken from one IP address in 10 minutes. The body adds retryAfter |
404 | code_not_found | No open request has this code | No request started by a member in Discord for this game has this code: it is incorrect, it was already used, or its 10 minutes are over |
409 | session_locked | This request was locked after too many attempts. Start again | The code belongs to an open request that was locked by this fifth failed attempt on it |
The checks run in this order: the fields, the age check, the proof, and only then the limits and the code.
Every error body also carries requestId, and a 400 with the code bad_request names the invalid field in field. Match on code, not on the message text, which can change.
The errors every request of a game shares (the key, the state of the game, the origin, 405, 429, 500) are on the Errors page.

