Skip to main content

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 — StripeWebhookEvent with 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_KEY and STRIPE_WEBHOOK_SECRET must be non-empty when using checkout/portal or the webhook endpoint; otherwise the app raises ImproperlyConfigured. 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

user and user_payment are None in the default handlers — they are provided by your signal receiver after looking up models from customer_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)

Source distribution for django-stripe-billing 0.1.4
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)

Table of built distributions (wheels) for django-stripe-billing 0.1.4
File Interpreter ABI Platform Actions
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page