Skip to Content
WidgetVanilla JS / CDN

Vanilla JS / CDN

This page shows you how to add the Unchurn widget to a non-React app — Rails, Laravel, plain HTML, or any server-rendered page — using the CDN script tag and window.unchurn.showCancelFlow.

Prerequisites

  • A page that renders a cancel button
  • For production: a backend endpoint (any language) that can sign an HMAC-SHA256 token
  • UNCHURN_SECRET and UNCHURN_MERCHANT_ID available on your server

Path A — Test / demo (no auth)

Test mode only — do not use in production. The unsigned CDN path below requires no server. Any browser script that guesses a subscription ID can trigger the flow for it. Use this only for UI experiments and demos. Switch to Path B before going live.

<!-- Any HTML page --> <script src="https://cdn.unchurn.dev/widget.js" async></script> <button id="cancel-btn">Cancel subscription</button> <script> document.getElementById('cancel-btn').addEventListener('click', function () { window.unchurn.showCancelFlow({ merchantId: 'mch_YOUR_MERCHANT_ID', subscriptionId: 'sub_STRIPE_TEST_SUB_ID', mode: 'test', }) }) </script>

mode: 'test' targets Stripe test-mode subscriptions and renders a TEST badge in the dialog. The backend rejects any attempt to mix unch_test_ tokens with live-mode subscriptions. See Test & live modes.

Verify: Click the button. The cancel flow overlay opens. No network signing occurs.


Path B — Production (server-signed token)

Production requires a signed authToken. Your backend mints it and your page passes it to showCancelFlow. The token is HMAC-SHA256 over a canonical payload — any backend that can compute HMAC works.

Step 1 — Mint the token on your server

The token format is:

unch_<mode>_<base64url(merchantId:subscriptionId:mode:expMs)>.<hex-hmac-sha256>

Ruby / Rails example:

# app/controllers/unchurn_controller.rb require 'openssl' require 'base64' require 'json' class UnchurnController < ApplicationController before_action :authenticate_user! def token subscription_id = current_user.stripe_subscription_id return head :unauthorized unless subscription_id merchant_id = ENV.fetch('UNCHURN_MERCHANT_ID') secret = ENV.fetch('UNCHURN_SECRET') mode = Rails.env.production? ? 'live' : 'test' exp_ms = ((Time.now.to_f + 300) * 1000).to_i # 5-minute TTL raw = "#{merchant_id}:#{subscription_id}:#{mode}:#{exp_ms}" payload = Base64.urlsafe_encode64(raw, padding: false) sig = OpenSSL::HMAC.hexdigest('SHA256', secret, payload) prefix = mode == 'live' ? 'unch_live_' : 'unch_test_' render json: { authToken: "#{prefix}#{payload}.#{sig}", expiresAt: Time.at(exp_ms / 1000.0).iso8601, merchantId: merchant_id, subscriptionId: subscription_id, mode: mode, } end end

PHP / Laravel example:

// routes/api.php → Route::post('/unchurn/token', [UnchurnController::class, 'token']); // app/Http/Controllers/UnchurnController.php public function token(Request $request): JsonResponse { $user = $request->user(); if (!$user?->stripe_subscription_id) { return response()->json(null, 401); } $merchantId = config('services.unchurn.merchant_id'); $secret = config('services.unchurn.secret'); $mode = app()->isProduction() ? 'live' : 'test'; $expMs = (int) ((microtime(true) + 300) * 1000); $raw = implode(':', [$merchantId, $user->stripe_subscription_id, $mode, $expMs]); $payload = rtrim(strtr(base64_encode($raw), '+/', '-_'), '='); $sig = hash_hmac('sha256', $payload, $secret); $prefix = $mode === 'live' ? 'unch_live_' : 'unch_test_'; return response()->json([ 'authToken' => "{$prefix}{$payload}.{$sig}", 'expiresAt' => (new \DateTime())->setTimestamp((int) ($expMs / 1000))->format('c'), 'merchantId' => $merchantId, 'subscriptionId' => $user->stripe_subscription_id, 'mode' => $mode, ]); }

Verify: Call your endpoint with a valid session and confirm the response contains all five fields and the authToken starts with unch_live_ or unch_test_.

Step 2 — Fetch the token and open the widget

<!-- production-cancel.html --> <script src="https://cdn.unchurn.dev/widget.js" async></script> <button id="cancel-btn">Cancel subscription</button> <script> document.getElementById('cancel-btn').addEventListener('click', async function () { const res = await fetch('/api/unchurn/token', { method: 'POST', credentials: 'same-origin', }) if (!res.ok) { console.error('Could not mint unchurn token', res.status) return } const data = await res.json() window.unchurn.showCancelFlow({ merchantId: data.merchantId, subscriptionId: data.subscriptionId, authToken: data.authToken, mode: data.mode, }) }) </script>

Verify: Clicking the button fetches your endpoint, then opens the widget with an authenticated session. No TEST badge appears in live mode.


Common pitfalls

script fires before window.unchurn is ready. The CDN script tag has asyncwindow.unchurn may not be defined when the page’s inline scripts run. Wrap calls in a click handler (as shown above) rather than in a top-level <script> block.

Token payload fields out of order. The canonical format is merchantId:subscriptionId:mode:expMs — colon-separated, in that order, base64url-encoded without padding. A reordered or padded payload produces a token that fails backend verification.

UNCHURN_SECRET exposed in HTML. The secret and the signing step must stay on the server. The browser only receives the finished authToken, never the raw secret.


Next steps

Last updated on