PlasmaStream

PlasmaStream API

Last updated September 14, 2026

The API lets a program of yours read your channel and act on it: your points leaderboard on your own website, a Stream Deck button that hands out points, an alert on stream when something happens somewhere PlasmaStream does not connect to. Everything a key does happens to the channel it belongs to, and only that channel.

Keys

Make a key on the API page of your dashboard. Each key is allowed to do only what you switch on when you make it:

ScopeOn the dashboardWhat it allows
readReadSee your channel, a viewer's points and watch time, and your leaderboards.
points:writeChange pointsGive viewers points, or take points away.
alerts:writeShow alertsPut an alert on your overlay, as if it had just happened.

A key is shown once, when you make it. Only a hash of it is kept, so it cannot be shown again: if you lose one, revoke it and make another. A channel can have 3 keys, or 10 with Premium.

Treat a key like a password. Keep it in a program on a computer you control, and never put one in a web page, where anybody can read it. The API does not answer scripts running in a browser on another site, so a key in a page would not work anyway.

Making a request

Send your key in an Authorization header on every request. Every answer is JSON.

curl https://plasmastream.live/api/v1/channel \
  -H "Authorization: Bearer psk_your_key_here"

A request with a body sends it as JSON:

curl -X POST https://plasmastream.live/api/v1/viewers/houtenxd/points \
  -H "Authorization: Bearer psk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"amount": 250, "reason": "Won the raffle"}'

Numbers in a body can be JSON numbers or text, so 250 and "250" both work. A key is never read from the URL, only from the header.

Limits

  • Each key can make 60 requests a minute, or 300 when the channel has Premium. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, the seconds until the minute starts again. Past the limit the answer is a 429 with a Retry-After header.
  • Alerts have a tighter limit of their own: a channel can be sent 10 a minute through the API, however many keys it has.
  • A request body can be at most 16 KB.

Errors

Anything refused answers with an HTTP status and a body like this:

{
  "error": {
    "code": "invalid_field",
    "field": "amount",
    "message": "amount has to be a whole number other than zero: positive to give points, negative to take them away."
  }
}

Check code in a program: it will not change. message is written for a person and might. field is only there when one field was the problem.

CodeStatusMeaning
missing_key401There is no Authorization header.
invalid_key401The key is not one, or it has been revoked.
missing_scope403The key is not allowed to do that.
rate_limited429Too many requests. Wait for Retry-After.
not_found404There is no such endpoint, or no such board.
invalid_json400The body is not JSON.
body_too_large413The body is over 16 KB.
invalid_field422One field is wrong, and field says which.
viewer_not_found404Nobody with that login has chatted in the channel.
viewer_excluded409That viewer is excluded from the channel's economy.
insufficient_points409They do not have that many points to take.
board_off409That leaderboard is switched off.
overlay_not_set_up409The overlay has not been added to OBS yet.
alert_off409That alert is switched off in the overlay studio.
server_error500Something broke on our side. Try again shortly.

Endpoints

GET/api/v1/channel

Needs the read scope.

The channel, whether it is live, and its headline numbers. stream is null while it is offline, and followers and subscribers are null until the bot has been able to count them.

{
  "login": "houtenxd",
  "displayName": "HoutenXD",
  "twitchId": "123456789",
  "live": true,
  "stream": {
    "title": "Chill games and chat",
    "category": "Just Chatting",
    "thumbnailUrl": "https://static-cdn.jtvnw.net/previews-ttv/live_user_houtenxd-1280x720.jpg"
  },
  "followers": 1520,
  "subscribers": 48,
  "streams": 212,
  "currency": "Points"
}

GET/api/v1/viewers/{login}

Needs the read scope.

One viewer, by their Twitch login in any case, with or without an @. Only people who have chatted in the channel are found. watchSeconds counts time in chat while the stream was live.

{
  "login": "mr_dino_saur_",
  "displayName": "Mr_Dino_Saur_",
  "twitchId": "987654321",
  "points": 12850,
  "watchSeconds": 187200,
  "level": 14,
  "xp": 9120,
  "streak": 6,
  "firstSeen": "2026-06-02T19:14:08.000Z",
  "lastSeen": "2026-09-13T22:41:50.000Z"
}

POST/api/v1/viewers/{login}/points

Needs the points:write scope.

Give a viewer points, or take them away.

FieldWhat it is
amountA whole number, positive to give and negative to take, at most 1,000,000 either way.
reasonOptional. Up to 200 characters, kept with the change so you can see later why it happened.

Taking more than the viewer has is refused with insufficient_points, and nothing changes. Points moved this way show in your channel analytics as coming from the API. The answer is the change and the viewer after it:

{
  "change": 250,
  "viewer": {
    "login": "mr_dino_saur_",
    "displayName": "Mr_Dino_Saur_",
    "twitchId": "987654321",
    "points": 13100,
    "watchSeconds": 187200,
    "level": 14,
    "xp": 9120,
    "streak": 6,
    "firstSeen": "2026-06-02T19:14:08.000Z",
    "lastSeen": "2026-09-13T22:41:50.000Z"
  }
}

A request that times out may still have gone through. Read the viewer before sending it again, or they may be paid twice.

GET/api/v1/leaderboards

Needs the read scope.

The boards the channel offers, out of points, level, watch and donations. They are the boards on the public leaderboard page, with the same people on them: anybody who asked not to be listed, anybody excluded from the economy, and the streamer are all left off.

{ "boards": ["points", "level", "watch"] }

GET/api/v1/leaderboards/{board}?limit=10

Needs the read scope.

One board. limit is how many places to return: 10 when it is left out, and at most 25, or 100 with Premium. The answer says the limit it used. value is points on the points board, a level on the level board, seconds on the watch board and cents on the donations board.

{
  "board": "points",
  "limit": 3,
  "entries": [
    { "rank": 1, "displayName": "IsThisShnub", "twitchId": "555000111", "value": 48210 },
    { "rank": 2, "displayName": "Scooter_02_", "twitchId": "555000222", "value": 36990 },
    { "rank": 3, "displayName": "Mr_Dino_Saur_", "twitchId": "555000333", "value": 12850 }
  ]
}

POST/api/v1/alerts

Needs the alerts:write scope.

Put an alert on the overlay, looking the way that alert is set up in the overlay studio. Only the alert: nothing is said in chat, nobody is paid, and no subathon time is added.

typeFields
followuser
subscribeuser, tier
resubuser, months, streak, tier
gift_subuser, count, tier
raiduser, viewers
cheeruser, bits
donationuser, amount, message

user is the name to show, up to 40 characters. tier is 1, 2 or 3 and 1 when left out. streak is the months when left out. For a donation, amount is in dollars, like 4.99, and message is optional, up to200 characters. Every other field listed is a whole number and required.

curl -X POST https://plasmastream.live/api/v1/alerts \
  -H "Authorization: Bearer psk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"type": "raid", "user": "Scooter_02_", "viewers": 42}'

The answer is a 202 with {"queued": true, "type": "raid"}. The overlay plays the alert within a second if OBS has it open. An overlay that is not open drops alerts older than two minutes rather than playing a backlog late.

Webhooks

A webhook is the other direction. Add one on the API page of your dashboard with a URL and the events you want, and PlasmaStream sends a POST to that URL the moment one happens. A channel can have 2 webhooks, or 10 with Premium.

  • The URL has to be https, on port 443 or 8443, somewhere on the public internet.
  • Answer with any 2xx status within 10 seconds. Anything else, including a redirect, which is not followed, counts as a failure.
  • The body of your answer is ignored, so an empty 204 is fine.

What arrives

HeaderWhat it holds
Content-Typeapplication/json
PlasmaStream-EventThe event, like raid.
PlasmaStream-DeliveryThis delivery's id, the same every time it is tried.
PlasmaStream-Signaturet=, the time it was signed, and v1=, the signature. See below.
{
  "id": "evt_4b9c1f0e2d8a4c67b3e5a1d0f9c2b7e8",
  "type": "raid",
  "createdAt": "2026-09-14T20:03:11.000Z",
  "channel": { "login": "houtenxd", "twitchId": "123456789" },
  "data": {
    "user": { "name": "Scooter_02_", "login": "scooter_02_", "twitchId": "555000222" },
    "viewers": 42
  }
}

id belongs to the event, and is the same on every webhook it went to. user is who it was about; login and twitchId are null when PlasmaStream does not know, such as an anonymous gift or a donation made signed out.

Events

EventWhendata
followSomebody follows, the first time they ever do.user
subscribeA new subscription, paid or Prime.user, tier
resubA subscriber renews and shares it in chat.user, tier, months, streak
gift_subSomebody gifts subs. A gift of twenty is one event.user, tier, count
raidAnother streamer raids with their viewers.user, viewers
cheerSomebody cheers bits in chat.user, bits
donationA donation through your page is paid, or approved if you check them first.user, amount, amountCents, message
stream_onlineYour Twitch stream goes live.streamId, title, category
stream_offlineYour Twitch stream ends.streamId
pingYou press Send a test on the dashboard.message

tier, months, streak, count, viewers, bits and amountCents are numbers. amount is the donation as shown, like $5.00.

Checking the signature

Anybody can send a POST to your URL, so check each delivery came from PlasmaStream before acting on it. The signature is an HMAC-SHA256, keyed with your signing secret (all of it, whsec_ included), of the t value, a full stop, and the body exactly as it arrived. Parsing the JSON and turning it back into text changes the bytes, and the signature will not match.

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as it arrived, before any JSON parsing.
export function isFromPlasmaStream(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((part) => part.split("=")));
  const timestamp = Number(parts.t);

  // Refuse anything signed more than five minutes from now: a replay.
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return false;
  }

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const given = String(parts.v1 ?? "");

  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
import hashlib, hmac, time

# raw_body: the request body exactly as it arrived, as bytes.
def is_from_plasmastream(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in signature_header.split(","))
    timestamp = int(parts.get("t", "0"))

    # Refuse anything signed more than five minutes from now: a replay.
    if abs(time.time() - timestamp) > 300:
        return False

    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Retries, duplicates and switching off

  • A delivery that fails is tried again after a minute, 5 minutes, 30 minutes, 2 hours and 6 hours: 6 attempts in all.
  • The same delivery can arrive twice, for instance when your server handled it but its answer never reached us. Keep the PlasmaStream-Delivery ids you have seen and ignore a repeat.
  • Deliveries are not guaranteed to arrive in order once one has been retried.
  • A webhook that fails 50 times in a row, with nothing delivered for a day, is switched off. The dashboard says why, and it can be turned back on there.
  • At most 500 deliveries wait for one webhook. Past that, new events for it are dropped until it catches up.

Changes

Everything here is version 1, and it will grow: new endpoints, new events, and new fields in answers and deliveries, so read the fields you need and ignore the rest. Nothing in version 1 will be renamed or removed. A change like that would be a version 2, running alongside it.

PlasmaStream

52 features for your Twitch chat: an economy, games, moderation, stream overlays and more. Free to use.

Free & Premium

Every core feature is free, with no paywall. Premium is optional and helps keep the bot running: $4.99/month, or $49.99/year (about $4.17 a month).

Both renew automatically through Stripe until you cancel. Cancel any time from your dashboard and Premium runs to the end of the period you have paid for. We never see or store your card.

Channels can also take donations from their viewers. PlasmaStream keeps 5% + $0.30 and the rest goes to the streamer. Donations are not refundable.

Support & community

Come and say hello, ask for help, or suggest a feature. It’s the fastest way to reach us.

Join the Discord

Prefer email? houtenbusiness@gmail.com

Explore

FeaturesMultistreamStream monitorArtistsAPI

Legal

Terms of ServicePrivacy Policy
© 2026 PlasmaStream·Payments by StripeNot affiliated with, endorsed by, or sponsored by Twitch.·ver 1.0.1 · updated Oct 20, 4:51 PM EDT