Link an account
Connect a member's ID in your game, or on another platform, to their Discord ID.
POST /v1/members/link · requires members:link
Once an account is linked, activity on your platform can be tied to the community member, and you can name the member by playerId in Complete a quest task.
Body parameters
Prop
Type
Deprecated names that still work
| Send | Deprecated alias, still accepted |
|---|---|
playerId | platformUserId |
playerName | platformUsername |
Send one of the two. If both are sent, the deprecated one is used. Deprecated names remain supported for as long as version 1 exists, and an error message names the one you sent.
Returns
Prop
Type
The response also includes the original field names. They contain the same values, and remain for as long as version 1 exists.
| Deprecated field, still sent | Same value as |
|---|---|
id | connectionId |
user_id | discordId |
platform_user_id | playerId |
platform_username | playerName |
is_active | active |
connected_at | connectedAt |
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/members/link \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "409876543210987654",
"platform": "roblox",
"playerId": "88213004",
"playerName": "novaplays"
}'The TypeScript example uses ceejay.members.link from the TypeScript SDK. It returns the data of the response below.
{
"success": true,
"data": {
"id": 311,
"server_id": "721104938271003842",
"user_id": "409876543210987654",
"platform": "roblox",
"platform_user_id": "88213004",
"platform_username": "novaplays",
"is_active": true,
"connected_at": "2026-10-01T09:14:22.118Z",
"connectionId": "311",
"discordId": "409876543210987654",
"playerId": "88213004",
"playerName": "novaplays",
"active": true,
"connectedAt": "2026-10-01T09:14:22.118Z",
"duplicate": false
}
}Known platforms
platform must name a platform that is known. There are three ways for it to be:
- CEEJAY knows it by name. These are
github,twitch,steam,twitter,xbox,playstation,metamask,epic,youtube,riot,battlenet,roblox,reddit,kick,patreon,tiktok,bluesky,lastfmandspotify. An X account is linked astwitter, and a wallet asmetamask. - Your community added it as a custom platform. Someone on its team presses Add Platform under Settings → Developers → Platforms and saves the platform in API Link mode. The value to send is the platform's Slug, the name it is stored under, shown on its card there: lower-case letters, digits and underscores, up to 50 characters.
- Your community already has at least one link for it, so a name you link under today keeps working.
Community details lists the first two under platforms. Any other name returns 400 with the code unknown_platform, and nothing is linked. This stops a typing mistake from becoming a platform of its own.
The name is trimmed and put in lower case before it is checked, so Twitch and twitch are the same platform. It can contain letters, digits and underscores, and nothing else.
A platform that is a game
On an instance that offers Connect, a game set up for your community is a platform from the day it is created: send the game's slug as platform. Use it when your players signed in to your game with Discord, so you already know which member each player is.
A link under a game's slug is a game connection at the verified level, vouched for by your server. Some things differ from any other platform:
playerIdhas to be an ID a game can use: at most 128 characters, starting with a letter or a digit, then letters, digits and. _ : @ | + = -. Any other ID returns400with the codebad_player_id.- The game has to be one your key can use. A paused game returns
409with the codegame_paused, a key limited to other games403withgame_not_allowed, a game made for children403withaudience_not_allowed, and a game that was removed404withgame_not_found. - A member who ended their connection to the game themselves cannot be linked to it again by a key:
409with the codemember_disconnected. The restriction clears when the member connects again. - A member whose connection to the game ended because the player is under age cannot be linked to it again by a key either:
403with the codeunder_age. This request does not include age information, so it cannot indicate that this has changed. The restriction clears when the member connects again through Connect. - A player the community blocked cannot be linked:
409with the codeplayer_blocked. - A connection the member made themselves through Connect is protected the way a sign-in is: a call that names a different
playerIdreturns409with the codelinked_by_sign_in. - The member is shown no page and gives no consent here. The game gets no player session for them until they have consented once through Connect.
- The community's own reward for linking an account is not granted for a game. CEEJAY's bot processes the connection instead, shortly after the response, as it does any connection to the game: it sends
player.connected, and where the connection meets the proof the game requires, it also adds theaccount.linkedentry (once for each member and game), releases rewards that were waiting for a linked account of this game, and grants the game's own reward for connecting, if one is set up. See Game rewards.
Every other platform name is handled as before.
The member has to be in your server
A link is made for a member of your server only. A member CEEJAY already has a profile for passes. An ID it has no profile for is looked up in Discord first:
- A member of your server passes.
- An ID that is not in your server returns
404with the codemember_not_found. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying. - When the Discord API is unavailable, the call returns
503with the codediscord_unavailable. Retry shortly.
Nothing is linked in the last two cases. If your game links an account the moment a player joins your server, be ready for one 404 and retry after 15 seconds.
The ID
playerId is trimmed before anything else: spaces at either end are removed, and an ID that is empty after that is rejected. Capitals and spaces at the ends are ignored whenever CEEJAY looks up the member an account is linked to, so 0xAbC and 0xabc are one account, as they are in Complete a quest task.
Links a key cannot change
Two cases return 409, and nothing is changed:
- The member linked the platform by signing in. A member who connected an account themselves, by signing in to the platform, has proved it is theirs. While that link is active, a call that names a different
playerIdfor that member and platform returns the codelinked_by_sign_in. Capitals and spaces at the ends do not make an ID different. Sending the same ID again is fine, and updates the name: the link keeps the capitalisation the member signed in with, andplayerIdin the response is the stored value. Once the member has disconnected the account, the platform can be linked again through the API. That link is a new one made by your key, even when it names the same account: it does not restore the member's sign-in. A link made by signing in before version 1.0.0, on a platform whose sign-in gives CEEJAY no token (Steam, wallets and Bluesky), cannot be distinguished from one made through the API and can still be replaced. - Another member has the account. An account belongs to one member of your server at a time. A
playerIdthat another member has an active link for, on the same platform, returns the codeplayer_already_linked. A link that was disconnected does not block this. Concurrent calls that send one account for two members are processed one at a time: the first links it, and the second gets this error. A member who already has an active link for that account can always send it again: their link is updated as before.
An account moves from one member to another only after the first member's link is disconnected. The API has no request that disconnects a link. A link the API points at a different account no longer carries the old account's sign-in, and neither does a link the API makes again after the member disconnected it. The member signs in to the platform again to connect it that way.
What a link does
- A member has one link per platform. Linking the same platform again replaces the ID, keeps the existing name if you send none, and reactivates the link if it had been disconnected.
- The link shows in the member's
connectionsin Get a member. - The first time a member links a platform, subscribers to the
account.linkedwebhook get an event. - To name a member by an X handle, a wallet, a GitHub login or a Twitch login in Complete a quest task, link that account under the platform
twitter,metamask,githubortwitch. - Rewards that were waiting for this linked account are fulfilled now. If your community grants a reward for linking an account, it is granted once per member per platform.
- For a platform that is a game, the reward for linking an account is not granted and the rest works differently: see A platform that is a game.
Retrying safely
Send an idempotencyKey with every call you might need to retry, such as after a timeout. It is a string of 1 to 128 characters that you choose, sent in the JSON body: an Idempotency-Key header is not read.
- Retrying with the same key returns the original response. When the same request is sent again with its key, CEEJAY returns the original response, with
duplicate: trueand the headerIdempotent-Replayed: true. - Each key is tied to one request payload. The same request means the same member, platform, ID and name. Two requests for one event need two keys, for example
match-9:coinsandmatch-9:xp. A key that was used for a different request, here or on any other request of the API, returns409with the codeidempotency_key_reused, and nothing is changed. - A rejected request does not use up the key. A call that was rejected can be retried with the same key once the cause is fixed.
- A key belongs to the API key that sent it. Another API key of your community can send the same text, and it is a different key.
- Keys are retained for 30 days. Use a value you can regenerate after a crash, such as an order ID or a match ID.
- On an instance that does not support idempotency keys yet, a call that sends one returns
503with the codeidempotency_unavailable, and nothing is changed. Community details reports whether keys are supported, underfeatures.retryKeys.idempotency_keyis not accepted on this endpoint; useidempotencyKey. - Without a key, a retried call is a new call.
Linking is also safe to repeat without a key: the same link sent twice updates the link it made.
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 error too |
400 | bad_request | discordId must be a valid Discord snowflake (17-20 digits) | discordId is missing or is not a string of 17 to 20 digits. A JSON number is rejected |
400 | bad_request | platform is required | platform is missing, empty or not text |
400 | bad_request | playerId is required | playerId is missing, is not text, or is empty once its spaces are removed |
400 | bad_request | platformUserId is required | The same check, when you sent the ID as platformUserId. Every message about the ID or the name uses the name you sent |
400 | bad_request | playerName must be a string | playerName was sent and is not text. A number is rejected |
400 | bad_request | platform cannot exceed 50 characters | platform is longer than 50 characters, as sent or once it is put in lower case |
400 | bad_request | playerId cannot exceed 255 characters | ID over 255 characters |
400 | bad_request | playerName cannot exceed 255 characters | Name over 255 characters |
400 | bad_request | platform cannot contain a null character | platform contains a null character (U+0000), which cannot be stored |
400 | bad_request | playerId cannot contain a null character | A null character in the ID |
400 | bad_request | playerName cannot contain a null character | A null character in the name |
400 | bad_request | platform can only contain letters, digits and underscores | platform contains a space, a hyphen or any other character |
400 | bad_request | idempotencyKey must be a string of 1-128 characters | The key is empty, longer than 128 characters or not text, or contains an unpaired surrogate, such as the JSON "\ud800" |
400 | bad_request | idempotencyKey cannot contain a null character | The key contains a null character (U+0000) |
400 | unknown_platform | Unknown platform. Add it as a custom platform under Settings, Developers, Platforms, then link again | The platform is not one CEEJAY knows by name, not a custom platform of your community, and one your community has no link for: see Known platforms |
403 | community_not_active | This community is no longer active on CEEJAY | The platform is a game, and the community removed CEEJAY from its server |
403 | game_not_allowed | This API key is limited to other games | The platform is a game, and the key is limited to other games of the community |
403 | audience_not_allowed | A game made for children cannot connect players or send player data | The platform is a game that is set up as made for children |
403 | under_age | This player cannot be connected | The platform is a game, and this member's connection to it ended because the player is under age. Only the member can lift this, by connecting again through Connect |
409 | game_paused | This game is paused | The platform is a game that is paused or suspended |
404 | game_not_found | Game not found | The platform is the slug of a game that was removed. Nothing is linked under it again |
400 | bad_player_id | playerId must be 1 to 128 characters: letters, digits and . _ : @ | + = -, starting with a letter or a digit | The platform is a game, and playerId is longer than 128 characters or contains a character outside the allowed set for a game's IDs. The body includes field |
404 | member_not_found | Member not found | CEEJAY has no profile for the ID, and it is not a member of your server. Nothing was changed. A member who has only just joined can take up to 15 seconds to be found: wait 15 seconds before retrying |
404 | not_available | This is not available on this instance yet. | This feature is not available on this instance yet. Community details reports the same under features. The body includes feature |
409 | linked_by_sign_in | The member linked this platform by signing in, so only they can change it | The member linked this platform themselves by signing in, the link is active, and the call names a different playerId |
409 | player_already_linked | This playerId is already linked to another member in this server | Another member of your server has an active link for this playerId on this platform |
409 | player_blocked | This player cannot be connected until the community lifts a block | The platform is a game, and the community blocked this player |
409 | member_disconnected | The member disconnected this game themselves, so only they can connect it again | The platform is a game, and this member ended their connection to it themselves. Only they can connect it again |
409 | idempotency_key_reused | This idempotencyKey was already used for a different request | Your API key already used this idempotencyKey for a different request: a request of another kind, or this one with other values. Nothing was changed |
409 | idempotency_in_progress | The first request with this idempotencyKey is still running. Try again in a few seconds. | The first request with this key is still being processed. Wait retryAfter seconds and retry |
503 | discord_unavailable | Could not check with Discord that this ID is a member of the server, so nothing was changed | CEEJAY has no profile for the ID and could not confirm with Discord whether it is a member of your server. Retry with a pause between attempts. If it keeps failing, CEEJAY's bot may no longer be in your server |
503 | idempotency_unavailable | idempotencyKey cannot be used on this community yet, so nothing was changed | You sent a key, and this instance does not support idempotency keys yet. Nothing was changed. Community details reports it as features.retryKeys |
The checks run in that order, and nothing is linked when one of them fails.
Every error body also includes requestId, and a 400 with the code bad_request identifies the invalid field in field. Match on code; message text can change.
The errors every request shares (401, 403, 405, 429, 500) are on the Errors page.

