Billing & Monetization

Subscription Flow

How merchants subscribe to your plugin's pricing plans using the App Bridge billing API.

Overview

Subscriptions are initiated from inside your plugin using the App Bridge billing API. The platform handles the approval page, payment setup, and lifecycle events. Your plugin reads and reacts to the subscription state.

Step 1 — Display Available Plans

Fetch your plans and show them to the merchant:

import { useAppBridge } from "@whatalo/plugin-sdk/bridge";

function PricingPage() {
  const bridge = useAppBridge();

  async function loadPlans() {
    const plans = await bridge.billing.getPlans();
    // plans: BillingPlanResponse[]
    // [{ slug, name, price, currency, interval, trialDays, features, isPopular }]
  }
}

Step 2 — Request a Subscription

Call requestSubscription with a plan slug. This sends a billing action to the admin host, which redirects the merchant to an approval page.

async function handleSubscribe(planSlug: string) {
  try {
    await bridge.billing.requestSubscription(planSlug);
    // The host handles the redirect — this function may not return
    // if the page navigates away during approval
  } catch (error) {
    bridge.toast.show("Could not start subscription", { variant: "error" });
  }
}

The merchant sees the plan details, price, and trial information, then confirms or cancels. Your plugin does not need to handle the approval redirect — the platform manages it.

Step 3 — Check Subscription Status

After the merchant approves, read the active subscription:

const sub = await bridge.billing.getSubscription();

if (!sub) {
  // No active subscription for this installation
  return;
}

console.log(sub.status);            // "trialing", "active", etc.
console.log(sub.planSlug);          // "pro"
console.log(sub.planName);          // "Pro"
console.log(sub.trialEndsAt);       // ISO date string or null
console.log(sub.cancelAtPeriodEnd); // boolean

Subscription States

StatusDescription
pendingAwaiting merchant approval on the confirmation page
trialingIn the free trial period — merchant is not charged
activeBilling is active, merchant is being charged each cycle
past_dueA payment failed — billing system is retrying
canceledSubscription has ended
expiredTrial or billing period ended without renewal

Cancellation

Cancel a subscription at the end of the current billing period. The merchant keeps full access until the period ends:

await bridge.billing.cancelSubscription();

After cancellation, sub.cancelAtPeriodEnd is true and sub.canceledAt contains the timestamp. The merchant is not charged again after the current period.

Reactivation

While the subscription is still in its active period, the merchant can undo a cancellation:

await bridge.billing.reactivateSubscription();

After reactivation, cancelAtPeriodEnd returns to false.

Plan Switching

Switch the merchant to a different plan. Proration is applied: the merchant receives credit for unused time on the current plan, applied immediately toward the new plan:

await bridge.billing.switchPlan("enterprise");

Subscription Response Shape

interface BillingSubscriptionResponse {
  planSlug: string;
  planName: string;
  /** "pending" | "trialing" | "active" | "past_due" | "canceled" | "expired" */
  status: string;
  trialEndsAt: string | null;        // ISO timestamp or null
  currentPeriodEnd: string | null;   // ISO timestamp or null
  cancelAtPeriodEnd: boolean;
  canceledAt: string | null;         // ISO timestamp or null
}

Error Handling

All billing methods throw on failure. Wrap them in try/catch:

try {
  await bridge.billing.cancelSubscription();
  bridge.toast.show("Subscription cancelled", { variant: "success" });
} catch (error) {
  bridge.toast.show("Could not cancel subscription", { variant: "error" });
}

Billing methods do not return { success: false } — they throw, so standard error handling applies.

Gating Features by Subscription

Use getSubscription to conditionally render premium features:

function Dashboard() {
  const bridge = useAppBridge();
  const [sub, setSub] = useState<BillingSubscriptionResponse | null>(null);

  useEffect(() => {
    bridge.billing.getSubscription().then(setSub);
  }, []);

  const isActive = sub?.status === "active" || sub?.status === "trialing";

  return isActive ? <PremiumContent /> : <UpgradePrompt />;
}

On this page