PubSub WebSocket Overview
Introduction
The Crowd Control PubSub WebSocket 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.
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. Crowd Control plugins are available for:
If you're still here, I hope you have some snacks! This guide extensively covers everything from connecting to the WebSocket 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 (namely the actual sending and receiving effects part) 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
Connecting
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 the WebSocket 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"
]
}
}
Authenticating
Upon connection, you'll likely want to allow your user to sign in to their Crowd Control account. To do so, you'll first need to fetch the ID of the WebSocket connection, which you can do by sending a whoami
request.
{
"action": "whoami"
}
To which the server will respond with a whoami
event of its own:
{
"domain": "direct",
"type": "whoami",
"payload": {
"connectionID": "aGVsbG8hISEhISE="
}
}
Using this, you can prompt the user to open a link to authorize themselves in their browser:
const url = `https://auth.crowdcontrol.live/?connectionID=${event.payload.connectionID}`
// https://auth.crowdcontrol.live/?connectionID=aGVsbG8hISEhISE=
Once they do, you'll receive another event from the server:
{
"domain": "direct",
"type": "login-success",
"payload": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsImp0aSI6Imp0aS0wMUo3Q05TSzU4WlhYQ0tQOEFFOTNNQkdYSiIsImNjVUlEIjoiY2N1aWQtMDFqN2NucnZwYmg1YXc0NXB3cGUxdnF2ZHciLCJvcmlnaW5JRCI6IjEwNjAyNTE2NyIsInByb2ZpbGVUeXBlIjoidHdpdGNoIiwibmFtZSI6ImxleGlraXEiLCJyb2xlcyI6WyJkZXYiXSwiZXhwIjoxNzI1OTI4NjIzLCJ2ZXIiOiIxOjIifQ.3teK7TKhXUTeUedTuWig76PoEwmOKmfCBb3hkxEt_Vo"
}
}
The token is a JWT which represents some basic information about the user. The token payload is Base64-encoded, and can be decoded to:
{
"type": "user",
"jti": "jti-01J7CNSK58ZXXCKP8AE93MBGXJ",
"ccUID": "ccuid-01j7cnrvpbh5aw45pwpe1vqvdw",
"originID": "106025167",
"profileType": "twitch",
"name": "lexikiq",
"roles": ["dev"],
"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!
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"
]
}
}
Note that you will also need to pass in the user's JWT token to subscribe to their protected endpoints, like the prv
domain.
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.
copy(`https://interact.crowdcontrol.live/#/${jwt.profileType}/${jwt.originID}`)
// https://interact.crowdcontrol.live/#/twitch/106025167
Managing a Game Session
Now that you're authenticated, you now have the power to manage the player's Game Session. 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 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 an Authorization
header with the former cc-auth-token INSERT_JWT_HERE
. 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 some JSON (you may need Accept: application/json
header) along the lines of:
{
"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 opt to 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
Information about effects and purchases are sent down the WebSocket using the rpc
action, with further information about the call embedded within the data
object payload. 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 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 to follow in the upcoming sections.
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()
.
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:
success
if you were successfully able to apply the viewer's requested effectfailTemporary
if the effect cannot be applied right now but would likely work another timefailPermanent
if 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. For example, if you have an effect which the viewer would expect to be somewhat random whether it might work (such as "Delete Held Item" when the streamer is trying not to hold an item), then we welcome you to sit on the effect for up to 60 seconds. If you still can't execute the effect in that time then we recommend you cancel it and send a failTemporary
.
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
Timed responses are much alike instant responses. In fact, if the effect failed to apply, then this whole section is irrelevant; just return one of the fail statuses. However, if the effect succeeded, then you will need to note two key differences to the arg object:
First, a new timeRemaining
field 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.
Second, the status
is changed from success
to timedBegin
.
That's not quite all, however. There are also several unique respons statuses you can (and in some cases, should) send after your initial timedBegin
response:
timedPause
, for if the player pauses the game, or the effect otherwise needs to pausetimedResume
, which should be self-explanatorytimedEnd
, which you are encouraged to send when the effect finishes
Note that, except for timedEnd
, the timeRemaining
field is required for all of these statuses, describing the new remaining time on the effect (it should be a new value, not the original one!).
Examples
So, putting it all together, let's craft our responses. To save some time and energy for you and I, we'll omit the greater scope of the call
and data
objects, focusing just on the arg object. See the prior section if you need a refresher on what it looks like in context.
We'll start by acknowledging the effect is running successfully:
{
"id": "023bcc7b-d9b8-4993-be74-07b0d5703180",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "timedBegin",
"timeRemaining": 1500,
"message": "",
"stamp": 1726075188
}
Then, 5 seconds later, the player pauses the game:
{
"id": "46174548-1287-4637-8bec-c36a52fd514e",
"request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
"status": "timedPause",
"timeRemaining": 1000,
"message": "",
"stamp": 1726075193
}
Then resumes 5 seconds later:
{
"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:
{
"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
Like effectResponse
, effectReport
uses the same rpc
action packet with type call
, however it accepts an args
array containing any number of objects which look like:
{
"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:
effect
for individual Effect IDs. If not specified, this is the default.category
for user-facing collections of effectsgroup
for 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.
Examples
In context, the request looks like:
{
"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!
What's Next?
If you've got this far, then you should have everything you need to integrate Crowd Control into your game! By this point you should likely be in contact with us already, but if not, please join our Discord (select "Community Developer" when prompted, then head over to #cc-developers) so we can get your game pack uploaded and ready for you and anyone else you want to invite to test! And for plugin developers, we'd love to hear from you as well: please share 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 on the Discord and we'll do our best to help you out.