Stripe Billing
Pathrule4 Rules • 4 Memories • 1 Skill
Rules, memories, and a review skill for adding Stripe billing to a product. Pre-scoped to your Stripe API routes and serverless functions so your AI assistant verifies webhook signatures, keeps handlers idempotent, and picks the correct payment API.
Suggested path map
Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.
/ workspace root
Send an idempotency key on every Stripe write request
Keep Stripe secrets server-side and use restricted keys
stripe-billing-review
api/
stripe/
Verify the webhook signature against the raw request body
Make webhook handlers idempotent and return 2xx fast
Webhooks grant entitlements, not the success redirect
DB mirrors Stripe; one checker reads active/trialing
Checkout Sessions vs PaymentIntents
Pin the API version on the webhook endpoint
Rules
4Verify the webhook signature against the raw request body/api/stripehighstrictReject any webhook whose Stripe-Signature does not verify against the unparsed body.
| 1 | Verify the `Stripe-Signature` header with the endpoint's signing secret using the official SDK (`stripe.webhooks.constructEvent`, or `constructEventAsync` on edge/Workers runtimes) before reading any field of the event. |
| 2 | |
| 3 | - Pass the EXACT raw bytes of the request body. Any re-serialization, key reordering, whitespace change, or Unicode normalization makes verification fail. In Express, mount `express.raw({ type: 'application/json' })` on the webhook route only, never a global `express.json()` ahead of it. In Next.js route handlers read `await req.text()`; in Supabase Edge Functions read `await req.text()` and never `await req.json()` first. |
| 4 | - Read the signing secret from server config, never from the request. |
| 5 | - Return `400` on verification failure and do not run any handler logic. |
| 6 | - During a signing-secret rotation Stripe signs with both the old and new secret over an overlap window, so deploy the new secret before retiring the old one. |
Make webhook handlers idempotent and return 2xx fast/api/stripehighadvisoryThe same event can arrive more than once; dedupe it and answer before heavy work.
| 1 | Stripe delivers events at least once, so the same event (and occasionally two distinct Event objects for one change) can arrive more than once. |
| 2 | |
| 3 | - Dedupe on `event.id`: record processed IDs and skip a repeat, or make the side effect idempotent (upsert by a stable key such as the subscription or invoice ID). For the rare two-Events case, key on `event.type` plus `data.object.id`. |
| 4 | - Return a `2xx` BEFORE any logic that could time out. Offload slow work (emails, accounting sync, provisioning) to a queue or background job and acknowledge the delivery immediately, otherwise Stripe retries a delivery you already handled. |
| 5 | - A non-2xx response triggers Stripe's retry schedule, so only return 4xx/5xx when you actually want a retry. |
Send an idempotency key on every Stripe write request/roothighadvisoryPass a stable Idempotency-Key on POST/create/update calls so retries cannot double-charge.
| 1 | This is a SEPARATE concern from webhook deduplication. It protects outbound calls TO Stripe, not events you receive. |
| 2 | |
| 3 | - Every Stripe POST (create/update of PaymentIntents, Subscriptions, Refunds, etc.) accepts an idempotency key. Pass one whenever a network error or your own retry could resend the request: `stripe.paymentIntents.create(params, { idempotencyKey })`. |
| 4 | - Derive the key from your own stable identity (for example `order:<id>` or a UUID you persist with the order), not a fresh random value per attempt, so a retry reuses the same key. |
| 5 | - Stripe remembers a key's result for 24 hours; reusing it returns the original response instead of performing the operation twice. |
| 6 | - The SDK auto-retries some transient failures with its own idempotency handling, but supply your own key for any operation you initiate or retry at the application layer. |
Keep Stripe secrets server-side and use restricted keys/roothighstrictOnly the publishable key is client-safe; secret and signing secrets live in server env.
| 1 | The secret key (`sk_...`) and the webhook signing secret (`whsec_...`) are server-side only. Only the publishable key (`pk_...`) may ship to the client. |
| 2 | |
| 3 | - Store both secrets in server/function environment variables, never in client bundles, repos, or logs. |
| 4 | - For server-side code use a Restricted API Key (RAK) scoped to exactly the resources the integration needs, so a leaked key limits blast radius. Reserve the full secret key for operations a RAK cannot perform. |
| 5 | - In edge or serverless functions, authenticate the caller (or verify the webhook signature for webhook routes) before any billing action; an unauthenticated function with a secret key is a charge-anyone hole. |
Memories
4Webhooks grant entitlements, not the success redirect/api/stripeTreat the client redirect as a hint; provision access only from verified webhook events.
| 1 | The redirect to your success URL is only a suggestion that the user came back; it can be skipped, replayed, or forged. The verified webhook is the source of truth for what actually happened. |
| 2 | |
| 3 | - Grant or revoke access from events such as `checkout.session.completed`, `customer.subscription.created/updated/deleted`, and `invoice.paid`, not from the success-page query params. |
| 4 | - On the success page, show a neutral "finishing up" state and read entitlement from your own database (which the webhook updates), rather than unlocking features directly from the redirect. |
DB mirrors Stripe; one checker reads active/trialing/api/stripeKeep a local subscription mirror updated by webhooks; gate access through a single status function.
| 1 | Do not call the Stripe API on every request to check billing; that adds latency and rate-limit risk. Keep a local mirror and treat Stripe as the source of truth that flows in through webhooks. |
| 2 | |
| 3 | - Maintain a `subscriptions` table mirroring Stripe: subscription ID, customer ID, `status`, price ID, `current_period_end`, and `cancel_at_period_end`. Update it on every relevant webhook. |
| 4 | - Gate access through ONE shared function so the rule lives in a single place. Only `active` and `trialing` grant access; `past_due`, `canceled`, `incomplete`, `unpaid`, etc. do not. |
| 5 | - Store the Stripe `customer` ID on the user as soon as it exists so later events map back to the right account. |
Checkout Sessions vs PaymentIntents/api/stripeDefault to Checkout (including subscriptions); use PaymentIntents only for fully custom flows.
| 1 | Stripe recommends the Checkout Sessions API for most integrations, and it remains the default choice in 2026. |
| 2 | |
| 3 | - Use Checkout with `mode: 'subscription'` for recurring billing: it handles SCA/3DS, tax, currency conversion, trials, Smart Retries, dunning, and proration with far less code. |
| 4 | - Use Checkout with `mode: 'payment'` for standard one-time payments. |
| 5 | - Reach for PaymentIntents with the Payment Element only when you must own every part of the in-app flow and are prepared to rebuild discount, tax, and currency logic yourself. |
| 6 | - Either way, drive entitlements from webhook events (see the entitlements memory), not the API response on the redirect. |
Pin the API version on the webhook endpoint/api/stripeSet an explicit API version so event shapes and SDK deserialization stay stable.
| 1 | Stripe pins your account to an API version on first request and releases new versions monthly (non-breaking) with breaking releases roughly twice a year. Event payload shapes follow the version the webhook endpoint is configured with. |
| 2 | |
| 3 | - Set an explicit API version on the endpoint and in the SDK client (`new Stripe(key, { apiVersion: '...' })`) rather than relying on the account default, so an account-level upgrade does not silently change the JSON you parse. |
| 4 | - With statically typed SDKs (Go, Java, .NET), the webhook endpoint's API version must match the version the SDK was generated for, or event deserialization fails. |
| 5 | - To upgrade: send the new `Stripe-Version` from staging, diff the responses and events, fix your code, then move the webhook endpoint and pinned version forward. |
Skills
1stripe-billing-review/rootChecklist for reviewing a Stripe billing change before merge.
| 1 | --- |
| 2 | name: stripe-billing-review |
| 3 | description: Review a Stripe billing change for security and correctness before merging. |
| 4 | --- |
| 5 | |
| 6 | # Stripe billing review |
| 7 | |
| 8 | ## Webhooks |
| 9 | - [ ] Signature verified with constructEvent/constructEventAsync against the RAW body before any logic |
| 10 | - [ ] No global JSON body parser runs ahead of the webhook route (Express raw, Next req.text(), Edge req.text()) |
| 11 | - [ ] Returns 400 on verification failure with no side effects |
| 12 | - [ ] Handler is idempotent: dedupes on event.id or upserts by a stable key |
| 13 | - [ ] Returns 2xx fast; slow work is offloaded to a queue/background job |
| 14 | - [ ] Webhook endpoint has an explicit pinned API version |
| 15 | |
| 16 | ## Entitlements and state |
| 17 | - [ ] Access is granted from webhook events, not the success-page redirect |
| 18 | - [ ] Local subscription mirror is updated by webhooks; one shared function gates access on active/trialing only |
| 19 | - [ ] Stripe customer ID is stored on the user |
| 20 | |
| 21 | ## Outbound calls and keys |
| 22 | - [ ] POST/create/update calls send a stable Idempotency-Key for safe retries |
| 23 | - [ ] Secret key and webhook signing secret are server-side only; only pk_ ships to the client |
| 24 | - [ ] Server uses a restricted API key scoped to what it needs |
| 25 | |
| 26 | ## Money |
| 27 | - [ ] Amounts are integers in the currency's minor unit (and account for zero-decimal currencies); no float math |
| 28 | - [ ] Correct API chosen: Checkout for standard/subscription, PaymentIntents only for fully custom flows |
Why this pattern
Billing code trusts unverified webhooks or double-applies retried events, causing security holes and wrong charges.
Built for teams adding subscriptions or payments with Stripe.
Keeps your assistant from:
- Acting on a webhook before verifying its signature
- Non-idempotent handlers that double-process retried events
- Driving entitlements from the client redirect instead of webhooks
- License
- Apache-2.0
- Version
- 1.0.0
- Updated
- 2026-06-09