Stream History
Overview
Stream History records every stream session - a run that can span several simultaneous platform broadcasts ("multistream") - and turns the raw event and chat data already collected in TimescaleDB into per-session reports: viewer curves, follower/subscriber growth, chat, emote and GIF totals, donations, and a per-platform breakdown.
The feature is platform-neutral by design: Twitch, YouTube and Kick each start
and end sessions from their live status, and adding a new platform never
changes the schema or the API. Core metrics are fixed fields;
anything platform-specific rides in a platform_metrics JSON object with
namespaced keys.
Stream History is gated behind the feature:stream_history feature flag
(enabled for the Free and Pro plans) and the specific history:read, history:share, history:export, history:delete, history:revenue-read, history:revenue-edit and history:keywords-edit permissions.
Session list & filtering
channelHistory / GET /v1/history returns paged sessions with combinable date, platform, category, type, and title filters, plus server-side sorting. channelHistoryFilterOptions supplies the platform and category choices. The dashboard keeps its filter, sort, and page state in the URL, shows monthly groups, a table on desktop and cards on phones, and preserves the query when returning from a report. A live session refreshes in place after each 60-second sample. Its running peak and average viewers, duration, messages, followers, paid and gifted Subs, chatters, platform figures and top lists come from a read-time overlay; unavailable values are explicitly marked unknown. YouTube subscriber gain is available after the stream ends.
channelHistoryOverview / GET /v1/history/overview returns totals, platform counts, month summaries, and activity days. Month/day grouping uses an IANA timeZone / time_zone (UTC by default); a session belongs to the local day it started. days covers today and the preceding 370 days, regardless of from/to, while respecting the other filters. A blank zone means UTC; an invalid one returns a bad request. The dashboard turns these days into an accessible activity calendar with a day tooltip and click-to-filter action. The overview revenue total and monthly revenue require history:revenue-read.
For dashboard metrics, pass range: "today" | "7d" | "30d" (REST range=). It fills range (local from/to and preceding-window bounds), current, previous, and a zero-filled daily kpiSeries. The KPI figures are sessions, duration, time-weighted average viewers, new followers, messages, paid Subs/Members and gifted Subs. The existing totals and estimated revenue then cover the selected window; the 371-day calendar days still covers its full span. range cannot be combined with from or to (400 on both protocols). Empty windows return zero figures rather than undefined growth.
latestSession / latest_session is the account's newest session even when other overview filters are set. It carries the session summary, up to 60 viewer-curve points per platform, the combined curve viewerTotal / viewer_total on the same bucket times (per bucket the highest per-minute sum across the selected platforms, not the sum of the per-platform bucket maxima), its persisted milestones, and live totals from the latest sampler probes. live is null before the first probe. GraphQL computes latestSession only when selected; REST includes latest_session in the overview. The dashboard reads the selected range again after a history:session_sampled WebSocket event instead of polling.
Platform filter. platforms / platforms= (twitch, youtube, kick) filters the list, the overview and the milestones by platform. The start page uses it for one platform switch that applies to every KPI, trend line, the timeline and the calendar. The list keeps sessions that include a selected platform. The overview counts only the selected platforms' share of each session. A multistream on Twitch and YouTube with only YouTube selected therefore counts YouTube's stream time, viewers, followers, messages and Subs/Members, not Twitch's. Stream time is the time a selected platform was live. Ø viewers weight each platform's average by its live time, and concurrent platforms add up. The peak is the sum of the selected platforms' peaks. The counters come from the per-platform breakdown. A session whose every platform is selected keeps its own values, so selecting everything equals no filter. The estimated revenue still covers whole sessions. latestSession follows the selection: it is the newest session with a selected platform, and its viewer curve, live totals and follower marks cover only those platforms. Follower-mark milestones are filtered by platform, while viewer, Sub and raid records stay. Each session can also carry platformStats / platform_stats: its per-platform card figures (stream time, peak and Ø viewers, followers, messages, Subs/Members, gifted Subs). Combining platforms with platform, or naming an unknown platform, returns a bad request on both protocols.
The sampled live totals keep gifted Subs, Bits, KICKs, Super Chats, Super Stickers and StreamElements tips separate. Money counters include a count, cents per currency, unknown-currency cents, and a converted total in the requested display currency. convertedCents is null if any part lacks a currency or ECB rate. Tips are session-wide and must not be added once per platform. Chat messages per minute come from the difference between the two newest probes. An already-running session starts these counters when sampling begins; older samples have liveCounters: null.
Milestones
channelHistoryMilestones(from, to) / GET /v1/history/milestones reads persisted milestones with history:read, up to 1,000. from includes its instant; to excludes it. Each milestone has an id, session id, kind, platform (for follower marks), value, previous value and achievement time (the session start). The four kinds are viewer_record, sub_record, biggest_raid and follower_mark. A record needs an earlier value to beat; equal values do not qualify. Follower marks need a previous recorded follower count for that platform, so they start only after two finalized streams with such counts. Milestones are computed when a session is finalized. Sessions finalized before milestones existed are computed once in the background after the API starts, so viewer records, Sub/Member records and biggest raids cover the whole history. For those older sessions a session with exactly one raid counts with that raid's size; a session with several raids has no known largest raid and is skipped. Older streams have no stored follower count, so they get no follower marks. channel_history.milestones_computed_at marks a computed session; the background run handles only finalized sessions without it, one API replica at a time and in batches. A second run changes nothing, and a failed session is tried again at the next start. The session report always includes milestones; a session list includes them when GraphQL selects the field or REST requests include=milestones. They are deleted with the session or account.
viewerSparkline / viewer_sparkline has at most 32 combined-viewer points per session. GraphQL computes it only when the list query selects it; REST uses include=sparkline. The dashboard's Viewer trend column draws a line and filled area, marks the peak, and marks the latest point in red while a stream is live. The list can also request revenue with include=revenue; values are omitted on REST and null on GraphQL without history:revenue-read. Raw samples are used when available, with a 5-minute rollup fallback for older sessions. The filtered list exports through REST GET /v1/history/export as CSV or JSON (up to 5,000 sessions); CSV contains streams.csv, months.csv, and totals.csv. Its menu identifies the selected currency and each stream day's ECB rate, or notes that revenue is omitted without permission. Streamer Mode warns when the download contains amounts. This binary download has no GraphQL operation.
Permissions
| Permission | Grants |
|---|---|
history:read | Session list, overview, reports and time-series stats |
history:share | Create, extend and revoke share links |
history:export | Export a filtered list or a finalized report |
history:delete | Delete a stored session |
history:revenue-read | Read revenue, including YouTube Analytics values where available, converted amounts and assumptions; optionally include the Lumio estimate in a share link |
history:revenue-edit | Replace revenue assumptions |
history:keywords-edit | Add, change and remove the account's tracked keywords; history:read permits reading and pattern tests |
Owner and Administrator hold all seven by default. Moderator holds history:read and history:export.
Reports
A report joins session aggregates, per-platform stream rows, 60-second viewer samples, chart markers, and frozen top lists. Its headline KPIs, platform comparison, moments, three charts on one time grid (each chart's crosshair and tooltip show only while you hover that chart), stream table, and chatter/emote/GIF panels adapt to screen width. Twitch ad breaks appear on the viewer chart and in an ad card. adBreaks / ad_breaks gives each break's time, duration, automatic flag and viewer counts; adStats / ad_stats gives counts, time, impressions, threshold and viewer-change estimates. Kick and YouTube do not supply ad breaks here.
Terminology follows the metric scope. Twitch and Kick call these Followers and Subs; YouTube calls them Subscribers and Members. A mixed aggregate reads Followers & Subscribers and Subs & Members. The German paid term is Subs, never Abo; rounded YouTube subscriber figures carry ≈. Twitch tier counts remain separate from YouTube members. See Platform adapters.
Shared Chat and chart markers
A Twitch stream row carries shared_chat_sources when the broadcast had a Shared Chat, and null otherwise. Each partner has its channel identity, message count, peak and average viewers, and active_from / active_to. For sessions with transition events, those boundaries are exact event times; a channel that returns has multiple spans in shared_chat_timeline. The timeline is derived from events while live and frozen with the report at finalization, so it survives event-log retention. Older streams without transition events use minute samples as an approximate fallback (± one minute). Each 60-second sample carries shared_viewers (the sum of own and partner counts) and shared_chat_viewers with partner counts and cumulative messages. A missing partner was not in that bucket; a null viewer count means the partner was present but not live. These are channel-level aggregates, never viewer identities.
Events from a Shared Chat partner channel never count toward your history. That includes Bits cheered in a partner's chat, partner subs, resubs, gift subs and raids. They appear in your event feed, labelled with the partner channel, but are marked is_shared_chat. The live report, the finalizer and every boot backfill skip them. Partner events therefore never reach platform_metrics.twitch.bits, the sub and gift counters, the report's KPI strip or the estimated Bits revenue. Only your own channel.cheer Bits count.
Each stream row also has tags (the tags at stream end) and category_changes / title_changes (at plus the value; the first entry is the state at stream start). Twitch and Kick fill them; YouTube returns empty lists. The report shows the tags as chips and, when the category changed during the stream, a category timeline under the stream row. Title changes are in the API and the JSON export only. Top chatter names and avatars, emote images, and GIF thumbnails are frozen when a session finalizes; a report keeps the fields and number of entries it was finalized with. The UI can render up to 20 top emotes; a report frozen with 10 stays at 10. Each top emote carries emote, provider, url, count and, for the hover card, the provider id, animated, owner_id and platform (the chat platform it was used on most; a tie goes to the alphabetically first). Emotes with the same name and provider but different ids are separate entries; the same 7TV, BTTV or FFZ emote used on several platforms is one entry. Reports frozen before these fields were added omit them. Top emote and top GIF tiles open the emote card on the dashboard and on a shared report; on a shared report the card makes no server requests and shows only the frozen fields. Top GIFs use 9 slots in a 3 × 3 grid, and top chatters use 10.
The session list can mark a Shared Chat session: sharedChat / shared_chat (GraphQL when selected, REST include=shared_chat) lists every channel of the session's Shared Chats, host first, then your own channel, then join order, and says whether a Shared Chat is running in the live session. Finalized sessions read the frozen timeline; a session that is live or not finalized yet derives it from transition events. Sessions stored before the timeline list only the partner channels. The Shared Chat filter accepts All / With / Without (sharedChat: WITH | WITHOUT or ?shared_chat=with|without) and applies to list, overview, trend, heatmap, export and dashboard start. It appears when Twitch history exists. A live session matches With from its begin event. The list row puts a three-avatar host-first marker beside its existing Twitch icon.
The report and Dashboard This stream card offer My channel (default) and Whole session for Shared Chat sessions. My channel's chat figures use only messages from the own room; its viewer figure is the own Twitch Helix count. Whole session shows Sum of all channels plus one channel line in host → own → partner order; a partner who left keeps its past line, with gaps rather than zeros where no count exists. The report marks joins and leaves on the chart's time axis and shows a Shared Chat session card with partner values once. Its stream-table row keeps a one-line sum. Revenue and support always come only from the own channel, including in Whole session. The selected view is URL state; list tiles, trends and export stay on My channel. A shared report link and the summary email also show only My channel.
latestSession.sharedChatSeries / latest_session.shared_chat_series supplies the Whole session chart on the same buckets as the normal viewer series. It is null when the session never had Shared Chat, Twitch is excluded, or the raw samples have expired. LiveSnapshot.sharedChat gives the live host, participants, partner viewers and messages, sum and session chat rate. The card updates on history:session_updated without a reload. Whole session leads the chat cell with session messages and shows the own-channel count below; its Chat/min headline uses sessionChatPerMin and shows — until a second sample exists. My channel uses the own rate.
Zuschauerzahlen im Shared Chat
Lumio treats Twitch Helix viewer_count as each channel's own count, including during Shared Chat. This has not been confirmed with a live side-by-side measurement. Twitch describes individual viewership beside unique Shared Viewership; its current API and EventSub surfaces do not expose the unique metric. Sum of all channels adds the channel counts and can count a viewer watching several channels more than once; it is never labelled as Twitch's unique count.
The server computes peak, low and median markers for the viewer chart and net gain and steepest-rise markers for growth charts. A shared time grid gives every chart and KPI trend line the same timestamps. Long sessions are downsampled with LTTB while preserving peaks and troughs.
Bot messages
Chat bots are left out of the chat figures, so command spam does not inflate them. A message counts as a bot message when any of these holds:
- Bot badge — Twitch
botorbot-badge, Kickbot. - Known bot — the curated list of common bots (Nightbot, StreamElements, Moobot, Fossabot, Botrix and others). Twitch and Kick match the login. YouTube has no bot badge and its display names are neither unique nor fixed, so YouTube bots are recognised only by channel ID (
UC…), from a list that holds only entries verified as the bot's chat account (today Nightbot and Fossabot). A YouTube viewer who merely uses a bot's name is never matched. - Your own bot account — the Twitch or Kick bot account connected under Bot Connections. A YouTube bot connection stores only the channel title, so it is not used.
- Your channel's bot list — platform accounts marked under Channel settings → Bots, matched by stable user or channel ID on Twitch, Kick and YouTube.
A broadcaster badge always wins, so a channel that writes through its own account is never counted as a bot. A login that merely contains "bot" is not matched. The chatter list uses the same definition, so a bot there is a bot here.
Bot messages are excluded from total messages, the messages-per-minute series, per-platform message and chatter counts, unique chatters, top chatters, emote and GIF totals, and top emotes and GIFs. Events (follows, subs, gifts, raids, Bits, donations) and viewer counts are unchanged. The filter is always on. botMessages / bot_messages gives the number of messages left out, including on a shared report. For a live session it is calculated from the retained chat log with the other chat figures. It is null when that live calculation is unavailable, and for an older session whose chat log was gone when bot filtering arrived — null means unknown, not zero.
A bot that has no bot badge and is not on the curated list, such as a third-party bot that writes through an ordinary account, is counted like a viewer. Lumio does not guess bots from their name.
The per-platform figures add up to the session figures. Unique chatters are counted per platform: nobody can be matched across platforms, so the same person on Twitch and YouTube counts once on each, and the session figure is the sum of the platform figures. Total messages and unique chatters also include chat from a connected platform that was not live during the session; the platform comparison lists only the platforms that streamed, so in that case its sum is lower than the session figure.
Sessions finalized before bot filtering are corrected once at API start while their chat log is still complete. Their counts, top lists, per-platform figures and stored per-minute samples only ever go down, and bot_messages is filled in. Marking an account bot also starts a background recalculation of retained sessions with complete chat logs. A session whose log the chat retention already removed, even in part, keeps its original figures. Removing a mark does not restore figures already recalculated.
AutoMod log
The report's Chat insights card has a private AutoMod tab for Twitch sessions when the reader has both history:read and chat:automod-read. automodLog / GET /v1/history/reports/\{id\}/automod reads held messages from platform_automod_messages for that session, with per-outcome counts and cursor pagination. The tab shows hold time, sender, flagged text, reason, outcome, moderator and decision source, and waiting time. It filters by open, allowed, denied or expired and sorts by hold time. It is not in the finalized report, shared reports, exports or popouts; sessions without a Twitch broadcast or outside chat retention have no log. Held messages never contribute to chat insights counts until Twitch publishes an allowed message as a new chat message.
Each entry, its text, flagged spans and blocked-term text included, is deleted once held_at is older than the account's chat plan window (Free 365 d, Pro 1,095 d, an account_limits override wins, at most 1,095 d), for every outcome. A message above the 4 KiB storage cap has no stored text (text_state: not_stored). The tab renders Twitch emotes and Cheers (cheer art from the channel's cheermote catalog at read time, with the amount) and matches 7TV, BTTV and FFZ emotes by word from the connected channels' emote sets. Its sender cell shows the avatar Lumio knew at hold time and the name, or initials without an avatar; it does not show badges. The channel's instructed erasure of a participant deletes their rows in that channel; holds after it are new channel data and are stored. A Lumio-level objection does not touch the log (it belongs to the channel), so an objecting participant is logged like anyone else, and disconnecting Twitch keeps the rows. See Data & Privacy.
Links and commands
The Chat insights card also has Announcements and Pins tabs. A Twitch
announcement carries its colour, time, author (avatar, name, bot flag) and
optional Shared Chat source. A pin records its platform, sender and moderator
(avatar, name, bot flag), planned duration, changes, actual duration and end
reason: expired, unpinned, replaced, message_deleted, stream_end,
ran_past_stream_end, unknown or active. A Twitch removal observed by
polling shows ≈ for its approximate time. The actual duration uses the
chat's pin wording: seconds or minutes under an hour, hours and minutes from one
hour, days and hours from one day, rounded down. The report contains up to 500
announcements and 200 pins and marks a truncated list. A session begun before
capture started says Not captured for this stream, even if it ended after
capture began.
Neither tab freezes message text in the report. It shows text while the chat
message is stored; pin text can also come from the retained channel pin event.
When no retained source has text, the row says Text no longer stored. The
same rule applies to a public share and an export. An instructed erasure masks
the person in these lists of that channel's reports and removes their message
reference from the frozen report; an objection does not touch them (it acts on
Lumio Stats only). The pin events themselves are channel-owned and stay in
platform_event_logs after a viewer's support-tool erasure.
Share links can include announcements and pins as separate sections,
both off by default and in the group that exposes viewer names. Exports carry
these sections in JSON, PDF and TXT; a CSV report ZIP includes
announcements.csv and pins.csv when the lists are present. PDF and TXT
word pin durations like the dashboard (minutes, hours or days); pins.csv
keeps the seconds and adds a worded duration column. Announcements
participate in ordinary chat message totals, chatter counts, commands and
keyword insights.
A report lists the links posted in chat and the chat commands viewers used during the session, in the same time window as the other chat figures. links and commands are on the report in GraphQL and REST and in the JSON export.
Links. A link is an http:// or https:// URL, a www. address, or a bare host.tld/path whose top-level domain is on a short list of common ones (.com, .gg, .tv, .de and others). That keeps chat noise such as lol.xd, v1.2 or z.B. out. Trailing punctuation and a closing bracket the link did not open are stripped, and e-mail addresses are ignored. Variants of the same link are merged into one entry. To compare them, the scheme and host are lower-cased, www. is dropped, tracking parameters (utm_*, fbclid, gclid, si) are removed and a trailing slash is ignored; the path keeps its case. url is that normalized form, display the link as it was first posted, and domain the host. A link posted twice in one message counts once. Bots are included and flagged here: a bot posting the Discord link forty times is useful to see, so every poster and occurrence carries is_bot, using the bot definition above.
Commands. A command is a message whose text starts with a command prefix followed by a name that starts with a letter, up to 32 characters of letters, digits, _ and - ([A-Za-z][A-Za-z0-9_-]{0,31}), and then a space or the end of the message. The prefixes come from the API setting [history] command_prefixes; the defaults are ! and # (see Installation). The setting must include the prefix of Lumio's own chat bots. With a multi-character prefix, the longest match wins. The prefix is part of the entry, so !discord and #discord are two entries, and a hashtag at the start of a message counts as a # command. !, !!!, #1, #2024 and a ! in the middle of a message are not commands. The rest of the message, trimmed and cut to 100 characters, is the command's argument text. Commands are what viewers type, so messages from bots are left out entirely: they are not counted and appear neither as posters nor as occurrences. known says whether the name, without its prefix, is an enabled Lumio bot command of the account or a global one on a platform where it was used. It is information only and does not change the counts.
Keywords. The account's tracked list forms a third Chat insights tab. Each of its at most 50 entries has a row, including entries with zero hits. Whole-word, contains and regex matching count a keyword once per message, anywhere in its text. Bot messages are included and flagged, as for links. A leading token that matches a keyword, such as #ad, counts as a keyword and not as a command. Each row also has bot_count, unique_users, first/last time, top posters and the first 100 occurrences. Changing the list recounts finalized sessions in the background while their full chat log remains; sessions without it keep their frozen keyword figures. A missing frozen list with no complete chat log returns null (unknown).
Each entry has count, first_seen_at, last_seen_at, the top 5 posters (platform, user_id, login, display_name, count, is_bot) with other_posters for the number of further people, and occurrences: the first 100 uses, oldest first, each with its time, platform and author. When there were more, occurrences_truncated is true and count holds the full number. A command entry adds name (prefix plus lower-cased name), prefix, unique_users, top_args (the 5 most used argument texts, trimmed and lower-cased, "" = no arguments) and args on each occurrence. Each list holds the 50 entries with the highest count.
Dissolved or deleted channels. When a channel is dissolved (at once, or at the end of its 7- or 30-day grace period) or deleted by an operator, all of its stream reports are deleted with it: sessions, streams, viewer samples, the viewer curve in both rollups and milestones (founder ruling ZAF-2626). Deleting a single report removes the same rows for that session. Only a finalized report can be deleted: while a session is live, in its grace window or waiting for the finalizer, deleteChannelHistory / DELETE /v1/history/reports/{id} refuses it with 409 (This stream session is still live; it can be deleted once it is finalized), because Lumio still writes samples and rollups for it. Scheduling a dissolution deletes nothing, and a cancelled one keeps the reports. See Accounts › Account Dissolution Flow.
Erased chatters. When a channel has a chatter's data erased (Chat → Erasure), their top-chatter entries, posters and occurrences stay in every finalized session of that channel with their counts but without identity: user_id is null, login and the name are empty, the avatar and a command's args are removed, and the entry carries erased: true (REST: the key appears only on erased entries; GraphQL erased: Boolean!). The report, the public share and every export show such an entry as "Deleted user" ("Gelöschter Nutzer" in German), never as an empty name. A later recount works from the chat log, which no longer holds the chatter's messages, so the name does not come back.
Suppressed chatters (instructed erasure). Each channel keeps its own suppression list (account_chatter_suppressions) of chatters it had erased by name (Chat → Erasure): the instructed erasure of one person in one channel adds them in the same operation, and the entry stays until the account is deleted or the channel withdraws its instruction. Each entry records who initiated the erasure (initiated_by): channel when the channel initiated it, subject when the person asked for it. Only a channel entry can be withdrawn; a subject entry stays (the withdrawal answers 409), a later channel erasure never turns it back into channel, and entries recorded before the initiator was stored count as subject. A withdrawal (POST /v1/admin/privacy/chat/erase/withdraw / GraphQL withdrawChannelChatterSuppression, operator-only with admin:privacy-erase-withdraw, audit privacy:channel_suppression_withdrawn) deletes that channel's entry only: sessions frozen from then on name the chatter again, while names the erasure already blanked and everything it deleted stay gone. Every list of that channel built afterwards — the top chatters when a session finalizes or is recounted, links, commands and keywords when they are frozen or recounted, and the lists of a live or unfrozen session built on read — shows a listed chatter the same way as an erased one: the entry and its counts stay, the identity is blanked and erased: true is set, so the report shows "Deleted user". On Twitch and Kick, entries without a user id are matched by the logins stored with the list entry. If the list cannot be read, the session is not frozen (the finalizer retries on its next run), and a report built on read shows no links, commands or keywords instead of named ones. Other channels' reports are not affected by it. The global Lumio list (chatter_suppressions, which also holds GDPR Art. 21 objections — Chat → Objection) acts only on the public Stats leaderboard, never on a channel's report, export or share link (Public Stats). A share link and its public export show the frozen lists as stored, without a read-time mask: past sessions are cleaned by the channel's instructed erasure, later ones are frozen blanked by the channel's list. Crawled sessions hold no chatter names and are not affected.
Links and commands are frozen when a session finalizes, so they outlive the chat log like the other top lists. A live session, and a session finalized before links and commands existed, builds them from the chat log on read while that log is still complete. Once the chat retention has removed it, even in part, both are null, which means unknown, not "none". Frozen links, commands, argument texts and poster names stay with the report after the chat log is deleted, capped as described above.
Revenue and currency
estimatedRevenue / estimated_revenue can appear on the report session, list rows, and overview. It is null in GraphQL or omitted from REST without history:revenue-read. The estimate combines these sources:
| Source | Fixed value or account setting | How a line is priced |
|---|---|---|
| Twitch Tier 1 / 2 / 3 subs and gifts | Fixed USD $4.99 / $9.99 / $24.99 | Count × tier price × editable Twitch share. Prime uses the fixed USD $4.99 Tier 1 value and the same share. An old gift without a recorded tier uses Tier 1 (assumed_tier_1). |
| Twitch Bits | Fixed USD $0.01 per Bit | Count × value; no additional share. Shared Chat partner Bits do not count. |
| Kick subs and gifts | Fixed USD $4.99 | Count × value × editable Kick share. |
| Kick KICKs | Fixed USD $0.01 per KICK | Count × value, without a share. The value is a third-party assumption, marked please_verify because Kick does not publish it. |
| YouTube memberships and gifts | Account-entered membership levels | Each paid month uses its level price × the membership share. A join and each membership milestone count as a paid month. A name without a matching entered price, or a month without a level name, uses the cheapest entered level and carries assumed_lowest_level. Without any entered level, memberships and gifts remain unpriced. |
| Twitch and Kick ads | Account-entered CPM, ad share and ad-free viewer share | For each break, viewers at its start × ceil(duration_seconds / 30) gives viewer spots. Sum them, remove the ad-free share, then multiply impressions ÷ 1,000 × CPM × ad share. Kick breaks come from the live-only ad-break-status poller, not a webhook. |
| YouTube ads | Account-entered CPM, mid-rolls per hour, ad share and ad-free viewer share | Viewer-hours (sampled viewer-minutes ÷ 60) × mid-rolls per hour × (1 − ad-free share) ÷ 1,000 × CPM × ad share. No pre-rolls are counted. The line exists only with a mid-roll rate and viewer samples and carries estimated_from_midrolls. |
| YouTube Super Chats and Super Stickers | Amounts reported by YouTube | Each has its own editable creator share (70% by default). These amounts appear under actuals; the payout after the share is an estimate. |
| Donations | Amounts reported by StreamElements | Actual gross amounts under actuals, before fees. |
The fixed values are read-only USD values in fixedValues / fixed_values; their source currency is converted at the stream-day ECB rate. CPMs, membership prices and payout currency are account assumptions. An ad break without a CPM still has a line with zero amount and cpm_missing; a YouTube stream without a mid-roll rate has no ad line. If a YouTube Analytics estimatedAdRevenue value becomes available, it replaces the mid-roll estimate rather than being added to it. Platform settlement, taxes, regional prices and silent renewals can differ from this estimate.
YouTube Analytics revenue
For a connected channel in the YouTube Partner Program that granted the optional yt-analytics-monetary.readonly scope, the report labels the source Estimated revenue according to YouTube Analytics. Its youtubeAnalytics / youtube_analytics state is pending until figures arrive, normally 2–3 days after the stream. While pending, the report labels and shows the Lumio estimate. With values it shows provisional or final and the fetchedAt / fetched_at date. YouTube may correct provisional values.
The reported lines youtube_analytics_ads, youtube_premium and youtube_transactions represent ads, Premium and a remainder derived from YouTube's reported total. They replace, rather than add to, Lumio's YouTube ad estimate, membership and gift estimates, and the Super Chat and Super Sticker amounts in the revenue breakdown. Other platforms and donations keep their own lines. A missing scope (not_granted), a channel outside the Partner Program (not_in_ypp), or a disabled Analytics API in the account's Google project (api_disabled) leaves the Lumio estimate in place and shows a hint only on the revenue area. See YouTube Analytics API setup. Existing connections without the scope receive one dismissible history-list announcement, not a standing connection warning.
channelHistoryYoutubeRevenueStatus / GET /v1/history/youtube-revenue/status reports the account's connection and Analytics state without amounts under history:revenue-read and feature:stream_history. Lumio batches due videos into at most one Analytics revenue request per channel per day, usually none; the Partner Program check is cached for a week. The requests count against the customer's own Google app quota.
Only account members with history:revenue-read can read the Analytics amounts. A share link and its public export always use the Lumio estimate, even when the private report uses Analytics; the public path does not load Analytics values or totals derived from them. Private report and list exports can contain YouTube Analytics amounts when permitted. Their menus warn that a downloaded file contains YouTube revenue and is outside Lumio's control afterward.
Stored Analytics rows are deleted when the connection is disconnected, the scope is removed or the grant is revoked, the authorization stays unconfirmed for 30 days, a video no longer exists on YouTube, the account is deleted, or the stream report is deleted (deleteChannelHistory / DELETE /v1/history/reports/{id}; rows of a video that another report still shows stay). Lumio confirms the authorization daily and the videos' existence every 30 days while rows exist, also when the channel has left the Partner Program or the Analytics API is disabled. While a channel dissolution is scheduled, Lumio fetches nothing: no new videos, Partner Program checks or revenue requests, only the deletion checks keep running. The owner's GDPR data export (Your channels) contains the channel's stored rows, fetch schedule and check state as channels/<channel-id>/youtube_revenue.json.
When more than one platform has revenue items, the report card offers All and tabs for Twitch, YouTube and Kick as applicable. All shows a clickable sum row per platform and donations, which have no platform. A platform tab shows its items and a subtotal when the amounts can be summed in one currency; a single-platform report has no tabs. The grand total, currency and assumptions action stay above the tabs, while the display-currency switch sits in the report header. Streamer Mode masks monetary sums and prices. grandTotal and averages use converted amounts where possible.
channelHistoryRevenueSettings / GET /v1/history/revenue-settings returns saved assumptions or defaults and the read-only fixed values. updateChannelHistoryRevenueSettings / PUT /v1/history/revenue-settings writes the editable values with history:revenue-edit and emits account:history_revenue_settings_updated only when values change. The event names the changed fields (youtube_member_levels for any added, removed or repriced level) but not their values, because the audit log is readable with audit-log:read alone and the assumptions are behind history:revenue-read. YouTube level names seen in retained events are available through channelHistoryYoutubeMemberLevelSuggestions / GET /v1/history/revenue-settings/youtube-member-level-suggestions under history:revenue-read.
The optional currency argument on history reads and exports defaults to the user's display currency. A Money value carries amountCents, currency, sourceAmountCents, sourceCurrency, rate, rateDate, and convertible (snake_case in REST). Conversion uses the ECB reference rate for the stream's UTC start date, falling back to the last rate within seven days. Missing rates leave the source value intact, mark it unconvertible, and exclude it from converted totals and averages. Historical Super Chat and Sticker sums are tracked per currency; old mixed-currency rows remain marked as such.
displayMoney / display_money on the report session gives the money the report shows in the source currency, converted into the display currency (or the currency argument). Its fields are currency, donationsCents, superchatsCents, superstickersCents and rateDate (snake_case in REST). Each amount sums every currency of its kind, and each part is converted at the stream-date rate as above, so mixed Super Sticker currencies end up as one figure. An amount is null when any part has no rate or no known currency, for example old mixed-currency Super Chat totals. A kind with no money is 0. The source amounts stay unchanged in donationsTotalCents / donationsCurrency and platformMetrics (donations_by_currency, youtube.superchats_amount_by_currency, youtube.superstickers_amount_by_currency). This conversion is not a revenue estimate: it needs only history:read, while estimatedRevenue stays behind history:revenue-read. It adds no data a history:read caller cannot already see, only a converted view of it. The session list leaves displayMoney null. On a shared report the donation and Super Chat/Sticker money stays stripped, so every amount is null, and currency is the link currency.
Export
A finalized report exports as PDF, CSV, TXT or JSON. The PDF has up to four A4 pages: Overview, Timeline, Revenue & Ads, and Chat & Streams; empty sections drop out. TXT follows the same sections, CSV adds revenue.csv and ad_breaks.csv, and JSON keeps machine field names. The Chat & Streams part has Links, Commands and Keywords tables when those lists are present. CSV adds links.csv, commands.csv and keywords.csv with one row per entry, plus link_posts.csv, command_posts.csv and keyword_posts.csv with one row per stored occurrence, times in the export time zone. A public export includes keywords only when the share link includes the keywords section. The PDF overview lists donations, Super Chats and Super Stickers in the display currency, or in their source currencies when a rate is missing. CSV summary.csv adds display_currency, display_rate_date and, for donations, Super Chats and Super Stickers, a *_display_cents row plus a *_source row with the source amounts per currency (200 EUR + 10000 JPY). JSON carries session.display_money next to the source fields. Exports honor locale, timezone, currency, revenue permission, and public link section choices. CSV text cells that could be interpreted as spreadsheet formulas receive a leading apostrophe; numeric cells remain numeric. A live report returns 409.
Stream summary email
When a session is finalized (its grace window closes), Lumio sends the account owner a summary of the stream. The email uses the C · Brief layout with average viewers, peak viewers, and live time as key figures. Viewer figures show their change from the previous completed stream when one exists. A short sentence lists follower, subscriber/member, and chat-message counts; gifted subscriptions, StreamElements tips, Bits, KICKs, Super Chats/Stickers, and raids appear there when non-zero. The email then shows a transparent viewer-and-chat chart, a platform table, and a button to the full report. The chart is a single CID PNG for light and dark layouts, with axes and legend labels as HTML text. The email does not show the report's peak/median markers or top lists.
The summary is sent only when all of these hold:
- the session lasted at least 5 minutes (test streams are skipped),
- e-mail delivery is enabled for the deployment (
email.enabled), and - the owner has not opted out (below).
Opt-out
The summary reuses the standard notification-preference system under the
stream_summary notification type, with four channels: off, in-app only, e-mail only, or in-app + e-mail (the default). Owners change it in Settings → Notifications. The in-app notification is delivered when the selected channel includes in-app delivery.
Shareable links
When a link includes revenue, its public report and export show only the Lumio estimate for YouTube, never Analytics values or totals derived from them.
A time-limited share token (lm_share_*) opens /share/history/\{token\}. Up to 20 active links per report can have a password and an export toggle. options controls the public sections (chat, top_chatters, ads, revenue, links, commands, keywords), include_revenue, and currency. For new links, chat and ads are on; top chatters, revenue, links, commands and keywords are off. links, commands and keywords show viewer names, and commands show the text viewers wrote, so each has its own switch; a section that is off is null on the shared page and absent from its export. A link created without options (every link from before share options existed) shows chat and top chatters, with no ads, revenue, links, commands or keywords. Ad breaks and the ad card are shared only when the creator turned on ads, so older links no longer show them. Including revenue requires history:revenue-read at link creation, and extending a link that shares revenue requires it too, on GraphQL and REST alike (403 with Missing permission: history:revenue-read). Share-link create, extend and revoke audit events carry the effective options.
The public response includes the permitted report sections, channel display information and expiry. Its viewer, chat and follower charts use the same samples as the owner's report, reduced to the public fields: viewers, messages, followers, subs and the platform counts. The separate live totals (gifted Subs, Bits, KICKs, Super Chats, Super Stickers and tips with their amounts) and every sample amount are left out, even on a link that shares revenue; shared revenue appears only as the estimated-revenue card. The page uses its own header and the standard legal footer. A password prompt exposes no report metadata.
Images and cookie consent on the shared page. People who open a share link are not Lumio customers. Channel and chatter avatars (Twitch, Kick, YouTube, Discord), emotes (Twitch, 7TV, BetterTTV, FrankerFaceZ, Kick), badges and Bits / KICKs graphics load through Lumio's image proxy (/img/… on lumio.vision), so the visitor's browser never contacts those servers and the images show without a consent choice. An image the proxy does not cover renders a neutral placeholder: initials instead of an avatar, the emote code as text, the cheer as text. GIPHY GIFs are not proxied: they load only after the visitor allows Functional cookies in the cookie-consent banner; until then a GIF chip with the caption stands in and the emote/GIF hover card is not mounted. The signed-in dashboard shows the same report with direct image URLs. A locked link falls into one of two classes, each with one fixed response:
- Unavailable — a malformed or unknown token, or an expired or revoked link, returns
410withSHARE_UNAVAILABLE. The page shows that the link is no longer available. - Password required — an active link with a password, opened with no password or a wrong one, returns
401withSHARE_PASSWORD_REQUIRED. The page shows the password prompt.
Rate limiting and password lockout. The public view and export are rate-limited per end-user IP. A request the web app forwards on a visitor's behalf counts against that visitor's IP, not against the web server. Wrong passwords lock a protected link: after 10 failed attempts on one link, or 20 from one client IP across links, within 15 minutes, every password attempt returns 429 with RATE_LIMIT_EXCEEDED until the window ends, including the correct password. Each attempt is counted before its password is checked, so the limit holds for simultaneous requests too, and a refused attempt also counts. A correct password is taken off the count again, so it does not count. An IPv6 client is counted per /64 network. Expired, revoked and unknown links never reach the password check, so they never count either. Password checks run off the request threads. When the rate limiter's store is unavailable, the public share endpoints return 503 rather than going unthrottled.
Expiry and revocation are checked first, so an expired link never asks for a password. Share tokens are 256-bit random values, so a guessed token gets the same 410 as an expired one and does not reveal whether a link exists. Only someone who already holds a real token learns whether it still works, which is what the page needs to tell them. A missing and a wrong password stay indistinguishable. Neither response carries a title, channel, date or expiry, and the social preview of a locked link is neutral. A public export requires the link's allow_export flag and contains only permitted sections. The plaintext token is returned once, at creation; the link list exposes only its prefix.
Live updates
While a session is live it appears in the list with a live badge, and the
WebSocket history:{account_id} channel emits history:session_started,
history:session_updated and history:session_ended events for live refresh.
channel_gate_for maps the channel to ChannelGate::Permission("history:read")
and channel_feature_for to feature:stream_history, so both must hold to
subscribe.
API
GraphQL POST /v1/gql is the primary structured read/write surface; GraphQL lists the queries and mutations. REST mirrors structured operations under /v1/history and serves the two file downloads: REST Stream History. The history:{account_id} WebSocket channel emits session started, updated, and ended events behind history:read and feature:stream_history.