What gets generated
Webhook handler with signature verification
# app/blueprints/stripe_webhook_bp.py
@stripe_webhook_bp.route("/webhooks/stripe", methods=["POST"])
def handle_webhook():
sig_header = request.headers.get("Stripe-Signature")
payload = request.data
try:
event = stripe.Webhook.construct_event(
payload, sig_header, current_app.config["STRIPE_WEBHOOK_SECRET"]
)
except (ValueError, stripe.error.SignatureVerificationError):
return jsonify({"error": "invalid signature"}), 400
# Replay protection — dedup by event ID
if WebhookEvent.query.filter_by(stripe_event_id=event.id).first():
return jsonify({"status": "already_processed"}), 200
# Dispatch to handler
handler = WEBHOOK_HANDLERS.get(event.type)
if handler:
handler(event)
return jsonify({"status": "ok"}), 200
Idempotency on charge attempts
Every payment-creation route accepts an idempotency key. The model has a UNIQUE constraint on idempotency_key, so duplicate submissions return the original payment instead of double-charging.
Subscription lifecycle
The full lifecycle is handled: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.trial_will_end. Each event maps to a domain handler that updates the workspace's plan, sends an email, or triggers a downgrade flow.
Failed payment retries
Stripe's smart retry is enabled by default. Generated app/services/dunning_service.py handles the case where retry exhausts and the subscription enters past_due — we send an email, gate the workspace's premium features, give a 7-day grace, then downgrade.
Refund flow
Refund routes are role-gated (admin or finance). Refunds go through Stripe with reason logged. Partial refunds supported. The audit log records actor, amount, original payment ID, and refund ID.
What ships in docs/
docs/decisions/ADR-0009-payment-provider-stripe.md— why Stripe over alternatives, with the rejected ones (Paystack, Flutterwave) discussed for non-Stripe-region use casesdocs/compliance/pci-scope-statement.md— usually SAQ-A because we tokenise via Stripe Elements; the scope statement explains exactly which routes touch payment-adjacent datadocs/decisions/ADR-0014-webhook-signature-verification.md— why signature verification + replay protection + idempotency are all required (not just one)docs/runbooks/stripe-webhook-failure.md— what to do when webhook delivery fails
Environment variables generated
STRIPE_API_KEY=sk_test_... # placeholder; you replace with your key
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID_BUILDER=price_... # one per pricing tier
STRIPE_PRICE_ID_PRO=price_...
Stripe documentation references
Internal links
- Invoicing use case
- E-commerce use case
- Marketplace use case for Connect-style splits
CTA
Try it — free plan, no credit card. archiet.com.
Generate a codebase with Stripe wired, look at the webhook handler and the idempotency layer, decide if that's the shape you'd ship.