It looks identical but it is not. Stripe signs the exact bytes it sent. JSON.stringify(JSON.parse(raw)) changes whitespace and sometimes key order, so the signature no longer matches.
Put it back to the raw body:
const raw = await req.text();
const event = stripe.webhooks.constructEvent(raw, req.headers.get("stripe-signature")!, process.env.STRIPE_WEBHOOK_SECRET!);Then add a comment above it, something like // raw body on purpose: Stripe signs the exact bytes. Agents read comments, and it stops the next "cleanup" from undoing it.