Webhooks
Stripe notifies your app of the real payment outcome by calling
/cfg/payments/webhook/stripe/. This is where a pending/processing payment
actually becomes succeeded, failed, or refunded.
The endpoint
POST /cfg/payments/webhook/stripe/ — served by StripeWebhookView.
- Not JWT-authenticated. Authentication is the Stripe signature:
authentication_classes = [],permission_classes = [AllowAny], and the view is CSRF-exempt. - The raw request body is never re-parsed (
parser_classes = []) — signature verification must run against the exact bytes Stripe sent. - Excluded from the generated OpenAPI client (
@extend_schema(exclude=True)) — it is not a client-callable API.
Signature verification
The view reads the raw request.body and the Stripe-Signature header and calls
the provider’s verify_and_parse_webhook. On a bad or absent signature it returns
400 {"detail": "Invalid signature."}.
Verification iterates over every configured webhook secret (the
comma-separated stripe_webhook_secret), so a
rotation
never drops a delivery.
Idempotent processing
On a valid signature the view enqueues the RQ task
django_cfg.apps.payments.tasks.process_webhook_event on the "default" queue
and immediately returns 200 {"received": True}.
RQ optional, with a synchronous fallback. If RQ/Redis is unavailable, the
view falls back to processing the event inline in the request. Either path
ends in the same idempotent PaymentService.handle_webhook.
handle_webhook is idempotent on the Stripe event id:
get_or_createaPaymentEventon the uniqueevent_id. A replayed event that is already processed is a no-op.- Link the related
Paymentand apply the event:payment.succeeded→ statussucceeded, run fulfillment, emitpayment_succeeded.payment.failed→ statusfailed, emitpayment_failed.refund→ setamount_refunded, statusrefunded.
- On success, stamp
processed_at. On a handler exception, record the message on the event’serrorfield and re-raise so the delivery can be retried.
Subscription-lifecycle events are parsed and acknowledged (the event is marked
processed and a 200 returned) but perform no DB action today — subscription
fulfillment is a later phase.
Local development
Use the Stripe CLI to forward live test webhooks to your dev server:
python manage.py payments_listenThis wrapper fetches the CLI signing secret (stripe listen --print-secret),
prints it as STRIPE__WEBHOOK_SECRET=whsec_... for you to set, then foregrounds
stripe listen --forward-to <your webhook url>. See
Management Commands.
Replaying a stored event
Because the raw payload is stored on every PaymentEvent, an event can be
re-processed after you fix a handler bug — no need to ask Stripe to resend.
# no-op if already processed
python manage.py payments_replay_webhook evt_1ABC...
# re-apply even if already processed (clears processed_at/error first)
python manage.py payments_replay_webhook evt_1ABC... --forceUnder the hood this is WebhookService.replay(event_id=..., force=...), which
re-parses the stored payload through the provider’s parser and runs the same
idempotent handle_webhook. It raises if no stored event matches the id.