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+ (host project must use Python 3.12+ for Django 6.x)
- ✅ Django 4.2, 5.x, and 6.x
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
Metadata
Release files for django-stripe-billing 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_stripe_billing-0.1.4.tar.gz | 27.6 kB | Without provenance | Details for django_stripe_billing-0.1.4.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_stripe_billing-0.1.4-py3-none-any.whl | Python 3 | none | any | Details for django_stripe_billing-0.1.4-py3-none-any.whl |
Total release size: 51.2 kB
Release files / django_stripe_billing-0.1.4.tar.gz
| Download URL | django_stripe_billing-0.1.4.tar.gz |
|---|---|
| Size | 27.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7e9263dcfebf2018ce184018b4c5241c382ccbaa5b5148f201b3e48e8be71206
|
|
BLAKE2b-256 checksum How to use checksums |
5c22b879f420e33f99a1d0d7e40a290f321773005bc8d76e4f55b32da146e821
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / django_stripe_billing-0.1.4-py3-none-any.whl
| Download URL | django_stripe_billing-0.1.4-py3-none-any.whl |
|---|---|
| Size | 23.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8fd63464a7515956bfcb01452b98f597aa1ceab75aa2e5886742267c11628701
|
|
BLAKE2b-256 checksum How to use checksums |
cfd5bd6ec75e78bfd89086d497880214e220381a40da8054873c66c10e7216a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Security for django-stripe-billing 0.1.4
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.