Southbill App Bridge
The official communication layer between an embedded app and the Southbill Dashboard
The Southbill App Bridge is the official, supported way for an embedded app to talk to the Southbill Dashboard around it. Your app stays sandboxed inside its iframe; everything that belongs to the host — navigation, external pages, OAuth, toasts, modals, resizing, session tokens — is requested through the Bridge and executed by Southbill.
Use the Bridge instead of raw postMessage, window.top.location, target="_blank" links or framing external providers. Those either break in the sandbox or are blocked by the host.
1. Load the SDK
<script src="https://www.southbill.com/southbill-app-bridge.js"></script>
const bridge = SouthbillAppBridge.create(); // reads ?host= from the URL
const ctx = await bridge.ready(); // waits for southbill:ready
console.log(ctx.installation_id, ctx.environment);
create() accepts { host } if you prefer to pass the host origin explicitly. It throws when no host query parameter is present — that means the page was not opened by Southbill.
The SDK is a thin wrapper: every call is a postMessage to the host origin, every answer is matched back by request id. Requests time out after 15 seconds and reject.
2. Lifecycle
| Step | What happens |
|---|---|
| 1 | Merchant clicks Open in Dashboard → Apps |
| 2 | Southbill loads embedded_url?session_token=…&installation_id=…&environment=…&host=… in a sandboxed iframe |
| 3 | The host posts southbill:ready with the app context |
| 4 | bridge.ready() resolves — your app may now use every Bridge function |
| 5 | Your app verifies the bootstrap session_token server-side and creates its own session |
Attach your listener (i.e. call create()) before any await so the ready event is never missed.
3. Function reference
bridge.ready(): Promise<Context>
Resolves with the context once the host announced itself. Safe to call multiple times.
bridge.context(): Promise<Context>
Requests the context again (e.g. after a long-running session).
{
"app_id": "app_531ecbddea174f1d87738613d999577f",
"app_name": "Syncify",
"installation_id": "insta_…",
"environment": "test",
"merchant_id": "…",
"locale": "en",
"host": "https://southbill.com",
"bridge_version": "1.0"
}
Context never contains secrets, API keys, OAuth tokens or the merchant login.
bridge.sessionToken(): Promise<{ token, expires_in }>
Returns a fresh short-lived (300 s) HS256 session token for the current installation. Use it whenever your backend needs to re-assert who the merchant is; never cache it beyond its exp. Rejects when the app was uninstalled or revoked (installation_inactive) — handle that by showing a reconnect hint.
const { token } = await bridge.sessionToken();
await fetch("/api/sync", { headers: { "X-Southbill-Session": token } });
bridge.resize(height) / bridge.autoResize()
Sets the iframe height, clamped to 400–4000 px. autoResize() observes your document and pushes changes automatically — call it once after ready().
bridge.toast(message, variant?)
Native Southbill toast. variant is success (default), error or info. Messages are truncated at 300 characters.
bridge.modal({ title, message, confirmLabel, cancelLabel, variant })
Host-level confirmation dialog rendered by Southbill (not inside your iframe, so it can overlay the whole dashboard). Resolves true / false. variant: "destructive" renders the confirm action as destructive. title max 120 chars, message max 300, button labels max 40.
const ok = await bridge.modal({
title: "Delete mapping?",
message: "This removes the product link for 42 products.",
confirmLabel: "Delete",
variant: "destructive",
});
bridge.navigate(path)
Internal Southbill navigation, e.g. bridge.navigate("/dashboard/orders"). Only paths starting with /dashboard are accepted; absolute URLs, protocol-relative paths and anything else are rejected with an error.
bridge.open(url)
Opens an https URL in a new browser tab (noopener,noreferrer). Use for documentation, your own dashboard, support pages.
bridge.redirect(url)
Full top-level redirect away from the dashboard. Use only when the merchant is intentionally leaving Southbill.
bridge.authorize(url, mode?)
The way to run OAuth or any third-party consent flow. mode is "popup" (default) or "redirect". Southbill performs the navigation at browser top level; if the popup is blocked it falls back to a top-level redirect.
await bridge.authorize(
"https://accounts.example.com/oauth/authorize?client_id=…&redirect_uri=https://app.example.com/callback&state=…"
);
Your own callback URL finishes the flow on your backend (store the result against installation_id), then the merchant returns to the embedded app. Never render an external authorization page inside the iframe — providers block framing and the sandbox forbids top-level navigation from your page.
bridge.close()
Leaves the embedded surface and returns the merchant to Dashboard → Apps.
4. Wire protocol (raw postMessage)
The SDK is optional. The protocol is stable and documented:
request : { type: "southbill:<action>", id?, ...payload } -> posted to `host`
success : { type: "southbill:<action>:result", id, ok: true, ...data }
failure : { type: "southbill:<action>:error", id, ok: false, message }
announce: { type: "southbill:ready", version, context }
Supported actions: ping, context, session-token, resize, toast, modal, navigate, open, redirect, authorize, close. Anything else is silently ignored.
const host = new URLSearchParams(location.search).get("host");
parent.postMessage({ type: "southbill:resize", height: document.body.scrollHeight }, host);
Backwards compatibility: the legacy id-less messages southbill:resize, southbill:toast, southbill:navigate and southbill:session-token keep working exactly as before. Existing embedded apps need no changes.
5. Security model
| Guarantee | Detail |
|---|---|
| Origin binding | Only messages whose event.origin equals the exact origin of your saved embedded_url are processed |
| Window binding | Only the actual iframe contentWindow is accepted — other frames or tabs cannot impersonate your app |
| Action allowlist | Unknown type values and malformed payloads are dropped without a reply |
| Navigation | navigate is restricted to /dashboard/*; no open redirects |
| External URLs | open, redirect and authorize require https: |
| Bounded input | Text truncated (300 chars), height clamped (400–4000 px) |
| No secret exposure | The host never returns Southbill API keys, OAuth client secrets or the merchant session. The only credential handed out is the 5-minute session token |
| Replies | Responses are posted back only to your exact app origin |
On your side: verify the session token server-side (signature, aud, iss, exp/nbf), never trust values that arrived through the iframe URL without verification, and set Content-Security-Policy: frame-ancestors https://southbill.com.
6. Errors
Bridge calls reject with an Error. Common messages:
| Message | Cause |
|---|---|
Bridge timeout: <action> |
No answer in 15 s — usually the wrong host origin |
Only /dashboard paths are allowed |
navigate received an external or malformed path |
An https URL is required |
open / redirect / authorize got a non-https URL |
Invalid height |
resize received a non-numeric height |
Context unavailable |
The host session is not ready yet — await ready() first |
installation_inactive |
The merchant uninstalled or revoked the app |
7. Minimal example
<script src="https://www.southbill.com/southbill-app-bridge.js"></script>
<script>
(async () => {
const bridge = SouthbillAppBridge.create();
const ctx = await bridge.ready();
bridge.autoResize();
if (ctx.environment === "test") bridge.toast("Sandbox mode", "info");
document.querySelector("#connect").onclick = () =>
bridge.authorize("https://accounts.example.com/oauth/authorize?client_id=…");
document.querySelector("#orders").onclick = () =>
bridge.navigate("/dashboard/orders");
})();
</script>