Skip to content

Build a custom overlay

Bloopbot's Overlay SDK connects a page in an OBS Browser Source to the events and data its owner has chosen (the data is read-only; the connection can also carry the page's OBS link, see Inside OBS). You render the UI. The SDK handles the V2 handshake, initial replay, reconnection, diagnostics and record refresh. This is the developer reference; streamers can follow the custom overlay setup guide.

Studio graphics

Building inside Studio instead of hosting your own page? A Custom graphic layer runs your HTML, CSS and JavaScript in the overlay itself, with a live preview and no OBS file to manage. Its API, bloop, is documented in Custom graphics API, and its libraries in Graphics libraries.

A graphic reads records, leaderboard boards and Stage plays through the capabilities it declares. Use this Overlay SDK when your page lives outside Bloopbot.

Quick start

  1. On Overlays, create a named custom overlay. Select the event types, variables and specific records it may read, then save access.
  2. Download its transparent starter HTML. The file contains no credential. Save it on the computer running OBS.
  3. Enter that absolute file path on the overlay's card, then copy the private OBS Browser Source URL into OBS. The URL supplies ?ws= to the SDK.
  4. Keep the OBS scene visible. Refresh status should show a connected source. Use an alert layer's Test on stream… action for a live overlay check.

A named overlay starts with no grants. Your code's subscriptions narrow those grants; they never expand them. Each named overlay has its own credential in its URLs: it reads the data you granted, and it also lets a page that connects with it report OBS's events and act as the channel's OBS link, which needs no grant. Keep the copied OBS and socket URLs private, like a password.

Single-file browser page

The downloaded starter uses the versioned browser build supplied by Bloopbot. You can also start from this minimal file:

HTML
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>My overlay</title>
<style>html,body { margin: 0; background: transparent; color: white; font: 48px sans-serif }</style>
<div id="event" aria-live="polite"></div>
<script src="https://YOUR-BLOOPBOT-HOST/assets/overlay-sdk-1.3.2.js"></script>
<script>
  const overlay = BloopbotOverlay.createOverlay({ events: ['follow'] });
  overlay.onEvent('follow', frame => {
    document.querySelector('#event').textContent = `${frame.name ?? 'Someone'} followed`;
  });
  overlay.onDiagnostic(issue => console.warn(issue.code, issue.message));
  overlay.start().catch(error => console.error(error));
</script>
</html>

Replace the script host with the host shown in your downloaded starter. Grant follow on the named overlay before using this example. Register handlers before start() so they see replayed state. The script path is versioned; pin it rather than requesting an unversioned latest build.

npm and TypeScript

Install @bloopbot/overlay-sdk in a separate overlay project and bundle it for your HTML page:

Shell
npm install @bloopbot/overlay-sdk
TypeScript
import { createOverlay } from '@bloopbot/overlay-sdk';

const overlay = createOverlay({
  events: ['follow', 'stage.alert'],
  variables: ['hype'],
  records: [{ kind: 'alert_design', id: 'follow' }],
});
overlay.onEvent('follow', frame => console.log('Follow', frame));
overlay.onEvent('stage.alert', frame => console.log('Alert', frame));
overlay.onState(state => console.log(state.status, state.variables.hype));
overlay.onDiagnostic(issue => console.warn(issue.code, issue.message));
await overlay.start();

The OBS URL supplies the socket URL. For local development, pass { url: 'ws://localhost:3000/overlay/…' } as the second argument to createOverlay; use a private test credential, never commit it. start() resolves after subscription acknowledgement and the initial state replay. If the first variable snapshot temporarily cannot be read, the server retries and withholds readiness until it has a complete snapshot, preserving live changes received while waiting. start() rejects when the initial V2 handshake fails. Call stop() when the page is torn down.

Subscription reference

createOverlay(subscriptions, options?) returns an OverlayReader. The required events array can be empty. Optional arrays are variables, messages, boards and records. Requests are fixed for one connection; recreate the client to change them. Names are deduplicated. Up to 64 events, 32 variables, 64 messages, 32 boards and 64 records may be requested. The complete V2 hello must also fit 65,536 serialized UTF-8 bytes; the client refuses an oversized request before opening a socket. Later client reports retain their 4,096-byte limit.

Request What it selects Access required
events Event families listed below Matching event grant
variables Declared variable names, such as hype Matching variable grant
messages Named custom messages, such as round-start custom event grant and selected Flow message names
boards loyalty, contrib:cheers:stream, contrib:gifts:alltime, contrib:total:stream, or var:hype leaderboard event grant; variable boards also need that variable grant
records Exact { kind, id } pairs Matching record grant

The server acknowledges accepted and rejected subscriptions. Rejections produce subscription_rejected diagnostics with kind, name and reason (unknown or not_granted); allowed subscriptions continue. The Data access panel sets event, message, variable and record grants. Message names are narrowed by the stored names and the client request. Old grants that omit messages retain family-wide permission; new saves supply the explicit list. Board keys are selected by the author within their family grant. Selecting an alert or saving message names queues or reuses ordinary delivery flows; review and publish them, then download a starter with the matching subscriptions.

OverlayReader exposes state, start(), stop(), onEvent(family, handler), onState(handler), onDiagnostic(handler) and getRecord(kind, id). Each listener returns an unsubscribe function. getRecord requires an exact record subscription and returns the record, or null when it is missing or the read was cancelled; other read failures reject. It does not grant arbitrary reads.

options accepts url, handshakeTimeoutMs (1–60,000; default 5,000), reconnect (default true) and recordRefreshMs (1,000–3,600,000; default 30,000). A connection that loses access closes without reconnecting; ordinary disconnects retry with backoff up to 30 seconds. If a page stops accepting data and its socket buffer fills, the server closes it with retryable code 1013; reconnection replays the current snapshots so the page does not remain ready with stale data.

Events and frames

Subscribe and listen using the family name. Each SDK socket message carries one event object with a type and event-specific fields. After subscription acknowledgement, Bloopbot replays retained state such as an active goal as individual events before signaling ready; register handlers before start() to receive them. Inspect a frame in your test overlay before relying on optional fields. The SDK types every one of EVENT_FAMILIES and each listener’s raw type through EventFrame<F>. Payload fields from known server events are typed; future additive fields remain unknown until narrowed. The wire envelope types cover the hello, acknowledgement, ready, variables, record and config frames, and the hosted-widget reports.

Families Examples
Stream activity follow, sub, resub, gift, cheer, raid, adbreak, charity
Interactions chat, chathighlight, poll, prediction, hypetrain, giveaway, queue, wheel, minigames, custom
Widgets and display clip, countdown, credits, contributions, emotewall, eventlist, goal, jar, leaderboard, mediashare, slideshow, tickerline
Stage stage.alert, stage.media, stage.minigame

A clip frame carries the clip a shoutout plays: op: 'play' with id, slug, title, broadcaster_name and duration_ms. Your page draws it yourself. Bloopbot does not embed Twitch's player for a custom overlay, and no Bloopbot page policy applies to a page you host, so embedding one is your page's job under Twitch's own embed rules: pass your page's host as parent, and keep the player at its own size with nothing over it, or Twitch will not autoplay it. Grant clip to receive these frames.

Stage frames have type: 'stage'; the SDK dispatches them to the corresponding stage.* listener by kind. Grant stage.alert and the matching alert_design:name record for alert designs. stage.media covers media started by a flow. For named custom messages, grant custom and list the message names in messages.

A vars frame updates state.variables; subscribed variable values are replayed during setup. sdk.record frames and periodic reads update state.records. onState receives a new state after each update. state.lastEventAt is a millisecond timestamp or null. Variable values are typed as string, finite number, boolean or null. State snapshots and nested records are frozen; each event handler gets a detached frame.

State and records

state.status is idle, connecting, ready, stale, expired or stopped. stale means the connection is retrying. expired means its credential or access is no longer valid; copy the new OBS URL after rotation. state.revision is the acknowledged grant revision, or null before acknowledgement.

Records are read-only snapshots keyed as kind:id in state.records. Each has kind, id, revision and data typed by that record kind: published widget, giveaway, alert design, theme or now-playing fields. Available kinds are widget, giveaway, nowplaying, alert_design and theme; nowplaying:current is the current-song record. Request specific IDs and grant those same IDs in Data access. The client reads granted records after readiness and refreshes them every 30 seconds by default. It ignores older revisions and removes a missing or revoked record on refresh. Do not treat a record as a command surface.

Record reads have an 8-second idle deadline and a 10-second total deadline, covering both response headers and the JSON body. A timeout rejects getRecord with Overlay record unavailable and retains the last successful snapshot. A failed scheduled refresh emits record_unavailable; the next refresh can retry. Catch a direct read's rejection and keep a fallback. stop() or reconnect cancels outstanding reads: they resolve null, and a late response cannot change the new connection's state.

TypeScript
const overlay = createOverlay({
  events: [],
  records: [{ kind: 'nowplaying', id: 'current' }],
});
overlay.onState(state => {
  const song = state.records['nowplaying:current'];
  document.querySelector('#song')!.textContent = song ? JSON.stringify(song.data) : 'No song';
});
await overlay.start();

In an OBS Browser Source, obs-browser gives the page window.obsstudio. SDK 1.2 and later then make your overlay a candidate for the channel's OBS link, as Bloopbot's designed overlays are: the page reports OBS's events and its own source being shown or hidden, so flows can start on OBS changed scene, OBS started streaming, OBS overlay shown/hidden and the rest, and it carries out the OBS actions flows send it, such as Switch OBS scene and Save OBS replay, up to its Browser Source's Page permissions. Streamers set those up as in Control OBS from flows.

Nothing in your code is needed, and no data access grant: the SDK opts in on its hello, reports over the same socket at a pace the server accepts, and keeps the OBS link's frames from your onEvent handlers and diagnostics. When several overlays are open in OBS, Bloopbot uses one page at a time (the highest Page permissions, then the first to report), so a command runs once. Outside OBS, the option does nothing.

To keep a page out of it, pass { obs: false } as createOverlay's second argument. That page never reports OBS's events, never acts on OBS, and OBS overlay shown/hidden cannot watch it. Only one reader in a page takes part, the first started with the option on. The page's OBS link id and secret stay in its sessionStorage; the SDK exposes neither, and the SDK offers no OBS API of its own to your code. For an overlay that should keep working with OBS while its scene is off screen, its Browser Source needs Shutdown source when not visible unticked.

Simulate without OBS

createSimulation(subscriptions) implements the same reader methods without a socket. Call start(), then emit(frame), setVariables(values) and setRecord(record) to exercise rendering. Simulation applies the requested subscriptions; it does not contact Twitch, OBS or Bloopbot.

TypeScript
import { createSimulation } from '@bloopbot/overlay-sdk';

const overlay = createSimulation({ events: ['follow'], variables: ['hype'] });
overlay.onEvent('follow', frame => console.log(frame.name));
await overlay.start();
overlay.setVariables({ hype: 42 });
overlay.emit({ type: 'follow', name: 'Ada' });
overlay.stop();

For a real end-to-end check, load the page in a visible OBS scene and trigger Test on stream… on an alert layer. That action queues a real overlay test but does not run its flow or send a Twitch event. The flow editor's Test fire checks flow decisions separately without sending to OBS.

Diagnostics and security

Listen to onDiagnostic during development. Codes include subscription_rejected, invalid_frame, connection_closed, connection_error, handler_error, unsupported_server, access_expired and record_unavailable. A rejected subscription usually means the named overlay's Data access grant is missing. unsupported_server means the endpoint did not finish the V2 handshake. A record read can be unavailable even while the socket is ready.

The OBS and socket URLs contain a private credential. Treat it like a password: do not include one in an HTML file, screenshot, source repository, log, AI brief or issue report. The downloaded starter and generated AI brief omit it. It gives read access to the data granted under Data access. Beyond that, any page that connects with it can opt in to the OBS link on its hello (no grant needed), report OBS events to Bloopbot, which can start the channel's flows, and become the page that carries out the channel's OBS actions up to its Page permissions. Writes to the channel's other data use separate product controls. Rotate private URL… invalidates the old URL, and with it the connections made through it; update every OBS source that used it. The SDK never needs a Twitch token.

Versions and compatibility

This page describes Overlay SDK 1.3.2 and protocol V2. The npm import and /assets/overlay-sdk-1.3.2.js browser build come from the same package source. Pin the package version or script path you tested. Bloopbot supports a protocol major version for at least 12 months after a replacement is introduced; a new major is announced with its migration path before support ends. A V2 client requires a Bloopbot endpoint that supports the V2 handshake; unsupported_server is the diagnostic when setup cannot finish. The package README includes separate-repository release instructions. Check the package release notes before upgrading a deployed overlay.

Migrating from 0.1.0

SDK 1.3.2 retains protocol V2; twitch.emoteUrl takes an optional fifth sourceAnimated argument so static 7TV emotes load with animation off (four-argument calls keep their behaviour). SDK 1.3.1 fixed lifecycle and connection recovery. SDK 1.3.0 retains protocol V2 and adds visibility and repeatable visual helpers to the graphics runtime, shared page animation frames and bounded record reads. Catch a failed getRecord read and retain your last good rendering; cancelled reads still return null. The complete V2 hello now has a 65,536-byte UTF-8 budget; later reports keep the 4,096-byte cap. SDK 1.2.1 retains protocol V2 and adds the questions event family: the viewer question the streamer shows on their overlay, with the next few waiting. SDK 1.2.0 retains protocol V2 and adds the OBS link inside OBS, on by default (obs: false turns it off); its hello may carry obs: true, which an older Bloopbot ignores. SDK 1.1.0 retains protocol V2 and adds typed payloads for every event family, protocol envelope and published record. SDK 1.0.0 retains protocol V2 and the same named-overlay methods. Configuration is copied at construction; mutating your original arrays no longer changes subscriptions. Stop clears data and cancels outstanding reads, and an immediate restart creates a fresh handshake. Reconnect clears old data when the new grant acknowledgement arrives and accepts only requested, granted families, names and records. Revoked access clears state and stops retries. Simulation applies those same filters, including raw variable frames. Handle asynchronous getRecord returning null after cancellation and unsubscribe listeners when you no longer need them. Listeners remain registered across stop/start so a restart can reuse your rendering callbacks.

Existing /assets/overlay-sdk-0.1.0.js, -1.0.0.js, -1.1.0.js, -1.2.0.js, -1.2.1.js, -1.3.0.js and -1.3.1.js pages keep their frozen browser artifacts; upgrade the script path or npm dependency explicitly after testing. Graphics manifest version 1 and the graphics runtime version (bloop.version, now 1.3) are versioned separately from wire protocol V2. The release's npm and browser artifacts come from the same package source. The standalone graphics clock has no GSAP/Pixi dependency: hosts supply their timeline factory and asset/data grants.