agentleFS
SkillMedevSkillMedev/full-stack-web-dev/skills/stripe-integration/SKILL.md

stripe-integration

Skill1 starsChanged 38 days agoMITView on GitHub

Implements Stripe payments, subscriptions, and webhooks so billing state stays correct - Checkout Sessions, signature-verified idempotent webhook handlers, dunning, and SCA handling. Use when someone asks "add Stripe subscriptions to my app", "why is my webhook signature verification failing", "my database says subscribed but Stripe says canceled", "how do I avoid double-charging on retries", or "how should I handle failed payments". Do NOT use for choosing price points, tiers, or packaging - use saas-pricing instead; for general-purpose webhook receiver hardening beyond Stripe, use webhook-receiver-hardener; for idempotency patterns outside payments, use idempotency-enforcer.

13 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

10 rules. 0 are common, 9 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 12 edited versions we read keep these.

  • 9

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Create the Checkout Session server-side.

    line 27
  2. Distinctive:

    Treat the success page as UI only ("thanks, your access is activating").

    line 29
  3. Distinctive:

    Every item is mandatory; each one missing is a specific production incident.

    line 45
  4. Very specific:

    Run stripe listen --forward-to localhost:PORT/webhook and use test-mode keys.

    line 69
  5. Distinctive:

    Do not gate access on the success redirect - it is spoofable and unreliable.

    line 108
  6. Distinctive:

    Do not parse the webhook body before signature verification - verification fails on mutated bytes and you will "fix" it by disabling …

    line 109
  7. Distinctive:

    Do not process an event ID twice - at-least-once delivery guarantees you will eventually receive duplicates.

    line 110
  8. Distinctive:

    Do not trust price IDs or amounts from the client - a tampered request buys the enterprise plan at the hobby price.

    line 111
  9. Distinctive:

    Do not auto-retry card declines or create more than one Stripe customer per user.

    line 112
  10. Distinctive:

    Do not do slow work inline in the handler - return 2xx fast, queue the rest.

    line 113

“Common” is measured: kept by at least 6 in 10 of the 12 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Stripe Expert

Billing bugs are the most expensive class of bug: an unverified webhook is an open door to fake "payment succeeded" events, a non-idempotent handler double-provisions on Stripe's at-least-once delivery, and state inferred from a client redirect grants free access to anyone who can type a URL. This skill wires Stripe so money moves correctly and your database never disagrees with Stripe about who has paid.

Core principle

Stripe is the source of truth for billing state. Your database is a mirror, updated only by verified webhooks. Never grant access from a client-side success callback or redirect - the redirect can be spoofed, dropped, or replayed; the webhook cannot.

Operating procedure

Step 1: Gather inputs

  1. Billing model - one-time purchase, flat subscription, per-seat, or usage-based? Default for SaaS: flat subscription via Checkout.
  2. Existing user model - is there a stripe_customer_id column? One Stripe customer per user, created lazily on first purchase.
  3. Access-gating point - the exact code path that checks "is this user paid?". It must read your mirrored subscription status, nowhere else.
  4. Dunning policy - how many days of grace on past_due before access is cut? Default: 7 days with email notices.

Step 2: Build the purchase flow (Checkout)

  1. Create the Checkout Session server-side. Never trust amounts or price IDs from the client - accept only an internal plan identifier and resolve the Stripe price ID server-side.
  2. Redirect the customer to the hosted page.
  3. Treat the success page as UI only ("thanks, your access is activating"). Entitlement flips when the webhook lands, typically within seconds.
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: priceId, quantity: 1 }],   // resolved server-side
  success_url: `${base}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${base}/pricing`,
  customer: stripeCustomerId,
}, { idempotencyKey: `checkout:${userId}:${planId}` });

Pass an Idempotency-Key on every create request (sessions, charges, customers) so a network retry cannot create two of anything.

Step 3: Build the webhook handler against this checklist

Every item is mandatory; each one missing is a specific production incident.

  • Signature verified with stripe.webhooks.constructEvent(rawBody, sig, endpointSecret) - reject with 400 on failure. Skipping this lets anyone POST fake payment events.
  • Raw request body used for verification. Body-parsing middleware (e.g. express.json()) mutates bytes and breaks the signature - exempt the webhook route.
  • Idempotent: store processed event.ids (unique-constrained table) and skip duplicates. Stripe delivers at-least-once; without this, a redelivery double-provisions.
  • Returns 2xx within a few seconds; heavy work (emails, provisioning) goes to a queue. Stripe retries non-2xx responses for up to ~3 days with exponential backoff - a slow handler turns retries into a duplicate storm.
  • Handles out-of-order delivery: apply the event's data (or re-fetch the object from the API) rather than assuming events arrive in sequence.
  • Minimum event set handled: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed.
  • Unhandled event types return 200, not 500 - otherwise Stripe retries events you never intended to process.

Step 4: Mirror subscription state

On each subscription event, upsert one row per user: stripe_customer_id, stripe_subscription_id, status, current_period_end, price_id. The access gate reads this row only. States to gate on: active and trialing get access; past_due gets the grace window from Step 1; canceled and unpaid do not.

Step 5: Handle failure and lifecycle paths

  • invoice.payment_failed: start dunning - notify the user, keep access through the grace window, cut on expiry or subscription.deleted.
  • Declines: catch StripeCardError and surface a friendly message. Retry only transient errors (network, 5xx, rate limit); never auto-retry a card decline - that is card-testing behavior and triggers fraud flags.
  • SCA/3DS: with Payment Intents, handle requires_action by completing authentication client-side. Checkout handles this for you - one more reason to prefer it.
  • Plan changes: decide proration explicitly (proration_behavior) and communicate the resulting invoice line to the user before confirming.
  • Trials: handle customer.subscription.trial_will_end (fires 3 days before) to warn the user before the first charge.

Step 6: Test before going live

Run stripe listen --forward-to localhost:PORT/webhook and use test-mode keys. Verify with stripe trigger: successful checkout, duplicate event delivery (handler must no-op the second), payment failure, and cancellation. A webhook path that has never seen a replayed event in test will double-provision in production.

Worked contrast

Bad - grants access from the redirect:

// success page handler
app.get('/success', async (req, res) => {
  await db.user.update({ where: { id: req.user.id }, data: { plan: 'pro' } }); // spoofable
  res.render('success');
});

Good - grants access from the verified, idempotent webhook:

app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], endpointSecret);
  } catch { return res.status(400).send('bad signature'); }

  const inserted = await db.processedEvents.insertIgnoreDuplicate({ id: event.id });
  if (!inserted) return res.sendStatus(200); // already handled

  if (event.type === 'checkout.session.completed') {
    await queue.enqueue('activate-subscription', event.data.object);
  }
  res.sendStatus(200);
});

Deliverable

Produce a working integration containing: the server-side Checkout Session endpoint with idempotency keys; a webhook handler passing every item on the Step 3 checklist; the mirrored subscription table and the single access-gate read path; the dunning policy wired to invoice.payment_failed; and a test log showing duplicate-delivery and failure events handled correctly via the Stripe CLI.

Do NOT

  • Do not gate access on the success redirect - it is spoofable and unreliable; only the verified webhook flips entitlement.
  • Do not parse the webhook body before signature verification - verification fails on mutated bytes and you will "fix" it by disabling verification.
  • Do not process an event ID twice - at-least-once delivery guarantees you will eventually receive duplicates.
  • Do not trust price IDs or amounts from the client - a tampered request buys the enterprise plan at the hobby price.
  • Do not auto-retry card declines or create more than one Stripe customer per user.
  • Do not do slow work inline in the handler - return 2xx fast, queue the rest.

Quality bar

  • Signature verification on raw body, with a 400 path that is actually exercised in tests.
  • Duplicate event delivery proven to no-op in a test run.
  • Zero client-supplied amounts or price IDs reach the Stripe API.
  • Access gating reads exactly one mirrored status field, and every subscription lifecycle state maps to a defined access decision.
  • stripe listen test log covers success, duplicate, failure, and cancellation.

Escalation

This skill covers integration mechanics, not pricing strategy - route tier and packaging questions to saas-pricing. Tax registration, revenue recognition, and regulated money-movement (marketplaces holding funds, Connect platform liability) warrant a payments-savvy accountant or lawyer, not just Stripe defaults.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.