Skip to content

Stripe billing setup

InvoicePDFs uses Stripe Checkout to start a subscription and the Stripe Customer Portal for all ongoing management (upgrade/downgrade, payment method, invoices, cancellation) — so the app carries the least possible billing burden. The backend (checkout / portal / webhook) lives in app/api/v1/billing.py and app/billing/stripe_service.py; the dashboard page is /ui/billing.

One-time setup

  1. Install the Stripe extra (it's optional so the app runs without it):

    uv sync --extra stripe        # or: pip install "invoicepdfs-app[stripe]"
    

  2. In the Stripe dashboard

  3. Create a Product + recurring Price for each paid tier (e.g. Pro, Business).
  4. Configure the Customer Portal (Billing → Customer portal): enable "update subscriptions" and pick the products customers may switch between.
  5. Add a webhook endpoint pointing at https://<your-host>/api/v1/billing/webhook, subscribed to: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed. Copy its signing secret.

  6. Environment variables

    STRIPE_SECRET_KEY=sk_live_...
    STRIPE_WEBHOOK_SECRET=whsec_...
    BILLING_SUCCESS_URL=https://<your-host>/ui/billing   # after checkout/portal
    BILLING_CANCEL_URL=https://<your-host>/ui/billing
    

  7. Wire plans to prices. Each internal Plan row that should be purchasable needs its stripe_price_id set to the matching Stripe Price id. Only plans with a stripe_price_id appear on the billing page and in GET /api/v1/billing/plans.

Flow

  • New customer/ui/billing shows "Choose a plan" → Subscribe creates a Checkout session (POST /api/v1/billing/checkout-session) and redirects to Stripe.
  • Existing subscriberManage billing opens the Customer Portal (POST /api/v1/billing/portal-session).
  • Webhooks keep Account.subscription_status and Account.plan_id in sync; quota enforcement (enforce_monthly_quota) then uses the resolved plan's monthly_render_quota.

When STRIPE_SECRET_KEY is unset the billing endpoints return 501 not_configured and the UI shows a "billing isn't configured" note — nothing else breaks.