Add Stripe Payments to FastAPI: Setup Guide (2026)
Add Stripe payments to your FastAPI app with Checkout Sessions, webhook signature verification, and subscriptions. Working Python code.
You built an API in FastAPI, you want to charge for it, and now you need to add Stripe payments to your FastAPI app. Stripe’s Python SDK is excellent, but the docs jump between hosted Checkout, the Payment Element, and raw payment intents without telling you which one fits a Python backend. This guide picks the path that gives you the least code for the most coverage — hosted Checkout Sessions plus webhooks — with FastAPI code that runs.
The reason Checkout fits FastAPI so well: your backend’s job is to create a session and verify webhooks, while Stripe hosts the entire payment page. You don’t render card fields or manage client-side payment state at all. For an API-first product or a Python backend serving any frontend, that’s the right division of labor.
What You Need First
- A Stripe account with your secret key and webhook signing secret (test mode to start)
- A FastAPI app (Python 3.10+)
- A success and cancel URL in your frontend (or just placeholder routes for now)
Because Checkout is hosted by Stripe, you don’t need any Stripe frontend libraries. Your FastAPI server does all the Stripe work.
Step 1: Install the Stripe Python SDK
pip install stripe fastapi uvicorn
The stripe package is the official Python SDK. Set your secret key once at startup, ideally from an environment variable:
# main.py
import os
import stripe
from fastapi import FastAPI
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
app = FastAPI()
Step 2: Create a Checkout Session
A Checkout Session is a single hosted payment flow. Your endpoint builds it with the line items and the URLs Stripe should redirect to afterward, then returns the session URL. Define your prices in the Stripe Dashboard and reference them by price ID so amounts live in Stripe, not in your code.
# main.py
from fastapi import HTTPException
from pydantic import BaseModel
class CheckoutRequest(BaseModel):
price_id: str
user_id: str
@app.post("/api/create-checkout-session")
async def create_checkout_session(data: CheckoutRequest):
try:
session = stripe.checkout.Session.create(
mode="payment", # use "subscription" for recurring plans
line_items=[{"price": data.price_id, "quantity": 1}],
success_url="https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url="https://yourapp.com/cancel",
client_reference_id=data.user_id,
)
return {"url": session.url}
except stripe.error.StripeError as e:
raise HTTPException(status_code=400, detail=str(e))
Two things to highlight. client_reference_id attaches your own user ID to the session — when the webhook fires later, you’ll know which user paid. And {CHECKOUT_SESSION_ID} is a literal placeholder Stripe substitutes into the success URL, so your success page can look up exactly which session completed.
Your frontend just redirects the browser to the returned url. No card inputs, no client-side Stripe code.
Step 3: Verify Webhooks (the Part That Matters)
The user might close the tab before the success redirect fires. The webhook is the only reliable signal that a payment actually completed, so you grant access there — not on the redirect.
The critical detail in FastAPI: you must read the raw request body for signature verification. FastAPI gives you await request.body(), which returns the exact bytes Stripe sent. If you let FastAPI parse the JSON first, verification fails.
# main.py
from fastapi import Request
WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"]
@app.post("/api/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body() # raw bytes — required for verification
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, WEBHOOK_SECRET
)
except (ValueError, stripe.error.SignatureVerificationError):
raise HTTPException(status_code=400, detail="Invalid signature")
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
user_id = session.get("client_reference_id")
# Grant access to user_id, send a receipt, update your database
elif event["type"] == "invoice.payment_failed":
# Notify the user, log the failure
pass
return {"received": True}
Make this handler idempotent. Stripe retries webhooks and may deliver the same event more than once. Store processed event["id"] values (in Redis or your database) and skip duplicates, so a retried checkout.session.completed doesn’t grant access twice or double-send a receipt.
Test it locally with the Stripe CLI:
stripe listen --forward-to localhost:8000/api/webhooks/stripe
It prints a whsec_ signing secret — use that as STRIPE_WEBHOOK_SECRET during development.
Step 4: Subscriptions
For recurring billing, switch the session mode to "subscription" and reference a recurring price:
session = stripe.checkout.Session.create(
mode="subscription",
line_items=[{"price": data.price_id, "quantity": 1}],
success_url="https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url="https://yourapp.com/cancel",
client_reference_id=data.user_id,
)
The session creation is nearly identical — Stripe handles the recurring schedule. What changes is the webhook events you listen for. For subscriptions, handle:
if event["type"] == "checkout.session.completed":
# Initial subscription started — provision access
pass
elif event["type"] == "invoice.payment_succeeded":
# Renewal succeeded — extend access
pass
elif event["type"] == "invoice.payment_failed":
# Payment failed — start dunning, warn the user
pass
elif event["type"] == "customer.subscription.deleted":
# Subscription canceled — revoke access
pass
These four events cover the bulk of subscription lifecycle management. Store the Stripe customer and subscription IDs against your user so you can look them up later — for example, to open the Stripe Customer Portal where users manage their own plans.
Skip the Plumbing
Checkout sessions, raw-body webhook verification, idempotency tracking, subscription lifecycle events, the customer portal — and that’s before you’ve written a line of auth. Every SaaS backend needs the same scaffolding, and it’s never the part that makes your product worth paying for.
Beag gives you authentication and Stripe payments as a drop-in, so you can add both to your app in minutes instead of rebuilding webhook handlers from scratch again. If you’ve integrated Stripe before, you already know how much of this is copy-paste with sharp edges.
Common Mistakes to Avoid
Parsing the body before verifying the signature. Use await request.body() and pass the raw bytes to construct_event. FastAPI’s automatic JSON parsing breaks verification.
Skipping idempotency. Stripe resends events. Without deduplication by event["id"], retries double-process payments. Track handled event IDs.
Granting access on the redirect. The success URL can be missed. Provision access in the checkout.session.completed webhook, which fires reliably.
Hardcoding prices in Python. Define prices in the Stripe Dashboard and reference them by price_id. Changing a price then means a dashboard edit, not a redeploy.
When to Reach for the Payment Element Instead
Hosted Checkout is the right default for a FastAPI backend, but if you need a fully custom, embedded checkout inside your own UI, you’d create a PaymentIntent in FastAPI and confirm it with Stripe’s frontend Payment Element. That’s the same backend pattern — create intent, return client secret, verify webhook — with more frontend work. For most API-first products, hosted Checkout is less code and ships faster.
FAQ
Should I use Checkout Sessions or Payment Intents with FastAPI?
Use hosted Checkout Sessions for most FastAPI backends. Stripe hosts the payment page, so your Python code only creates sessions and verifies webhooks — no client-side card handling. Use Payment Intents with the frontend Payment Element only when you need a fully embedded, custom checkout UI.
Why does my FastAPI Stripe webhook fail signature verification?
Almost always because the body got parsed before verification. Read it with await request.body() to get the raw bytes Stripe signed, and pass those directly to stripe.Webhook.construct_event. Any JSON parsing first will fail.
How do I link a Stripe payment back to my user in FastAPI?
Pass your internal user ID as client_reference_id when creating the Checkout Session. It comes back on the checkout.session.completed webhook event, so you know exactly which user to provision.
Can I add authentication to my FastAPI app the same way?
You can build JWT or session auth in FastAPI yourself, or use Beag to get auth and Stripe billing pre-wired so the two work together without custom glue code.
Wrapping Up
Adding Stripe to FastAPI is mostly about two endpoints: one that creates a hosted Checkout Session, and one that verifies webhooks against the raw request body and provisions access idempotently. Let Stripe host the payment page and your Python backend stays small.
If you’d rather skip the boilerplate entirely, Beag handles auth and payments together. For a frontend-rendered approach, compare this with adding Stripe payments to React.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →