Creating an Installment Plan

    Implement an installment plan by using a subscription and a webhook to automatically limit the number of recurring invoices.

    You may want to offer installment plans that provide your customers with a flexible payment option instead of a single up-front cost. For example, an antiques business selling rare furniture offers an installment plan that spreads the cost over 10 months.

    This is a commonly requested feature that we’re working on. It isn’t available yet but this recipe shows how to model an installment plan using a subscription and a webhook. Subscriptions implement recurring charges that repeat indefinitely. Your integration listens for webhook notifications to count the number of payments, and cancels the subscription after the final payment.

    A product and a customer are prerequisites for this recipe. This guide shows how to:

    1. Build a webhook handler
    2. Configure a webhook
    3. Sign up a customer for an installment plan
    4. Test your server logic
    5. Go live

    Building a webhook handler

    A webhook handler is a script written in a server-side programming language, like Ruby (used in this recipe), that listens for and executes logic based on events from Stripe. It exists on your server with your application code. The webhook configuration tells Stripe what kind of notifications to send. The webhook handler and configuration need to be in place before you create installment plans and make charges.

    This script listens for the invoice.payment_succeeded event, counts the number of successful payments, and cancels the subscription when the final payment is received. The example in this recipe implements a plan of 10 monthly installments. It uses a metadata key-value pair called installments_paid, which indicates a subscription is an installment plan. The metadata is defined and initialized when you sign up a customer for an installment plan. Each time a payment is received, the script increments installments_paid by 1 and then cancels the subscription on the 10th payment.

    Let’s start building the script by creating a new Ruby file called installment_webhook_handler.rb and adding the following code:

    require 'sinatra'
    require 'stripe'
    require 'socket'
    
    set :secret_key, ENV['STRIPE_KEY']
    Stripe.api_key = settings.secret_key
    
    # You can find your endpoint's secret in your webhook settings
    secret = '{{ENDPOINT_SECRET}}'
    
    # Responds to webhooks sent by Stripe.
    post "/webhook" do
    
        # Retrieve the payload from the webhook.
        payload = (request.body.read)
    
        # Verify signature to be sure that the event came from Stripe.
        signature = request.env['HTTP_STRIPE_SIGNATURE']
        event nil
    
        begin
            event = Stripe::Webhook.construct_event(payload, signature, secret)
        rescue JSON::ParserError => e
            # Invalid payload
            # Recommendation: Log problem for investigation.
    
            status 400
            return
        rescue Stripe::SignatureVerificationError => e
            # Invalid signature
            # Recommendation: Log problem for investigation.
    
            status 400
            return
        end
    
        # Respond to Stripe with 200 to acknowledge that the endpoint
        # received the webhook.
        status 200
    
        # Execute only for `invoice.payment_succeeded` events.
        if event.type.eql?('invoice.payment_succeeded')
            increment_payments_count(event) # see below
        end
    
    end # post "/webhook" do
    
    

    The script first reads the environment variable that stores your Stripe secret key. Keeping the key in an environment variable stores it away from the code base. Next, it checks the signature to verify that the webhook comes from Stripe. We recommend that you log parse and signature errors so that you can investigate them.

    The script also responds to the webhook with a 200 HTTP status code to acknowledge receipt of the event. Because your script may make network calls or have complex logic, it should respond to the webhook immediately and then perform the rest of its duties. This keeps Stripe from resending the event.

    The last block inspects the event type. If it’s an invoice.payment_succeeded event, the script continues.

    Incrementing the number of payments

    Now you need to check if the payment is for an installment plan and not an on-going subscription. The installments_paid metadata key-value pair is where you save the payment count. If installments_paid exists, the subscription is an installment plan. The script adds the current payment to the count and saves it back to Stripe:

    def increment_payments_count(event)
        # Grab the subscription line item.
        sub =  event.data.object.lines.data[0]
    
        # Execute only for installment plans.
        if !sub.metadata[:installments_paid].nil?
        # Recommendation: Log invoices and check for duplicate events.
        # Recommendation: Note that we send $0 invoices for trials.
        #                 You can verify the `amount_paid` attribute of
        #                 the invoice object before incrementing the count.
    
            # Retrieve and increment the number of payments.
            count = sub.metadata[:installments_paid].to_i
            count += 1
            # Metadata is not write-protected; creating a database is an alternative.
    
            # Save incremented value to `installments_paid` metadata of the subscription.
            subscription_object = Stripe::Subscription.retrieve(sub[:id])
            subscription_object.metadata[:installments_paid] = count
            subscription_object.save
    
            # Check if all 10 installments have been paid.
            # If paid in full, then cancel the subscription.
            if count >= 10
                subscription_object.delete
            end
        end
    end
    

    The last piece of the script checks for the final payment. If it’s the 10th payment, the subscription is canceled and no further charges are made.

    In some cases, we may send duplicate events. For example, if we time out while waiting for an acknowledgement or if we think there was an error. This webhook handler uses the installments_paid value that is sent with the event so that each unique event produces the same count, even if it is duplicated. Another best practice for making your event processing idempotent is to log the invoices. Additionally, if you choose to use an idempotent key, be sure to account for events being resent beyond the 24-hour key expiration.

    Remember to also consider invoices for $0 that we send for subscription trials. Your handler can check the amount_paid attribute of the invoice object before incrementing the count.

    Configuring the webhook

    Now that your webhook handler is ready to receive webhooks, you can configure payment event notifications. Your handler needs to be available publicly on the web for Stripe to communicate with it. To do this, you can deploy the script to a server or test it locally using a service like ngrok.

    Once your webhook handler is accessible, configure it using the Dashboard.

    1. Click + Add endpoint.
    2. Enter the URL of your server or the tunnel that ngrok assigned to you.
    3. For the Filter event, select Select types to send and under that, select invoice.payment_selected.

    You can use the Send test webhook button to test the webhook. However, the test event is an empty object with zeros, such as evt_00000000000000. The attributes don’t correspond to real resources in your account so you can’t retrieve them through the API. You can test the webhook by generating an event with data. Do this by creating a customer in test mode in your account, and then creating a subscription to trigger an event with data in it.

    Signing up a customer for an installment plan

    To sign up a customer, you first need to set up a subscription. Subscriptions require a plan be attached to a product, and a customer be subscribed to the plan. Create a plan that makes monthly charges. Next, create a subscription that associates the plan with a customer. When you create the subscription, define an installments_paid metadata and initialize it to 0. This Ruby code snippet is in a separate script that you run to create an installment plan:

    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    Stripe::Subscription.create(
        customer: '{{CUSTOMER_ID}}',
        items: [{
            plan: '{{PLAN_ID}}',
        }],
        metadata: {
            installments_paid: 0,
        },
    )
    

    With this approach, we treat installment plans like subscriptions. When projecting your future revenue recognition, you need to account for these subscriptions, which show up as recurring indefinitely but actually have a limited duration. Also, the endpoint for previewing the upcoming invoice continues to show future invoices until these subscriptions are canceled.

    Testing your server logic

    To test your server logic, you need to generate some invoices. You can do this using subscription trials in test mode. When you create a subscription trial, an initial invoice is sent. After you end the trial, a second invoice is sent. This means two invoice.payment_succeeded events can be generated, and an installment plan with two payments can be tested.

    To test your logic using a trial, modify the line in your webhook handler that checks if count == 10 to count == 2. In a separate script, create a subscription trial with installments_paid initialized to 0:

    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    Stripe::Subscription.create(
        customer: '{{CUSTOMER_ID}}',
        items: [{
           plan: '{{PLAN_ID}}',
        },],
        metadata: {
           installments_paid: 0,
        },
        trial_period_days: 1,
    )
    

    You can confirm the creation of the installment plan by using the API to list the subscriptions. Note that installments_paid is incremented to 1 immediately, provided the customer is being automatically charged upon invoicing. This is because Stripe sends the first invoice when the subscription is created. Now end the trial:

    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    subscription = Stripe::Subscription.retrieve(
        id: '{{SUBSCRIPTION_ID}}'
    )
    subscription.trial_end = 'now'
    subscription.save
    

    When the trial ends, the script counts the second invoice and cancels the subscription. You can confirm the cancellation by retrieving the canceled subscription using the API

    Going live

    After you test your installment plan, you’re ready to go live. You need to complete five steps to move from test to live mode:

    1. In your installment_webhook_handler.rb file, change the test Stripe API key to your live key.

    2. In the same file, change count == 2 back to count == 10.

    3. Add your updated files to your web server.

    4. Make sure you’re viewing live data in the Dashboard. Switch off your Dashboard’s Viewing test data option, and create your plan in live mode. Visit your Dashboard and create the plan again exactly as you did in test mode.

    5. Visit your account’s webhooks settings and add the endpoint URL again, to register it in live mode.

    Further reading

    Questions?

    We're always happy to help with code or other questions you might have! Search our documentation, contact support, or connect with our sales team. You can also chat live with other developers in #stripe on freenode.

    Was this page helpful? Yes No

    Send

    Thank you for helping improve Stripe's documentation. If you need help or have any questions, please consider contacting support.