Offer free trials without an upfront payment method using Stripe Checkout
Follow along to learn how to use Stripe Checkout to collect a customer's information and start a free trial without requiring payment details. Let's get started!
Today we're going to start from a Stripe Sample, a prebuilt example with some simple views, a couple of server routes, and a webhook handler:
stripe samples create checkout-single-subscription trials
We'll use Ruby for our server, but you should be able to follow along in your favorite server-side language.
Use the arrow keys to navigate: ↓ ↑ → ←
? What server would you like to use:
java
node
php
python
↓ ▸ ruby
In the root of our new trials
directory, you'll notice a file called sample-seed.json
. We can use the Stripe CLI to execute the API calls in this seed fixture to create products and prices. If you haven’t already installed the Stripe CLI, follow the instructions in the documentation, then run the fixture:
stripe fixtures sample-seed.json
New products: starter and professional
That fixture creates two new products called starter
and professional
. Each product has 2 related Prices – one for monthly and one for annual. From the Stripe Dashboard, we can grab the monthly Price ID for the starter
product. This price is configured to collect $12 per month, and we’ll pass this as an argument to the API call when creating the Checkout Session. Want to learn more about modeling your business with Products and Prices? Have a look at this past article.
Now we can open up our server directory and update the .env
file with our new Price IDs. The .env
file should look like this but have your Price IDs and API keys:
BASIC_PRICE_ID="price_1MbOQMCZ6qsJgndJy04BjDxe"
DOMAIN="http://localhost:4242"
PRO_PRICE_ID="price_1MbOQOCZ6qsJgndJrpCrN3KK"
STATIC_DIR="../client"
STRIPE_PUBLISHABLE_KEY="pk_test_vAZ3g..."
STRIPE_SECRET_KEY="rk_test_51Ece..."
STRIPE_WEBHOOK_SECRET="whsec_9d75cc1016..."
Next, we’ll install dependencies for the server.
bundle install
We can start the Sinatra server with ruby server.rb
and visit localhost:4242.
Selecting the starter plan redirects the customer to Stripe Checkout, where they will enter payment details. Notice that nothing about this page signals that we're on a trial yet. That's because we have not modified the params for creating the Checkout Session to start a trial without payment method upfront.
Configuring the Checkout Session for trials
From the /create-checkout-session
route, we're creating a Checkout Session and redirecting. We have an entire Checkout 101 series for you to get up to speed quickly. It's available in the documentation or here on YouTube. Here’s the code for reference:
post '/create-checkout-session' do
begin
session = Stripe::Checkout::Session.create(
success_url: ENV['DOMAIN'] + '/success.html?session_id={CHECKOUT_SESSION_ID}',
cancel_url: ENV['DOMAIN'] + '/canceled.html',
mode: 'subscription',
line_items: [{
quantity: 1,
price: params['priceId'],
}],
)
rescue => e
halt 400,
{ 'Content-Type' => 'application/json' },
{ 'error': { message: e.error.message } }.to_json
end
redirect session.url, 303
end
When we create the Checkout Session, we can pass a hash into subscription_data
. This allows us to configure the subscription created by Stripe Checkout. Inside of subscription_data
we can set trial_period_days
to an integer number of days that we want to offer a trial. We also want to set payment_method_collection
to if_required
so we don’t require payment details upfront.
Here’s the new API call for creating Checkout Sessions:
session = Stripe::Checkout::Session.create(
success_url: ENV['DOMAIN'] + '/success.html?session_id={CHECKOUT_SESSION_ID}',
cancel_url: ENV['DOMAIN'] + '/canceled.html',
mode: 'subscription',
line_items: [{
quantity: 1,
price: params['priceId'],
}],
subscription_data: {
trial_period_days: 14,
},
payment_method_collection: 'if_required',
)
Now, the button to start a subscription on the starter plan redirects customers to the Stripe-hosted checkout page. Rather than needing to enter payment details upfront, customers only need to enter their email address. This starts a free 14-day trial and then collects $12 per month after that, assuming the customer sets up payment details.
I recommend using the customer portal to enable customers to add and update payment methods on file. To see the customer portal in action, click the manage billing button on the success page after subscribing. The API call to create a customer portal session is simple; you need only specify the customer. I prefer storing the ID of the customer alongside the authenticated user, but you can also pull the customer’s ID from the Checkout Session object directly:
session = Stripe::BillingPortal::Session.create({
customer: checkout_session.customer,
return_url: return_url
})
# Redirect to session.url
Emailing customers when trials end
At the end of a trial, you’ll want the customer to convert to paid and enter their payment details. One way to encourage customers to come back onto your site to enter their card is to email them just before the trial ends. Stripe offers a feature to automatically email customers when trials end with the Billing scale plan. Sign up for Billing scale and configure your email settings here. Alternatively, you can listen for the trial_will_end
webhook notification and send your email with a link to the customer portal.
You’ll find this view in your Stripe dashboard under Settings > Customer portal. Customers can follow the customer portal link URL to manage their billing from the portal.
post '/webhook' do
# You can use webhooks to receive information about asynchronous payment events.
# For more about our webhook events check out https://stripe.com/docs/webhooks.
webhook_secret = ENV['STRIPE_WEBHOOK_SECRET']
payload = request.body.read
if !webhook_secret.empty?
# Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured.
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, webhook_secret
)
rescue JSON::ParserError => e
# Invalid payload
status 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
puts '⚠️ Webhook signature verification failed.'
status 400
return
end
else
data = JSON.parse(payload, symbolize_names: true)
event = Stripe::Event.construct_from(data)
end
if event.type == 'customer.subscription.trial_will_end'
customer_portal = "https://billing.stripe.com/p/login/test_7sIcQT9yjgqxewEdQQ"
puts "Email customer #{customer_portal}"
end
content_type 'application/json'
{
status: 'success'
}.to_json
end
You might wonder how we can test that our billing logic will work as expected at the end of the 14-day trial. Test Clocks are purpose-built for testing these scenarios. To learn more about Test Clocks, check out this video. This snippet shows how you would create a test clock, create a new customer with reference to the clock, then use that customer with the Checkout Session:
test_clock = Stripe::TestHelpers::TestClock.create(
frozen_time: Time.now.to_i,
)
customer = Stripe::Customer.create(
test_clock: test_clock.id,
)
session = Stripe::Checkout::Session.create(
customer: customer.id,
success_url: ENV['DOMAIN'] + '/success.html?session_id={CHECKOUT_SESSION_ID}',
cancel_url: ENV['DOMAIN'] + '/canceled.html',
# mode: 'subscription',
mode: 'payment',
line_items: [{
quantity: 1,
# price: params['priceId'],
price: 'price_1MRMnoCZ6qsJgndJJ9JrzPgs',
}],
subscription_data: {
trial_period_days: 14,
},
payment_method_collection: 'if_required',
)
Next steps
Now you know how to offer free trials without payment methods upfront using Stripe Checkout. This approach has several benefits, chief among them increased conversion because of a lower entry bar. You might also want to specify whether to cancel or pause the subscription if the customer didn’t provide a payment method during the trial period. No matter what your subscription use case, you can now build it with Checkout.