Streams¶
A Go Live stream is a screen share on a member's existing voice connection. A stream key names one stream. The operations here record a region preference and manage the stream's JPEG preview image.
Every route on this page is user-only. A bot token is rejected with 403 ACCESS_DENIED.
Stream lifecycle¶
Starting and stopping a stream is a Gateway operation. Going live publishes a screen share track on the LiveKit participant the member already holds, and the publisher advertises it by setting self_stream on that connection with Voice State Update. TEBHOS opens no second connection and issues no new grant. Voice states the full model.
Stream key¶
A stream key is one path segment with exactly three colon-separated parts. The form is {guild_id}:{channel_id}:{connection_id} for a guild voice stream and dm:{channel_id}:{connection_id} for a private call stream.
| Part | Type | Description |
|---|---|---|
| scope | string | The literal dm for a private call, or the decimal guild ID for a guild voice channel |
| channel_id | string | The decimal ID of the voice channel or private channel the stream runs in |
| connection_id | string | The connection_id of the publisher's voice connection, the one in its voice state and in its media participant identity |
A key with a different part count, an empty channel or connection part, a non-numeric channel part, or a scope that is neither dm nor a number returns 400 BAD_REQUEST.
Every route resolves the channel from the channel part of the key. Upload stream preview and Create stream preview upload URL also take a channel_id in their body, which must equal the key's channel part, or the request returns 400 BAD_REQUEST.
TEBHOS checks the scope against the resolved channel. A guild-scoped key must name the channel's own guild, and a dm key must name a channel that belongs to no guild. A mismatch returns 403 FORBIDDEN.
Access rules¶
Get stream preview requires read access. Every other route on this page requires mutation access.
Read access is access to the resolved channel: the channel must exist, the caller must be a member of its guild (or a recipient of the private channel), and the caller must hold VIEW_CHANNEL and CONNECT there. The caller does not have to own the stream, so any member who can join the channel can read its preview.
| Condition | Response |
|---|---|
| The channel does not exist, the caller is not a guild member, or the caller is not a recipient of the private channel | 404 NOT_FOUND |
The caller lacks VIEW_CHANNEL or CONNECT |
403 FORBIDDEN |
Mutation access requires everything read access does. A guild channel also requires the STREAM permission (403 FORBIDDEN without it). The caller must hold a voice connection in the resolved channel whose connection_id is the connection part of the key, according to the Gateway's voice states. A caller that holds no such connection is rejected with 403 FORBIDDEN, even when it owns the channel.
A preview can exist with no live stream
A caller can record a region and upload a preview for any voice connection it holds, whether or not it is streaming.
Stream preview upload object¶
A short-lived URL that accepts one stream's preview image. Create stream preview upload URL returns it, and the client uploads the image with a single PUT to upload_url.
Structure¶
| Field | Type | Description |
|---|---|---|
| upload_url | string | A presigned object storage URL that accepts the preview bytes with one PUT request |
| method | string | Always PUT |
| content_type | string | The media type to send with the PUT. The request's content_type is returned verbatim, and an absent field yields image/jpeg |
| expires_at | ISO8601 timestamp | The time the URL expires |
| expires_in | integer | The lifetime of the URL in seconds, always 86400 |
| max_bytes | integer | The maximum accepted byte count, always 1000000 |
A client MUST send the URL exactly as returned and MUST send the returned content_type. The URL accepts repeated PUT requests until it expires, and each one replaces the previous preview.
Example¶
{
"upload_url": "https://cdn.example.com/stream_previews/1189375284394692608-1189375284394692610-abc123-3f9a1c2d4e5b6a70.jpg?X-Amz-Signature=...",
"method": "PUT",
"content_type": "image/jpeg",
"expires_at": "2026-09-24T12:00:00.000Z",
"expires_in": 86400,
"max_bytes": 1000000
}
Modify stream region¶
PATCH /streams/{stream_key}/stream
Records the preferred media region for a stream and returns 204 with an empty body. The caller must satisfy the mutation access boundary.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| stream_key | string | The stream key whose region is set |
JSON body¶
| Field | Type | Description |
|---|---|---|
| region? | ?string | The RTC region ID, 1 through 64 characters. Omitting it stores no region |
A region identifier is the id of an RTC region object, as returned by List RTC regions. A value outside the length bound returns 400 BAD_REQUEST.
Response¶
| Status | Body | Condition |
|---|---|---|
| 204 | empty | The preference was recorded |
| 400 | error response | The stream key is malformed, or region is out of bounds |
| 403 | error response | The key's scope disagrees with the channel, the caller lacks a required permission, or it holds no matching voice connection |
| 404 | error response | The channel does not exist or is not accessible to the caller |
Side effects¶
The preference is kept for one day. The call does not move the stream, TEBHOS does not validate the region against the caller's accessible regions, and no Gateway Dispatch is emitted.
Rate limit¶
20 requests per 10 seconds for each user and stream key, on the channel:stream:update bucket.
Get stream preview¶
GET /streams/{stream_key}/preview
Returns the stored preview bytes for a stream. The caller must satisfy the read access boundary.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| stream_key | string | The stream key whose preview is returned |
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | preview bytes | A stored preview exists |
| 400 | error response | The stream key is malformed |
| 403 | error response | The key's scope disagrees with the channel, or the caller lacks VIEW_CHANNEL or CONNECT |
| 404 | error response | The channel does not exist or is not accessible, or no preview is stored for this key |
The 200 response carries Cache-Control: no-store, private and Pragma: no-cache. Its Content-Type is the media type recorded when the preview was written, and image/jpeg when none was recorded.
A stored preview expires one day after it was written. A read after that point, a stored object that is missing or unreadable, and an object larger than 1,000,000 bytes all answer 404. A client SHOULD treat a 404 whose channel is otherwise accessible as "no preview yet".
The uploaded bytes are not inspected on every path, so a client MUST NOT treat Content-Type as proof of the payload format and MUST decode defensively.
Rate limit¶
60 requests per 10 seconds for each user and stream key, on the channel:stream:preview:get bucket.
Upload stream preview¶
POST /streams/{stream_key}/preview
Replaces the stream's preview with a JPEG sent inline as base64 and returns 204 with an empty body. The caller must satisfy the mutation access boundary.
To upload the image bytes with a separate PUT request, use Create stream preview upload URL.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| stream_key | string | The stream key that receives the preview |
JSON body¶
| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the channel the stream is active in, equal to the key's channel part. A string or a number |
| thumbnail | base64 string | The base64-encoded JPEG, 1 through 2,000,000 characters |
| content_type? | string | The declared media type |
A content_type containing jpeg or jpg in any case is accepted without inspecting the bytes. Any other value, and an absent field, is accepted only when the decoded bytes begin with FF D8 and end with FF D9. The decoded image is at most 1,000,000 bytes. TEBHOS stores the accepted content_type verbatim, or image/jpeg when the field is absent. The image may also be scanned for content-policy violations.
Response¶
| Status | Body | Condition |
|---|---|---|
| 204 | empty | The preview was accepted |
| 400 | error response | The stream key is malformed, the body channel disagrees with the key, thumbnail is empty, too long, not valid base64, or decodes to nothing, the image is not a JPEG, it exceeds 1,000,000 bytes, or it violates the content policy |
| 403 | error response | The key's scope disagrees with the channel, the caller lacks a required permission, or it holds no matching voice connection |
| 404 | error response | The channel does not exist or is not accessible to the caller |
Side effects¶
The decoded bytes replace whatever preview the stream key already had, and the preview expires one day after the write. No Gateway Dispatch is emitted, so viewers see the new image on their next Get stream preview read.
Rate limit¶
20 requests per 10 seconds for each stream key, on the stream:preview:post bucket.
Create stream preview upload URL¶
POST /streams/{stream_key}/preview/upload-url
Returns a stream preview upload object granting a bounded PUT upload of the JPEG preview. The caller must satisfy the mutation access boundary.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| stream_key | string | The stream key that receives the preview |
JSON body¶
| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the channel the stream is active in, equal to the key's channel part. A string or a number |
| content_type? | string | The declared media type. Only a value containing jpeg or jpg in any case is accepted, and an absent field means image/jpeg |
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | stream preview upload object | The upload URL was issued |
| 400 | error response | The stream key is malformed, the body channel disagrees with the key, or the declared media type is not JPEG |
| 403 | error response | The key's scope disagrees with the channel, the caller lacks a required permission, or it holds no matching voice connection |
| 404 | error response | The channel does not exist or is not accessible to the caller |
Side effects¶
There is no separate completion call. After the PUT succeeds, Get stream preview returns the uploaded image; until then it returns 404 or the previous image. The preview record written by this call expires one day later, so a URL used after that writes an object no read can find. No Gateway Dispatch is emitted.
The upload is not inspected
The presigned URL enforces the signed media type and size only as far as object storage does. An object above max_bytes that still gets stored is never served: Get stream preview answers 404 for it.
Rate limit¶
20 requests per 10 seconds for each stream key, on the stream:preview:upload_url bucket.
Delete stream preview¶
DELETE /streams/{stream_key}/preview
Deletes the stored preview for a stream and returns 204 with an empty body. Deleting an absent preview also returns 204. The caller must satisfy the mutation access boundary.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| stream_key | string | The stream key whose preview is deleted |
Response¶
| Status | Body | Condition |
|---|---|---|
| 204 | empty | The preview was deleted, or none was stored |
| 400 | error response | The stream key is malformed |
| 403 | error response | The key's scope disagrees with the channel, the caller lacks a required permission, or it holds no matching voice connection |
| 404 | error response | The channel does not exist or is not accessible to the caller |
Side effects¶
The preview stops being readable, and its stored object is removed. No Gateway Dispatch is emitted.
Rate limit¶
20 requests per 10 seconds for each stream key, on the stream:preview:delete bucket.