Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fourthwall.com/llms.txt

Use this file to discover all available pages before exploring further.

A streaming alert overlay turns shop activity into on-stream engagement. A card animates in when an order or tip lands, the chat sees it, the creator reacts to it live. This guide walks through the architecture for building one against Fourthwall webhooks — modeled on Fourthwall’s first-party Alerts app. The overlay itself is a plain webpage your creators drop into OBS, Streamlabs, or any streaming tool that supports HTML browser sources. Webhooks land on your server, and you need to push them out to the OBS browser page in real time. This guide will also show you how to set up an embedded settings page for your app. This way, the creator can manage their overlay settings without leaving Fourthwall.
A complete, runnable implementation of this guide — install, embedded settings, webhook receiver, SSE transport, and overlay — lives in the streaming-alerts example on GitHub.

Architecture

Five moving parts:
  1. Install + webhook registration. On install, your app runs the OAuth flow once, then subscribes to ORDER_PLACED and DONATION for that shop, scoped to your webhook receiver URL.
  2. Embedded settings page. An iframe Fourthwall renders inside the dashboard, identified by a signed handoff. This is the creator’s control surface: overlay URL, test alert, and privacy/enable toggles.
  3. Webhook receiver. A signed HTTP endpoint that validates the payload, looks up the owning shop, and hands the event to the fan-out layer.
  4. Push transport. A long-lived connection from the browser source to your server. The path-of-least-resistance is Server-Sent Events (SSE) — one direction, automatic reconnect, no protocol upgrade.
  5. Overlay page. A static HTML/JS page identified by a per-shop slug. It subscribes to the stream, queues incoming events, and animates them one at a time.

1. Install the App

Use the standard App OAuth flow so the creator can authorize your app against their shop with one click at install. On callback, exchange the code for an access token and resolve the shop with GET /shops/current. You need the token just long enough to register the webhooks (next step) — you don’t have to store it. Afterward, the embedded settings handoff re-identifies the shop on every request by signature, so persist only what you key off per shop (the webhook secret, the toggles).
// after OAuth callback
const shop = await fetch(
  "https://api.fourthwall.com/open-api/v1.0/shops/current",
  { headers: { Authorization: `Bearer ${accessToken}` } }
).then(r => r.json());

2. Register the Webhooks

Immediately after connecting a shop, register a single webhook subscribed to the alert-worthy events. Subscribing once per shop — instead of per overlay session — keeps the configuration stable across reconnects.
POST https://api.fourthwall.com/open-api/v1.0/webhooks
Authorization: Bearer {accessToken}
Content-Type: application/json

{
  "url": "https://your-app.com/webhooks/fourthwall",
  "allowedTypes": ["ORDER_PLACED", "DONATION"]
}
The response includes a secret you’ll use to verify signatures on every incoming event.
Also subscribe to PLATFORM_APP_DISCONNECTED. When a creator disconnects your app from their dashboard, this event lets you clean up their stored tokens and stop trying to push to a stale overlay.

3. Receive and Fan Out

Your webhook receiver:
  1. Verifies the X-Fourthwall-Hmac-Apps-SHA256 header against the body using the stored secret.
  2. Resolves the shop from the webhook payload (shopId is included).
  3. Pushes the event onto the in-memory channel (or pub/sub topic) for that shopId.
  4. Returns 200 OK immediately — alert overlays are fire-and-forget; if no overlay is listening, the event is silently dropped.
// /api/webhooks/fourthwall
export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get("x-fourthwall-hmac-apps-sha256");
  if (!verifySignature(body, sig, secret)) return new Response("bad sig", { status: 401 });

  const event = JSON.parse(body);
  await channel(event.shopId).publish(event);   // e.g. Redis pub/sub, in-memory EventEmitter
  return new Response("ok");
}

4. Push to the Overlay

The simplest transport that works in every browser source is Server-Sent Events. One endpoint per shop, the browser opens an EventSource, your server holds the connection open and writes a line of JSON for each event.
// /events/:shopId  (SSE endpoint)
export async function GET(req: Request, { params }: { params: { shopId: string } }) {
  const stream = new ReadableStream({
    start(controller) {
      const sub = channel(params.shopId).subscribe(event => {
        controller.enqueue(`data: ${JSON.stringify(event)}\n\n`);
      });
      req.signal.addEventListener("abort", () => sub.unsubscribe());
    },
  });
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    },
  });
}
WebSockets work too, but bring upgrade-handshake complexity for no real gain — alerts are one-way push.

5. Build the Overlay Page

The creator copies a URL like https://your-app.com/overlay/{shopId} and pastes it into OBS as a browser source. The page opens an EventSource, queues events, and animates them serially so two cards never overlap.
<!-- /overlay/[shopId] -->
<div id="card" hidden></div>
<script>
  const queue = [];
  let playing = false;

  const es = new EventSource(`/events/${SHOP_ID}`);
  es.onmessage = e => {
    queue.push(JSON.parse(e.data));
    drain();
  };

  async function drain() {
    if (playing || !queue.length) return;
    playing = true;
    const event = queue.shift();
    await renderCard(event);   // animate in, hold, animate out, play sound
    playing = false;
    drain();
  }
</script>
A few non-obvious details worth getting right:
  • Reconnection. EventSource retries automatically, but back off exponentially on the server side (e.g. 1s × 2^n) so a flapping creator doesn’t hammer you.
  • Identifying the shop. Put the shopId in the URL path, not in a query parameter — browser sources sometimes strip query strings on reload.
  • No replay. Don’t try to backfill missed events when the overlay reconnects. The creator wasn’t on screen to react to them anyway; replaying stale alerts feels worse than dropping them.
  • Idempotency. Webhooks can be retried. Dedupe on the event ID before fanning out so the overlay doesn’t show the same alert twice.
  • Sound. Browsers gate audio behind a user gesture, but OBS auto-grants it for browser sources. Test in OBS, not in a regular browser tab.

6. The Settings Page

The creator manages everything from an embedded settings page — the iframe Fourthwall renders inside the dashboard. Set its URL under Settings URL on your Platform App. Fourthwall loads it with a signed handoff:
https://your-app.com/?shop_id={id}&hmac={sig}&timestamp={ts}
Verify the hmac before trusting the request — it’s what authenticates the shop, on the page load and on every API call the page makes back to you. It’s HMAC-SHA512, Base64-encoded (distinct from the SHA256 hex used for webhook delivery) over timestamp={ts}&shop_id={id}&app_id={app_id}, signed with the HMAC secret from your Platform App’s basic information.
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyEmbeddedSettings({ shopId, appId, timestamp, hmac, secret }) {
  const data = `timestamp=${timestamp}&shop_id=${shopId}&app_id=${appId}`;
  const expected = createHmac("sha512", secret).update(data).digest("base64");
  const a = Buffer.from(expected), b = Buffer.from(hmac);
  return a.length === b.length && timingSafeEqual(a, b);
}
Once verified, render the controls: the copyable overlay URL, a test alert button that publishes a synthetic event onto the shop’s channel, and the privacy/enable toggles below. See the Embedded Settings guide for the full contract.

Privacy

Supporters can be sensitive about having their real name on a livestream. Give the creator a toggle — on the settings page above — to replace the supporter’s name with a generic label (“Anonymous”, “A supporter”) before the alert is pushed to the overlay. Apply the transformation server-side so the overlay never sees the real name.

Reference

Runnable Example

The full implementation of this guide

App OAuth

Connect a shop with one click

Embedded Settings

Render your settings page in the dashboard

Manage Webhooks via API

Register subscriptions programmatically

Verify Signatures

Validate the HMAC header

ORDER_PLACED event

Purchase webhook payload

DONATION event

Tip webhook payload

Retry Policies

What happens when your receiver is down