Offer free trials without an upfront payment method using Stripe Checkout
Tags: stripe, payments, tutorial, ruby
Published: 2023-02-15 19:13:37

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!

{% embed https://www.youtube.com/watch?v=x9Grjc-8tbw %}

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:


```bash
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.


```bash
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](https://stripe.com/docs/stripe-cli#install), then run the fixture:


```bash
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](https://dashboard.stripe.com/test/products?active=true), 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](https://dev.to/stripe/modeling-your-saas-business-with-products-and-prices-59e0).


<img src="https://cjav.dev/wo-trial/01-price-copy.png" />


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:

```yaml
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.

```bash
bundle install
```


We can start the Sinatra server with `ruby server.rb` and visit [localhost:4242](http://localhost:4242).


<img src="https://cjav.dev/wo-trial/02-landing.png" />


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.

<img src="https://cjav.dev/wo-trial/03-without-trial.png" />


## 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](https://stripe.com/docs/videos/checkout-101) or [here on YouTube](https://www.youtube.com/watch?v=TJCdUYQTLJU&list=PLy1nL-pvL2M5cO2i3lSYtwyqZh3EeGR9L). Here’s the code for reference:


```ruby
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`](https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-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:


```ruby
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.

<img src="https://cjav.dev/wo-trial/04-with-trial.png" />

I recommend using the [customer portal](https://stripe.com/docs/customer-management/integrate-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](https://stripe.com/docs/api/customer_portal/sessions/create) 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:


```ruby
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](https://dashboard.stripe.com/settings/billing/automatic). Alternatively, you can listen for the `trial_will_end` webhook notification and send your email with a link to the customer portal.


<img src="https://cjav.dev/wo-trial/05-settings.png" />


You’ll find this view in your Stripe dashboard under [Settings > Customer portal](https://dashboard.stripe.com/test/settings/billing/portal). Customers can follow the customer portal link URL to manage their billing from the portal.


```ruby
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 be wondering how we can test that our billing logic will work as expected at the end of the 14-day trial. [Test Clocks](https://stripe.com/docs/billing/testing/test-clocks) are purpose-built for testing these scenarios. To learn more about Test Clocks, check out [this video](https://www.youtube.com/watch?v=0GsVXfDPavg). 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:


```ruby
    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. There are several benefits to this approach, chief among them **increased conversion** because of a lower bar of entry. You might also want to s[pecify whether to cancel or pause the subscription if the customer didn’t provide a payment method during the trial period](https://stripe.com/docs/billing/subscriptions/trials#configure-free-trials-without-payment-methods-to-cancel). No matter what your subscription use case, you can now build it with Checkout.


## About the author

![CJ Avilla](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mef4k8k1jhnula9b1mxc.png)


[CJ Avilla](https://cjav.dev) ([@cjav_dev](https://twitter.com/cjav_dev)) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a [YouTuber](https://www.youtube.com/CJAvilla). 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 🚲.