Subscription Walls
The Subscription Wall recovers revenue by controlling app access when a subscription is paused or a payment has failed. For a paused subscription the user is asked to resume to regain access; for a failed payment the user is asked to update their payment method. In both cases the user is guided through the right recovery flow so they can keep the subscription active and get access back.
On this page
- Pause Wall
- Failed Payment Wall
- Installation
- Step 1: Place the embed code and launch snippet
- Step 2: Generate security token
- Update: launch Churn Solution
- Configuration options
- Access modes
- Soft Wall
- Hard Wall
- Grace period (failed payments only)
- Dynamic control with shouldShowWall
- System events
- Lifecycle events
- Subscription actions
- Failed payment flow
- Pause flow
- Full configuration example
- Paid Memberships Pro
Pause Wall
The Pause Wall applies when a customer has paused their subscription. Depending on configuration, it can be a dismissible wall or a full block that stays until the subscription is resumed.

Failed Payment Wall
The Failed Payment Wall applies when a subscription is still active but a payment attempt has failed. It asks the user to update their payment method to restore access.

Installation
Step 1: Place the embed code and launch snippet
Paste the embed code before the closing </body> tag on every page where the walls should appear. The script loads Churn Solution and checks the subscription each time the page opens, showing the Pause Wall or the Failed Payment Wall when one applies. The options object controls each wall's behaviour; every setting and callback is listed under Configuration options. Your App ID is on the Integration page after you connect your payment provider.
<script>
!function(){
if (!window.churnSolution || !window.churnSolution.ready) {
window.churnSolution = { ready: true };
const s = document.createElement('script');
s.src = 'https://app.churnsolution.com/sdk/index.min.js';
s.async = true;
const e = document.getElementsByTagName('script')[0];
e.parentNode.insertBefore(s, e);
}
}();
</script>
<script>
function runWallScript() {
window.churnSolution?.checkWall({
subscriptionId, // your subscription ID from your payment provider
appId: 'APP_ID', // your App ID
options: {
softWall: false, // false = hard (blocking); true = dismissible
gracePeriodDays: 7, // failed-payment only: soft until invoice.created + days, then hard
shouldShowWall: (session) => {
// session: { type, invoice, subscription, customer, resumes_at }
// type: FAILED_PAYMENT | PAUSE
return true; // true | false | 'soft' | 'hard'
},
},
});
}
function onChurnSolutionLoad(callback, timeout = 10000, interval = 100) {
const startTime = Date.now();
const check = () => {
if (typeof window.churnSolution?.checkWall === 'function') callback();
else if (Date.now() - startTime < timeout) setTimeout(check, interval);
else console.warn('Churn Solution script did not load in time.');
};
check();
}
onChurnSolutionLoad(() => runWallScript());
</script>
Step 2: Generate security token Optional
Generate a hash on your server so every request to Churn Solution is secure, then send the hash to the front end and pass it as authKey in the launch call. Examples in several backend languages are on your gateway page. Your secret key is on the Account settings page — keep it on your server and never send it to the browser.
const crypto = require('crypto');
// POST /api/churn-solution — requires a logged-in user
app.post('/api/churn-solution', requireAuth, (req, res) => {
const subscriptionId = req.user.subscriptionId; // from YOUR session, never the request body
const authKey = crypto
.createHmac('sha256', process.env.CHURNSOLUTION_SECRET_KEY)
.update(subscriptionId)
.digest('hex');
res.json({ authKey, subscriptionId });
});
// app/api/churn-solution/route.ts
import crypto from 'crypto';
import { getSession } from '@/lib/auth';
export async function POST() {
const session = await getSession();
if (!session) return new Response('Unauthorized', { status: 401 });
const subscriptionId = session.user.subscriptionId;
const authKey = crypto
.createHmac('sha256', process.env.CHURNSOLUTION_SECRET_KEY!)
.update(subscriptionId)
.digest('hex');
return Response.json({ authKey, subscriptionId });
}
import hmac, hashlib, os
from flask import Flask, jsonify
from auth import current_user, login_required
app = Flask(__name__)
@app.post("/api/churn-solution")
@login_required
def churn_solution():
subscription_id = current_user().subscription_id
auth_key = hmac.new(
os.environ["CHURNSOLUTION_SECRET_KEY"].encode(),
subscription_id.encode(),
hashlib.sha256,
).hexdigest()
return jsonify(authKey=auth_key, subscriptionId=subscription_id)
<?php
// Laravel route: POST /api/churn-solution (auth middleware)
Route::post('/api/churn-solution', function () {
$subscriptionId = auth()->user()->subscription_id;
$authKey = hash_hmac('sha256', $subscriptionId, env('CHURNSOLUTION_SECRET_KEY'));
return response()->json(['authKey' => $authKey, 'subscriptionId' => $subscriptionId]);
})->middleware('auth');
# Rails controller
class ChurnSolutionController < ApplicationController
before_action :authenticate_user!
def create
subscription_id = current_user.subscription_id
auth_key = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('CHURNSOLUTION_SECRET_KEY'), subscription_id)
render json: { authKey: auth_key, subscriptionId: subscription_id }
end
end
func churnSolutionHandler(w http.ResponseWriter, r *http.Request) {
user := mustUser(r) // your auth
mac := hmac.New(sha256.New, []byte(os.Getenv("CHURNSOLUTION_SECRET_KEY")))
mac.Write([]byte(user.SubscriptionID))
authKey := hex.EncodeToString(mac.Sum(nil))
json.NewEncoder(w).Encode(map[string]string{
"authKey": authKey, "subscriptionId": user.SubscriptionID,
})
}
// Spring Boot
@PostMapping("/api/churn-solution")
public Map<String, String> churnSolution(@AuthenticationPrincipal AppUser user) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(System.getenv("CHURNSOLUTION_SECRET_KEY").getBytes(UTF_8), "HmacSHA256"));
String authKey = HexFormat.of().formatHex(mac.doFinal(user.getSubscriptionId().getBytes(UTF_8)));
return Map.of("authKey", authKey, "subscriptionId", user.getSubscriptionId());
}
// ASP.NET Core minimal API
app.MapPost("/api/churn-solution", (ClaimsPrincipal user) =>
{
var subscriptionId = user.FindFirst("subscription_id")!.Value;
var key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("CHURNSOLUTION_SECRET_KEY")!);
using var hmac = new HMACSHA256(key);
var authKey = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(subscriptionId))).ToLowerInvariant();
return Results.Ok(new { authKey, subscriptionId });
}).RequireAuthorization();
Update: launch Churn Solution
After your endpoint returns an auth key, add it to the launch call from Step 1 as authKey.
Before
window.churnSolution?.checkWall({
subscriptionId,
appId: 'APP_ID',
options: { /* ... */ },
});
After
window.churnSolution?.checkWall({
authKey, // hash generated by your server
subscriptionId,
appId: 'APP_ID',
options: { /* ... */ },
});
Configuration options
Access modes
Soft Wall
A soft wall lets users close it and keep using the application.
softWall: true
Hard Wall
A hard wall blocks access completely until the user resumes the subscription or updates their payment.
softWall: false
Grace period (failed payments only)
A grace period allows continued access for a set number of days before the wall is enforced.
gracePeriodDays: 7
Dynamic control with shouldShowWall
The softWall setting applies one mode to every user. The shouldShowWall callback lets you decide per user, at runtime, whether to show a wall and whether it should be soft or hard. It runs each time checkWall is called and receives a single session argument describing the current subscription state.
| Session property | Description |
|---|---|
type |
The wall type: FAILED_PAYMENT or PAUSE. |
invoice |
The related invoice for failed payments, including amount, currency and status. |
subscription |
The subscription, including its plan, status and metadata. |
customer |
The customer, including email and any custom metadata. |
resumes_at |
For paused subscriptions, the time the subscription is scheduled to resume. |
| Return value | Outcome |
|---|---|
true |
Shows the wall using the mode set by softWall or gracePeriodDays. |
false |
Doesn't show the wall; the user keeps full access. |
'soft' |
Forces a dismissible soft wall, overriding other settings. |
'hard' |
Forces a blocking hard wall, overriding other settings. |
Use it to match the recovery experience to your business rules: hard-block large unpaid invoices while giving small balances a dismissible wall, keep trusted or high-tier customers on a soft wall, or skip the wall for a segment identified through subscription or customer metadata.
shouldShowWall: (session) => {
const { type, invoice, subscription, customer, resumes_at } = session;
// Never wall your internal or VIP accounts
if (customer?.metadata?.plan_tier === 'vip') return false;
if (type === 'FAILED_PAYMENT') {
// Hard-block large unpaid invoices, keep small ones dismissible
return invoice?.amount_due >= 10000 ? 'hard' : 'soft';
}
if (type === 'PAUSE') {
// Only wall paused subscriptions that are still active plans
return subscription?.metadata?.enforce_pause === 'true';
}
return true; // fall back to the softWall / gracePeriodDays settings
},
shouldShowWall runs on the client, so treat it as a presentation rule. Churn Solution always re-validates access enforcement on its side.
System events
System events apply globally to both the Pause Wall and the Failed Payment Wall.
Lifecycle events
| Callback | Fires when |
|---|---|
onWallActivated({ type }) |
Either wall becomes visible to the user. |
onWallClose({ type }) |
The user closes or exits the wall. |
onError(error, type) |
An error occurs within the wall system. type is one of WALL_INITIALIZATION_ERROR, FAILED_PAYMENT_WALL_UPDATE_CARD_ERROR, PAUSE_WALL_RESUME_ERROR, PAUSE_WALL_CANCEL_ERROR. |
Subscription actions
Subscription actions fire based on the recovery flow the user is in.
Failed payment flow
onUpdatePaymentInformation(customer) — fires after the user successfully updates their payment details.
Pause flow
onResumeSubscription(customer) — fires when the user resumes their subscription.
onCancelSubscription(customer) — fires when the user cancels a paused subscription.
Full configuration example
This example combines every option in a single checkWall call: the gating settings, the shouldShowWall decision callback, and all lifecycle and subscription-action callbacks for both wall types.
window.churnSolution?.checkWall({
authKey, // hash generated by your server
subscriptionId, // your subscription ID from your payment provider
appId: 'APP_ID', // your App ID
options: {
// Gating (applies to both wall types)
softWall: false, // false = hard (blocking, non-dismissable)
gracePeriodDays: 0, // failed-payment only: soft until (invoice.created + days), then hard
shouldShowWall: (session) => true, // true | false | 'soft' | 'hard'
// Generic callbacks
onWallActivated: ({ type }) => console.log('wall activated, type:', type),
onWallClose: ({ type }) => console.log('wall closed (soft), type:', type),
onError: (error, type) => console.log('wall error', type, error),
// Failed-payment wall
onUpdatePaymentInformation: (customer) => console.log('payment updated, customer:', customer),
// Pause wall
onResumeSubscription: (customer) => console.log('subscription resumed, customer:', customer),
onCancelSubscription: (customer) => console.log('subscription cancelled, customer:', customer),
},
});
Paid Memberships Pro
If you use Paid Memberships Pro, you don't need to change any code. Contact the Churn Solution team and we enable and configure the Pause Wall and Failed Payment Wall for your site. The integration is fully managed and requires no additional implementation.
Contact the team