Skip to Content
GuideSDK 3.0 — SaaS Web

SaaS Subscription with SDK 3.0 (Web)

End-to-end guide for adding a subscription paywall to a web app / SPA using SDK 3.0. Bundled npm package, no iframe, Shadow DOM rendering, built-in auth.

Complexity: Beginner-to-Intermediate Perfect for: Web apps, SaaS dashboards, SPAs, marketing sites with a build step Time: ~20 minutes

What We’ll Build

  • Paywall modal that opens on “Upgrade” click — no iframe, no layout conflicts
  • Built-in email / OTP / OAuth login (managed by AuthClient)
  • Subscription gate: check the user’s plan on page load, show premium features only to paying users
  • Reaction to purchase_completed — instantly unlock the feature without page reload

Architecture

Set Up the Paywall

Create the paywall

Create a paywall and pick SDK 3.0 as the SDK version. SDK 3.0 paywalls have no separate Client / Server mode toggle — the SDK handles both the modal and headless flows.

Add a payment processor

Create a payment processor (Stripe / Paddle / Chargebee / Freemius), then connect it to the paywall. Start in test mode.

Add subscription plans

In the paywall settings, add at least one recurring plan (e.g. monthly + yearly). Mark one as recommended — it gets the highlight in the modal.

Note your paywallId

Take the numeric ID from the paywall URL in the dashboard — you’ll pass it to the SDK.

Integrate the SDK

Install

pnpm add @monetize.software/sdk # or: npm i @monetize.software/sdk

Initialize once at app boot

// src/paywall.ts import { PaywallUI } from '@monetize.software/sdk/ui'; export const paywall = new PaywallUI({ paywallId: '3', apiOrigin: 'https://YOUR_DOMAIN', auth: true // managed AuthClient: email + OTP + OAuth });

auth: true is enough for most apps — the SDK creates an AuthClient internally, persists the session, and refreshes tokens. If your app already has its own login, you can pass user data into the SDK with billing.setIdentity({ email, userId }) or run the headless flow entirely from your backend.

Open the modal

import { paywall } from './paywall'; document.getElementById('upgrade-btn')!.addEventListener('click', () => { paywall.open(); });

Gate features by subscription

Use the BillingClient exposed on paywall.billing to read user state. It’s bootstrapped lazily on first call and cached cross-tab via storage events.

import { paywall } from './paywall'; async function gateFeature() { const user = await paywall.billing.getUser(); if (user.has_active_subscription) { enablePremium(); return; } // Not subscribed — open paywall and wait for purchase paywall.open(); }

user.has_active_subscription is the coarse boolean — covers active subscription, lifetime payment, or active trial. For per-plan logic, walk user.purchases (each item has id, status, current_period_end, cancel_at_period_end). For renewal / cancel UI, use billing.listPurchases() — see Customer portal.

For a side-effect-free check (no bootstrap call, no modal mount) prefer paywall.getAccess() — it returns a discriminated union granted | blocked with reason: 'has_subscription' | 'trial_blocked' | 'visibility_blocked' | 'no_subscription':

const access = await paywall.getAccess(); if (access.access === 'granted') enablePremium(); else paywall.open();

React to purchase

const unsubscribe = paywall.on('purchase_completed', ({ priceId, sessionId }) => { enablePremium(); // Optional: tell your backend the user just paid, so you can pre-warm their workspace fetch('/api/post-purchase', { method: 'POST', body: JSON.stringify({ sessionId, priceId }) }); }); paywall.on('close', () => { // user dismissed the modal without buying }); paywall.on('error', (err) => { // network / config errors; payment failures come via 'purchase_failed' console.error(err); });

paywall.on() returns an unsubscribe function — call it on component unmount. Full event list: Events.

Production Checklist

  • paywallId and apiOrigin come from env vars, not hardcoded strings
  • In dev you use a test paywall + test payment processor (separate paywallId)
  • You unsubscribe from paywall.on(...) listeners on component unmount (React/Vue)
  • auth: true matches your privacy policy — review where the SDK stores tokens (Security)

Need server-side subscription sync? Webhooks are the source of truth for the database state — purchase_completed can be missed if the tab closes mid-checkout. See the Headless guide → Sync Subscriptions on Your Backend for the full handler.

Next Steps