Reusable Django app for Stripe Checkout billing: webhook handling, checkout sessions, signals, and admin.
Project description
django-stripe-billing
A reusable Django app for Stripe Checkout billing. Handles webhook intake, idempotency, async Celery processing, checkout sessions, billing portal, and Django signals — so each of your SaaS projects gets billing in minutes, not days.
Features
- ✅ Stripe Checkout —
create_checkout_session()helper - ✅ Customer Portal —
create_billing_portal_session()helper - ✅ Subscription management — cancel, change plan helpers
- ✅ Webhook receiver — signature verification, livemode guard, idempotency
- ✅ Async processing — Celery task with exponential backoff on transient errors
- ✅ Django signals — connect custom logic per event type without touching the package
- ✅ Outgoing webhooks — Zapier / Make / n8n integration per event type
- ✅ Admin —
StripeWebhookEventwith status badges and full-text search - ✅ Python 3.10+, Django 4.2+
Quick Start
1. Install
pip install django-stripe-billing
# Optional: async webhook processing (recommended for production)
pip install django-stripe-billing[celery]
# or, during development:
pip install -e /path/to/django-stripe-billing
Without the [celery] extra, webhooks are processed synchronously in the request. With [celery], processing runs in a Celery task for a fast HTTP response and automatic retries.
2. Add to INSTALLED_APPS
INSTALLED_APPS = [
...
'django_stripe_billing',
]
3. Configure
# settings.py
STRIPE_BILLING = {
'STRIPE_SECRET_KEY': env('STRIPE_SECRET_KEY'),
'STRIPE_WEBHOOK_SECRET': env('STRIPE_WEBHOOK_SECRET'),
'STRIPE_MODE': 'live', # 'live' or 'test'
'ENVIRONMENT': env('ENVIRONMENT', default='production'),
'APP_TITLE': 'My SaaS',
'APP_DOMAIN': 'https://myapp.com',
'SUPPORT_EMAIL': 'support@myapp.com',
'EMAIL_SUBJECT_PREFIX': '[My SaaS]',
# Optional: outgoing webhooks for Zapier / Make / n8n
'OUTGOING_WEBHOOK_URLS': {
'invoice.payment_succeeded': env('WEBHOOK_PAYMENT_SUCCEEDED', default=''),
'invoice.payment_failed': env('WEBHOOK_PAYMENT_FAILED', default=''),
'customer.subscription.deleted': env('WEBHOOK_SUBSCRIPTION_DELETED', default=''),
},
}
Zero-config migration: If you already have
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET, etc. as top-level settings, the package reads them automatically — no changes required.Required for production:
STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETmust be non-empty when using checkout/portal or the webhook endpoint; otherwise the app raisesImproperlyConfigured. See INTEGRATION.md for adding the package to existing projects (e.g. mobileapi_dev, signupcheck, tpscheck_uk).
4. Add URL
# urls.py
from django.urls import path, include
urlpatterns = [
path('webhooks/', include('django_stripe_billing.urls')),
# Exposes: POST /webhooks/stripe/
]
5. Run migrations
python manage.py migrate
Creating a Checkout Session
from django_stripe_billing.checkout import create_checkout_session
session = create_checkout_session(
price_id='price_1Nxyz...',
success_url=request.build_absolute_uri('/payment-success/?session_id={CHECKOUT_SESSION_ID}'),
cancel_url=request.build_absolute_uri('/billing/'),
user=request.user,
metadata={'plan_id': 'pro_monthly'},
subscription_metadata={'plan_id': 'pro_monthly', 'new_signup': 'true'},
)
return redirect(session.url, code=303)
Opening the Customer Portal
from django_stripe_billing.checkout import create_billing_portal_session
session = create_billing_portal_session(
customer_id=request.user.userprofile.stripe_customer_id,
return_url=request.build_absolute_uri('/billing/'),
)
return redirect(session.url, code=303)
Connecting Business Logic via Signals
Each Stripe event fires a Django signal. Connect your app-specific logic here:
# myapp/signals.py
from django.dispatch import receiver
from django_stripe_billing.signals import (
payment_succeeded,
payment_failed,
subscription_deleted,
trial_will_end,
)
from myapp.models import UserProfile
@receiver(payment_succeeded)
def on_payment_succeeded(sender, invoice, **kwargs):
customer_id = invoice.get('customer')
try:
profile = UserProfile.objects.get(stripe_customer_id=customer_id)
except UserProfile.DoesNotExist:
return
# Update plan, reset credits, etc.
profile.plan = _resolve_plan(invoice)
profile.save()
@receiver(payment_failed)
def on_payment_failed(sender, invoice, attempt_count, **kwargs):
customer_id = invoice.get('customer')
try:
profile = UserProfile.objects.get(stripe_customer_id=customer_id)
except UserProfile.DoesNotExist:
return
if attempt_count == 1:
send_payment_failed_email_1(profile.user)
elif attempt_count == 2:
send_payment_failed_email_2(profile.user)
@receiver(subscription_deleted)
def on_subscription_deleted(sender, subscription, **kwargs):
customer_id = subscription.get('customer')
try:
profile = UserProfile.objects.get(stripe_customer_id=customer_id)
except UserProfile.DoesNotExist:
return
old_plan = profile.plan
profile.plan = 'free'
profile.save()
send_subscription_cancelled_email(profile.user, old_plan)
Register your signal receivers in apps.py:
# myapp/apps.py
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
import myapp.signals # noqa: F401
Available Signals
| Signal | kwargs |
|---|---|
payment_succeeded |
invoice, user, user_payment |
payment_failed |
invoice, user, attempt_count |
checkout_completed |
session, user_payment |
subscription_updated |
subscription, user |
subscription_deleted |
subscription, user, old_plan |
charge_refunded |
charge, user, amount_display |
trial_will_end |
subscription, user, trial_end_date |
subscription_paused |
subscription, user |
payment_method_attached |
payment_method, user |
useranduser_paymentareNonein the default handlers — they are provided by your signal receiver after looking up models fromcustomer_id.
Custom Webhook Handlers
Override or add handlers to the registry:
from django_stripe_billing.webhooks import registry
@registry.handler('invoice.payment_succeeded')
def my_custom_handler(data_object):
# This replaces the built-in handler for this event type
...
# Or register a handler for an event type not built-in:
@registry.handler('customer.created')
def handle_customer_created(data_object):
...
Subscription Helpers
from django_stripe_billing.checkout import cancel_subscription, change_subscription_price
# Cancel at end of current period (default)
cancel_subscription(customer_id='cus_...')
# Cancel immediately
cancel_subscription(customer_id='cus_...', at_period_end=False)
# Change plan
change_subscription_price(customer_id='cus_...', new_price_id='price_new...')
Architecture
django_stripe_billing/
├── conf.py # Settings helper (STRIPE_BILLING dict + legacy fallback)
├── models.py # StripeWebhookEvent (idempotency + audit trail)
├── signals.py # Django signals, one per Stripe event type
├── webhooks.py # HandlerRegistry + built-in handlers
├── checkout.py # create_checkout_session(), create_billing_portal_session(), etc.
├── utils.py # number_to_currency(), fire_outgoing_webhook()
├── tasks.py # Celery task: async processing + exponential backoff
├── views.py # HTTP webhook receiver (signature verify, livemode guard)
├── admin.py # StripeWebhookEvent admin
└── urls.py # path('stripe/', stripe_webhook)
How a webhook flows through the package:
Stripe → POST /webhooks/stripe/
↓ views.py — verify signature, guard livemode, persist StripeWebhookEvent
↓ tasks.py — Celery task picks up event, calls registry.dispatch()
↓ webhooks.py — built-in handler fires Django signal + outgoing webhook
↓ your app — signal receiver implements business logic
Integration into existing projects
If your project already uses Stripe with top-level settings (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_MODE, etc.), see INTEGRATION.md for step-by-step integration with minimal change, including projects that already have a custom webhook handler or Celery.
Stripe best practices (built in)
- Webhook signature verification — All requests are verified with
STRIPE_WEBHOOK_SECRET; invalid payloads return 400. - Livemode guard — Test events are ignored in production and live events in non-production, so keys and environment stay consistent.
- Idempotency — Events are stored by
stripe_event_id; duplicates return 200 without reprocessing. - Never rely only on client-side success — Use the webhook and signals to update plans and fulfil access; see Stripe docs.
Development
git clone https://github.com/visian-systems/django-stripe-billing
cd django-stripe-billing
pip install -e ".[dev]"
# With Celery for local webhook testing:
pip install -e ".[dev,celery]"
pytest
Running tests
From the project root:
pytest
For verbose output and short tracebacks:
pytest -v --tb=short
Tests use an in-memory SQLite database and mock Stripe/HTTP; no real API keys are required. See TESTING.md for layout, options, and TDD notes.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_stripe_billing-0.1.1.tar.gz.
File metadata
- Download URL: django_stripe_billing-0.1.1.tar.gz
- Upload date:
- Size: 26.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de69d7a34692afee16b80c92b1fb23dd852cd748a0e712d052586323c1fce6cf
|
|
| MD5 |
d23466d3d44bfb7f94fb11b16c85dded
|
|
| BLAKE2b-256 |
c0a16cf807d49d3728b0ff8d4e9579edf8a7b965be8c63941ea6cdd72dc3c31b
|
File details
Details for the file django_stripe_billing-0.1.1-py3-none-any.whl.
File metadata
- Download URL: django_stripe_billing-0.1.1-py3-none-any.whl
- Upload date:
- Size: 22.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a37c91068864de238bd7be28b31a1b7a6d8f7427b0decca7e172a92f9b14bb27
|
|
| MD5 |
14578555986a4593a051122231ad49ed
|
|
| BLAKE2b-256 |
475fdf75ef4d115c27419b431ebaba886b3e4c0308773900791d9ea83b1680c5
|