Skip to main content

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 Checkoutcreate_checkout_session() helper
  • Customer Portalcreate_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
  • AdminStripeWebhookEvent with 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_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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_stripe_billing-0.1.0.tar.gz (26.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_stripe_billing-0.1.0-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

File details

Details for the file django_stripe_billing-0.1.0.tar.gz.

File metadata

  • Download URL: django_stripe_billing-0.1.0.tar.gz
  • Upload date:
  • Size: 26.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.1

File hashes

Hashes for django_stripe_billing-0.1.0.tar.gz
Algorithm Hash digest
SHA256 005e0817e98e39b6384ca1705e26c2c9814a1cde45438f386e8360c6b269bf99
MD5 56e2be1829dfe623b3d64d9bc919c787
BLAKE2b-256 a54ab13db5c3980efd325ad86d657894b7f7fb9781a03e0bef4feab5f687099b

See more details on using hashes here.

File details

Details for the file django_stripe_billing-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_stripe_billing-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9194d8e4c16d409d16280684c5fb91e838f869a7f05212f46705651f18f9a02e
MD5 026f5ced9c3331145b94566576ef4740
BLAKE2b-256 768f59a919a49755b1537b864bbed948f47a7057382660b33bfd7e4fd1ad4adc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page