A small, production-minded FastAPI service that takes payments with Stripe Checkout and keeps orders correct with a verified, idempotent webhook. It shows the three things that break most Stripe integrations:
- Trusting the success page instead of the webhook. Orders change state only from signed Stripe events, never from the redirect.
- Unverified or replayed webhooks. Signatures are checked with HMAC-SHA256 (constant-time compare), secret rotation is supported, and events older than 5 minutes are rejected.
- Duplicate deliveries and out-of-order events. Each event id is stored in the same database transaction as its effect, so retries do nothing, and a late "failed" event can never overwrite a "paid" order.
No Stripe SDK: the signature scheme is implemented directly from Stripe's documentation, so the logic is readable and testable.
$ python -m pytest -q
................
16 passed in 0.44s
Verified on Python 3.12 / FastAPI 0.141 on Windows 10. The tests use an in-memory database and a fake Stripe API (httpx.MockTransport), so they need no keys and no network. They cover: order creation and the exact parameters sent to Stripe (including an idempotency key), paid / unpaid / async-failed / refunded transitions, duplicate events, bad signatures, replayed timestamps, secret rotation, tampered payloads and malformed headers.
| Method | Path | What it does |
|---|---|---|
| POST | /checkout |
Creates a pending order and a Stripe Checkout Session; returns the payment URL |
| GET | /orders/{id} |
Order status: pending, paid, failed or refunded |
| POST | /webhooks/stripe |
Verifies the signature and applies the event once |
Handled events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, charge.refunded. Anything else is acknowledged with 200 so Stripe stops retrying.
python -m venv .venv && .venv\Scripts\activate # or source .venv/bin/activate
pip install -r requirements.txt
copy .env.example .env # fill in TEST keys only
uvicorn app.main:app --reload
stripe listen --forward-to localhost:8000/webhooks/stripe # Stripe CLI, prints the whsec_ secretThen POST /checkout with {"amount_cents": 4999, "product_name": "Test"}, open the returned URL and pay with Stripe's test card 4242 4242 4242 4242.
- Secret keys stay server-side; nothing sensitive reaches the browser.
Idempotency-Keyon session creation prevents double sessions if a request is retried.- SQLite keeps the example self-contained; the same pattern works with Postgres (
INSERT ... ON CONFLICT DO NOTHING).
Marvin Palencia, founder of DeMark Studio, Miramichi, New Brunswick, Canada. Available for Stripe integrations, Python automation and FastAPI back-ends. Portfolio: marvin.demarkstudio.ca
MIT License.