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.1.tar.gz (26.2 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.1-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

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

Hashes for django_stripe_billing-0.1.1.tar.gz
Algorithm Hash digest
SHA256 de69d7a34692afee16b80c92b1fb23dd852cd748a0e712d052586323c1fce6cf
MD5 d23466d3d44bfb7f94fb11b16c85dded
BLAKE2b-256 c0a16cf807d49d3728b0ff8d4e9579edf8a7b965be8c63941ea6cdd72dc3c31b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for django_stripe_billing-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a37c91068864de238bd7be28b31a1b7a6d8f7427b0decca7e172a92f9b14bb27
MD5 14578555986a4593a051122231ad49ed
BLAKE2b-256 475fdf75ef4d115c27419b431ebaba886b3e4c0308773900791d9ea83b1680c5

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