From stripe-consistency
Write, review, refactor, or debug Node.js code that uses the stripe SDK (PaymentIntents, SetupIntents, Checkout Sessions, Subscriptions, webhooks, stripe.webhooks.constructEvent) using one canonical, modern integration style. Use this skill whenever code charges a card, creates a checkout flow, handles a Stripe webhook, fixes "No signatures found matching the expected signature for payload," migrates off legacy Charges/Tokens/Sources flows, or when the user asks "why are my webhook signatures failing," "why was the customer charged twice," or "why is the amount off by 100x." Trigger it even when the user just says "add payments to this app" or shows a stack trace mentioning StripeSignatureVerificationError or StripeCardError — without saying the word "Stripe idioms."
How this skill is triggered — by the user, by Claude, or both
Slash command
/stripe-consistency:stripe-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The Stripe API is stable and extremely well represented in training data — which is the
The Stripe API is stable and extremely well represented in training data — which is the
problem: that data spans a decade of integration styles. Generated code mixes the legacy
era (stripe.charges.create with card tokens, Sources, client-side stripe.createToken)
with the modern era (PaymentIntents, SetupIntents, Checkout Sessions, webhook-driven
fulfillment). This skill pins one canonical style — the current PaymentIntents/Checkout
model — plus the handling rules (integer amounts, idempotency, raw-body webhooks) whose
violation silently corrupts money.
| Always | Never | Why |
|---|---|---|
stripe.paymentIntents.create(...) / Checkout Sessions | stripe.charges.create(...) directly | Charges-first flows predate SCA/3DS; PaymentIntents is the payment primitive and handles authentication state machines. |
| Payment Element / Checkout on the client | stripe.createToken(card) → server token charge | Tokens/Sources are legacy; raw card data on your server expands PCI scope. |
amount: 1999 (integer, smallest unit) | amount: 19.99 | Amounts are integers in the smallest currency unit. Float dollars either errors or charges the wrong amount. JPY and other zero-decimal currencies use whole units — no ×100. |
{ idempotencyKey } options on every mutating call that may be retried | bare create calls in retry loops / job queues | A network timeout + retry without a key = double charge. |
new Stripe(key, { apiVersion: "..." }) (pinned) | omitting apiVersion | Unpinned clients shift behavior when the account default version moves. |
stripe.webhooks.constructEvent(rawBody, sig, secret) | trusting req.body JSON as an event | Unverified webhooks let anyone "confirm" payments to your endpoint. |
express.raw({ type: "application/json" }) on the webhook route | global express.json() before signature check | Signature is computed over the exact raw bytes; parsed-then-restringified JSON never matches. The #1 webhook failure. |
Fulfill from checkout.session.completed / payment_intent.succeeded webhooks | fulfilling on the client success redirect | Redirects can be skipped, replayed, or forged; only the webhook (or a server-side retrieve) proves payment. |
Idempotent webhook handlers (dedupe on event.id / your own state) | assuming each event arrives exactly once | Stripe retries; duplicates are guaranteed eventually. |
expand: ["customer", "latest_invoice.payment_intent"] | N+1 retrieve calls for nested IDs | One request instead of a chain; nested objects come inline. |
for await (const x of stripe.X.list(...)) (auto-pagination) | reading only the first list page | has_more: true ignored = silently missing records. |
metadata: { orderId } for reconciliation | parsing descriptions or matching on amounts | Metadata round-trips through webhooks; amounts collide. |
House style for a payment + webhook pair:
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2024-06-20" });
const intent = await stripe.paymentIntents.create(
{
amount: 1999, // $19.99 — integer cents
currency: "usd",
customer: customerId,
automatic_payment_methods: { enabled: true },
metadata: { orderId: order.id },
},
{ idempotencyKey: `pi-create-${order.id}` }
);
// Webhook route: raw body, verify, ack fast, process async.
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, req.headers["stripe-signature"], process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
res.sendStatus(200); // ack before slow work
queue.enqueue(event); // handler must tolerate duplicates
});
Math.round(dollars * 100) at the one conversion boundary, integers
everywhere else. 19.99 * 100 === 1998.9999... — truncation undercharges by a cent.amount: 1000 is ¥1000 for JPY but $10.00 for USD. Multiplying
JPY by 100 overcharges 100×. Branch on the currency's decimal class.starting_after + has_more.event.id,
event.type, and your own metadata keys.StripeCardError (decline — show err.code /
decline_code to the user, do not retry the same card blindly) vs
StripeInvalidRequestError (your bug — fix the code) vs StripeRateLimitError/connection
errors (retry with backoff + the same idempotency key).Target the current stripe-node v14+ / current API versions. The key line is the
PaymentIntents era (SCA, 2019): everything before it — Charges-as-entrypoint, Tokens,
Sources — is legacy and should not appear in new code, though training data is saturated
with it. Pin apiVersion in the constructor so the SDK types and the wire behavior agree;
test with test-mode keys (sk_test_...) and test clocks for subscription time travel.
constructEvent, fast 2xx, async + idempotent
handlers, fulfillment only from checkout.session.completed / payment_intent.succeeded.expand and auto-pagination for reads; put reconciliation keys in metadata.For the fuller migration map (legacy flow → modern flow), expanded webhook and error-handling
patterns, and more worked examples, read references/stripe-patterns.md.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub guidogl/stripe-consistency --plugin stripe-consistency