Payments
Built-in payments engine for django-cfg (django_cfg.apps.payments): a
provider-agnostic, one-time checkout flow (Stripe-first) with idempotent webhook
ingestion, refunds, and reconciliation. Everything is configured from a single
PaymentsConfig field on your DjangoConfig.
The Stripe SDK is an optional extra. Install it with pip install django-cfg[payments].
Design at a glance
The engine is built around three deliberate seams so it stays decoupled from your domain models:
- Amounts are integer minor units. Every amount — request, storage, refund —
is an integer in the currency’s smallest unit (e.g. cents). No floats, no
Decimaldrift. - The provider is the only vendor-aware layer. The
stripeSDK is imported in exactly one place (providers/stripe.py). Adding PayPal/crypto later means writing one provider class — nothing else changes. - Ownership and “what was paid for” are seams, not FKs. A payment belongs to
an owner (the billable party — a user by default, or your org model). What
it was paid for is a loose
(reference_kind, reference_id)string pair, so the engine carries zero knowledge of your order/plan models.
Subscriptions are a later phase. The provider layer already implements the Stripe subscription methods, but the service layer today only fulfills one-time payments — subscription webhook events are parsed and acknowledged, not acted on.
Quick start
1. Install the extra
pip install django-cfg[payments]2. Enable it in your config
from django_cfg import DjangoConfig, PaymentsConfig
class MyConfig(DjangoConfig):
payments = PaymentsConfig(
# owner defaults to AUTH_USER_MODEL — set this only for org-level billing
# owner_model="organizations.Organization",
# owner_resolver="apps.billing.owner.resolve",
fulfillment_hook="apps.orders.hooks.activate_order",
)Enabling payments installs the cfg_payments app, its migrations, and mounts
its URLs at /cfg/payments/.
3. Provide Stripe credentials
Secrets fall back to the environment, so hosts that already follow the
STRIPE__* convention need no explicit values in config:
STRIPE__SECRET_KEY=sk_test_...
STRIPE__PUBLISHABLE_KEY=pk_test_...
STRIPE__WEBHOOK_SECRET=whsec_...4. Run migrations
python manage.py migrate5. Verify the wiring
python manage.py payments_doctorpayments_doctor checks that the provider resolves, credentials are present, and
no payments are stuck or events unprocessed.
How a payment flows
Amounts below are all in minor units — 1000 = $10.00 USD.
- Checkout. Your frontend
POSTs to/cfg/payments/checkout/with an amount. The engine creates aPaymentrow (statuspending), asks the provider to create a Stripe PaymentIntent, stores its id, and returns theclient_secretfor the client to confirm. - Confirmation. The client confirms the PaymentIntent with Stripe.js using
the returned
client_secretandpublishable_key. - Webhook. Stripe calls
/cfg/payments/webhook/stripe/. The engine verifies the signature, records aPaymentEvent(idempotent on the Stripe event id), flips thePaymenttosucceeded, runs yourfulfillment_hook, and emits thepayment_succeededsignal. - Reconciliation (safety net). If a webhook is ever missed, a scheduled job
polls the provider for payments stuck in
processingand applies the real status.
Where to go next
PaymentsConfig model — provider registry, currency, ownership and fulfillment seams, Stripe credentials.Data ModelPayment and PaymentEvent — fields, status lifecycle, minor-units, idempotency.Checkout & APIThe REST API, owner resolution, and confirming a PaymentIntent from the frontend.WebhooksSignature verification, idempotent processing, and replaying a stored event.Refunds & ReconciliationFull/partial refunds and the safety net for missed webhooks.ProvidersThe PaymentProvider interface, the Stripe implementation, and custom providers.Management CommandsThe five payments_* commands: doctor, listen, reconcile, refund, replay.Signals & Taskspayment_succeeded / payment_failed signals and the RQ background tasks.