RFC 0016 - EventSub WebSockets

Summary

We created EventSub to be a transport-neutral solution for delivering real-time event notifications. Last fall, we released EventSub with support for sending notifications over webhooks. We are now planning to add support for sending notifications over WebSockets.

Motivation

Many developers build applications that run on computers and devices they don’t control. For example, a browser application to manage Channel Points rewards or a game that triggers a boss fight when there’s a Hype Train.

These applications need to create subscriptions and receive notifications from EventSub, but don’t always have a server available that can receive notifications over webhooks.

Proposed Solution

Applications will be able to receive EventSub notifications over WebSockets.

A developer creates a new WebSocket by connecting to wss://eventsub.wss.twitch.tv. Once connected, the first WebSocket message from Twitch to the client will include the WebSocket’s id, which will be used when creating EventSub subscriptions.

Creating, viewing, and deleting EventSub subscriptions for WebSockets will use the same EventSub APIs as webhooks, except the transport field will be different. To create a subscription for WebSockets, a developer specifies “method”: “websocket” and uses the id value they received earlier.

This flow is summarized in the following diagram:

If a WebSocket disconnects, all subscriptions associated with that WebSocket will be automatically disabled. If the developer recreates the WebSocket connection, they must recreate the subscriptions for that WebSocket.

Detailed Changes

Authorization

EventSub allows developers to create subscriptions using an application access token. With the introduction of WebSockets, EventSub will add support for creating WebSocket subscriptions with user access tokens.

Subscriptions created for an application (using an application access token) are separate from subscriptions created for users (using a user access token). They cannot create/view/delete subscriptions for one another. For example, a bad actor can’t use their user access token to delete subscriptions for another user.

WebSockets

The number of allowed WebSocket connections depends on the type of authorization token used. When using an application access token, the developer may create up to 100 WebSocket connections. When using a user access token, the developer may create up to 3 WebSocket connections per user.

Once a developer successfully connects, Twitch will send a welcome message over the WebSocket connection:

{
  "metadata": {
    "message_id": "befa7b53-d79d-478f-86b9-120f112b044e",
    "message_type": "websocket_welcome",
    "message_timestamp": "2019-11-16T10:11:12.123Z"
  },
  "payload": {
      "websocket": {
        "id": "34dc753d-2173-4645-8d76-39636b9c6d32",
        "status": "connected",
        "minimum_message_frequency_seconds": 10,
        "connected_at": "2019-11-16T10:11:12.123Z"
      }
   }
}

This welcome message contains the WebSocket’s id, which is used when creating EventSub subscriptions (described later). If no EventSub subscriptions are created for this WebSocket within 10 seconds, the WebSocket will be disconnected.

Every healthy WebSocket will receive a message at least every minimum_message_frequency_seconds. Before this time limit, if no notifications have been delivered Twitch will fill the silence by sending a keepalive message:

{
  "metadata": {
    "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
    "message_type": "websocket_keepalive",
    "message_timestamp": "2019-11-16T10:11:12.123Z"
  },
  "payload": {
    "websocket": {
      "id": "34dc753d-2173-4645-8d76-39636b9c6d32",
      "status": "connected",
      "minimum_message_frequency_seconds": 10,
      "connected_at": "2019-11-16T10:11:12.123Z"
    }
  }
}

If a WebSocket doesn’t receive any messages for longer than minimum_message_frequency_seconds, the developer should assume the connection has died and create a new WebSocket.

The Twitch server that manages a WebSocket connection may need to go down, such as during maintenance. 30 seconds before this happens a message will be sent that contains a new url, which the developer should immediately use to create a new WebSocket connection. All existing subscriptions will already exist at the new URL. After 10 seconds, the developer can disconnect from the old WebSocket.

{
  "metadata": {
    "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
    "message_type": "websocket_reconnect",
    "message_timestamp": "2019-11-18T09:10:11.234Z"
  },
  "payload": {
    "websocket": {
      "id": "34dc753d-2173-4645-8d76-39636b9c6d32",
      "status": "reconnecting",
      "minimum_message_frequency_seconds": 10,
      "url": "wss://eventsub.wss.twitch.tv?...",
      "connected_at": "2019-11-16T10:11:12.123Z",
      "reconnecting_at": "2019-11-18T09:10:11.234Z"
    }
  }
}

Note that EventSub WebSockets only supports outgoing messages. If a developer sends data to Twitch over the WebSocket, they will be disconnected.

{
  "metadata": {
    "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
    "message_type": "websocket_disconnect",
    "message_timestamp": "2019-11-18T09:10:11.234Z"
  },
  "payload": {
    "websocket": {
      "id": "34dc753d-2173-4645-8d76-39636b9c6d32",
      "status": "disconnected",
      "disconnect_reason": "client_sent_inbound_traffic",
      "minimum_message_frequency_seconds": 10,
      "connected_at": "2019-11-16T10:11:12.123Z",
      "disconnected_at": "2019-11-18T09:10:11.234Z"
    }
  }
}

EventSub

Creating a subscription for WebSockets uses the same API as for webhooks, except the transport field describes the WebSocket connection that will receive notifications. Additionally, a WebSocket connection is limited to 100 subscriptions.

POST https://api.twitch.tv/helix/eventsub/subscriptions

-- Headers --
Client-ID: 	crq72vsaoijkc83xx42hz6i37
Authorization: Bearer C0BIYxs4JvnBWqvAmBvjfFc
Content-Type: application/json

-- Request Body --
{
  "type": "channel.follow",
  "version": "1",
  "condition": {
    "broadcaster_user_id": "12826"
  },
  "transport": {
    "method": "websocket",
    "id": "5dc6c6b3-ae73-4757-b029-c0b00017e8e3"
  }
}

When a subscription is triggered, a notification is sent on the WebSocket connection:

{
  "metadata": {
    "message_id": "befa7b53-d79d-478f-86b9-120f112b044e",
    "message_type": "notification",
    "message_timestamp": "2019-11-16T10:11:12.123Z",
    "subscription_type": "channel.follow",
    "subscription_version": "1"
  },
  "payload": {
    "subscription": {
      "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
      "status": "enabled",
      "type": "channel.follow",
      "version": "1",
      "cost": 1,
      "condition": {
        "broadcaster_user_id": "12826"
      },
      "transport": {
        "method": "websocket",
        "id": "5dc6c6b3-ae73-4757-b029-c0b00017e8e3"
      },
      "created_at": "2019-11-16T10:11:12.123Z"
    },
    "event": {
      "user_id": "1337",
      "user_login": "awesome_user",
      "user_name": "Awesome_User",
      "broadcaster_user_id": "12826",
      "broadcaster_user_login": "twitch",
      "broadcaster_user_name": "Twitch"
    }
  }
}

Note that the above is similar to the structure of a webhook HTTP request, where HTTP headers map to the metadata section and the body is the payload section.

If a subscription is revoked, a revocation message is sent on the WebSocket connection:

{
  "metadata": {
    "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
    "message_type": "revocation",
    "message_timestamp": "2019-11-16T10:11:12.123Z",
    "subscription_type": "channel.follow",
    "subscription_version": "1"
  },
  "payload": {
    "subscription": {
      "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
      "status": "authorization_revoked",
      "type": "channel.follow",
       "version": "1",
       "cost": 1,
       "condition": {
         "broadcaster_user_id": "12826"
       },
       "transport": {
         "method": "websocket",
         "id": "5dc6c6b3-ae73-4757-b029-c0b00017e8e3"
       },
       "created_at": "2019-11-16T10:11:12.123Z"
     }
   }
}

There will also be an endpoint to view existing WebSocket connections.

GET https://api.twitch.tv/helix/eventsub/websockets

-- Headers --
Client-ID:     crq72vsaoijkc83xx42hz6i37
Authorization: Bearer C0BIYxs4JvnBWqvAmBvjfFc
Content-Type:  application/json

-- Response Body --
{
  "data": [
    {
      "id": "5dc6c6b3-ae73-4757-b029-c0b00017e8e3",
      "status": "connected",
      "connected_at": "2019-11-16T10:11:12.123Z"
    },
    {
      "id": "71728a2b-9ea9-4f91-ad31-3b104a9297f1",
      "status": "client_disconnected",
      "created_at": "2019-11-17T11:12:13.456Z",
      "disconnected_at": "2019-11-17T12:13:14.567Z"
    }
  ]
}

FAQ

Will there be support for creating multiple subscriptions in a single request?
Support for subscription batching is considered a separate feature from this RFC and thus would be implemented separately. It may appear before or after the release of WebSockets.

Can I create subscriptions over the WebSocket rather than the EventSub API?
No. Subscriptions are created using the EventSub API that exists today. In the future we may consider adding support for creating subscriptions over the WebSocket, but that feature won’t be available at launch.

How will subscription costs work with WebSockets?
In regards to EventSub Subscription Limit Changes, the logic for determining a subscription’s cost will be the same regardless of whether the transport is webhooks or WebSockets. However when using a user access token, the default value of max_total_cost will be 10 rather than the default of 10,000 for an application access token.

15 Likes

I like it. My sole comment will just be stealing what is being said on discord:
Can we have equivalence with PubSub in regards to the Timeout on initial connection? It is 15 seconds for PubSub, which can be used a bit faster than having to establish an entirely seperate HTTP connection. In environments with bad connections, these 5 seconds can make a world of difference.

3 Likes

May be out of scope for this but any plans for supporting reconnecting to a session again and buffering events for a bit in case of connection deaths?

Lots of websocket based event APIs I’ve seen and worked with (like from payment processors or also Discord) will have some form of buffering of events for a connection if it randomly dies and allow some small amount of time (like 5 mins) to reconnect a session and will then replay all missed events.

Like for example returning a reconnect token in the initial welcome message and then if the connection dies letting you reconnect to something like wss://eventsub.wss.twitch.tv/reconnect?token={token}&last_message={last_message_id_you_still_got} and replaying all events that happened in the meantime before then going on normally with new ones.

Without that functionality keeping a consistent state of data and not missing anything will usually require extra API requests and make logic way more difficult for devs.

As an example say you wanna listen to all new follows or subs to a channel for a notification tool and the users internet connection dies for 30 secs then after creating a new EventSub websocket and new follow topic subscription for it you basically have to make a Helix Channel Follows API request and check those recent follows from the API against your last received one from the dead websocket to make sure you didn’t miss any and handle any you did miss.

Then multiply that effort for every possible topic you wanna listen to, many which don’t even really have appropriate APIs to easily get recent data making that even more of a pain.

This is of course already kind of a thing with existing server based EventSub but there you already have retry support on Twitch’s side making short term server problems not an issue and also in general with servers just being more stable making that way less problematic. But with this being way more frontend focused and often used on user machines and user internet connections random disconnects and connection deaths should be way more common.

I understand that this could be massive extra effort implementation wise but just pointing out that support for reconnecting sessions in some way makes usage so much easier for devs.

Also way better experience for end users too because if you look at tons of streamer targeted software out there using Twitch websocket PubSub currently most devs simply don’t even bother doing the API thing to stay consistent after disconnects and will just miss anything that happened during them meaning tons of those tools are just kinda slightly broken.

3 Likes

I agree that there needs to be some retry or replay method. Webhooks has a retry for temp outages. Since it’s supposed to be transport agnostic then there should also be retry window. Since there isn’t a guaranteed helix catch up method for all endpoints also.

2 Likes

Just waiting on this to start fully using EventSub

This looks like a step in very right direction.

Only comment I could have on it is that it’s very unclear how subscription would work and be done here without having any server (as EventSub guide right now requires you to have own server for callback to register a subscription);
Also how subscription would be granted (or, more exactly, what scopes application needs for which subscription).

Scopes for each type are noted on the relevant Type here EventSub Subscription Types | Twitch Developers

You need a server or something that provides an oAuth flow to do the initial grant.

It is possible to do the initial grant via an implicit oAuth flow, which generally only needs a locally hosted webpage to capture the response access token.

EventSub over websockets will use a user token which you can easily get with an implict oAuth route.

So EventSub over websockets, can/will work the same as doing oAuth/calling the API if you have no server.

The only “issue” you have is the setup to get a token. In this example a lot of people use a third party token generator and copy/paste the token in, which is not advised.

So you could provide a web page that users auth your client on, that displays the token and then they user copy pastes the token.

For example: Twitch Implicit Auth Example this example will do an oAuth loop and return the token to the user. Which they could then copy/paste into your program that does the EventSub websocket. And since this only uses a clientID it’s safe to put it on a “server thats not mine” such as GitHub pages.

So, in short, you still need a server/method to get a user oAuth token, regardless of how you use the “private” Twitch API endpoints and topics.