Skip to content

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:

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:

json
{
  "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:

json
{
  "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.

json
{
  "action": "whoami"
}

To which the server will respond with a whoami event of its own:

json
{
  "domain": "direct",
  "type": "whoami",
  "payload": {
    "connectionID": "aGVsbG8hISEhISE="
  }
}

Using this, you can prompt the user to open a link to authorize themselves in their browser:

ts
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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "domain": "direct",
  "type": "subscription-result",
  "payload": {
    "success": ["pub/ccuid-01j7cnrvpbh5aw45pwpe1vqvdw"],
    "failure": []
  }
}

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.

ts
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:

json
{
  "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:

json
{
  "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:

json
{
  "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.

json
{
  "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:

json
{
  "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:

json
{
  "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 effect
  • failTemporary if the effect cannot be applied right now but would likely work another time
  • failPermanent 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:

json
{
  "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 pause
  • timedResume, which should be self-explanatory
  • timedEnd, 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:

json
{
  "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:

json
{
  "id": "46174548-1287-4637-8bec-c36a52fd514e",
  "request": "968b64c6-c65f-4e42-85b0-e9ff868309a4",
  "status": "timedPause",
  "timeRemaining": 1000,
  "message": "",
  "stamp": 1726075193
}

Then resumes 5 seconds later:

json
{
  "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:

json
{
  "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:

json
{
  "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 effects
  • group 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:

json
{
  "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.