django-stripe-billing
is a Django app to manage Stripe billing plans.
- Add "django-stripe-billing" to your
requirements.txt
- You may now need to install it with
pip install -r requirements.txt
. - Add "billing" to your INSTALLED_APPS setting like this:
INSTALLED_APPS = [ ... 'billing', ... ]
- Include the billing URLconf in your project urls.py like this:
path('billing/', include('billing.urls')),
- OPTIONAL: Use celery for webhook processing:
pip install celery
and add it to requirements. If you don't install celery, it will process webhooks synchronously. - Set the Django settings.
- Run
python manage.py migrate
to create the billing models. - Run
python manage.py billing_init
, which will create Customer objects for existing Users. If you don't do this, you may run into errors. - Add this to your user admin file:
import billing.admin ... class UserAdmin(DefaultUserAdmin): ... inlines = [billing.admin.CustomerAdminInline, billing.admin.StripeEventAdminInline]
- Once you have configured Stripe, create the desired billing
Plan
s in the Django admin. - Add the
billing.mixins.BillingMixin
to the view where a user might manage their billing (e.g. a "Settings" view).
- There must be at least one Paid billing plan to use this Mixin.
- In your Stripe dashboard, you must configure it to cancel a customer's subscription if all retries for a payment fail.
- Do not allow the Customer to update their email address in Customer Portal.
- Do not allow the Customer to update billing information (other than payment method) or view invoice history.
- Update the branding of Checkout/Portal to match the branding of your site.
- In your Stripe dashboard, set up a product (with an optional statement descriptor), and set up a price for that product.
- In the Stripe dashboard, the following webhooks should be set to point to
https://production.url/billing/stripe/webhook/
:customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
BILLING_STRIPE_API_KEY
: The Stripe API key.- Required
- If this is a real Stripe API key, it should be set in an environment variable.
- You may use the word
mock
for a mocked Stripe client. This can be useful in local development. You can't use Checkout/Portal, but any other function that calls out to stripe will just call a mock. - You must use a real test environment Stripe API key if using Stripe Checkout / Portal while developing locally.
- Obviously, only use a live environment Stripe API key in production.
BILLING_APPLICATION_NAME
: The name of the application.- Required
- The Stripe customer metadata will store this in the
application
key.
BILLING_CHECKOUT_SUCCESS_URL
- Required
- Where Stripe Checkout should redirect on success.
- This view should parse Django messages.
- Must be an absolute URL or begin with a
/
.
BILLING_CHECKOUT_CANCEL_URL
- Required
- Where Stripe Checkout should redirect on cancel.
- This view should parse Django messages.
- Must be an absolute URL or begin with a
/
.
BILLING_PORTAL_RETURN_URL
- Optional
- Where Stripe Customer Portal should redirect back to when done.
- If not provided, a
return_url
attribute must be specified in thePOST
when creating a Customer Portal.
BILLING_STRIPE_WH_SECRET
- Optional
- If set, this should be in an environment variable.
- If this is set, Stripe webhook processing will verify the webhook signature for authenticity.
POST
tobilling:create_checkout_session
to create a Stripe Checkout Session.- You must include 2 kwargs of a slugified plan name
slug
and the planpk
. - We require both to enhance the privacy of private paid plans.
- You must include 2 kwargs of a slugified plan name
POST
tobilling:create_portal_session
to create a Stripe Billing Portal Session.- Form data must contain
return_url
which is the URL to go back to once the Customer is done with the Portal. If this is omitted, it defaults to theLOGIN_REDIRECT_URL
.
- Form data must contain
- A
BillingMixin
is available inbilling.mixins.BillingMixin
. This defines aget_context_data(self, **kwargs)
method that returns the following context:billing_enabled
is a convenience check for whether billing is enabled.stripe_session_url
for the form button to take you to the Stripe Checkout/Portal for thePAID_PUBLIC
plan.stripe_session_button_text
text for the button describing what it will do.billing_state_note
describes basic info about the Customer's current subscription status.current_plan
the the instance of the Customer's Plan.current_plan.name
andcurrent_plan.display_price
are useful if you want to display those things to the user.stripe_session_type
is eithercheckout
orportal
or None (if it's not showing a Stripe url at all).
- To do
PAID_PRIVATE
plans, justPOST
to the appropriatebilling:create_checkout_session
URL, which stays private because you need to pass both the slug and the pk.
- The app should automatically create a Default Free plan during installation.
- Users should have a first name, last name and email.
- Deleting a User or setting User.is_active to false will cancel any active Stripe subscriptions.
- Updating a User's first name, last name or email will update it on Stripe.
- The app will assign a stripe
customer_id
to theCustomer
the first time theUser
requests to create a subscription. Before that, theuser.customer.customer_id
will benull
. - All paid plans must have a Stripe
price_id
. - The app will automaticaly create a free_default plan the first time its needed if one doesn't exist and it will default to whatever defaults are specified in the Limits. You can modify the plan or even delete it, but there must always be 1 free_default plan and if there is not, the app will create it the next time it needs it.
- A user with a paid plan that has expired will drop to the limits set in the free_default plan.
- A user with a free private (i.e. staff) plan that has expired will drop to the limits set in the free_default plan. A user with a free private plan where there is no current_period_end set will be treated as NO expiration date on the plan and will continue to enjoy the free private plan indefinitely.
- Incoming webhooks from Stripe are treated as authoritative, so if you have someone on a private free plan, for example, and a Stripe webhook for a paid plan comes in, it will use the latter. So be sure to make changes in Stripe to match whatever changes you make to a Customer in the admin.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
py.test
- Install the Stripe CLI. It's simple on Linux, just extract the
tar.gz
file and put the file in yourPATH
. - Create the file in
~/.config/stripe/config.toml
with this format:
[default]
device_name = "<choose a name>"
test_mode_api_key = "<test environment secret or restricted key>"
test_mode_publishable_key = "<test environment publishable key>"
- Run
stripe listen --forward-to localhost:8000/billing/stripe/webhook/
- If you want to re-send an event:
stripe events resend evt_<evtid>
To create a migration, run DJANGO_SETTINGS_MODULE=billing.tests.settings django-admin makemigrations
.
From time to time, you may want to delete all Stripe test data via the dashboard. If you do that, your API keys should remain the same and won't need to be updated. But you will need to create a product and price in the Stripe dashboard and update any paid Plan
instances to reflect the new price_ids
.
There are six models in this application: Limit
, Plan
, PlanLimit
, Customer
, StripeSubscription
, and StripeEvent
. We'll focus on the first five, as StripeSubscription
is supposed to be a faithful representation of a Subscription on Stripe, and StripeEvent
is for webhook processing.
The Limit
model defines the specific features of your application that are regulated by billing.
For example, if you can send emails via your application, you might have a Limit
named Max Emails
to limit how many emails a user can send.
There are 1 or more Plans
that have a many-to-many relationship with Limit
through the PlanLimit
model.
It's through those relationships you set the limits for the various plans. For example, if you have a free plan
that can send 1 email per day and paid plan that can send 5 emails per day, each of those plans would have a M2M
relationship with the Limit
named Max Emails
. In the through model, PlanLimit
you set the value of the Limit
for that Plan
in an InlineAdmin
.
So far, so good. But what if your Plan
forgets to set one of the Limits
? What's the value of the Limit
for that Plan
? For that reason, each Limit
also defines a default
value that is used if a Plan
hasn't set
that particular Limit
.
Plans
can be one of four types: free_default
, free_private
, paid_public
, paid_private
.
- There must at all times be exactly one
free_default Plan
. This is the plan that a user defaults to when they create an account. Or if their credit card doesn't go through. It's the 'fallback' plan when no other plan has been selected. If you have a free tier, it would be sensible to configure it as this plan. This plan must be free and does not interface with Stripe. - A
free_private
plan is a plan that you can assign staff to have free access at a paid level or with some higher than normal limits. - A
paid_public
plan must have a correspondingprice_id
in Stripe. - A
paid_private
plan must have a correspondingprice_id
in Stripe.
free_default
versus Limit
defaults. A source of confusion can be what is the difference between the limit values configured in the free_default Plan
and the defaults set on the Limit
instances themselves? The Limit
defaults attach when any Plan
does not define a value for that particular Limit
. There has to be some value for a Limit
in, say, a paid Plan
even when that Plan
does not specifically define the Limit
.
The free_default Plan
is simply another Plan
that may or may not define Limits
. If it does not, then functionally there is no difference between the two since the plan will fall back to the defaults set on the Limits
. If it does specifically define values for Limits
, those values as defined become what a user falls back to when their credit card expires or they cancel their paid subscription.
Practically speaking, the real reason Limits
have defaults is because there is no simple way to enforce that a Plan
will have a many-to-many relationship with every single Limit
defined in the database. If we could enforce that easily, there would be no need for defaults on the Limit
instances themsleves.
There must always be one and only one free_default Plan
. It's created in a data migration and this condition is enforced via a database constraint.
A paid_public
plan is subscribable by all users.
A paid_private
plan is subscribable by a user only by given access to a specific link to do so.
For non-paid plans, the Customer
model is pretty straightforward. The only attributes of real significance is the linked Plan
, the customer_id
, which is generated by Stripe the first time a User subscribes, and current_period_end
.
current_period_end
is when the Customer's
Plan
will end if not renewed. After this time, the Customer
falls back to the free_default
Plan. If a Customer
's Plan
is of type free_default
, the Customer
cannot have a current_period_end
since that wouldn't make any sense, i.e., what would the Customer
fall back to.
Every User
must have a related Customer
. If the User
does not have a Customer
, it will automatically create one on save.
For paid Plans
, things are a little more complicated. Before we dive into it, first a brief primer on Stripe's subscription model.
Stripe's Subscription model can have a status
of: incomplete
, incomplete_expired
, active
, past_due
, canceled
, trialing
, or unpaid
.
incomplete
means a Customer's credit card was attached to them and a subscription was created but the card was declined. The Customer has 23 hours to fix it and if they don't, the subscription gets incomplete_expired
which is functionally the same as canceled
. I.e., no invoices will be created or paid in those states. We don't use this because we just ignore incomplete subscriptions and let them expire. A user creates a fresh one
in the Checkout if they come back.
past_due
occurs when a recurring payment fails. The payment is retried according to settings in the Stripe dashboard. Once Stripe gives up, the status changes to canceled
.
We don't use trialing
, which is useful if you want to have trials where the customer puts in their credit card before the trial. We don't use unpaid
, which is an alternative way of handling permanent recurring payment failures instead of making the status cancelled
.
There is a field on Customer called payment_state
that is a function of the Stripe subscription state.
There is a property on Customer called state
that is calculated from all the other attributes on Customer. These can be used for easy representation of Customer state on the frontend.
You can see what they are in billing.models
. This can be improved and should probably operate more like a state machine.
- Multiple paid plans. Will need to write tests to upgrade/downgrade plans and those should be their own endpoints probably.
- Grace periods for expired payments
- Trial periods
- Coupons for friends
- "When a subscription changes to past_due, your webhook script could email you about the problem so you can reach out to the customer, or the script could email the customer directly, asking them to update their payment details." Although maybe we could rely on this: https://stripe.com/docs/billing/subscriptions/overview#emails
- Interstitial pages to wait for webhooks. Imagine you do the checkout flow then it redirects you to a spinner page that polls an endpoint for where to go next. If the webhook hasn't been received redirect back for 3 seconds. If it has, redirect to the success page.
- Move away from Portal. Maybe wouldn't be terrible since all the heavy lifting happens in webhook processing. The interstitial pages would be useful here.
To publish a new release, push a git tag.