Apps

Create, configure, submit and publish an app in the Southbill App Store.

Create an app

From draft to a complete listing.

Apps are created straight from the dashboard: Apps → New app creates an Untitled app draft and opens the publishing wizard. Nothing is public until you submit and we approve.

The wizard

Step What you fill in
App basics Name, tagline, category, description
Branding & media Icon, screenshots
Availability & pricing Markets, setup effort, EUR pricing
Links & compliance Website, support, privacy policy, data handling
OAuth & webhooks Redirect URIs, credentials, event subscriptions
Submit for review Version number, requested scopes, notes for the reviewer

Every step saves as you continue — there is no separate save button.

Naming rules

  • 3–40 characters, unique in the App Store.
  • No "Southbill" prefix, no other brand names you do not own.
  • The tagline is one sentence, sentence case, no marketing punctuation.

Description that passes review

Cover: what the app does, which merchants it is for, what happens after install, and which data it reads. Reviewers reject descriptions that only list features without explaining the merchant outcome.

Media requirements

Asset Requirement
Icon Square PNG, at least 512×512, no rounded corners baked in
Screenshots 2–6, 16:9, real product UI — no mockups with fake numbers

Deleting a draft

Drafts, rejected apps and apps with changes requested can be deleted while they have 0 installations. Published apps cannot be deleted; unpublish them instead so existing merchants keep working.

Review & publishing

What we check, how long it takes and how to pass first time.

Submitting

Submitting freezes a version: listing content plus the requested scopes. The app moves to in_review and the listing becomes read-only until we respond.

Statuses

Status Meaning
draft Work in progress, only visible to your organization
in_review Submitted, waiting for a reviewer
changes_requested Fixable issues — edit and resubmit
rejected Not eligible in its current form
published Live in the App Store
unpublished Hidden from the store, existing installs keep working

What reviewers check

  1. Scope hygiene — every requested scope is justified by the described functionality. Asking for payments:create for a reporting app is the most common rejection.
  2. Working install — the OAuth flow completes against a real redirect URI and the app renders something useful.
  3. Listing accuracy — screenshots show the actual product, pricing matches what you charge.
  4. Legal pages — reachable privacy policy and support contact, and a data-handling statement.
  5. Webhook health — your endpoint answers 2xx within 10 seconds during the test delivery.
  6. Stability — no error spikes in your sandbox logs for the submitted version.

Timing

Reviews are usually answered within 3 business days. Resubmissions after changes requested are prioritized.

Publishing

On approval:

  • in live mode the one-time publishing fee is charged to your billing method;
  • the listing goes public in the App Store;
  • merchants can install immediately.

Publishing is blocked while your payout account is restricted or disabled.

Updating a published app

Editing a published listing creates a new version in draft. The live listing keeps serving until the new version is approved. Adding new scopes requires re-consent: existing installations keep the old scopes until the merchant approves the update.

Installs & OAuth

The authorization code flow with PKCE, step by step.

Merchants install your app through OAuth 2.0 — authorization code with PKCE. No client secret is ever needed in a browser.

1. Send the merchant to consent

https://southbill.com/oauth/authorize
  ?client_id=sb_client_live_123
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=merchant:read%20payments:read
  &state=<random>
  &code_challenge=<base64url(sha256(verifier))>
  &code_challenge_method=S256

The merchant sees your icon, name and a plain-language list of every scope with its risk level, then approves or declines.

2. Exchange the code

curl -X POST https://api.southbill.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "ac_…",
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "sb_client_live_123",
    "code_verifier": "…"
  }'
const res = await fetch("https://api.southbill.com/v1/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "authorization_code",
    code,
    redirect_uri: "https://yourapp.com/callback",
    client_id: "sb_client_live_123",
    code_verifier: verifier,
  }),
});
const tokens = await res.json(); // { access_token, refresh_token, installation_id, … }
tokens = requests.post(
    "https://api.southbill.com/v1/oauth/token",
    json={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": "https://yourapp.com/callback",
        "client_id": "sb_client_live_123",
        "code_verifier": verifier,
    },
    timeout=30,
).json()
<?php
$ch = curl_init("https://api.southbill.com/v1/oauth/token");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "grant_type" => "authorization_code",
        "code" => $code,
        "redirect_uri" => "https://yourapp.com/callback",
        "client_id" => "sb_client_live_123",
        "code_verifier" => $verifier,
    ]),
]);
$tokens = json_decode(curl_exec($ch), true);
{
  "access_token": "sbat_live_…",
  "refresh_token": "sbrt_live_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "merchant:read payments:read",
  "installation_id": "inst_live_7c1f92a8",
  "environment": "live"
}

Store the tokens per installation, never globally.

3. Refresh

{ "grant_type": "refresh_token", "refresh_token": "sbrt_live_…", "client_id": "sb_client_live_123" }

Refresh tokens rotate: the response contains a new refresh token and the old one dies. If a refresh returns invalid_grant, treat the installation as gone and stop calling.

Redirect URI rules

  • HTTPS only, exact match, no wildcards. http://localhost is allowed in test mode.
  • Up to five URIs per app.
  • Changing a URI on a published app requires a new version.

Uninstall

When a merchant uninstalls, tokens are revoked immediately and you receive an app.uninstalled webhook. Delete the merchant's data within 30 days unless law requires otherwise.

Suspension

If a merchant is suspended, API calls for that installation return 403 merchant_suspended. Back off and retry later — do not delete data on a suspension.

Listing checklist

Everything that must be true before you submit.

Copy this into your tracker and tick it off.

Basics

  • Name is unique, 3–40 characters, no foreign brands
  • Tagline explains the outcome in one sentence
  • Category matches what the app actually does
  • Description names the merchant problem, the flow after install, and the data used

Media

  • Square icon ≥ 512×512
  • 2–6 real screenshots, 16:9

Availability & pricing

  • Markets selected
  • Pricing model chosen (free / one-time / subscription) and priced in EUR
  • Pricing details string matches your real prices

Links & compliance

  • Website, support URL and privacy policy reachable over HTTPS
  • Data handling described: what you store, where, for how long
  • Support email answered by a human

Technical

  • Redirect URIs exact, HTTPS
  • OAuth flow completes in a sandbox environment
  • Webhook endpoint verifies signatures and answers 2xx fast
  • Only the scopes you use are requested
  • No errors in the sandbox logs for this version

Business

  • Payout account verified (live only)
  • Billing method on file for the publishing fee (live only)

Embedded apps

Render your app inside the merchant dashboard

Your app can render its own UI inside the Southbill merchant dashboard, so merchants never leave Southbill to configure or use it.

1. Enable the embedded surface

In your app detail page (Developer Dashboard → Apps → your app → Embedded app) set:

Field Notes
embedded Turns the surface on
embedded_url HTTPS only. The page Southbill loads in the iframe
embedded_nav_label Optional label shown to the merchant (defaults to the app name)
embedded_height Optional initial height, clamped to 400–4000 px

Merchants then see an Open button next to your app in Dashboard → Apps, which opens /dashboard/apps/{installation_id}.

2. Bootstrap parameters

Southbill loads your embedded_url with:

?session_token=<JWT>&installation_id=insta_...&environment=test|live&host=https://southbill.com

Treat session_token as a one-time bootstrap value: exchange it for your own session server-side and drop it from the URL. Never store it in logs.

3. Where to find your App ID (aud) and signing secret

Both live in the same place: Developer Dashboard → Apps → your app → step OAuth & webhooks → card Embedded app.

Value Where Looks like
App ID (aud) Embedded app card, field App ID (aud) (copy button) app_531ecbddea174f1d87738613d999577f
Session token signing secret Embedded app card, Reveal button random high-entropy string
OAuth client_id / client_secret OAuth credentials card — different values, used for the OAuth install flow, never for token verification sbc_…

The value shown as App ID in the app Overview header is only a human-readable app number — it is not the aud value.

4. Verify the session token

The token is an HS256 JWT signed with your per-app signing secret (reveal it in the Embedded app card). It is an identity assertion only — it grants no API access.

{
  "iss": "https://southbill.com",
  "aud": "<your public app id>",
  "sub": "<installation public id>",
  "iat": 1730000000,
  "nbf": 1729999995,
  "exp": 1730000300,
  "jti": "…",
  "merchant_id": "…",
  "merchant_name": "Acme GmbH",
  "merchant_country": "DE",
  "installation_id": "insta_…",
  "environment": "test",
  "scopes": ["store:read", "orders:write"]
}

Always verify server-side: signature, aud equals your app id, iss, exp/nbf, and that the installation belongs to the merchant you are about to act on. Lifetime is 300 seconds.

To call the Southbill API, keep using the OAuth access token you received at install time — the session token is not accepted by the API.

5. Southbill App Bridge (recommended)

Full reference: Southbill App Bridge — every function, the wire protocol, the security model and error list.

The Southbill App Bridge is the official and recommended way for an embedded app to talk to the surrounding dashboard. Your app stays sandboxed in its iframe; anything that belongs to the Southbill host — navigation, external pages, OAuth, toasts, modals, resizing, session tokens — goes through the Bridge.

<script src="https://www.southbill.com/southbill-app-bridge.js"></script>
const bridge = SouthbillAppBridge.create();   // reads ?host= automatically
await bridge.ready();
Function Purpose
bridge.ready() Resolves once the host announced itself (southbill:ready), with the app context
bridge.context() Safe context: app_id, app_name, installation_id, environment, locale, host. Never secrets
bridge.sessionToken() Fresh short-lived (5 min) embedded session token
bridge.resize(px) / bridge.autoResize() Iframe height, clamped to 400–4000 px
bridge.toast(msg, variant) Native Southbill toast (success | error | info, max 300 chars)
bridge.modal({ title, message, confirmLabel, cancelLabel, variant }) Host-level confirm modal, resolves true/false
bridge.navigate("/dashboard/...") Internal Southbill navigation; other paths are rejected
bridge.open(url) Opens an https page in a new browser tab
bridge.redirect(url) Full top-level redirect away from the dashboard
bridge.authorize(url, mode) OAuth / external authorization at browser top level (popup default, or redirect)
bridge.close() Leaves the embedded app and returns to Dashboard → Apps

External authorization

External OAuth or third-party consent pages must never be loaded inside the embedded iframe — most providers block framing and the sandbox forbids top-level navigation from your page. Request it through the Bridge instead:

await bridge.authorize("https://accounts.example.com/oauth/authorize?client_id=…&redirect_uri=…");

Southbill performs the navigation at the browser top level and your own callback URL finishes the flow on your backend.

Wire protocol (raw postMessage, backwards compatible)

The SDK is a thin wrapper. Requests are { type: "southbill:<action>", id?, ...payload } posted to the host origin; the host answers southbill:<action>:result or southbill:<action>:error with the same id. Supported actions: ping, context, session-token, resize, toast, modal, navigate, open, redirect, authorize, close. The legacy id-less messages (southbill:resize, southbill:toast, southbill:navigate, southbill:session-token) keep working unchanged.

const host = new URLSearchParams(location.search).get("host");
parent.postMessage({ type: "southbill:resize", height: document.body.scrollHeight }, host);

Security

Southbill validates the exact iframe window and the exact embedded_url origin of every message, ignores unknown actions and malformed payloads, restricts navigate to /dashboard/*, requires https for every external URL, and never returns Southbill secrets, API keys or the merchant login to your app. The only credential the Bridge hands out is the short-lived session token.

6. Requirements

  • HTTPS with a valid certificate; the page must be frameable by Southbill (do not send X-Frame-Options: DENY; use Content-Security-Policy: frame-ancestors https://southbill.com).
  • The iframe is sandboxed. Third-party cookies are unreliable — keep state on your backend keyed by installation_id.
  • Handle environment explicitly: sandbox installs must never touch live data.
  • Revoked or uninstalled installations stop receiving tokens (409 installation_inactive); fail gracefully.

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>