PubSub WebSocket Overview
Introduction
The Crowd Control PubSub is the backbone of our desktop app, extension, and overlay. However, it is also available to third-party applications and standalone games. It allows your application to be informed when a viewer purchases a Crowd Control effect, to which games can respond with messages of their own informing us of whether it applied successfully.
PubSub is available to use primarily over WebSocket, though an HTTP translation layer exists for clients that lack WebSocket support. Some RPC calls are available only over HTTP.
Who is this for?
The WebSocket is used by all sorts of applications, including all of our in-house ones, as well as third-party ones, such as streamer.bot, donation goal overlays, and standalone games.
However, if you are a game developer working in a popular engine, then there's a very high chance that you don't need to work with our WebSocket directly! We've worked with several talented developers to bring plugins to all your favorite game engines which do the hard work of managing logins and sessions and WebSocket listeners for you. Check out the home page for more information.
If you're still here, I hope you have some snacks! This guide extensively covers everything from connecting to the WebSocket/HTTP servers all the way to managing and responding to effects. It's written primarily for game developers and plugin authors, but authors of third-party apps and overlays will find a lot of value here as well (though be sure to check out the PubSub reference for more info on your use cases).
Before you start
Before hooking your game up to Crowd Control's production servers, you should know that some parts of the process will require some manual approval and processing from our developers. We suggest first starting out by drafting some effect ideas, trying them out in your game, and writing up a pack file.
Examples
Some examples of PubSub WebSocket implementations can be found here:
- TypeScript: example project, the official companion piece to this guide
- Java: library
- C++: library
Connecting
First you'll need to figure out if your software supports connecting to WebSockets. In most cases it should be able to, perhaps with the requirement of some third-party libraries. In the rare case of a modding platform that supports only HTTP, we offer some wrapper endpoints that keep track of recent messages for you. The HTTP wrapper is tailored exclusively to games and is not intended for general non-game projects.
To connect to the WebSocket, you'll need a WebSocket library with support for TLS 1.2 or greater and User-Agent headers.
The WebSocket address is wss://pubsub.crowdcontrol.live/. Be sure to include a custom User-Agent representing your game or plugin.
Messages
Messages sent across PubSub are simple JSON objects. For a brief introduction:
Events
Messages coming from Crowd Control are called Events or Broadcast Events. They are identified by a domain and a type, and usually contain a field payload which provides further information. For example, a Game Session Start event would look like:
{
"domain": "pub",
"type": "game-session-start",
"payload": {
"gameSessionID": "game_session-01j7cnqydbe54ne4t7rh4p3p15"
}
}Domains are used to organize events, as well as to limit who can access them. For example, to receive prv events for a streamer, you will need their authorization credentials. More on authorizing and subscribing later.
Requests
Messages going to Crowd Control are called Requests. They are identified by an action, and usually contain a field data which provides further information. In older versions of the API, the data field was required to be a JSON string instead of an object, however both are now supported.
For example, a Subscribe request would look like:
{
"action": "subscribe",
"data": {
"topics": ["pub/ccuid-01j7cnrvpbh5aw45pwpe1vqvdw"]
}
}HTTP
Whether you're primarily using the HTTP wrapper or not, you will likely end up making some HTTP requests. The HTTP API address is https://openapi.crowdcontrol.live/. All paths listed in this documentation are relative to this address.
Like PubSub, communication is facilitated through JSON. Outgoing requests to the server should include the header Content-Type: application/json. The exact shape and strucutre of the input/output objects varies across different calls; examples for each important endpoint are listed throughout the document as necessary.
Most API endpoints expect to be authorized using a user's token, though a generic app token is available for some PubSub-wrapper endpoints. Endpoints are authenticated using the header Authorization: cc-auth-token INSERT_JWT_HERE. Obtaining the relevant JWTs will be described later.
Creating an Application
Upon connecting to our WebSocket, you'll likely want to allow your user to sign in to their Crowd Control account. To do this, you will first need to create an Application for your project. (Note that some non-game projects may not need to do this; the pub subscription topic is available without an account which contains some basic info about requested effects and coins exchanged.)
An Application is an entity in our backend that determines what permissions your project is able to access on a user's account. At this time, Applications need to be created manually by our developers. Rest assured we are available almost every day and are very quick to respond. See What's Next for information on getting in touch. We'll need to know the name of your project and what permissions from the table below you'll need for your users.
For games, a standard set of scopes would be profile:read session:write session:control.
Most games will also have their application flagged "public". Public applications lack the traditional authentication secret that would be protected by a server, and instead authenticate using just the application's public ID as well as a unique token your code randomly generates per login attempt.
Scopes
The Confidential field denotes that the scope is unavailable for Applications with poor security abilities, for example locally hosted game servers.
Additionally, requesting Confidential scopes will restrict your application to interfacing with only one Game Pack. Exceptions may be granted for applications with secure web servers that requests are sent through.
| ID | Description | Conf. |
|---|---|---|
profile:read | Identify the user's profile (name, URL) | ❌ |
session:read | View information about your game sessions | ❌ |
session:write | Start and stop game sessions | ❌ |
session:control | 'rpc' action (report effect success/failure/hidden/unavailable) | ❌ |
pubsub.private:read | Subscribe to sensitive events, including monetization data | ❌ |
custom-effects:read | Read custom effects | ❌ |
custom-effects:write | Create and edit custom effects | ❌ |
token:extend | Extend tokens for longer periods of time | ✅ |
Authenticating
Once you've got your appID (and secret if applicable), you can store it in your code to use for authentication.
First, send a request to PubSub to generate an auth code for your user:
// In your code, create a JSON object like the following,
// then stringify it and send it to PubSub as a text string message!
{
"action": "generate-auth-code",
"data": {
"appID": "INSERT_ID_HERE",
// Required if your application is flagged Public
// This should be randomly generated per login attempt in two steps:
// - codeVerifier: 32 random bytes base64(url-safe)-encoded
// (`crypto.randomBytes(32).toString('base64url')`)
// - codeChallenge: codeVerifier sha256 base64(url-safe)-encoded
// (`crypto.createHash('sha256').update(codeVerifier).digest().toString('base64url')`)
// Note the codeVerifier is used in a later stage,
// so store it in a variable for now.
"codeChallenge": "FAhsS5c6dzEKL__IWg7_A1ghczNbymYWvVaZE_D8Xts",
// Required if your application is flagged Public
// Currently must be this exact string
"codeChallengeMethod": "S256",
// Optional subset of scopes granted to your Application
// if you need less than usual
"scopes": ["session:read", "session:write"],
// Optional subset of packs granted to your Application
// if you need less than usual
"packs": ["Minecraft"],
// Optionally request a Data URL-encoded image of a QR code to scan to login
"qrCode": true
}
}To which the server will respond with an application-auth-code event:
// You should receive this message as a stringified JSON object.
{
"domain": "direct",
"type": "application-auth-code",
"payload": {
"code": "SXQBJ8AF",
"url": "https://auth.crowdcontrol.live/code/SXQBJ8AF",
// If requested:
"qrCode": "data:image/png;base64,…"
}
}From here, you can present the code/URL/QR code to the user in whatever way you see fit. Once they open the URL, they will be informed of what permissions your Application is requesting to use on their account, then they will be prompted to sign in with their Crowd Control account.
When using the WebSocket, you'll receive updates when the user finishes the login process, which you can react to. HTTP users will just have to make an educated guess (poll every minute or on app focus until the 15min timeout). The WebSocket notifications are described in the block below:
WebSocket Notifications
If the user experienced an error logging in, then an application-auth-code-error event will be sent to you:
{
"domain": "direct",
"type": "application-auth-code-error",
"payload": {
"message": "Unknown error with Code request"
}
}Else, you will receive an application-auth-code-redeemed event:
{
"domain": "direct",
"type": "application-auth-code-redeemed",
"payload": {
"code": "SXQBJ8AF" // For keeping track of multiple users on one socket
}
}This signifies that you can now obtain the user's token.
Note that we don't do any cron or scheduling in the backend so we will not emit an event if 15 minutes elapse and the code expires.
To obtain the user token, send a POST to https://openapi.crowdcontrol.live/auth/application/token with your Application's ID, the generated auth code, and either your Application's secret or the code verifier generated earlier.
{
"appID": "INSERT_ID_HERE",
"code": "SXQBJ8AF",
// Required if your application IS NOT flagged Public
"secret": "INSERT_SECRET_HERE",
// Required if your application IS flagged Public
"codeVerifier": "MF35hT1BF6AnTqJuVihF10TTUIRD58Rtee6-FJpgLT0"
}You will receive in response:
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo"
}or, an error message if the code provided is valid, but the user has not yet used it to provide authorization.
The token is a JWT which represents some basic information about the user. JWTs consist of three Base64-encoded blocks separated by .; the second is the token payload, and can be decoded to:
{
"type": "user",
"jti": "jti-01J7CNSK58ZXXCKP8AE93MBGXJ",
"ccUID": "ccuid-01j7cnrvpbh5aw45pwpe1vqvdw",
"originID": "106025167",
"profileType": "twitch",
"name": "lexikiq",
"roles": ["dev"],
"app": {
"appID": "ccaid-01jp3av56v4m33njwgkh4zh1vd",
"scopes": ["profile:read", "session:read", "session:write"],
"packs": ["Minecraft"]
},
"exp": 1725928623,
"ver": "1:2"
}This token (especially the JTI) is what will allow you to perform authenticated requests so be careful how you store and use it!
Token Extending
Tokens are valid for 24 hours. If you have the token:extend scope, then you can create an extended token by sending a POST to https://openapi.crowdcontrol.live/auth/token/extend with your JTI:
{
"jti": "jti-01J7CNSK58ZXXCKP8AE93MBGXJ"
}You will receive in response:
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo"
}Subscribing
Now that you know who you are, you can start subscribing to event domains. There are several different event domains- you've already seen direct, which you're automatically subscribed to- but the main one you'll need is the streamer's pub domain. To subscribe to it, you can send this example Request shown earlier, using the ccUID from the JWT:
{
"action": "subscribe",
"data": {
"token": "INSERT_JWT_HERE",
"topics": ["pub/ccuid-01j7cnrvpbh5aw45pwpe1vqvdw"]
}
}Passing in the token is highly recommended and may become required in the future. It is currently required only when subscribing to the protected prv domain, along with the pubsub.private:read scope.
The WebSocket will then emit an Event describing which of the requested topics you successfully subscribed to:
{
"domain": "direct",
"type": "subscription-result",
"payload": {
"success": ["pub/ccuid-01j7cnrvpbh5aw45pwpe1vqvdw"],
"failure": []
}
}Obtaining an Interact Link
The Crowd Control Interact Link is the link that a streamer shares with their viewers on any platform to allow them to purchase coins and interact with the game. We recommend providing a button in your game to give the streamer their Interact Link. Note that this requires the profile:read scope.
copy(`https://interact.crowdcontrol.live/#/${jwt.profileType}/${jwt.originID}`)
// https://interact.crowdcontrol.live/#/twitch/106025167Managing a Game Session
Now that you're authenticated, you now have the power to manage the player's Game Session (if you requested session:write). When a Game Session is active, viewers will be able to purchase effects (more on how to define those elsewhere on this website), which the app will then inform you of through Effect Request events.
Note that from this point on, you'll need to impersonate an existing game to test off of or have a game pack approved and published to our servers. As long as you have an appropriate-looking JSON, we can usually get your pack privately published pretty promptly. The quickest way to reach us is joining our Discord server, selecting "Community Developer" in the onboarding questions, then posting in #cc-developer.
To manage a game session, you will make HTTP calls to our API server. Our API server, like the WebSocket, accepts data in the format of a JSON string. Requests should contain a header Authorization: cc-auth-token INSERT_JWT_HERE along with the aforementioned JSON content headers. As always, you should also be sure to include a User-Agent header.
To start a session, send a POST to https://openapi.crowdcontrol.live/game-session/start with a body of:
{
"gamePackID": "INSERT_GAME_ID",
"effectReportArgs": []
}This request requires your game's pack ID (which we'll give you when you share your JSON with us), and optionally some arguments specifying which effects are currently unavailable, which we'll ignore for now.
In response, you'll get:
{
"gameSessionID": "INSERT_SESSION_ID"
}With this completed, the session is now started! Viewers should now be able to redeem effects from the Interact Link, and your game should receive some corresponding events over the WebSocket, although of course they won't do anything yet.
You may consider different ways of presenting session management to the player. Some developers require the user to opt-in to the session every time they play from the game settings, while other developers directly ask the user every time they start up the game. Get creative, the crowd is yours to control! 😉
Closing a Game Session
When the player closes the game, you can likewise send a POST to https://openapi.crowdcontrol.live/game-session/stop with a body of:
{
"gameSessionID": "INSERT_SESSION_ID"
}The gameSessionID can be obtained from the response detailed earlier, or you can provide just an empty object {} and we'll close whatever session is active.
Listening for Effects
Now that you're subscribed to the pub domain and have an active session, you should start receiving Events for incoming effect requests.
{
"domain": "pub",
"type": "effect-request",
"payload": {
"requestID": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"effect": {
"effectID": "freeze",
"type": "game",
"name": "Can't Move",
"description": "Temporarily prohibits player movement",
"category": ["Movement"],
"duration": 15,
"__s__": true,
"image": "https://resources.crowdcontrol.live/images/Minecraft/Minecraft/icons/freeze.png"
},
"game": { "gameID": "Minecraft", "name": "Minecraft" },
"gamePack": {
"gamePackID": "Minecraft",
"name": "Minecraft",
"platform": "PC"
},
"sourceDetails": { "type": "crowd-control-test", "__s__": true },
"target": {
"ccUID": "ccuid-01gyb4k468pg3zn56ve3hx3qbg",
"name": "lexikiq",
"profile": "twitch",
"originID": "106025167",
"image": "https://static-cdn.jtvnw.net/jtv_user_pictures/b9b1d74d-190c-4787-8d8e-09eb8241e70e-profile_image-300x300.png",
"__s__": true
},
"requester": {
"ccUID": "ccuid-01gyb4k468pg3zn56ve3hx3qbg",
"name": "lexikiq",
"profile": "twitch",
"originID": "106025167",
"image": "https://static-cdn.jtvnw.net/jtv_user_pictures/b9b1d74d-190c-4787-8d8e-09eb8241e70e-profile_image-300x300.png",
"__s__": true
},
"timestamp": 1725981424721
},
"timestamp": 1725981424743
}There's a lot here, but for now we'll just focus on the Effect metadata from .payload.effect, as this contains all of the core information you'll need to handle effects.
The most important part is the effectID field. This comes from the game pack file that you wrote earlier and is what uniquely identifies each of the effects in your game.
Another important field is duration, which will be present for timed effects. It dictates how long the effect should last in seconds. Streamers are generally free to edit this value from the Crowd Control Effect Manager, so be careful not to hardcode in any durations. The value can be set to anything from 1s to 180s.
With this, you should be able to start triggering effects in your game! However, the effects will currently permanently display as "queued" and eventually get refunded, as you are not telling us the outcome of the purchase, so let's take a look at that next.
Intro to Effect Responses
Your app sends information about effects and purchases to the WebSocket using the rpc action, with further information about the call embedded within the data object payload. The session:control scope is required to use this action. Let's start by writing the base of the data payload common to all rpc calls:
{
"token": "INSERT_JWT_HERE",
"call": {
"type": "call",
"id": "GENERATE_ID_HERE",
"method": "INSERT_METHOD_HERE",
"args": []
}
}The JWT is how we identify which streamer you're sending a call for, as well as verifying that you have permission to act on behalf of the streamer.
The id is some value to uniquely identify your message in case the server needs to respond to it in some way. Internally, for this field we use uuid.v4().
The method should be "effectResponse" for communicating the outcome of a purchase, or "effectReport" for communicating if an effect should be hidden or disabled. More details on the method and args are to follow shortly.
Instant Effect Responses
Let's start writing some responses to simple non-timed effects.
For effectResponse, the args array contains just one object which looks like this:
{
"id": "GENERATE_ID_HERE",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "success",
"message": "",
"stamp": 1726072095
}The id field is another randomly generated ID, generally uuid.v4().
The request field should match the value event.payload.requestID from the invoking effect-request event. This is how we determine what effect purchase you are reporting the status of.
The status field is one of the following:
successif you were successfully able to apply the viewer's requested effectfailTemporaryif the effect cannot be applied right now but would likely work another timefailPermanentif you have no idea what the requested effect is (maybe it's for a newer version of the game?)
NOTE
Although we (and our viewers) generally expect a response to an effect ASAP, some delays are reasonable. Examples would be the game being paused, loading, or an effect being somewhat random whether it might work (such as "Delete Held Item" when the streamer is trying not to hold an item). We welcome you to sit on a request for up to 60 seconds, but at that time you should cancel it and send a failTemporary.
When delaying a request, keep in mind that the request may be cancelled prematurely by the streamer. If this occurs, you should remove the request from your internal queue system. To tell when this occurs:
You will receive an effect-failure event very similar to the effect-request event.
Likewise, if a timed effect already started but was ended prematurely by the streamer, you will receive a very similar timed-effect-update event with a .payload.status value of "end".
The message field is a message which may be displayed to the purchasing user. This is typically only filled (and recommended) when an effect has failed to activate and you wish to explain why, though note that you must always at least provide a blank string "".
The stamp field is simply a Unix timestamp in seconds representing when the arg was generated.
Examples
Putting it all together, our request looks something like this:
{
"action": "rpc",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo",
"call": {
"type": "call",
"id": "eae19101-0606-2cca-b9bd-f070647b2273",
"method": "effectResponse",
"args": [
{
"id": "eae19101-2904-7e86-ef60-23939c056e37",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "success",
"message": "",
"stamp": 1726072095
}
]
}
}
}Timed Effect Responses
After you respond success to a timed effect, you should also emit a timedBegin response. You may also emit further responses if the timed effect gets paused or cancelled.
Timed Effect Responses share a very similar payload to Instant Effect responses, with just two key differences to the args object:
- A new
timeRemainingfield is added to specify how many milliseconds remain before the effect ends. This is used in all Timed Effect Responses to keep the Crowd Control Overlay in sync with the game. At the start, this should be equal to the effect's duration (times 1000). In later responses, it should be appropriately decreased corresponding to the elapsed time. - The
statusis changed fromsuccessto one of the following:
timedBegin, emitted alongside asuccessresponsetimedPause, emitted later if (for example) the game is pausedtimedResume, emitted later if (for example) the game is unpaused
There is also a timedEnd response that you should emit when the effect ends, either on-time or prematurely. Note that you should not include timeRemaining with this response as it is assumed to be 0.
Examples
So, putting it all together, let's craft our responses. We'll start by acknowledging the effect is running successfully:
{
"action": "rpc",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo",
"call": {
"type": "call",
"id": "eae19101-0606-2cca-b9bd-f070647b2273",
"method": "effectResponse",
"args": [
{
"id": "eae19101-2904-7e86-ef60-23939c056e37",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "success",
"message": "",
"stamp": 1726072095
},
{
"id": "023bcc7b-d9b8-4993-be74-07b0d5703180",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "timedBegin",
"timeRemaining": 1500,
"message": "",
"stamp": 1726072095
}
]
}
}
}Then, 5 seconds later, the player pauses the game:
{
"action": "rpc",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo",
"call": {
"type": "call",
"id": "eae19101-0606-2cca-b9bd-f070647b2273",
"method": "effectResponse",
"args": [
{
"id": "46174548-1287-4637-8bec-c36a52fd514e",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "timedPause",
"timeRemaining": 1000,
"message": "",
"stamp": 1726075193
}
]
}
}
}Then resumes 5 seconds later:
{
"action": "rpc",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo",
"call": {
"type": "call",
"id": "eae19101-0606-2cca-b9bd-f070647b2273",
"method": "effectResponse",
"args": [
{
"id": "144aae42-09c9-44dc-afe1-35dc423196c0",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "timedResume",
"timeRemaining": 1000,
"message": "",
"stamp": 1726075197
}
]
}
}
}And finally, 10 seconds later, the effect stops:
{
"action": "rpc",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo",
"call": {
"type": "call",
"id": "eae19101-0606-2cca-b9bd-f070647b2273",
"method": "effectResponse",
"args": [
{
"id": "b678cb36-d61c-4858-b012-26769b225c65",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "timedEnd",
"message": "",
"stamp": 1726075206
}
]
}
}
}You'll notice that, although the id and stamp changed throughout, the request remained constant.
Effect Reports
Effect reports allow you to disable (grey out) or completely hide effects from the purchase menu. Some examples of when you might use this:
- During a timed effect, if it can only be used once at a time, then you might disable it
- If an effect is only supported in Multiplayer but the player selected Singleplayer, then you might hide it
- When a cutscene is active then you might disable all effects
- If an effect is not supported in the current version of the game, then you might hide it
effectReport is used similarly to effectResponse, you need to set "method": "effectReport" and supply to args an array of objects like the following:
{
"id": "GENERATE_ID_HERE",
"identifierType": "effect",
"ids": ["kill_player", "damage_player"],
"status": "menuUnavailable",
"stamp": 1726072095
}The id and stamp fields are as discussed prior.
The identifierType field determines whether what type of object you are reporting the status of:
effectfor individual Effect IDs. If not specified, this is the default.categoryfor user-facing collections of effectsgroupfor internal collections of effects
INFO
For more information on specifying categories and groups, refer to Creating a Game Pack.
The ids field contains one or more IDs corresponding to the specified ID type.
The status field declares the new state of the effect. The available options are menuVisible, menuHidden, menuAvailable, and menuUnavailable.
WARNING
Effect visibility and availability are stored separately! If you mark an effect as menuHidden and menuUnavailable then later want to allow the effect, you must mark the effect both as menuVisible and menuAvailable. Setting one will not set the other.
Example
{
"action": "rpc",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo",
"call": {
"type": "call",
"id": "eae19101-0606-2cca-b9bd-f070647b2273",
"method": "effectReport",
"args": [
{
"id": "eae19101-2904-7e86-ef60-23939c056e37",
"identifierType": "effect",
"ids": ["kill_player", "damage_player"],
"status": "menuUnavailable",
"stamp": 1726072095
}
]
}
}
}Lastly, if you remember the effectReportArgs from earlier, this object is what you can pass in there! You can use this to mark effects as unavailable or hidden from the very start of the session.
Effect Metadata
packMetadataChanged allows you to store information about the state of the game. Its primary purpose is for injecting variables into effect names and descriptions, such as Kill {{PLAYER_1}} -> Kill Yoshi. We also recommend adding some data to track progress throughout your game, as we sometimes use them for fun statistics or for tracking players during live race events, but it's totally optional.
The args field accepts an array with two items, the first a string representing the ID of your Game Pack (i.e. Minecraft) and the second an arbitrary JSON object.
Example
{
"action": "rpc",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo",
"call": {
"type": "call",
"id": "eae19101-0606-2cca-b9bd-f070647b2273",
"method": "packMetadataChanged",
"args": [
"Minecraft",
{
"speedrun_checkpoint": "enter_nether",
"player_name": "lexikiq",
"creepers_killed": 15
}
]
}
}
}Custom Effects
Basic custom effects can be uploaded to a streamer's game menu. This can be useful for testing new effects without a JSON deploy, or for generating truly custom effects at runtime. We have used it internally to allow mods to add their own effects, to allow streamers to write their own effects in node graphs, etc.
Writing Custom Effects
Before you can add custom effects, the meta object of your Game Pack JSON must define "allowCustomEffects": true, and your Application must have the custom-effects:write scope.
Custom effects are submitted via a PUT request (not POST) to https://openapi.crowdcontrol.live/menu/custom-effects (see Managing a Game Session for a refresher on expected headers). The body should be a JSON string with the ID of the game to upload effects for and the operations to perform on the custom effects. Here's an example payload using some of the available options:
{
"gamePackID": "UnityDemo",
"operations": [
{
"mode": "replace-all",
"effects": {
"give_weapon": {
"name": "Give Weapon",
"price": 100,
"description": "Gives the player a random weapon",
"quantity": {
"min": 1,
"max": 10
}
},
"explosives_everywhere": {
"name": "Blow up the world",
"price": 1000,
"description": "Spawns explosives everywhere",
"category": ["World Effects"],
"parameters": {
"options_param_id": {
"type": "options",
"name": "Explosive Type",
"options": {
"option_one": {
"name": "Drill Explosives"
},
"option_two": {
"name": "Flattening Explosives"
}
}
},
"options_hex_id": {
"type": "hex-color",
"name": "Explosive Color"
}
},
"image": "explosive.png",
"duration": {
"value": 10
},
"sessionCooldown": 0.5,
"userCooldown": 0.5
}
}
}
]
}The operations[].mode field should be one of the following:
mergeto merge the effects with the existing onesreplace-allto replace all existing effects with the new onesreplace-partialto replace only the specified effects with the new one
The operations[].effects field is a map of Effect IDs to Game Effect Definitions.
The operations field accepts up to 5 items.
A game pack cannot have more than 75 custom effects at once (subject to increases as necessary).
Deleting Custom Effects
To delete custom effects with no replacement, you can submit a POST to https://openapi.crowdcontrol.live/menu/custom-effects/delete with the aforementioned headers. A JSON string is accepted for input.
If effectIDs is omitted, all custom effects are deleted.
{
"gamePackID": "UnityDemo",
"effectIDs": ["give_weapon", "explosives_everywhere"]
}Getting Custom Effects
To view a user's current custom effects, you can submit a GET to https://openapi.crowdcontrol.live/menu/custom-effects with the aforementioned headers. A JSON string is accepted for input.
{
"gamePackID": "UnityDemo"
}A JSON string is returned as output. Note that customEffects may be null or missing if no effects are currently saved.
{
"customEffects": {
"give_weapon": {
"name": "Give Weapon",
"price": 100,
"description": "Gives the player a random weapon",
"quantity": {
"min": 1,
"max": 10
}
},
"explosives_everywhere": {
"name": "Blow up the world",
"price": 1000,
"description": "Spawns explosives everywhere",
"category": ["World Effects"],
"parameters": {
"options_param_id": {
"type": "options",
"name": "Explosive Type",
"options": {
"option_one": {
"name": "Drill Explosives"
},
"option_two": {
"name": "Flattening Explosives"
}
}
},
"options_hex_id": {
"type": "hex-color",
"name": "Explosive Color"
}
},
"image": "explosive.png",
"duration": {
"value": 10
},
"sessionCooldown": 0.5,
"userCooldown": 0.5
}
}
}Ping/Pong & Reconnecting
Due to some limitations of our serverless provider, the WebSocket can be prone to disconnecting. Here's a few tips to keep it open:
- The socket will disconnect after 10 minutes inactivity. You can thus send
{"action":"ping"}9 minutes after the last exchanged message to avoid this. You should reconnect the socket if you don't receivepongwithin 15 seconds afterwards. - The socket will disconnect 2 hours after connection. You should reconnect to the socket at this time; a close event is not always properly emitted so we suggest using a timer.
Upon reconnecting to the socket, you should extend your token if necessary, then re-subscribe to your topics.
Logging Out
To add a "Log Out" option to your software, you can have a button send an authorized DELETE request to /auth/token. No payload is necessary beyond the auth headers; the server should respond 200 OK and the token will be invalidated.
What's Next?
If you've got this far, then you should have everything you need to integrate Crowd Control into your game! To get in contact with us, we offer a few options:
- For immediate support, we have a Discord server
- To request credentials or submit new games/updates, head to #cc-help-n-questions and open a Private Ticket
- Once you do this we'll also grant you access to our #cc-developers chat for further support
- If email is more your speed, we can also be reached at
developer@crowdcontrol.live - Developer portal coming soon
When requesting credentials, use the /linkaccount command on the Discord to connect your Crowd Control account so we can grant you access to connecting to the game while it's private. Feel free to share the usernames of other users you would like to have access as well, or if you would like to move towards a public release.
Plugin developers, please share with us some links to your plugin so we can promote them!
If there's some further functionality you're looking for that you can't find here, such as getting events for coin purchases or more information on what data you can get from effects, please consult the PubSub reference! If you still can't find what you're looking for, reach out and we'll do our best to help you out.
