DEV Community

Cover image for Offer free trials without an upfront payment method using Stripe Checkout
CJ Avilla for Stripe

Posted on • Originally published at cjav.dev

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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..."
Enter fullscreen mode Exit fullscreen mode

Next, we’ll install dependencies for the server.

bundle install
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You might be wondering 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',
    )
Enter fullscreen mode Exit fullscreen mode

Next steps

Now you know how to offer free trials without payment methods upfront using Stripe Checkout. There are several benefits to this approach, chief among them increased conversion because of a lower bar of entry. 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.

About the author

CJ Avilla

CJ Avilla (@cjav_dev) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a YouTuber. He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲.

Top comments (0)