Payments

App screenshot

Payments are powered by Stripe. The payments app includes a workflow to handle the customer subscription lifecycle.

Quick Start for the Flat pricing model

Subscriptions work out of the box. Here are the steps to quickly get up and running and start collecting payments.

  • Create a Stripe account and set up your API keys via the Stripe

  • Add your api keys to the environment.

    • When working locally add the keys to the .env file. On fly.io or other cloud provider you can add these via the dashboard.

  • Setup Webhooks

  • Create new products via the Stripe dashboard.

See the Other Pricing Models section below for Usage based and Per seat/user pricing.

Overview

The starter kit uses the django fsm library to manage state for subscriptions.

  • Every workspace/customer starts out in the not_subscribed state.

  • The default flow starts with a trial.

    • Trial days can be changed via the config settings. This can also be disabled completely.

  • Payment processing is triggered by a user, directly from the dashboard.

How subscriptions work

Subscriptions can be divided into two logical parts.

  • Managing payments, subscriptions, security, fraud , etc - this is generic and common for most SaaS businesses and is handled by Stripe.

  • Business-specific customizations - this includes managing the customer life cycle, what emails to send when a customer first signs up for a trial, communication via emails, and reminders, creating checkout sessions, and communicating with Stripe, etc - this is managed in the app.

Business logic workflow

  • Vanty uses a finite state model (FSM via the django_fsm library) to manage business logic relating to complex subscriptions and other workflows.

  • Business Logic for the payments workflow is contained in the SubscriptionPaymentsMixin class.

    • Use this class to easily manage how each subscription transitions between different states e.g. from trialing active canceling

    • You can also include any side effects here, for example mailing a customer after successful payment, activating a product, following up on late payments, etc.

  • The SubscriptionPaymentsMixin is an abstract class that inherits from models.Model

    • No database table is created for this class during migration, instead, each subscription is attached to a tenant.

    • This makes it easier to transfer ownership of workspaces between users or manage billing usage for an organization.

  • You could also choose to attach each subscription to another entity, for example, a User . Only do this if you are positive you will not change your billing model. If you simply want to limit the access to 1 user per paid account, change this via the TENANT_MODE setting instead.

Product metadata for the pricing page

Product metadata, for example, the features list shown on the pricing pages is stored in the database and can be edited via the dashboard.

  • Model - Information about active subscription products is stored in a cached model see apps/dashboard/models/payments.py::ProductMetadata. You can use this data on your pricing or landing page and it should still be fast.

  • Each stripe product is linked to a local instance that stores data about features, etc. Price and product data are still controlled via the stripe dashboard.

  • Updating the price in Stripe will also automatically update the relevant metadata if the sync job is enabled.

Configuring Stripe

To start using Stripe you'll need to update your environment variables in config/settings/base.py or populate them in the appropriate environment variables file or UI if using Heroku or Render.

API Keys

Stripe secret keys, products can be updated directly from the Admin dashboard. Please note that when you first start, you will should still add the Stripe API keys to your environment.

python

STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY", default="sk_test_")
STRIPE_LIVE_MODE = env.bool("STRIPE_LIVE_MODE", False) # Change to True in production

Webhooks

Webhooks can be managed directly from the Django admin dashboard. This is not yet available via the control panel. Changing the webhook endpoint on the dashboard changes it on Stripe.

python

DJSTRIPE_WEBHOOK_SECRET = env( "DJSTRIPE_WEBHOOK_SECRET", default="whsec_yourwebhook")
DJSTRIPE_WEBHOOK_VALIDATION = env( "DJSTRIPE_WEBHOOK_VALIDATION", default="retrieve_event")
DJSTRIPE_WEBHOOK_URL = env("DJSTRIPE_WEBHOOK_URL", default="billing/webhooks/$")
    

Subscriber model

The subscriber model represents the app’s subscription holder. For Vanty this is the tenant.

python

DJSTRIPE_SUBSCRIBER_MODEL = "tenant.Tenant"

The subscription model must have an email field. The tenant model defines an email field that uses the owner as a subscription holder. This can be changed to another user such as a user with a billing role for example.

python

@property 
def email(self): 
    return self.owner.email

 

Modelling Products

Stripe supports multiple price models for recurring purchases. They fall into two main categories.

  • Licensed - A fixed amount is determined and charged upfront.

  • Metered - Usage is monitored over a period and the customer is charged in arrears.

There are other more advanced models but they basically fall into the same two categories or combine them in different ways.

Vanty supports the common models out of the box. Refer to the stripe docs for more detailed information on how to set up your product.

Getting started

  • At a minimum, you will have to set up some products and attach prices to those products. The products can be added from the stripe dashboard .

  • The app relies on stripe checkout to manage payments, renewals, cancelations, etc. This includes fraud checks, customer data security, etc so you can focus on your business.

  • Billing management is also handled by stripe via the customer portal. You will have to add/update your customer portal settings here .

Other Supported Price Models

The starter kit supports 3 pricing models out of the box.

Flat Pricing or Good Better Best (Default)Required setup

  • Create your products on stripe

  • Sync the products via the admin dashboard.

  • Edit the imported products to include a feature list and set the products to active.

Per Seat / User

Customers choose the number of seats they will use and are charged based on that amount

Required Setup

  • Create your products on stripe

  • Sync the products via the admin dashboard

  • Set the STRIPE_PER_SEAT_BILLING_ENABLED environment variable to true. This ensures users cannot exceed the max number of users allowed on the workspace.

  • Set the products as active and update your feature list.

Usage based pricing

Required Setup

Customers are charged based on how much of a service they consume in the period.

  • Create your products on stripe

  • Sync the products via the admin dashboard

Update the AppUsageRecord model and the codebase.

The Starter Kit already includes a basic model for recording usage. This is named AppUsageRecord to avoid confusion/conflicts with dj_stripe UsageRecord model.

  • The AppUsageRecord model includes three fields for measuring the quantity, timestamp, and tenant. This needs to be extended to suit your business e.g. include other metadata such as service name.

  • Update the codebase to record the quantity as your service is consumed.

Reporting to stripe -

Stripe expects you to maintain your own system for recording usage. This information must be passed on to stripe.

This can be done using a cron job that updates the usage for each tenant on a daily basis for example.

python
# apps.payments.tasks

@db_periodic_task(crontab(day="7"))
def update_stripe_reporting(): 
# subscription states to report on, 

    valid_states = ["active", "trialing", "past_due", "unpaid"]  
# fetch all tenants. 
    tenants = Tenant.objects.filter(subscription__state__in=valid_states)
 # update stripe.
    for tenant in tenants: 
        tenant.update_stripe_usage_record()  
  • To create a new record, use the dj_stripe UsageRecord model . This will allow you to keep your data in sync.

  • By default, this action increments the usage count on Stripe. If you aggregate the data yourself, i.e. send only the total balance at a point in time, the action parameter must use set.

Checkout Sessions

Once a user selects a product, a stripe checkout session is created and they are automatically redirected to the stripe. The app keeps track of all active checkout sessions

Sessions will remain active for at least 24 hours unless explicitly canceled by users.

The app & API only include basic views to trigger/manage new stripe checkout sessions. This is by design as session checkout management is delegated to stripe.

Standard emails are included for managing communication of subscription information/events with end-users. These can be modified, see the section on Emails below.

Testing your payments workflows

  • The test suit includes unit testing for the payments checkout flow with the stripe webhooks responses mocked.

  • You will want to test with live data in your test environment to test webhooks, follow the links on your stripe dashboard to install the stripe cli, and configure webhooks.

    text
    # install stripe
    pip install stripe-cli
    
    # login
    stripe login
    
    # listen to webhooks
    stripe listen --forward-to=localhost:8000/billing/webhooks/
    Checking for new versions... your webhook secret is
  • Copy the webhook secret to your settings file and start testing

Emails

The Starter Kit includes emails for subscription confirmations, cancellations, and notifications for when a trial will end. These can be found in templates/emails/payments . They can also be updated directly from the dashboard. See the email app for more information.

When updating emails via the dashboard

  • Check that the correct variables are still included.

  • Preview the templates before using them.

  • If something goes wrong you can always delete and refresh from the codebase.