Nobody has started this yet — be first.
Business impact
Every subscription activation, plan change, cancellation, and payment-failure notice from Stripe arrives through this one endpoint. Right now, POST /webhooks/stripe rejects every genuinely valid Stripe-signed request as 400 invalid_signature, because the code verifies the signature against a re-serialized copy of the body instead of the exact bytes Stripe sent and signed. In practice: a customer pays, Stripe fires checkout.session.completed, the webhook 400s, Stripe retries on its own schedule and eventually gives up, and the tenant's subscription is never marked active -- the customer paid and still sees "upgrade required." Nothing about this fails loudly on our side, so the realistic way this gets noticed is a support ticket from a paying customer who cannot access what they paid for.
Problem
stripe_webhook verifies the Stripe signature against json.dumps(request.get_json()) instead of the untouched raw request bytes Stripe actually signed.
Current behavior
stripe_webhook parses the body with request.get_json(silent=True) first, then re-serializes the resulting dict with json.dumps(...) to build raw_body before calling verify_and_parse -- but Stripe's signature was computed over its own compact, minified JSON formatting, which json.dumps's default key order and whitespace do not reproduce, so verification fails for every real payload.
Expected behavior
POST /webhooks/stripe verifies the signature against request.get_data() -- the untouched raw bytes of the request body -- and only parses JSON after that verification succeeds.
Steps to reproduce
Why this matters
This is the single most common real-world Stripe (or any HMAC-signed webhook) integration bug, and it is exactly why request.get_data() has to be the first thing touched in the handler, before any JSON parsing of any kind, even parsing that looks harmless. The fix is not "parse JSON differently" -- it is "verify against the bytes you actually received, and only parse afterward, from the same untouched bytes."
Suggested approach
Look at how stripe_webhook currently obtains raw_body, and compare it to app/billing/stripe_service.py's own docstring describing the rule. request.get_data() is available on the request object already imported into this file.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_07_webhook_signature_raw_body.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/webhook-signature-raw-bodyFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/webhook-signature-raw-bodySubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inacmeglobexmake seed
*.localhost subdomains resolve to 127.0.0.1 out of the box in curl and every modern browser -- no /etc/hosts editing needed:
curl -s -X POST http://acme.localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
--cookie-jar /tmp/acme.cookies --cookie /tmp/acme.cookies \
-d '{"email": "owner@acme.test", "password": "owner-acme"}'
curl -s http://acme.localhost:5000/api/tenant/settings --cookie /tmp/acme.cookies
For tooling that cannot fake subdomains (some HTTP clients, CI), send X-Tenant-Slug instead -- honored only when TRUST_TENANT_HEADER=true (the Docker dev setup enables it; production does not):
curl -s http://localhost:5000/api/tenant/settings -H "X-Tenant-Slug: acme"
You do not strictly need Docker to work the tickets, but -- unlike a project that can fake its backing services -- this one genuinely needs a real Postgres and Redis, because the whole point is proving isolation against the real database, not a mock. Without Docker: cp .env.example .env, point DATABASE_URL/ADMIN_DATABASE_URL/TEST_DATABASE_URL/TEST_ADMIN_DATABASE_URL/REDIS_URL at a local Postgres/Redis (docker/init-db.sql documents the exact roles/databases the app expects -- notably the saas_admin/BYPASSRLS role migrations need and the plain saas role every request uses), then:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/flask db upgrade
.venv/bin/pytest practice_tickets/tests/test_ticket_07_webhook_signature_raw_body.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
./practice_tickets/run_tickets.sh 07 12 13 # just a few, by number
All 15 tests fail at the start; as you fix each ticket in app/ (or, for TICKET-12, in migrations/versions/0002_row_level_security.py, plus re-applying it), its test -- and only its test -- should flip to passing. make test runs the project's own main suite; several of its tests (RBAC, tenant-spoofing, and billing-webhook tests among them) fail at the start for the same underlying reasons as specific tickets -- fixing those tickets fixes those failures too, not a separate task.
Level 1
Fix a bug
Read existing behaviour, correct it.