Sign in →
Embed Widgets1 min read

AforoSubscribeButton

Authenticated subscribe CTA. Checks for an existing subscription to prevent double-subscribing, then routes to checkout in redirect, event-only, or embedded-flow mode.

Updated 2026-06-15Suggest edits
Docs Embed Widgets SubscribeButton
Live preview — synthetic data
AforoSubscribeButton
Authenticated CTA — initiates checkout for an offering.
Loads the widget from https://embed-demo.aforo.ai with synthetic data.

What it renders#

A single CTA button with state-aware copy. On mount, the widget:

  1. Exchanges the bridge token for an in-memory session JWT.
  2. Calls a server-side BFF endpoint to check whether this customer already has an active subscription to the offering.
  3. If not subscribed: shows "Subscribe" (or your ctaText). Clicking initiates checkout.
  4. If already subscribed: shows a disabled "Already subscribed" affordance with a link to the subscription manager.
INFO
The anti-double-subscribe check is UX, not security. Aforo also enforces uniqueness at the database layer — even if the check is bypassed, a duplicate subscription create will be rejected. The button just gives the customer a clearer signal.
From Operator Portal → Settings.
From Operator Portal → Embed Studio → Keys.
Minted by your backend per customer session — see Authentication.
Script tag
<div data-aforo-widget="subscribe-button"
     data-aforo-tenant-slug="your-tenant"
     data-aforo-embed-key="ek_live_replace_me"
     data-aforo-bridge-token="bridge.replace.me"
     data-aforo-offering-id="off_pro"
     data-aforo-mode="redirect"></div>

<script src="https://cdn.aforo.ai/embed/loader.js" async></script>
NPM / React
import { AforoSubscribeButton } from '@aforoai/storefront-widgets';

export function Pricing() {
  return (
    <AforoSubscribeButton
      tenantSlug="your-tenant"
      embedKey="ek_live_replace_me"
  bridgeToken="bridge.replace.me"
  offeringId="off_pro"
  mode="redirect"
    />
  );
}
Preview with my account
Fill in Tenant slug above to enable this preview.

Mount examples#

app/subscribe/page.tsx
import { AforoSubscribeButton } from '@aforoai/storefront-widgets';

export default async function SubscribePage() {
  const session = await getSession();
  const bridgeToken = await mintBridgeToken({
    externalId: session.customerId,
    email: session.email,
  });

  return (
    <AforoSubscribeButton
      tenantSlug="acme"
      embedKey="embk_live_…"
      bridgeToken={bridgeToken}
      offeringId="off_pro"
      mode="redirect"
      returnUrl="https://shop.example.com/welcome"
    />
  );
}

Props reference#

PropTypeDefaultDescription
tenantSlugstring(required)Your tenant slug
embedKeystring(required)Embed key from Embed Studio
bridgeTokenstring(required)Short-lived bridge token minted by your backend
offeringIdstring(required)Aforo offering ID to subscribe to
mode"redirect" | "event-only" | "embedded-flow""redirect"Checkout dispatch mode
ctaTextstring?"Subscribe"Override the button copy
size"sm" | "md" | "lg""md"Button size — min-widths 120/160/200px
returnUrlstring?current pageWhere to redirect after checkout (redirect mode)
theme"auto" | "light" | "dark""auto"Color scheme
themeOverridesPartial<ThemeTokens>?Per-widget theme tuning
onSubscribed(payload) => voidReact callback after subscription created

Three modes#

redirect (default)

Standard hosted checkout. On click, the widget calls Aforo's checkout-initiate endpoint to create a cart, then redirects the browser to the gateway-hosted checkout page (or your own checkout if you've configured it). The customer lands back at returnUrl after completion.

event-only

For parent pages that own the checkout rendering. The widget emits aforo.subscribe.checkout_requested with the full payload and does not redirect. Your parent page handles the rest (open your own modal, navigate to your own route, etc.). The widget waits for an inbound aforo:checkout-completed postMessage to flip into the "subscribed" state.

page.tsx
<AforoSubscribeButton
  tenantSlug="acme"
  embedKey="embk_live_…"
  bridgeToken={token}
  offeringId="off_pro"
  mode="event-only"
/>

{/* Parent-page listener */}
<script>
  window.aforoEmbed?.on(
    'aforo.subscribe.checkout_requested',
    async ({ offeringId }) => {
      const subscription = await myOwnCheckout(offeringId);
      // Tell the widget the cart completed:
      const iframe = document.querySelector('iframe[src*="embed.aforo.ai"]');
      iframe?.contentWindow?.postMessage(
        { type: 'aforo:checkout-completed', payload: { subscriptionId: subscription.id } },
        'https://embed.aforo.ai',
      );
    },
  );
</script>

embedded-flow

Renders an inline AforoCheckoutFlow in place of the button after the customer clicks. No redirect, no popup — the entire checkout flow lives on your page.

page.tsx
<AforoSubscribeButton
  tenantSlug="acme"
  embedKey="embk_live_…"
  bridgeToken={token}
  offeringId="off_pro"
  mode="embedded-flow"
/>

See the for the inline rendering details and payment provider support.

Events#

SubscribeButton emits these events. See the for envelope shape.

  • aforo.subscribe-button.ready — fires once on mount
  • aforo.subscribe-button.disabled — fires when the anti-double-subscribe check reports an active subscription
  • aforo.subscribe.checkout_requested — fires on click before the redirect (so your analytics can capture intent even if the customer abandons)
  • aforo.subscription.created — fires after checkout completes (advisory only — webhook is authoritative)
  • aforo.subscribe.error — fires on any error path with typed code + non-PII message

Limitations#

  • One offering per button. If you want a customer to choose between multiple plans, use (which renders one CTA per offering) plus per-offering URL overrides.
  • Coupon codes are Phase 1. The V49 coupon engine is shipped server- side; the customer-facing input UI lands in a future minor.
  • Trial mode display is v2. The button doesn't currently surface "14-day free trial" copy automatically based on the offering's trial config. Use ctaText as a manual workaround.