Developer API

REST reference

Read and change the queue, Treats, Gem messages, memberships and Trigger Tickets.

Besides events, a token can read a creator's data and do what the dashboard does: open the queue, move entries along, hide a Treat, grant Trigger Tickets. Every call goes through the same code as the dashboard, so a chat command and a click behave exactly alike, down to the events they send.

All endpoints live under https://gleem.gg/api/v1, take and return JSON, and need a token with the scope shown. The complete, machine-readable description is the OpenAPI 3.1 spec. Load it into Postman, Insomnia or a client generator.

Endpoints

Method Path Scope Does
GET /me none The token's creator, scopes and event stream.
POST /socket/auth events:read, irl:read or stream:read Signs a WebSocket subscription. See Real-time events.
GET /queue queue:read The open queue, its entries, and entries still owed from closed queues.
POST /queue/open queue:write Opens a queue, closing one that is already open.
POST /queue/close queue:write Closes the queue.
PATCH /queue/entries/{id} queue:write Sets an entry to pending, active or completed.
POST /queue/entries/{id}/refund queue:write Refunds and cancels an entry.
GET /treats treats:read Every Treat by category, hidden ones included.
PATCH /treats/{id} treats:write Shows, hides, pins or unpins a Treat.
GET /tips tips:read Gem messages, newest first.
GET /leaderboard tips:read The 25 viewers who spent the most Gems.
GET /memberships members:read Membership tiers and how many viewers hold each.
POST /tickets/grant tickets:write Grants Trigger Tickets to a viewer.
GET /irl/devices irl:read Your Gleem IRL devices and how their streams are doing.
GET /irl/devices/{uuid} irl:read One IRL device and how its stream is doing.
GET /irl/devices/{uuid}/playback irl:read The SRT URL to watch a device's stream in OBS. See Watching a device's stream.
POST /irl/devices irl:write Registers a phone as an IRL device. See The Gleem IRL app.
PATCH /irl/devices/{uuid} irl:write Renames an IRL device.
DELETE /irl/devices/{uuid} irl:write Removes an IRL device and its credential.
POST /irl/devices/{uuid}/commands irl:write Starts, stops, restarts or updates an online device.
PUT /irl/devices/{uuid}/bitrate irl:write Moves a device's bitrate window.
GET /irl/pair/{code} irl:write The IRL Sidekick waiting to be paired under a code.
POST /irl/pair/approve irl:write Approves an IRL Sidekick's pairing by the code it shows.
POST /irl/pair/decline irl:write Declines an IRL Sidekick's pairing.
GET /hosting/rentals hosting:read Your OBS rentals, newest first.
GET /hosting/rentals/{uuid} hosting:read One OBS rental, with its obs-websocket while it runs.
GET /stream stream:read Your stream on each platform: live, viewers, title, category. See Stream Manager.
PATCH /stream/{platform} stream:write Changes the title and category.
GET /stream/{platform}/categories stream:read Searches categories.
GET /stream/{platform}/chatters stream:read Who is in chat right now. See Chatters.
GET /stream/chat stream:read The last day's chat, newest first.
GET /stream/activity stream:read The last day's follows, subs, gifts, raids, redemptions and Gems.
POST /stream/{platform}/chat/messages stream:write Sends a chat message as you.
DELETE /stream/{platform}/chat/messages/{id} stream:write Deletes a chat message.
POST /stream/{platform}/bans stream:write Times out or bans a user.
DELETE /stream/{platform}/bans/{user_id} stream:write Lifts a timeout or ban.
PATCH /stream/{platform}/redemptions/{id} stream:write Fulfils or refunds a channel point redemption. See Redemptions.
GET /stream/quick-actions stream:read The Gleem IRL app's Quick Actions layout.
PUT /stream/quick-actions stream:write Saves the Quick Actions layout.
GET /webhooks webhooks:write Your app's webhooks for this creator. OAuth apps only.
POST /webhooks webhooks:write Registers a webhook. See App webhooks.
DELETE /webhooks/{id} webhooks:write Deletes one of your app's webhooks.

The queue

curl -X POST https://gleem.gg/api/v1/queue/open \
  -H "Authorization: Bearer gleem_pat_…" \
  -H "Content-Type: application/json" \
  -d '{"total_slots": 10, "closes_at": "23:00"}'
{
    "session": {
        "id": 55,
        "status": "open",
        "total_slots": 10,
        "closes_at": "23:00",
        "timezone": "Europe/Berlin",
        "waiting": 0,
        "opened_at": "2026-09-26T19:04:11Z"
    }
}

total_slots and timezone are optional. Without them, the queue uses the size and timezone the creator used last, or 10 slots and UTC for a first queue, so a bare !openqueue works. closes_at is optional too; without it the queue stays open until it is closed.

GET /queue returns the open session (or null), its entries, and leftovers: entries still pending or active from queues that have since closed, the same list the dashboard shows. Each entry has the same fields as the treat.redeemed event, with the entry's own id, status and position at the top level and requested_at added.

Refunding gives back what the viewer paid: Gems to the balance they came from, or a Trigger Ticket. Refunding an entry twice answers 409 with the code already_refunded, and pays out only once.

Treats

curl -X PATCH https://gleem.gg/api/v1/treats/7 \
  -H "Authorization: Bearer gleem_pat_…" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'

Send is_active, is_pinned or both. They set the value rather than toggling it, so a command that runs twice does not undo itself.

If the Treat has a Twitch Channel Points reward, hiding the Treat pauses the reward too. When Twitch does not go along, the Treat still changes on Gleem and the answer says "channel_points_synced": false.

Trigger Tickets

curl -X POST https://gleem.gg/api/v1/tickets/grant \
  -H "Authorization: Bearer gleem_pat_…" \
  -H "Content-Type: application/json" \
  -d '{"twitch_user_id": "123456", "amount": 2}'
{
    "member": { "id": 345, "display_name": "Viewer", "twitch_id": "123456" },
    "amount": 2,
    "balance": 3
}

Name the viewer by twitch_user_id or twitch_login. A chat bot already knows the user id, which needs no lookup, so prefer it. Tickets go into the creator's current stream, the same as a grant from the dashboard. A viewer who has never used Gleem gets an account on the spot. An unknown login answers 422 with the code unknown_twitch_user.

Gem messages

GET /tips pages through Gem messages, newest first: per_page (up to 100, default 25), page, and since to start from a date. The answer has data, meta (current_page, last_page, per_page, total) and links (next, prev). For messages as they arrive, use the tip.created event instead of polling.

IRL devices

For stream tools that react to an IRL stream dropping, such as the OBS IRL Control plugin, which switches to a fallback scene. Needs Gleem IRL on the account; without it both endpoints answer 403 with the code feature_unavailable.

curl https://gleem.gg/api/v1/irl/devices/9d3c2f4e-6b1a-4c8e-9f2d-1a2b3c4d5e6f \
  -H "Authorization: Bearer gleem_pat_…"
{
    "uuid": "9d3c2f4e-6b1a-4c8e-9f2d-1a2b3c4d5e6f",
    "name": "Backpack",
    "board": "rk3588",
    "hardware": "Orange Pi 5 Plus",
    "version": "0.0.3",
    "temperature_c": 61.5,
    "online": true,
    "last_seen_at": "2026-09-29T10:15:30+00:00",
    "stream": {
        "state": "live",
        "sid": "0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b",
        "started_at": "2026-09-29T10:02:10+00:00",
        "error": null,
        "encoder_connected": true,
        "bitrate_bps": 5000000,
        "rtt_ms": 72.5,
        "links": [
            {
                "link_id": 0,
                "iface": "wwan0",
                "state": "live",
                "rtt_ms": 60,
                "loss_pct": 0.4,
                "tx_bps": 3000000
            },
            {
                "link_id": 1,
                "iface": "wwan1",
                "state": "live",
                "rtt_ms": 110,
                "loss_pct": 1.25,
                "tx_bps": 1000000
            }
        ],
        "healthy": true
    },
    "ingest": {
        "region": "fsn1",
        "publishing": true,
        "last_reported_at": "2026-09-29T10:15:28+00:00",
        "current_bps": 4600000,
        "average_bps": 4800000,
        "peak_bps": 6100000
    },
    "phone": null,
    "audio": null,
    "interfaces": [
        { "name": "wwan0", "kind": "modem", "state": "up", "addresses": null },
        { "name": "wwan1", "kind": "modem", "state": "up", "addresses": null },
        { "name": "wlan0", "kind": "wifi", "state": "down", "addresses": null }
    ],
    "bitrate": {
        "min_kbps": 1000,
        "max_kbps": 8000
    },
    "poll_interval_seconds": 2,
    "telemetry_socket": {
        "url": "wss://gleem.gg/ws/app/<key>?protocol=7&client=gleem-api&version=1.0",
        "channel": "private-irl-device.9d3c2f4e-6b1a-4c8e-9f2d-1a2b3c4d5e6f.telemetry",
        "network_channel": "private-irl-device.9d3c2f4e-6b1a-4c8e-9f2d-1a2b3c4d5e6f.network",
        "auth_url": "https://gleem.gg/api/v1/socket/auth"
    }
}

stream.healthy is the one field to switch scenes on. It is true while the device is live with its encoder connected and at least one live link, and ingest is passing the stream on to viewers and has reported it within the last 20 seconds. stream is null while the device is offline or idle, and ingest is null without an open session.

stream.rtt_ms is the round trip the bonded stream sees: the mean over the live links, weighted by how much each one carries, so an idle spare link with a bad connection does not make a good stream look bad. A link carries no address; see Network addresses.

Nothing is measured when you ask: the device reports every 5 seconds and ingest every 10. Poll no faster than poll_interval_seconds, which also keeps a tool well inside the rate limit. GET /irl/devices returns every device as data, sorted by name.

board is what the device runs on: a board such as rk3588 for an IRL Sidekick, or android or ios for a phone running the Gleem IRL app. phone is what such a phone last said about itself (battery_pct, charging, thermal as nominal, fair, serious or critical, camera, preset), and null for an IRL Sidekick or while the device is offline. version is the IRL Sidekick version the device runs, or the app's version for a phone. temperature_c is the hottest temperature sensor the device last reported, in °C, and null while it is offline or for a device that reports none, such as a phone. bitrate is the window the device is set to stream with (min_kbps, max_kbps), as the dashboard shows it, and null while neither gleem.gg nor the device has set one, such as for a phone. To follow a device live instead of polling, subscribe to telemetry_socket.channel with a token holding irl:read; see Real-time events.

Network addresses

interfaces lists the device's network interfaces as it last reported them (name, kind as modem, wifi, ethernet, usb_tether or other, and state as up or down), and is null while the device is offline. Their IP addresses locate the streamer's modems, so irl:read alone never gets them: addresses is null unless the token also holds the sensitive irl:network scope, and then it is the list of IPv4 and IPv6 addresses the device reported for that interface. A token Gleem hands to a rented OBS machine never gets them, whatever its scopes.

Field irl:read irl:read + irl:network
Everything else on a device yes yes
interfaces[].name, .kind, .state yes yes
interfaces[].addresses null yes
WebSocket telemetry_socket.channel yes yes
WebSocket telemetry_socket.network_channel (addresses, and each ingest link's remote) no yes

See Network addresses for what each WebSocket channel carries.

Watching a device's stream

GET /irl/devices/{uuid}/playback returns the URL OBS pulls the device's stream from, the same one its page in the dashboard shows. The Gleem OBS plugin uses it for its IRL Sidekick source.

{
    "url": "srt://fsn1.ingest.gleem.gg:8890?streamid=read:live/9d3c2f4e-6b1a-4c8e-9f2d-1a2b3c4d5e6f:play:eyJ2Ijox…&latency=1000000",
    "protocol": "srt",
    "region": "fsn1",
    "latency_ms": 1000,
    "key_version": 1
}

Treat url as a secret: anyone holding it can watch the stream. It does not expire, but when the creator asks for a new URL in the dashboard, key_version goes up and the old URL stops working within 10 seconds. Keep it in memory and fetch it again when a pull is refused, rather than storing it. Without an ingest server able to serve the device the answer is 409 with the code playback_unavailable.

The Gleem IRL app

The endpoints with irl:write are what the Gleem IRL app uses: pairing IRL Sidekicks, controlling them and the phone itself, the way the dashboard does. They go through the same code as the dashboard, so the app and the dashboard behave alike. Each needs Gleem IRL on the account, like the endpoints above.

Registering a phone

A phone that streams with the Gleem IRL app becomes an IRL device of its own. Because the app is signed in as the creator already, there is no code to show and approve: one request creates the device and hands out its credential.

curl -X POST https://gleem.gg/api/v1/irl/devices \
  -H "Authorization: Bearer gleem_oat_…" \
  -H "Content-Type: application/json" \
  -d '{"name": "Pixel 9", "board": "android", "hardware": "Google Pixel 9 Pro", "app_version": "1.0.0"}'
{
    "device": {
        "uuid": "4e1f9a2b-7c3d-4b8e-a6f5-0d9c8b7a6e5f",
        "name": "Pixel 9",
        "board": "android",
        "hardware": "Google Pixel 9 Pro",
        "online": false,
        "last_seen_at": null,
        "stream": null,
        "ingest": null,
        "phone": null,
        "audio": null,
        "poll_interval_seconds": 2,
        "telemetry_socket": {
            "url": "wss://gleem.gg/ws/app/<key>?protocol=7&client=gleem-api&version=1.0",
            "channel": "private-irl-device.4e1f9a2b-7c3d-4b8e-a6f5-0d9c8b7a6e5f.telemetry",
            "network_channel": "private-irl-device.4e1f9a2b-7c3d-4b8e-a6f5-0d9c8b7a6e5f.network",
            "auth_url": "https://gleem.gg/api/v1/socket/auth"
        }
    },
    "credential": "gid_k2m9q7f3x8ab_Zx8…",
    "realtime": {
        "url": "wss://gleem.gg/ws/app/<key>?protocol=7&client=irl-sidekick&version=1.0",
        "channel": "private-irl-device.4e1f9a2b-7c3d-4b8e-a6f5-0d9c8b7a6e5f",
        "auth_url": "https://gleem.gg/api/v1/irl/broadcasting/auth"
    }
}

board is android or ios; hardware and app_version are optional. The answer is the same as an IRL Sidekick gets when its pairing is approved. The gid_ credential is shown only here: keep it in the phone's secure storage. From then on the phone talks to the device API with it, exactly like an IRL Sidekick: it heartbeats, listens on realtime.channel for commands, and reports their results. Its heartbeat adds a phone object with its battery, charging and thermal state, camera and preset. Registering again creates another device; remove the old one with DELETE /irl/devices/{uuid}.

Pairing an IRL Sidekick

An IRL Sidekick asking to be paired shows a code such as GLEEM-7K3M-Q9TX. The code can be sent with or without the prefix and dashes and in any case. To show the creator which device is waiting before they approve it, look the code up:

curl https://gleem.gg/api/v1/irl/pair/7K3M-Q9TX \
  -H "Authorization: Bearer gleem_oat_…"
{
    "code": "GLEEM-7K3M-Q9TX",
    "hardware": "Orange Pi 5 Plus",
    "version": "0.0.3",
    "expires_at": "2026-10-02T14:10:00+00:00"
}

code is the code in its full form, hardware what the device said it is (null if it did not say), version the IRL Sidekick version it runs, and expires_at when the code stops working. Looking a code up does not answer it.

Then approve the pairing, optionally with a name:

curl -X POST https://gleem.gg/api/v1/irl/pair/approve \
  -H "Authorization: Bearer gleem_oat_…" \
  -H "Content-Type: application/json" \
  -d '{"code": "7K3M-Q9TX", "name": "Backpack"}'

The answer is 201 with the new device. The IRL Sidekick collects its credential with its next poll. POST /irl/pair/decline with the same code declines it instead and answers 204. A code that is unknown, expired or already answered gets 404 with the code pairing_not_found from all three endpoints. Each allows 20 requests a minute.

Remote control

curl -X POST https://gleem.gg/api/v1/irl/devices/9d3c2f4e-6b1a-4c8e-9f2d-1a2b3c4d5e6f/commands \
  -H "Authorization: Bearer gleem_oat_…" \
  -H "Content-Type: application/json" \
  -d '{"kind": "restart", "target": "stream"}'
{
    "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "kind": "restart",
    "status": "sent",
    "result": {},
    "error": null,
    "payload": { "target": "stream" },
    "issued_at": "2026-09-30T10:15:30+00:00",
    "expires_at": "2026-09-30T10:16:30+00:00"
}

kind is stream.start, stream.stop, restart (with target stream, sidekick or device) or update.install, or one of the browser-source audio and Bluetooth commands below with its payload. A command only goes to a device that is online: nothing is queued, so a start never fires minutes after it was wanted. An offline device answers 409 with the code device_offline. A command the device has not answered within 60 seconds expires unrun. Its outcome arrives on the device's telemetry channel and on the dashboard. update.install is refused with 422 while the device streams, while an install runs, or when its last heartbeat offered no update.

Browser-source audio and Bluetooth

An IRL Sidekick can play browser sources (alerts, TTS, sound widgets) on its own speaker and mix them into its stream, each with its own gain. Your tool keeps the sources and their URLs and sends them with the same endpoint, as {"kind": …, "payload": {…}}:

kind payload
audio.sources.set revision (yours, a number that grows) and sources: every source, each {id, name, url, output, stream}. id is 1–64 letters, digits, - or _; url is http(s), at most 2,048 characters; output (the Sidekick's speaker) and stream are {enabled, gain_db}, gain from −60 to 12 dB.
audio.source.update id and any of name, url, output, stream
audio.source.remove id
audio.output.set target: auto (a connected Bluetooth speaker, else the audio jack), none, device with a device id from audio.outputs, or bluetooth with a paired speaker's bluetooth_address
audio.test optional output (default true) and stream (default false), at least one true: plays a short two-tone chime on the Sidekick's speaker and/or into its stream. It needs a source with a route on; otherwise the device answers not_running.
bluetooth.power on
bluetooth.scan optional seconds, 1–30
bluetooth.pair, bluetooth.connect, bluetooth.disconnect, bluetooth.remove address, like AA:BB:CC:DD:EE:FF

The payload is checked here as the device checks it: a mistake is a 422. What the device makes of it shows in its audio object, both in GET /irl/devices/{uuid} and in its telemetry events (WebSocket): available (and unavailable_reason), the revision it applied, the sources without their URLs, what plays where (output, with active and error), the outputs it has, and bluetooth with the devices nearby. audio is null while the device is offline or for one that reports none, such as a phone or an older IRL Sidekick. Overlay URLs carry tokens, so gleem.gg keeps a command's URLs only until the device has answered, and never shows them again.

PUT /irl/devices/{uuid}/bitrate with min_kbps and max_kbps (500 to 50,000, the minimum no higher than the maximum) moves the bitrate window. An online device gets it at once and applies it to a running stream: the answer has "delivery": "command" and the command. An offline one gets it with its next heartbeat: "delivery": "next_heartbeat" and "command": null. So does one that drops off between the check and the command.

PATCH /irl/devices/{uuid} with name renames a device. DELETE /irl/devices/{uuid} removes it with its credential: its next request is refused and it shows itself unpaired. Its stream history stays.

OBS rentals

For showing a running OBS Server Hosting rental and connecting to its OBS, for example from the Gleem IRL app. Needs OBS Server Hosting on the account; without it both endpoints answer 403 with the code feature_unavailable. Only the renter sees a rental.

curl https://gleem.gg/api/v1/hosting/rentals/2b7e0c1a-4f3d-4a8e-9c6b-5d1e2f3a4b5c \
  -H "Authorization: Bearer gleem_oat_…"
{
    "uuid": "2b7e0c1a-4f3d-4a8e-9c6b-5d1e2f3a4b5c",
    "status": "running",
    "active": true,
    "price_per_hour": "0.0577",
    "price_cents": 6,
    "currency": "EUR",
    "accrued": "0.0433",
    "accrued_cents": 4,
    "max_hours": 4,
    "width": 1920,
    "height": 1080,
    "framerate": 30,
    "requested_at": "2026-09-30T09:58:12+00:00",
    "started_at": "2026-09-30T10:00:05+00:00",
    "ended_at": null,
    "expires_at": "2026-09-30T14:00:05+00:00",
    "ended_reason": null,
    "machine": {
        "uuid": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2918",
        "name": "Studio PC",
        "gpu": "NVIDIA GeForce RTX 4070",
        "region": "eu-central",
        "city": "Frankfurt"
    },
    "obs_websocket": {
        "url": "wss://obs.gleem.gg/2b7e0c1a-4f3d-4a8e-9c6b-5d1e2f3a4b5c",
        "host": "obs.gleem.gg",
        "port": 45500,
        "password": "…"
    }
}

status is requested, provisioning, running, stopping, ended or failed. price_per_hour is the agreed price per hour and accrued what the rental has cost so far, both as decimal strings with four places, since hosts can price below a cent. price_cents and accrued_cents are the same amounts rounded to whole cents, kept for existing clients. expires_at is when it hits its hard cap, counted from started_at.

obs_websocket is how to reach the rental's OBS with any obs-websocket 5 client, through Gleem's relay. It is there only while the rental is not finished, and null afterwards. The password controls the OBS, so treat it like the token.

GET /hosting/rentals pages through the rentals, newest first, with per_page (up to 100, default 25) and page, in the same data, meta and links shape as GET /tips.

Starting and ending rentals stay on the dashboard for now. The hosting:write scope is reserved for them, so an app that will need it can ask for it already.

Stream Manager

What the Gleem IRL app's Stream Manager runs on: your stream on Twitch, its chat and activity, and acting on it as you. gleem.gg calls Twitch with your own Twitch sign-in, so a tool never holds a Twitch token, and messages, bans and a new title come from your account, not from a bot. Live chat and activity arrive over the WebSocket on the Stream Manager channel; the endpoints here fill in what happened before the tool connected.

Paths with {platform} take twitch. Kick and YouTube are added in the same shape later, so read platform instead of assuming Twitch.

The Stream Manager is part of the Gleem IRL beta. Without Gleem IRL on the account, every endpoint here answers 403 with the code feature_unavailable, after the scope check, and the Stream Manager channel is not signed.

Stream status

curl https://gleem.gg/api/v1/stream \
  -H "Authorization: Bearer gleem_oat_…"
{
    "platforms": [
        {
            "platform": "twitch",
            "connected": true,
            "channel": {
                "id": "4711",
                "login": "streamer",
                "display_name": "Streamer"
            },
            "live": true,
            "viewers": 321,
            "started_at": "2026-10-02T09:00:00Z",
            "title": "IRL in Berlin",
            "category": {
                "id": "509658",
                "name": "Just Chatting",
                "box_art_url": "https://static-cdn.jtvnw.net/ttv-boxart/509658-144x192.jpg"
            },
            "capabilities": {
                "chat_read": true,
                "chat_send": true,
                "chat_delete": true,
                "ban": true,
                "edit_info": true,
                "follows": true,
                "redemptions": true,
                "chatters": true
            },
            "missing_scopes": [],
            "reconnect_url": null,
            "error": null
        }
    ],
    "socket": {
        "url": "wss://gleem.gg/ws/app/<key>?protocol=7&client=gleem-api&version=1.0",
        "channel": "private-creator.12.stream",
        "auth_url": "https://gleem.gg/api/v1/socket/auth"
    }
}

Every platform is listed, connected or not. The answer is cached for about 15 seconds, so polling faster gains nothing. While offline, live is false, viewers and started_at are null, and title and category are what the channel is set to. error is platform_unavailable when the platform could not be asked; the rest of the entry still says what the connection allows.

capabilities says what the connection allows. Signing in to gleem.gg with Twitch only asks for what Gleem needs of everyone (Channel Points, subscriptions and letting Gleem's bot into your chat), so a Twitch connection lacks the Stream Manager's permissions until you reconnect it once for the Stream Manager: missing_scopes names what is missing, and reconnect_url is where to send you to do that. It is a page on gleem.gg to open in a browser: it signs you in on gleem.gg first if needed, then asks Twitch for the extra permissions on the Twitch account already linked to your page, and refuses any other. Show a "Reconnect Twitch" prompt while missing_scopes is not empty. Signing in again later does not take the permissions away. chat_read also needs Gleem's bot, which reads chat for you.

Title and category

curl -X PATCH https://gleem.gg/api/v1/stream/twitch \
  -H "Authorization: Bearer gleem_oat_…" \
  -H "Content-Type: application/json" \
  -d '{"title": "IRL in Berlin", "category_id": "509658"}'

Send title (up to 140 characters), category_id, or both; a null category_id clears the category. The answer is the platform's entry from GET /stream, read fresh. To find a category, GET /stream/twitch/categories?query=just returns up to limit (default 20, at most 50) matches as data, each with id, name and box_art_url.

Chat and moderation

POST /stream/twitch/chat/messages with text (up to 500 characters) and optionally reply_to_message_id sends a message as you, and answers 201 with its message_id. The message also arrives on the channel as a chat.message, like everyone else's. Twitch can accept the request and still drop the message (AutoMod, a blocked term, a chat mode): that answers 422 with the code message_dropped and Twitch's drop_reason (code, message).

DELETE /stream/twitch/chat/messages/{id} deletes one message. POST /stream/twitch/bans with user_id bans a user; with duration_seconds (1 to 1,209,600, two weeks) it is a timeout instead, and reason is optional. The answer has user_id and expires_at (null for a ban). DELETE /stream/twitch/bans/{user_id} lifts either. A timeout or ban clears the user's messages, and the channel says so with chat.user.cleared.

These act as you, so they can fail the way Twitch fails, each with the Developer API's error shape and platform in it:

Status Code Means
403 platform_reconnect_required The connection lacks a permission this needs. missing_scopes names it (it can be empty when Twitch said so itself), reconnect_url fixes it.
409 platform_not_connected No Twitch account is connected. reconnect_url connects one, with the Stream Manager's permissions.
422 platform_rejected Twitch refused, for example a user who is already banned. platform_status and platform_message are Twitch's own.
422 message_dropped Twitch did not deliver the chat message. See drop_reason.
429 platform_rate_limited Twitch is rate limiting. Retry shortly.
502 platform_unavailable Twitch could not be reached.

On top of the general rate limit, each creator may search categories 60 times a minute, send 100 chat messages a minute, take 60 moderation actions (deletes, bans, unbans) a minute and change the title or category 30 times a minute, from all their apps together. Going over answers 429 with rate_limited and Retry-After, as everywhere else.

Redemptions

PATCH /stream/twitch/redemptions/{id} with status fulfilled or canceled marks a channel point redemption done, or refunds the viewer's points. {id} is the redemption_id of an activity.redemption. The reward is taken from that card; for one older than the backlog, send its reward_id too. The answer has redemption_id, reward_id and the new status, the card in GET /stream/activity takes it, and the channel announces activity.redemption.updated.

Twitch only lets the app that created a reward update its redemptions, so this works for rewards Gleem created (Treats, the Trigger Ticket reward, the fishing game's reward) and not for rewards made on Twitch or by another tool. can_manage on each activity.redemption says which: show the buttons only when it is true. It needs channel:manage:redemptions (capabilities.redemptions), which every Twitch sign-in asks for.

Status Code Means
403 reward_not_manageable The reward was not created by Gleem.
404 redemption_not_found Not in the backlog and no reward_id given, or Twitch does not know it, or it is no longer unfulfilled there.
409 redemption_already_processed The card already says fulfilled or canceled; status says which. Twitch is not asked.

The other errors are those of Chat and moderation. Fulfils and refunds count towards the 60 moderation actions a minute.

Chatters

GET /stream/twitch/chatters lists who is in chat right now, as Twitch's Get Chatters reports it:

{
    "platform": "twitch",
    "data": [
        {
            "user_id": "128393656",
            "login": "smittysmithers",
            "display_name": "SmittySmithers"
        }
    ],
    "total": 8,
    "cursor": "eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6NX19"
}

A page holds up to limit chatters (1 to 1,000, default 1,000). total counts everyone in chat across all pages; pass cursor back as cursor for the next page, which is null on the last one. Each page is cached for about 20 seconds, so polling faster gains nothing. There is no search: filter the pages you have. Twitch's list lags a few minutes behind who actually joins and leaves.

It needs moderator:read:chatters, which Twitch connections made before it was added lack: until you reconnect, the answer is 403 with platform_reconnect_required and missing_scopes, and capabilities.chatters in GET /stream is false. A platform without a chatter list answers 422 with platform_unsupported. Each creator may read 60 pages a minute.

Recent chat and activity

GET /stream/chat returns the last day's chat messages and GET /stream/activity the last day's follows, subs, gifts, raids, channel point redemptions and Gems, both newest first, as data plus has_more. Each item is the same envelope the channel delivers. Page with limit (up to 100, default 50) and before, an envelope id; platform keeps one platform (twitch, or gleem for Gems). Chat a moderator removed is included with deleted: true. Anything older than a day is gone.

Quick Actions

The Gleem IRL app keeps its Quick Actions grid on gleem.gg, so it follows you to another phone. GET /stream/quick-actions returns it:

{
    "tiles": [
        {
            "id": "brb",
            "type": "obs.scene",
            "label": "BRB",
            "icon": "pause",
            "config": { "scene": "BRB" }
        },
        {
            "id": "mute",
            "type": "mic.toggle",
            "label": null,
            "icon": null,
            "config": null
        }
    ],
    "revision": 3,
    "updated_at": "2026-10-02T10:15:30Z"
}

PUT /stream/quick-actions with tiles replaces it. Tiles stay in the order sent, at most 48. id must be unique in the layout; id and type use letters, digits and . _ : -, up to 64 characters; label is up to 40 characters; config is any object. The whole layout may not exceed 32 KB. What a type means is up to the app. Send the revision you read to save only if nobody saved in between: otherwise the answer is 409 with the code revision_conflict and the current layout. An account that never saved one gets an empty layout at revision 0.