Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to implement subscription trials with Stripe

Add free trials to Stripe subscriptions using trial_period_days on Checkout Sessions or Subscription creation. With payment method upfront, set trial_period_days in subscription_data. Without payment method, use payment_method_collection: 'if_required' and configure trial_settings.end_behavior to cancel or pause when the trial ends. Always listen for the customer.subscription.trial_will_end webhook to prompt users before their trial expires.

What you'll learn

  • How to add free trials to Stripe subscriptions with and without payment method
  • How to configure trial_period_days in Checkout Sessions and API
  • How to handle trial expiration with webhooks
  • How to prevent trial abuse with email verification
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read15-20 minutesStripe API v2024-12-18+, any backend languageMarch 2026RapidDev Engineering Team
TL;DR

Add free trials to Stripe subscriptions using trial_period_days on Checkout Sessions or Subscription creation. With payment method upfront, set trial_period_days in subscription_data. Without payment method, use payment_method_collection: 'if_required' and configure trial_settings.end_behavior to cancel or pause when the trial ends. Always listen for the customer.subscription.trial_will_end webhook to prompt users before their trial expires.

How Free Trials Work in Stripe Subscriptions

Stripe supports two trial models: trials with a payment method collected upfront (higher conversion) and trials without a payment method (lower friction). Both use trial_period_days to set the trial length. During the trial, the subscription status is 'trialing' and no charges are made. When the trial ends, Stripe automatically charges the customer's payment method or takes the configured end_behavior action. The customer.subscription.trial_will_end webhook fires approximately 3 days before trial expiration, giving you time to notify users.

Prerequisites

  • A Stripe account with API keys (test mode is fine)
  • Products and Prices created in the Stripe Dashboard
  • A server running Node.js with the stripe package installed
  • A webhook endpoint configured to receive Stripe events

Step-by-step guide

1

Create a Checkout Session with a trial period

The most common trial setup collects a payment method upfront during checkout but doesn't charge until the trial ends. Add trial_period_days to the subscription_data object in your Checkout Session. The customer enters their card details during checkout, but the first invoice is for $0. After the trial period, Stripe automatically charges the subscription price.

typescript
1const session = await stripe.checkout.sessions.create({
2 mode: 'subscription',
3 line_items: [{ price: 'price_xxx', quantity: 1 }],
4 subscription_data: { trial_period_days: 14 },
5 success_url: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',
6 cancel_url: 'https://yoursite.com/cancel',
7});

Expected result: Customer completes checkout, enters payment details, and starts a 14-day free trial with no charge.

2

Create a trial without requiring a payment method

For maximum signups, offer trials without collecting a card. Set payment_method_collection to 'if_required' and configure what happens when the trial ends without a payment method. Options are 'cancel' (terminates the subscription), 'pause' (pauses until payment method added), or 'create_invoice' (creates an unpaid invoice).

typescript
1const session = await stripe.checkout.sessions.create({
2 mode: 'subscription',
3 line_items: [{ price: 'price_xxx', quantity: 1 }],
4 subscription_data: {
5 trial_period_days: 14,
6 trial_settings: {
7 end_behavior: { missing_payment_method: 'cancel' },
8 },
9 },
10 payment_method_collection: 'if_required',
11 success_url: 'https://yoursite.com/success',
12 cancel_url: 'https://yoursite.com/cancel',
13});

Expected result: Customer starts a trial without entering any payment information.

3

Create a trial via the Subscriptions API directly

If you manage your own checkout UI, create subscriptions with trials programmatically. Create or retrieve a customer, then create a subscription with trial_period_days. The subscription starts in 'trialing' status immediately. For trials with payment method, attach a PaymentMethod to the customer first.

typescript
1const subscription = await stripe.subscriptions.create({
2 customer: 'cus_xxx',
3 items: [{ price: 'price_xxx' }],
4 trial_period_days: 14,
5 payment_behavior: 'default_incomplete',
6 expand: ['latest_invoice.payment_intent'],
7});
8console.log(subscription.status); // 'trialing'
9console.log(subscription.trial_end); // Unix timestamp

Expected result: Subscription created with status 'trialing' and trial_end set to 14 days from now.

4

Listen for the trial_will_end webhook

Stripe fires customer.subscription.trial_will_end approximately 3 days before the trial expires. Use this webhook to send reminder emails prompting users to add a payment method or upgrade. Also listen for customer.subscription.updated to detect when trials transition to active or canceled status.

typescript
1app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
2 const sig = req.headers['stripe-signature'];
3 const event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
4
5 switch (event.type) {
6 case 'customer.subscription.trial_will_end': {
7 const sub = event.data.object;
8 console.log(`Trial ending for ${sub.customer}`);
9 // sendTrialEndingEmail(sub.customer);
10 break;
11 }
12 case 'customer.subscription.updated': {
13 const sub = event.data.object;
14 if (sub.status === 'active') {
15 console.log('Trial converted to paid!');
16 }
17 break;
18 }
19 }
20 res.json({ received: true });
21});

Expected result: Your server receives a webhook ~3 days before trial ends and can send reminder emails.

5

Grant access based on subscription status

In your application logic, check the subscription status to determine access. Both 'trialing' and 'active' statuses should grant full access. When the status changes to 'canceled', 'past_due', or 'unpaid', restrict access. Store the subscription status in your database and update it via webhooks.

typescript
1function hasAccess(subscriptionStatus) {
2 return ['trialing', 'active'].includes(subscriptionStatus);
3}

Expected result: Users in 'trialing' or 'active' status can access premium features; others are restricted.

Complete working example

trial-subscription.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3const app = express();
4
5// Webhook endpoint — must be before express.json()
6app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
7 const sig = req.headers['stripe-signature'];
8 let event;
9 try {
10 event = stripe.webhooks.constructEvent(
11 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
12 );
13 } catch (err) {
14 return res.status(400).send(`Webhook Error: ${err.message}`);
15 }
16
17 switch (event.type) {
18 case 'customer.subscription.trial_will_end':
19 console.log('Trial ending soon:', event.data.object.customer);
20 break;
21 case 'customer.subscription.updated': {
22 const sub = event.data.object;
23 console.log(`Subscription ${sub.id} status: ${sub.status}`);
24 break;
25 }
26 case 'invoice.paid':
27 console.log('Payment succeeded:', event.data.object.subscription);
28 break;
29 case 'invoice.payment_failed':
30 console.log('Payment failed:', event.data.object.subscription);
31 break;
32 }
33 res.json({ received: true });
34});
35
36app.use(express.json());
37
38// Create trial checkout session
39app.post('/create-trial', async (req, res) => {
40 try {
41 const session = await stripe.checkout.sessions.create({
42 mode: 'subscription',
43 line_items: [{ price: req.body.priceId, quantity: 1 }],
44 subscription_data: { trial_period_days: 14 },
45 success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
46 cancel_url: `${req.headers.origin}/cancel`,
47 });
48 res.json({ url: session.url });
49 } catch (err) {
50 res.status(500).json({ error: err.message });
51 }
52});
53
54app.listen(4242, () => console.log('Server on port 4242'));

Common mistakes when implementing subscription trials with Stripe

Why it's a problem: Not handling the trial_will_end webhook

How to avoid: Always listen for customer.subscription.trial_will_end to send reminder emails ~3 days before trial expiry.

Why it's a problem: Treating 'trialing' as inactive in your access logic

How to avoid: Both 'trialing' and 'active' subscription statuses should grant full access to premium features.

Why it's a problem: Not protecting against trial abuse with multiple signups

How to avoid: Use Stripe Radar's trial fraud blocking (stops 62% of abuse), implement email verification, and check for duplicate emails.

Why it's a problem: Using a 30-day trial when 14 days would convert better

How to avoid: Data shows 7-14 day trials convert best. Longer trials lead to users forgetting they signed up.

Best practices

  • Use 7 or 14-day trials for optimal conversion rates
  • Always collect a payment method upfront unless you have a specific strategy for cardless trials
  • Send a reminder email 3 days before trial ends using the trial_will_end webhook
  • Grant full access during trials — restricted trials reduce perceived value
  • Track trial-to-paid conversion rates in your analytics
  • Enable Stripe's trial messaging in Dashboard for card network compliance
  • Use test mode to verify the complete trial lifecycle before going live

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to add a 14-day free trial to my Stripe subscription using Node.js and Express. The trial should collect a credit card upfront but not charge until day 14. Show me the Checkout Session creation and webhook handler for trial_will_end events.

Stripe Prompt

Add a 14-day free trial to my Stripe subscription flow. Use Checkout Sessions with trial_period_days: 14 in subscription_data. Collect payment method upfront. Add webhook handler for customer.subscription.trial_will_end to send reminder emails. Include access control that grants full access for both 'trialing' and 'active' statuses.

Frequently asked questions

Can I change the trial length after a subscription is created?

Yes, update the trial_end on an existing subscription via API. Set it to a new Unix timestamp or 'now' to end the trial immediately, triggering a prorated charge.

What happens when a trial ends and the payment fails?

The subscription moves to 'past_due' status and Stripe's Smart Retries begin automatically, recovering 56-57% of failed payments on average.

Can I offer a trial without Stripe Checkout?

Yes. Create a subscription directly via the API with trial_period_days. Handle your own payment form using Stripe Elements and attach a PaymentMethod first.

How do I prevent trial abuse?

Enable Stripe Radar's trial fraud blocking (stops 62% of abuse), implement email verification, track device fingerprints, and check for duplicate emails.

Does the customer see a $0 charge during the trial?

Yes, Stripe creates a $0 invoice when the trial starts if a payment method is collected, confirming the card is valid without charging it.

What if I need help with complex trial and subscription logic?

For trials with usage-based billing, tiered access, or complex conversion funnels, RapidDev's engineers can implement the complete Stripe integration including webhooks and dunning flows.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.