Subscription Management
Everything you need to manage recurring billing through stryhub. From creating subscriptions to handling upgrades, cancellations, trials, and failed payments — all powered by Stripe.
Overview
stryhub gives you full subscription management on top of your existing Stripe account. You don't need to build billing infrastructure from scratch — connect your Stripe account, and stryhub handles the rest.
Key capabilities:
- Automatic billing — Stripe handles recurring charges on your behalf
- Customer portal — Let customers manage their own subscriptions (update payment method, change plan, cancel)
- Upgrades & downgrades — Automatic proration when customers change plans
- Pause & resume — Temporarily stop billing without canceling
- Free trials — Offer trial periods before the first charge
- Smart dunning — Stripe automatically retries failed payments using machine learning
- Real-time webhooks — Get notified of every subscription event in your SaaS
- Analytics — Track MRR, churn rate, active subscriptions, and revenue from the dashboard
How It Works
stryhub sits between Stripe and your SaaS, acting as a management layer:
- Stripe stores your products, prices, and handles payment processing
- stryhub reads your products automatically, generates checkout pages, manages subscriptions, and tracks analytics
- Your SaaS receives webhook events from stryhub to grant/revoke access, send emails, and update your database
Subscription Lifecycle
Every subscription goes through a series of states. Understanding these states is essential for managing access in your SaaS.
| Status | Meaning | Action in your SaaS |
|---|---|---|
trialing |
Customer is in a free trial period. No charge yet. | Grant full access. Show trial expiration date. |
active |
Subscription is paid and in good standing. | Grant full access. |
past_due |
Most recent payment failed. Stripe is retrying. | Keep access (grace period). Notify customer to update payment method. |
paused |
Billing is temporarily paused. Subscription still exists. | Limit or revoke access. Show "subscription paused" message. |
unpaid |
All retry attempts failed. No more automatic retries. | Revoke access. Prompt customer to pay manually or update payment method. |
canceled |
Subscription has been canceled. This is a terminal state. | Revoke access. Offer resubscription option. |
incomplete |
First payment attempt failed or requires authentication. | Do not grant access yet. Wait for payment to complete. |
State transitions
trialing ──> active ──> canceled
│
├──> past_due ──> active (payment succeeds)
│ │
│ └──> unpaid ──> canceled
│
└──> paused ──> active (resumed)
│
└──> canceled
incomplete ──> active (payment succeeds)
│
└──> incomplete_expired (23h timeout)
Setting Up Products
stryhub has a complete product and price management system built into the dashboard. You can create products, set prices, customize checkout options, and get shareable payment links — all in one step, without ever touching the Stripe Dashboard.
Creating a product in stryhub
- In the admin dashboard, go to Programs and click the Products icon next to your program
- Click New Product
- Fill in the product details:
- Name — the product name your customers will see
- Description — optional description
- Image — product image (JPEG, PNG, or WEBP, max 2MB)
- Statement descriptor — what appears on the customer's credit card statement (max 22 characters)
- Unit label — e.g., "license", "seat", "user"
- Set the price:
- Amount — the price value (e.g., 29.90)
- Currency — BRL, USD, or EUR
- Type — Recurring (subscription) or One-time (single purchase)
- Billing interval (for recurring): Daily, Weekly, Monthly, Quarterly, Yearly, or custom
- Configure checkout options (see below)
- Click Create
Checkout customization
When creating a product (or editing a payment link later), you can configure what data is collected at checkout:
| Option | Description |
|---|---|
| Collect name | Require customer's full name |
| Collect phone | Require phone number |
| Require billing address | Collect full billing address |
| Collect tax ID | Collect tax identification (CPF/CNPJ) |
| Custom fields (up to 2) | Add custom text fields to the checkout form (e.g., "Company name", "License key") |
| Completion message | Custom message shown after successful purchase (max 500 characters) |
| Payment methods | Choose which methods to accept: Card, Boleto, PIX (PIX is excluded for subscriptions automatically) |
Adding more prices to a product
You can add multiple prices to the same product — for example, a monthly and an annual plan:
- Go to Products in your program
- Click the + button next to the product
- Set the new price amount, currency, and interval
- Each price gets its own checkout link and Payment Link
Price configuration examples
| Scenario | Price type | Interval |
|---|---|---|
| Monthly subscription | Recurring | Every 1 month |
| Annual subscription (discounted) | Recurring | Every 1 year |
| Quarterly billing | Recurring | Every 3 months |
| One-time purchase | One-time | N/A |
Managing products and prices
From the Products page in the dashboard, you can:
- Archive/unarchive products and prices — archived items are hidden from checkout but preserved in Stripe
- Hide prices from affiliates — control which prices affiliates can promote
- Set custom commission per price — override the program's default commission rate for specific prices
- Edit payment link options — update checkout customization (data collection, custom fields, completion message) at any time
Already have products in Stripe?
If you're connecting an existing Stripe account that already has products and prices, they sync automatically. You don't need to recreate anything.
- Products and prices are imported when you connect your Stripe account
- Use the "Sync Data" button in the admin dashboard to import existing subscriptions and customers
- You can create new products in stryhub alongside your existing Stripe products
Checkout & Subscription Creation
When you create a product in stryhub, two types of checkout links are generated automatically:
1. Payment Link (direct sales)
A shareable Stripe Payment Link is created automatically for each price. You can find it in the Products page — just copy the link and share it anywhere (website, email, social media). No affiliate tracking needed.
2. Affiliate checkout link
For affiliate-tracked sales, the format is:
https://app.stryhub.com/checkout/{affiliate_ref_code}/{stripe_price_id}
Affiliates see their personalized checkout links in the "My Links" page. Each link has their ref code embedded, so commissions are tracked automatically.
What happens after checkout
- Customer completes payment on the Stripe-hosted checkout page
- Stripe creates the subscription and charges the first invoice
- stryhub receives the event and creates the subscription record
- stryhub fires the following webhooks to your SaaS:
checkout.completed— checkout session completedpayment.completed— first payment succeededsubscription.created— new subscription is active
- If the sale came through an affiliate link, the commission is calculated and transferred automatically
Supported payment methods
You choose which payment methods to accept when creating the product:
- Credit/debit cards — Visa, Mastercard, Amex, and others
- Boleto — Brazilian bank slip (available for subscriptions and one-time)
- PIX — instant Brazilian payment (one-time purchases only)
Handling Webhooks
This is the most important section if you want to automate access control in your SaaS. stryhub forwards subscription events to your webhook endpoints as simplified, consistent payloads.
Subscription events reference
| Event | When it fires | What to do in your SaaS |
|---|---|---|
checkout.completed |
Customer completes a purchase | Create user account, grant access, send welcome email |
payment.completed |
Payment succeeds (initial or recurring) | Extend access period, send receipt |
subscription.created |
New subscription starts | Store subscription ID, provision features |
subscription.renewed |
Recurring payment succeeds | Extend access period |
subscription.canceled |
Subscription is canceled | Revoke access, send cancellation email |
subscription.paused |
Subscription billing is paused | Limit or revoke access, notify user |
subscription.resumed |
Paused subscription reactivated | Restore full access |
payment.failed |
Payment attempt fails | Notify customer, show payment update prompt |
subscription.past_due |
Invoice is overdue | Warn customer, consider a grace period before revoking access |
customer.created |
New customer registered | Create customer record in your database |
Example: full webhook handler
Here's a complete example of how to handle subscription events in your SaaS. This code verifies the webhook signature and routes each event to the appropriate handler.
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.STRYHUB_WEBHOOK_SECRET;
// IMPORTANT: use express.raw() to get the raw body for signature verification
app.post('/webhooks/stryhub', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-stryhub-signature'];
// 1. Verify signature
if (!verifySignature(req.body.toString(), signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
const { type, data } = event;
// 2. Handle each event type
switch (type) {
case 'checkout.completed':
// New customer just purchased — create their account
await createUserAccount({
email: data.customer.email,
name: data.customer.name,
stripeCustomerId: data.customer.stripe_customer_id,
subscriptionId: data.subscription_id,
plan: data.mode, // 'subscription' or 'payment'
});
break;
case 'subscription.created':
// Subscription is now active — grant access
await grantAccess(data.customer.stripe_customer_id, data.subscription_id);
break;
case 'subscription.renewed':
// Recurring payment succeeded — extend access
await extendAccess(data.customer.stripe_customer_id, data.subscription_id);
break;
case 'subscription.canceled':
// Subscription canceled — revoke access
await revokeAccess(data.customer.stripe_customer_id, data.subscription_id);
await sendEmail(data.customer.email, 'Your subscription has been canceled');
break;
case 'subscription.paused':
// Subscription paused — limit access
await pauseAccess(data.customer.stripe_customer_id, data.subscription_id);
break;
case 'subscription.resumed':
// Subscription resumed — restore access
await grantAccess(data.customer.stripe_customer_id, data.subscription_id);
break;
case 'payment.failed':
// Payment failed — notify customer
await sendEmail(data.customer.email, 'Your payment failed. Please update your payment method.');
break;
case 'subscription.past_due':
// Invoice overdue — show warning (keep access during grace period)
await flagPastDue(data.customer.stripe_customer_id, data.subscription_id);
break;
case 'customer.created':
// New customer — store in your database
await createCustomerRecord(data.customer);
break;
}
// 3. Always respond 200 quickly
res.status(200).json({ received: true });
});
function verifySignature(rawBody, signatureHeader, secret) {
const parts = {};
signatureHeader.split(',').forEach(p => {
const [key, value] = p.split('=');
parts[key] = value;
});
const signedContent = `${parts['t']}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(signedContent).digest('hex');
return crypto.timingSafeEqual(Buffer.from(parts['v1'], 'hex'), Buffer.from(expected, 'hex'));
}
import hmac, hashlib, json, os
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['STRYHUB_WEBHOOK_SECRET']
@app.route('/webhooks/stryhub', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Stryhub-Signature', '')
# 1. Verify signature
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
event = json.loads(request.data)
event_type = event['type']
data = event['data']
# 2. Handle each event type
if event_type == 'checkout.completed':
create_user_account(
email=data['customer']['email'],
name=data['customer']['name'],
stripe_customer_id=data['customer']['stripe_customer_id'],
subscription_id=data.get('subscription_id'),
)
elif event_type == 'subscription.created':
grant_access(data['customer']['stripe_customer_id'], data['subscription_id'])
elif event_type == 'subscription.renewed':
extend_access(data['customer']['stripe_customer_id'], data['subscription_id'])
elif event_type == 'subscription.canceled':
revoke_access(data['customer']['stripe_customer_id'], data['subscription_id'])
send_email(data['customer']['email'], 'Your subscription has been canceled')
elif event_type == 'subscription.paused':
pause_access(data['customer']['stripe_customer_id'], data['subscription_id'])
elif event_type == 'subscription.resumed':
grant_access(data['customer']['stripe_customer_id'], data['subscription_id'])
elif event_type == 'payment.failed':
send_email(data['customer']['email'], 'Your payment failed. Please update your payment method.')
elif event_type == 'subscription.past_due':
flag_past_due(data['customer']['stripe_customer_id'], data['subscription_id'])
elif event_type == 'customer.created':
create_customer_record(data['customer'])
# 3. Always respond 200 quickly
return jsonify({'received': True}), 200
def verify_signature(raw_body, signature_header, secret):
parts = dict(p.split('=', 1) for p in signature_header.split(','))
signed_content = f"{parts['t']}.{raw_body.decode('utf-8')}"
expected = hmac.new(secret.encode(), signed_content.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(parts['v1'], expected)
<?php
$webhookSecret = getenv('STRYHUB_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_STRYHUB_SIGNATURE'] ?? '';
// 1. Verify signature
if (!verifySignature($rawBody, $signature, $webhookSecret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$event = json_decode($rawBody, true);
$type = $event['type'];
$data = $event['data'];
// 2. Handle each event type
switch ($type) {
case 'checkout.completed':
createUserAccount(
$data['customer']['email'],
$data['customer']['name'],
$data['customer']['stripe_customer_id'],
$data['subscription_id'] ?? null
);
break;
case 'subscription.created':
grantAccess($data['customer']['stripe_customer_id'], $data['subscription_id']);
break;
case 'subscription.renewed':
extendAccess($data['customer']['stripe_customer_id'], $data['subscription_id']);
break;
case 'subscription.canceled':
revokeAccess($data['customer']['stripe_customer_id'], $data['subscription_id']);
sendEmail($data['customer']['email'], 'Your subscription has been canceled');
break;
case 'subscription.paused':
pauseAccess($data['customer']['stripe_customer_id'], $data['subscription_id']);
break;
case 'subscription.resumed':
grantAccess($data['customer']['stripe_customer_id'], $data['subscription_id']);
break;
case 'payment.failed':
sendEmail($data['customer']['email'], 'Payment failed. Please update your payment method.');
break;
case 'subscription.past_due':
flagPastDue($data['customer']['stripe_customer_id'], $data['subscription_id']);
break;
case 'customer.created':
createCustomerRecord($data['customer']);
break;
}
// 3. Always respond 200 quickly
http_response_code(200);
echo json_encode(['received' => true]);
function verifySignature(string $rawBody, string $signatureHeader, string $secret): bool {
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
$signedContent = "{$parts['t']}.{$rawBody}";
$expected = hash_hmac('sha256', $signedContent, $secret);
return hash_equals($parts['v1'] ?? '', $expected);
}
?>
200 status code within 15 seconds. If you need to do heavy processing (e.g., sending emails, querying external APIs), acknowledge the webhook first and process asynchronously. See the Webhook Integration guide for signature verification details and retry policy.Customer Portal
The Stripe Billing Portal is a secure, Stripe-hosted page where your customers can manage their own subscriptions — without you building any UI.
What customers can do
- Update their payment method (credit card, etc.)
- Change their plan (upgrade or downgrade)
- Cancel their subscription
- View invoice history and download receipts
Configuration
In the stryhub admin dashboard, go to Billing Portal to configure which features are available:
| Setting | What it controls |
|---|---|
| Allow cancellation | Whether customers can cancel their subscription from the portal |
| Allow plan changes | Whether customers can upgrade/downgrade to different prices |
| Allow payment updates | Whether customers can update their payment method |
Opening the portal for a customer
From the stryhub admin dashboard, go to Customers, click on a customer, and use the "Open Customer Portal" button. This generates a secure, one-time link that redirects the customer to their Stripe-hosted management page.
Portal capabilities at a glance
- 40+ languages — The portal UI is automatically localized based on the customer's browser language. No translation work required on your end.
- 14+ payment methods — Customers can pay with credit/debit cards, Apple Pay, Google Pay, SEPA Direct Debit, and various local payment methods depending on their region.
- Fully hosted by Stripe — Zero frontend development required. The portal is a complete, production-ready interface hosted on Stripe's infrastructure.
Built-in churn prevention
When a customer attempts to cancel their subscription through the portal, Stripe can automatically present a discount offer to retain them. This is configured in the Stripe Dashboard under Billing Portal settings — you can define the discount percentage and duration (e.g., "50% off for 3 months").
Additionally, when customers do proceed with cancellation, their cancellation reasons are collected automatically. These insights are available in your Stripe Dashboard and help you understand why customers leave, enabling data-driven product improvements.
Upgrades & Downgrades
When a customer changes their plan (e.g., from Basic to Pro), Stripe automatically handles proration — calculating the credit for unused time on the old plan and charging the difference for the new plan.
How proration works
Example: Customer on $10/month upgrades to $20/month on day 15 of a 30-day cycle:
- Credit for unused time on old plan: $5.00 (15 remaining days at $10/month)
- Charge for remaining time on new plan: $10.00 (15 remaining days at $20/month)
- Net charge: $5.00 (invoiced immediately or added to next invoice)
How customers upgrade/downgrade
- Through the Customer Portal (if plan changes are enabled)
- Through the Stripe Dashboard directly
- Via the Stripe API from your SaaS backend
When a plan change happens, stryhub fires a subscription.created webhook with the updated subscription details (new price, new amount).
Cancellations
Subscriptions can be canceled in two ways:
| Method | Behavior | Recommended for |
|---|---|---|
| At end of period | Customer keeps access until the current billing period ends. No refund. | Most use cases. Better customer experience. |
| Immediately | Subscription ends right away. Access is revoked immediately. | Policy violations, fraud, or customer requests immediate stop. |
Who can cancel
- Admin — from the stryhub dashboard (Subscriptions page), with option to choose immediate or end-of-period
- Customer — through the Customer Portal (if cancellation is enabled)
Webhook fired
When a subscription is canceled, stryhub fires subscription.canceled with the customer details and cancellation timestamp. Your SaaS should revoke access when it receives this event.
Pausing & Resuming
Instead of canceling, you can pause a subscription. This stops payment collection while keeping the subscription record intact — making it easy for the customer to come back.
How it works
- Pause: Payment collection stops. Invoices generated during pause are voided. The subscription status changes to
paused. - Resume: Payment collection restarts. The next invoice is generated at the beginning of the new billing cycle. The subscription returns to
active.
Admin actions
From the stryhub dashboard, go to Subscriptions, find an active subscription, and click Pause. To resume, find the paused subscription and click Resume.
Webhooks fired
subscription.paused— when the subscription is pausedsubscription.resumed— when the subscription is resumed
Free Trials
Free trials let customers try your product before being charged. During the trial, a subscription exists with status trialing.
Setting up trials
Free trials are configured at the price level in Stripe. You can set this up when creating a price in the Stripe Dashboard:
- Edit your product's price and look for the Free trial option
- Set the number of trial days (e.g., 7 or 14)
- Choose what happens when the trial ends without a payment method:
- Cancel — subscription is canceled automatically
- Pause — subscription pauses until customer adds a payment method
Trial timeline
| When | What happens |
|---|---|
| Trial starts | Subscription created with status trialing. No charge. |
| 3 days before trial ends | Stripe sends customer.subscription.trial_will_end event. Use this to remind the customer. |
| Trial ends | First invoice is generated. If payment succeeds, status changes to active. |
| Trial ends (no payment method) | Subscription is canceled or paused based on your configuration. |
Failed Payments & Dunning
When a recurring payment fails, Stripe doesn't give up immediately. It uses Smart Retries — a machine-learning system that analyzes payment patterns to determine the optimal time to retry.
What happens when a payment fails
- Stripe attempts to charge the customer's payment method
- If it fails, Stripe schedules automatic retries over the next few weeks
- The subscription status changes to
past_due - stryhub fires
payment.failedandsubscription.past_duewebhooks - Stripe sends dunning emails to the customer automatically
- If a retry succeeds, the subscription returns to
active - If all retries fail, the subscription may be canceled (configurable in Stripe Dashboard)
What your SaaS should do
| Event | Recommended action |
|---|---|
payment.failed |
Show a banner in your app: "Your payment failed. Please update your payment method." Include a link to the Customer Portal. |
subscription.past_due |
Implement a grace period (e.g., 7 days). Keep access but show warnings. After the grace period, limit access to essential features only. |
subscription.canceled |
Revoke access completely. Offer a link to resubscribe. |
Configuring retry behavior
In your Stripe Dashboard > Settings > Billing > Automatic collection, you can configure:
- Retry schedule — Smart Retries (recommended) or custom schedule
- Failed payment action — Cancel subscription, mark as unpaid, or leave as past_due
- Customer emails — Enable/disable automatic dunning emails from Stripe
How Smart Retries work
Smart Retries are powered by Stripe's machine learning engine, not fixed schedules. The ML model analyzes multiple signals to determine the optimal retry timing for each individual charge:
- Timezone & time of day — retries are scheduled when the customer's bank is most likely to approve the charge
- Device & behavior patterns — historical payment success patterns for the specific customer
- Card network signals — real-time data from Visa, Mastercard, and other networks about card status
- Cross-network data — aggregated insights from billions of payments processed across Stripe's entire network
Stripe performs up to 8 automatic retry attempts over approximately 2 weeks, with each retry timed for maximum likelihood of success.
Automatic Card Updater
One of the most common causes of involuntary churn is expired or replaced credit cards. Stripe eliminates this problem with the Automatic Card Updater.
When a customer's bank issues a new card — whether due to expiration, loss, or a routine replacement — Stripe automatically receives the updated card details directly from Visa and Mastercard networks. The subscription continues without interruption, and the customer doesn't need to do anything.
Global Payments & Compliance
stryhub + Stripe supports payments from customers worldwide with enterprise-grade compliance built in.
Multi-Currency Support
Accept payments in 135+ currencies with automatic currency conversion. Customers pay in their local currency while you receive funds in yours. Stripe handles all exchange rate calculations.
3D Secure & SCA Compliance
For European customers, Strong Customer Authentication (SCA) is required under PSD2. Stripe automatically triggers 3D Secure authentication when needed, ensuring your subscriptions comply with European regulations without any additional development.
Tax Automation (Stripe Tax)
Stripe Tax automatically calculates and collects the correct tax amount based on your customer's location:
- VAT — European Union
- GST — Australia, New Zealand, Singapore, and others
- Sales Tax — United States (state and local)
Stripe monitors your tax obligations and alerts you when you approach registration thresholds in new jurisdictions. Tax-compliant invoices are generated automatically.
Migrating to stryhub
If you already use Stripe (or another platform), migrating to stryhub is straightforward. Your existing data stays intact.
Step-by-step migration
- Create your stryhub account at app.stryhub.com
- Connect your existing Stripe account — go to Settings and click "Connect Stripe Account." Your products, prices, and customers sync automatically.
- Sync existing data — use the "Sync Data" button in the admin dashboard to import all existing subscriptions and customers from Stripe.
- Configure webhook endpoints — go to Webhooks and add your SaaS endpoint to start receiving events.
- Update your SaaS — implement a webhook handler (see Handling Webhooks above) to manage access based on subscription events.
- Configure the Customer Portal — go to Billing Portal and enable the features you want (cancellation, plan changes, payment updates).
- Replace checkout links — swap your old checkout URLs with stryhub checkout links. Existing subscriptions continue to work — only new purchases use the stryhub checkout.
- Set up affiliates (optional) — create an affiliate program to grow your sales. See the Affiliate Programs guide.
Coming from another platform?
If you're migrating from a platform that doesn't use Stripe (e.g., Paddle, Gumroad, PayPal), you'll need to:
- Create a Stripe account if you don't have one
- Recreate your products and prices in Stripe
- Ask existing customers to resubscribe through the new stryhub checkout link
- Run both systems in parallel until all customers have migrated