Reference
window.landmark
Your element's HTML/CSS/JS runs inside a sandboxed iframe (sandbox="allow-scripts" only — no parent access, no cookies). The window.landmark object is your bridge to the stream: the event that fired it, the live event feed, your tunable config, persistent storage, and text-to-speech. Standard browser APIs (DOM, Canvas, requestAnimationFrame, fetch to same-origin, timers) all work. localStorage does not — use landmark.vars instead.
At a glance
window.landmark = {
event: { platform, type, usd_amount, payload, occurred_at }, // trigger
mode: "alert" | "persistent", // one-shot pop-up vs long-lived widget
preview: boolean, // true in the editor preview (no billing)
config: { …knobs }, // your _schema controls, keyed by id
events: [ …recent ], // live feed, newest first, capped at 200
on(type, cb) => unsubscribe, // subscribe to the live feed
vars: { get(key) => Promise, set(key, value) => Promise }, // JSONB storage
tts(text, opts?), // speak text on the live overlay
dismiss() // close an alert early
}Properties
landmark.event- The trigger that mounted this element (read-only). Shape:
{ platform, type, usd_amount, payload, occurred_at }. For a persistent element this is a placeholder ({ platform: "", type: "", payload: {} }) until events arrive — rely onlandmark.on()instead. See event payloads for the fields insidepayload. landmark.mode"alert" | "persistent""alert"— a one-shot pop-up that auto-dismisses after itsduration_ms."persistent"— a long-lived widget (chat box, leaderboard, goal bar, always-on prize wheel) that mounts once and reacts to the live feed. Branch on it if your element supports both.landmark.previewbooleantruein the editor / hub preview,falseon the live OBS overlay. The preview silences audio (so a grid of previews doesn't blast sound) and makeslandmark.tts()a no-op (so previews never bill). You rarely need to read this directly — the runtime already gates sound and TTS for you.landmark.configRecord<string, unknown>- Values from your
_schemaknobs, keyed by spec id, so streamers can tune your element without editing code. Each schema entry is{ id, label, type, default, min?, max?, step?, options? }wheretypeis one ofcolor,number,slider,text,select,toggle. Always default-guard:const c = landmark.config || ; landmark.eventsArray<Event>- Recent live events, newest first, capped at 200. Handy to backfill a chat box or leaderboard on mount before you start listening with
on(). Each entry has the same shape as the callback inlandmark.on().
Methods
landmark.on(type, cb)→() => void- Subscribe to the live event feed.
typeis an event type string ("chat","follow","subscribe","cheer","superchat","gift","raid","redemption","like","share","viewers") or"*"for all. The callback gets one event object{ platform, type, usd_amount, payload, occurred_at }. Returns an unsubscribe function. This is what makes a chat box, live leaderboard, or always-on wheel possible.const off = landmark.on("cheer", (e) => { spawnConfetti(e.payload.amount); // bits if ((e.usd_amount || 0) >= 5) landmark.tts(e.payload.user + " cheered big!"); }); // later: off(); // stop listening landmark.vars.get(key)→Promise<value | null>landmark.vars.set(key, value)→Promise<true>- Async persistent JSONB storage scoped to this element — counters, leaderboards, anything that survives between fires. Values must be JSON-serializable. (
localStorageis unavailable in the sandbox; use this instead.) On the live overlay these read/write the token-authed variables store; in the editor preview they're held in memory (and can be seeded so a preview shows real-looking numbers).const n = ((await landmark.vars.get("count")) || 0) + 1; await landmark.vars.set("count", n); landmark.tts(text, opts?)- Read
textaloud through your TTS.opts.voice(string) is optional. On the live overlay it enqueues a read on your account, synthesized and billed through your normal TTS pipeline — an AI voice is Landmark Usage, a standard voice is free. In the editor preview it's a no-op, so previews never bill.textis truncated to 600 characters; empty/whitespace strings are ignored. Use it for moments worth speaking (goal reached, a big tip) — never per chat line.landmark.tts("We just hit the goal!", { voice: "Brian" }); landmark.dismiss()- Close an alert early (before its
duration_ms). No-op for persistent elements.
Live events
Every callback from landmark.on() (and every entry in landmark.events) is one normalized event:
{
platform: "twitch" | "google" | "kick" | "rumble" | "tiktok" | "portal" | "game:<slug>",
type: "chat" | "follow" | "subscribe" | "cheer" | "superchat" |
"gift" | "raid" | "redemption" | "like" | "share" | "viewers",
usd_amount: number | null, // normalized USD for money events; null otherwise
occurred_at: "2026-06-25T12:00:00Z",
payload: { /* type-specific — see below */ }
}platform is "google" for YouTube. Always guard for missing fields — payload shapes vary by platform (a Twitch cheer has bits; a TikTok gift has diamonds). Fields marked optional below are only present on some platforms.
chat
payload.userstring- Display name of the chatter.
payload.textstring- The message text (emote shortcodes stripped to readable names where applicable).
payload.user_id·user_loginstring?- Platform user id / login when available (Twitch sends both; YouTube/Kick send
user_id; TikTok sendsuserId+uniqueId). payload.badgesArray<{ type, img? }>?- Canonical role badges.
type∈broadcaster,owner,mod,lead_mod,vip,sub,member.imgis the real badge-image URL when the platform supplied one. Omitted when there are no badges. payload.fragmentsArray<{ type, text, id? }>?- Present only when the message contains emotes.
typeis"text"or"emote"; emote fragments carry anidfor rendering the CDN image. When absent, just usetext. payload.is_mod·is_ownerboolean?- YouTube convenience flags (derived from badges on other platforms).
landmark.on("chat", (e) => {
const p = e.payload;
addLine(p.user, p.text, p.badges || []); // platform = e.platform
});follow
payload.userstring·payload.user_idstring?- Who followed. No
usd_amount(follows are engagement, not revenue).
subscribe
payload.userstring- Who subscribed / became a member.
payload.tierstring?- Twitch tier (
"1000"/"2000"/"3000") or YouTube member-level name. payload.memberbooleantrue= YouTube channel membership;false= a paid Twitch/Kick/Rumble sub. The canonical matcher uses this to tell the two apart.payload.months·payload.streak·payload.milestonenumber?- Cumulative months (Twitch resub / YouTube milestone), optional streak, and YouTube milestone month. Present on resub / milestone announcements.
payload.textstring?- The user-typed resub / milestone message, when shared.
usd_amountnumber- Normalized streamer income (Twitch T1 ≈ $2.50, T2 $5.00, T3 $12.50; YouTube member / Kick / Rumble $4.99).
cheer (Twitch bits)
payload.userstring·payload.amountnumber- Who cheered and how many bits.
payload.textstring?- The cheer message.
payload.anonymousbooleantruefor an anonymous cheer.payload.bit_image·payload.bit_tierstring? / number?- Animated cheermote artwork URL + tier min-bits, when resolved.
usd_amountnumber- bits × $0.01.
superchat (YouTube super chats / stickers, Rumble rants)
payload.userstring- Who paid.
payload.amountstring- The display amount, e.g.
"$5.00","CA$10.00","₹1,000.00". (A formatted string, not a number —usd_amountis the numeric value.) payload.textstring?- The super chat / rant message (absent on super stickers).
payload.currency·payload.tierstring? / number?- ISO currency code + YouTube super-chat tier, on the Data-API path.
payload.stickertrue?- Present when it's a super sticker rather than a super chat.
usd_amountnumber- Converted to USD from the local amount via live FX rates.
gift (gift-subs, sub bombs, TikTok / Kick item gifts)
payload.userstring- The gifter (
"Anonymous"for anonymous Twitch bombs). payload.kind"subscription" | "item""subscription"= gifted subs / membership gifts;"item"= a TikTok/Kick virtual gift.payload.totalnumber?- How many subs were gifted in a community bomb.
payload.communityboolean?truefor a community gift bomb (one gifter → many recipients).payload.tier·payload.anonymousstring? / boolean?- Sub tier, and whether the bomb was anonymous (Twitch).
payload.giftName·payload.diamonds·payload.totalDiamonds·payload.repeat(TikTok)- Gift name, diamonds per gift, combo total diamonds, and final combo count (e.g. ×50 roses fire one event).
payload.gift{ id, name, iconUrl, cost }?- Resolved gift artwork/cost for TikTok/Kick item gifts, when known.
usd_amountnumber- Gift value in USD (per-sub rate × count, or diamonds × $0.005 for TikTok).
0for a received-gift redemption.
raid (Twitch / Rumble)
payload.userstring·payload.amountnumber- The raiding channel and the viewer count it brought. No
usd_amount.
redemption (channel points / Portal redeems)
payload.userstring- Who redeemed.
payload.rewardstring- The reward title / label.
payload.input·payload.textstring | null- The viewer's typed input, when the reward requires one.
payload.reward_id·payload.slug·payload.surfacestring?- Stable reward id (Twitch/Kick), and the redeem slug + surface (Portal / game redeems) so you can target a specific redeem across renames.
payload.cost·payload.statusnumber? / string?- Channel-point cost and redemption status (Twitch).
like · share · viewers
like(TikTok)payload.user,payload.likeCount,payload.totalLikes.share(TikTok)payload.user(+uniqueId,userId).viewers(all platforms, sampled)payload.viewerCount— periodic viewer-count heartbeat. Treat as approximate; not all platforms emit on every change.
Built-in sounds
These ship with Landmark and are free to reference with new Audio() (same-origin):
/sounds/elements/confetti.mp3— celebratory pop/sounds/elements/wheel-spin.mp3·/sounds/elements/wheel-win.mp3— spin + win/sounds/elements/goal-reached.mp3— goal chime/sounds/elements/counter.mp3— short blip
Audio plays on the live overlay (and is silenced in previews). In some browsers autoplay unlocks after the overlay's first gesture — fine for event-driven plays. Expose a sound on/off toggle in _schema so streamers can mute.
Worked example — a persistent chat box
Backfills from landmark.events on mount, then keeps up via landmark.on("chat"), honoring a _schema "max messages" knob:
const box = document.getElementById("box");
const MAX = (landmark.config && landmark.config.max) || 30;
function render(e) {
const p = e.payload || {};
const row = document.createElement("div");
row.textContent = (p.user || "?") + ": " + (p.text || "");
box.appendChild(row);
while (box.children.length > MAX) box.removeChild(box.firstChild);
box.scrollTop = box.scrollHeight;
}
// Backfill the last few lines (events is newest-first → reverse to oldest-first).
landmark.events.filter(e => e.type === "chat").slice(0, MAX).reverse().forEach(render);
// Then stay live.
landmark.on("chat", render);