Skip to main content

useLumioEvent

useLumioEvent(type) returns the latest payload of one event type, or null until that type arrives. The return type follows the literal event name.

import { Lumio, Text, useLumioEvent } from "@zaflun/lumio-sdk";

function CheerTotal() {
const cheer = useLumioEvent("twitch:cheer");
return cheer ? <Text content={`${cheer.bits} bits`} /> : null;
}

Lumio.render(<CheerTotal />, { target: "layer" });

cheer.bits is a number. The hook returns the payload itself: use cheer.bits, rather than cheer.data.bits. The SDK subscribes to the host's event.platform notification, keeps the latest payload for the requested type, and unsubscribes when the component unmounts or the type changes. It does not replay events that arrived before the component subscribed; only the host replays the current Spotify state once on mount in OBS (see Spotify state on mount).

Signature​

function useLumioEvent<K extends LumioEventType>(
eventType: K
): LumioEventPayloadMap[K] | null;

LumioEventType, LumioEventPayloadMap, and LumioEventPayload<K> come from @zaflun/lumio-protocol. Its LUMIO_EVENT_TYPES list is the source for autocomplete; lumioEventPayloadSchema(type) and parseLumioEventPayload(type, value) provide runtime validation when you handle external data. The schemas retain extra provider fields, while typed fields follow the backend's golden event fixtures. Fields present only after enrichment are optional.

For example, twitch:follower has user_name, user_id, and followed_at; avatarUrl may be absent. The field names match the actual event payload, including snake_case where the provider or backend uses it. Your editor shows the fields of each type; the payload types are the reference, so this page does not repeat them.

Delivery​

The host page that renders your extension reads the account's live streams and sends each event to your worker as the event.platform notification { eventType, data }. This happens on every surface: the overlay layer and widget browser source in OBS, the widget editor preview and the designer preview in the dashboard. It happens only while the surface is open.

  • Platform events (Twitch, YouTube, Kick, Spotify, StreamElements): data is the event's stored payload.
  • Chat (chat:message, twitch:chat, youtube:chat, kick:chat): data is the chat message (ChatMessagePayload). Every message arrives twice, once as chat:message and once as its platform's chat type.
  • twitch:ban / twitch:unban: data is the moderation-log entry (ModerationLogPayload).

Your manifest's permissions decide what you receive:

Event typesRequired permission
chat:message, twitch:chat, youtube:chat, kick:chat, twitch:ban, twitch:unbanchat:read
Every other supported event typeevents:read

Without the permission the host does not forward the event, and without either permission it does not open a stream at all.

Spotify state on mount​

In OBS (the overlay layer and the widget browser source), the host also replays the account's current Spotify playback state when your extension starts, so a reload does not leave a now-playing display empty until the next track. It arrives once, after your first render and before any live event, as a spotify:track whose payload carries replay: true. It needs events:read and the account's Music feature, and it is skipped when nothing is playing or loaded. The dashboard previews do not replay it.

A replay is not a track change. If you count tracks or list them, skip payloads with replay:

useLumioEventListener("spotify:track", (track) => {
if (track.replay) return; // the state on mount, not a new track
setPlayed((count) => count + 1);
});

Pause and resume, also from the Spotify app itself, arrive as live spotify:pause / spotify:play events.

Test events​

A streamer can send test events from the dashboard, the API or an AI app. They arrive like real events, with two extra payload fields that only the server sets: isTest: true and showOnStream (the sender's "show on stream" choice). A stored event never carries them, so history never contains a test. The dashboard previews receive every test. The OBS surfaces (overlay layer and widget browser source) receive a test only with showOnStream: true.

Show a test marked as a test, never as a real follow, sub or donation. isTestEvent(payload) from @zaflun/lumio-sdk reads the marker (isTest or is_test set to true):

import { isTestEvent, useLumioEventListener } from "@zaflun/lumio-sdk";

useLumioEventListener("twitch:follower", (follow) => {
addAlert({ name: follow.user_name, test: isTestEvent(follow) });
});

The built-in Alert Box, Event List and Spotify Now Playing show a TEST label on a test, and so does the Chat Box on a chat message marked as a test. To try your own rendering, build a test payload with createEvent(type, overrides, { test: true }) from @zaflun/lumio-devkit ({ test: { showOnStream: true } } for one shown on stream).

Twitch reward lifecycle events (twitch:reward_add, twitch:reward_update, and twitch:reward_remove) are live only; they are absent from event history. youtube:gift_membership_received is stored in the event history but never sent live, so an extension surface does not receive it.

Supported event types​

PlatformEvent types
Twitchtwitch:follower, twitch:subscribe, twitch:gift, twitch:resub, twitch:cheer, twitch:raid, twitch:reward, twitch:reward_status, twitch:reward_add, twitch:reward_update, twitch:reward_remove, twitch:hype_train, twitch:hype_train_end, twitch:poll, twitch:poll_end, twitch:prediction, twitch:prediction_lock, twitch:prediction_end, twitch:goal, twitch:goal_end, twitch:ad_break, twitch:ban, twitch:unban, twitch:stream_online, twitch:stream_offline, twitch:stream_update
YouTubeyoutube:subscribe, youtube:member, youtube:superchat, youtube:supersticker, youtube:gift_membership, youtube:gift_membership_received, youtube:poll, youtube:stream_online, youtube:stream_offline
Kickkick:follower, kick:subscribe, kick:resub, kick:gift, kick:kicks, kick:reward, kick:reward_status, kick:stream_online, kick:stream_offline, kick:stream_update
Spotifyspotify:track, spotify:play, spotify:pause, spotify:skip, spotify:volume, spotify:queue_add, spotify:device, spotify:playlist_add, spotify:playlist_remove, spotify:playlist_create, spotify:playlist_edit, spotify:playlist_delete
StreamElementsstreamelements:tip
Shopifyshopify:order, shopify:order_paid, shopify:product
Chat, per platformtwitch:chat, youtube:chat, kick:chat
Chat, all platformschat:message

Use chat:message when you handle chat the same way on every connected platform.

Example: Follower alert​

Needs events:read.

import { Lumio, Box, Text, useLumioEvent } from "@zaflun/lumio-sdk";
import { useEffect, useState } from "react";

function FollowerAlert() {
const follow = useLumioEvent("twitch:follower");
const [visible, setVisible] = useState(false);

useEffect(() => {
if (!follow) return;
setVisible(true);
const timer = setTimeout(() => setVisible(false), 5000);
return () => clearTimeout(timer);
}, [follow]);

if (!follow || !visible) return null;

return (
<Box style={{ background: "rgba(99, 102, 241, 0.9)", borderRadius: 12, padding: "16px 24px" }}>
<Text content={`New follower: ${follow.user_name}!`} variant="heading" />
</Box>
);
}

Lumio.render(<FollowerAlert />, { target: "layer" });

Example: Now playing (Spotify)​

Needs events:read. The payload is Spotify's now-playing state, as used by the system Spotify Now Playing layer.

import { Box, Image, Text, useLumioEvent } from "@zaflun/lumio-sdk";

function NowPlaying() {
const nowPlaying = useLumioEvent("spotify:track");
if (!nowPlaying?.is_playing) return null;

const { track } = nowPlaying;
return (
<Box style={{ display: "flex", gap: 12, alignItems: "center" }}>
<Image src={track.album_art} alt="Album art" style={{ width: 48, height: 48, borderRadius: 4 }} />
<Box>
<Text content={track.name} />
<Text content={track.artist} variant="muted" />
</Box>
</Box>
);
}

Try it locally​

Run lumio dev and send an event from a second terminal:

lumio trigger twitch:cheer --bits 500

The devkit's mock host builds the remaining fields from a fixture and validates the result before delivery. Its event catalog marks the types the host does not forward to extension surfaces.

Notes​

  • shopify:* events come from every Shopify store the account connected; shop.label and shop.domain say which one. shop is absent on events stored before stores carried it.
  • In shopify:order and shopify:order_paid, treat customer_display as untrusted buyer text and render it as text, never HTML. The buyer label, line_items, and total_price can each be absent according to consent and the merchant's settings.
  • The hook returns a new object for every event, so use it as a useEffect dependency to react to each one. Events that arrive before the next render replace each other; to handle every one (a chat list, a counter), use useLumioEventListener.
  • Declare events:read and/or chat:read in your manifest's permissions, or the hook never fires (see Delivery).
  • Events from before the surface opened, or while it was closed, are not replayed. The one exception is the current Spotify state in OBS (see Spotify state on mount).