Sign in →
Embed Widgets1 min read

AforoUpgradeCancel

Self-service plan upgrade / downgrade / cancel wizard with proration preview and deflection offers. Backed by the V50 cancellation feedback API for operator-side churn analytics.

Updated 2026-06-15Suggest edits
Docs Embed Widgets UpgradeCancel
Live preview — synthetic data
AforoUpgradeCancel
Upgrade / downgrade / cancel flow with deflection.
Loads the widget from https://embed-demo.aforo.ai with synthetic data.

What it renders#

A multi-step wizard letting a customer change or cancel their subscription without leaving your storefront. The widget runs in two modes set by the mode prop:

  • upgrade — pick a target offering → see a prorated proration preview (credit, debit, net charge today, next-renewal amount) → confirm. Backend infers UPGRADE / DOWNGRADE / LATERAL from the offering price delta.
  • cancel — pick a reason → see a category-specific deflection offer (20% discount, downgrade prompt, support hand-off, or no offer) → confirm with IMMEDIATE or PERIOD_END choice.

Both modes finish with a success state showing the invoice id, credit-note id, or scheduled effective date. The flow renders as a modal dialog by default and can be rendered inline via renderMode="inline".

INFO
Every cancel goes through the V50 cancellation-feedback audit table, so operator-facing churn dashboards in the Aforo Product UI auto-light up — reason category, deflection offer shown, retention outcome, and free-form reason detail all flow into the same analytics surface.
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="upgrade-cancel"
     data-aforo-tenant-slug="your-tenant"
     data-aforo-embed-key="ek_live_replace_me"
     data-aforo-bridge-token="bridge.replace.me"
     data-aforo-subscription-id="sub_xxx"
     data-aforo-mode="upgrade"></div>

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

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

Mount examples#

app/account/subscription/page.tsx
'use client';
import { useState } from 'react';
import { AforoUpgradeCancel } from '@aforoai/storefront-widgets';

export default function ManageSubscription({ subscriptionId, bridgeToken }: {
  subscriptionId: string;
  bridgeToken: string;
}) {
  const [open, setOpen] = useState<'upgrade' | 'cancel' | null>(null);

  return (
    <>
      <button onClick={() => setOpen('upgrade')}>Change plan</button>
      <button onClick={() => setOpen('cancel')}>Cancel subscription</button>

      {open && (
        <AforoUpgradeCancel
          tenantSlug="acme"
          embedKey="embk_live_…"
          bridgeToken={bridgeToken}
          subscriptionId={subscriptionId}
          mode={open}
          onCompleted={(result) => {
            console.log('done', result);
            setOpen(null);
          }}
          onCancelled={() => setOpen(null)}
        />
      )}
    </>
  );
}

Props reference#

PropTypeDefaultDescription
tenantSlugstring(required)Your tenant slug
embedKeystring(required)Embed key from Embed Studio
bridgeTokenstring(required)Short-lived JWT minted server-side per customer
subscriptionIdstring(required)The subscription the wizard targets
mode"upgrade" | "cancel"(required)Which flow to render
renderMode"inline" | "modal""modal"Modal opens in a dialog overlay; inline renders inside the parent
defaultApplyAt"IMMEDIATE" | "PERIOD_END""IMMEDIATE"Pre-select the apply-at radio on the confirm step
localestring?navigator.languageBCP-47 locale for currency + date formatting
themeOverridesPartial<ThemeTokens>?Per-widget theme tuning
onCompleted(result) => voidFires when the wizard finishes successfully
onCancelled() => voidFires on Esc, backdrop click, or accepted deflection
onError(error) => voidMirrors the aforo.upgrade-cancel.error event

Events#

See the for the full vocabulary. UpgradeCancel emits 8 outbound events; all payloads are non-PII.

EventWhenPayload
aforo.upgrade-cancel.readyFirst paint of the wizard{ tenantId, mode, subscriptionId }
aforo.upgrade-cancel.step_changedEach step transition{ from, to, mode }
aforo.upgrade-cancel.preview_fetchedProration preview returned{ changeType, netCents, currency, prorationFactor }
aforo.upgrade-cancel.deflection_offer_shownCancel-step-2 deflection rendered{ reason, offer }
aforo.upgrade-cancel.deflection_offer_acceptedCustomer took the offer{ reason, offer }
aforo.upgrade-cancel.completedWizard finished successfully{ mode, upgrade?, cancel? } (see UpgradeCancelResultPayload)
aforo.upgrade-cancel.abandonedEsc / backdrop / explicit Cancel{ step, mode }
aforo.upgrade-cancel.errorPreview or mutation failed{ code, message } (typed; never PII)
WARNING
FR-SEC-23 — webhook authoritative. The aforo.upgrade-cancel.completed event is advisory only. The authoritative signal for a plan change or cancellation is the server-side webhook (subscription.upgraded / subscription.cancelled). Use the event to update UI; never use it to grant or revoke entitlements in your own system.

Upgrade flow#

  1. Step 1 — Pick target offering. The wizard fetches your published headless config and lists upgrade / downgrade / lateral candidates. The customer radios one.
  2. Step 2 — Review proration. The widget calls POST /preview-change and renders a credit / debit / net-cents card with a plain-English summary line generated by the backend. The IMMEDIATE / PERIOD_END toggle re-fetches the preview on change.
  3. Step 3 — Confirm. Clicking Confirm calls POST /upgrade with an Idempotency-Key header. Anti-double-submit ref guards against double-firing.
  4. Success. The wizard shows the new offering name, status (ACTIVE or ACTIVE_PENDING_CHANGE), and the invoice id (IMMEDIATE) or scheduled-for date (PERIOD_END).

Cancel flow + deflection#

The cancel flow has a deflection step keyed on the customer's stated reason. Six canonical reasons are surfaced as Step 1 radio options. The deflection offer rendered in Step 2 is fixed per reason in Phase 1 — per-tenant customization is on the roadmap (see Limitations).

ReasonDeflection offerretentionOfferShown
Too expensive20% off for 3 monthsDISCOUNT_20_PCT
Not using it enoughSwitch to a smaller planDOWNGRADE
Missing a feature I needEmail-sales prompt (no offer)NONE
Technical issuesSupport hand-off (no offer)NONE
Switched to a competitorFeedback request (no offer)NONE
OtherNo deflection — confirm to continueNONE

Step 3 (confirm) lets the customer pick IMMEDIATE or PERIOD_END. IMMEDIATE returns a credit-note id for the prorated refund. PERIOD_END returns the effective-at date (≈ current period end). Backend stamps every cancel through V50 with cancel_channel = EMBED so operator analytics can split self-serve embed cancels from in-app cancels.

PRO TIP
Wire aforo.upgrade-cancel.deflection_offer_accepted to your CRM to track retention wins by reason category. The payload carries the offer key + reason for downstream segmentation.

IMMEDIATE vs PERIOD_END#

  • IMMEDIATE — change applies now. Upgrades generate a prorated invoice today. Downgrades and cancellations generate a prorated credit note. Customer sees the change immediately in entitlements.
  • PERIOD_END — change is scheduled for the current period end. Customer keeps current entitlements until then. No invoice or credit note is generated; the change applies when the renewal job runs.

Default is IMMEDIATE. Override with the defaultApplyAt prop to start the wizard on PERIOD_END (common for cancel flows where you want to give customers the rest of the period they've already paid for).

Common patterns#

Wiring deflection_offer_accepted to your CRM. The event fires once per accepted offer with the reason + offer key. Send it to your CRM as a retention event so customer-success can follow up.

parent.ts
AforoEmbed.on('aforo.upgrade-cancel.deflection_offer_accepted', (e) => {
  // e.payload = { reason: 'TOO_EXPENSIVE', offer: 'DISCOUNT_20_PCT' }
  hubspot.track('embed_retention_win', {
    reason: e.payload.reason,
    offer: e.payload.offer,
  });
});

When to use IMMEDIATE vs PERIOD_END. Default upgrades to IMMEDIATE (customers want their new entitlements now). Default cancels to PERIOD_END so customers retain access for what they've already paid for:

ManageSubscription.tsx
<AforoUpgradeCancel
  mode={mode}
  subscriptionId={subscriptionId}
  bridgeToken={token}
  tenantSlug="acme"
  embedKey="embk_live_…"
  defaultApplyAt={mode === 'cancel' ? 'PERIOD_END' : 'IMMEDIATE'}
/>

Try it in the sandbox. The embed sandbox mounts the live widget against synthetic offerings — flip the mode toggle to switch between upgrade and cancel flows.

Limitations#

  • No mode="both". Earlier drafts showed a shell value letting one widget render upgrade and cancel tabs. The contract was dropped pre-launch — render two widget instances with different mode values instead.
  • Deflection offers are fixed per reason category. Phase 1 hardcodes the four offer types (20% discount / downgrade / support / no offer). Per-tenant customization (e.g. swap the 20% offer for a longer trial) is a Phase 2 follow-up.
  • No PAUSE_3_MONTHS offer (FR-WIDGET-UC-5). The shape exists in the public retention-offer enum; the actual pause-flow wiring is Phase 2.
  • Refund preview at cancel-IMMEDIATE is the backend's number, not a UI preview. The widget shows the credit-note id and amount after the cancel completes; it doesn't render a refund-preview row at Step 2. Phase 1 limitation.
  • No live SSE refresh. If the parent page leaves the wizard open long enough for the subscription to change underneath, the wizard doesn't auto-refresh — submit returns a typed error which the widget surfaces inline. SSE wiring is on the embed-events roadmap.
  • Multi-step upgrade for plans with mandatory add-ons is Phase 2. Today the widget assumes the target offering is self-contained.