plugins/stripe-billing-master/skills/stripe-webhook-idempotency/SKILL.md
Server-side Stripe webhook idempotency patterns. PROACTIVELY activate for: (1) Stripe webhook handler design, (2) Transactional dedup via stripe_processed_events, (3) credit_transactions.idempotency_key UNIQUE partial indexes, (4) Idempotency-Key header priority (header > body > server UUID), (5) Idempotency key format/charset/length validation at the handler edge, (6) FOR UPDATE row locking when a UPDATE depends on a prior SELECT, (7) Webhook signature verification (stripe.webhooks.constructEventAsync, tolerance, raw-body reading), (8) Retry-safe endpoints with randomUUID fallback, (9) Durable checkpoint row ordering (checkpoint FIRST, mutation SECOND). Provides: complete webhook handler skeleton, Idempotency-Key validator, dedup SQL, FOR UPDATE pattern, signature verification example.
npx skillsauth add JosiahSiegel/claude-plugin-marketplace stripe-webhook-idempotencyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Concept | Source of truth | Default |
|--|--|--|
| Signature verification | stripe.webhooks.constructEventAsync(rawBody, sig, secret, tolerance) | tolerance = 300s |
| Event dedup (short-term) | stripe_processed_events (30d retention) | primary key: event_id |
| Balance-change dedup (durable) | credit_transactions.idempotency_key UNIQUE partial index | retention = forever |
| Idempotency key priority | Idempotency-Key header > body.idempotency_key > crypto.randomUUID() | validate len<=128, [A-Za-z0-9_-] |
| Checkpoint ordering | Checkpoint INSERT -> user UPDATE | never reverse |
| Row lock | .for("update") on the snapshot row | required when UPDATE depends on prior SELECT |
Use for every Stripe webhook handler and every client-retry-safe mutation endpoint (any POST route that creates a billable entity, e.g., /api/v1/orders, /api/v1/jobs, or your project's equivalent).
Related skills:
stripe-billing-master:stripe-refund-dispute-lifecyclestripe-billing-master:stripe-credit-audit-trailstripe-billing-master:stripe-list-pagination-previous-attributesawait db.transaction(async (tx) => {
// 1. Durable checkpoint FIRST
const [inserted] = await tx.insert(creditTransactions).values({ /* ... idempotencyKey: "dispute_hold:<id>" */ })
.onConflictDoNothing({ target: creditTransactions.idempotencyKey })
.returning({ id: creditTransactions.id });
// 2. Dedup
if (!inserted) return;
// 3. Mutate
await tx.update(users).set({ /* ... */ }).where(/* ... */);
});
const MAX_LEN = 128;
const VALID = /^[A-Za-z0-9_-]+$/;
export function validateIdempotencyKey(key: string | null): { valid: true; key: string | null } | { valid: false; error: string } {
if (!key || key.trim() === "") return { valid: true, key: null }; // caller falls back to randomUUID
if (key.length > MAX_LEN) return { valid: false, error: "Idempotency-Key too long" };
if (!VALID.test(key)) return { valid: false, error: "Idempotency-Key invalid charset" };
return { valid: true, key };
}
// In the handler:
const headerKey = req.headers.get("Idempotency-Key");
const bodyKey = body.idempotency_key;
const raw = headerKey ?? bodyKey ?? null;
const v = validateIdempotencyKey(raw);
if (!v.valid) return apiError(400, "VALIDATION_ERROR", v.error);
const key = v.key ?? crypto.randomUUID();
Use constructEventAsync — synchronous constructEvent blocks the worker on large payloads.
const event = await stripe.webhooks.constructEventAsync(
rawBody, // raw bytes; Next.js: await req.text() BEFORE any json()
sigHeader,
env.STRIPE_WEBHOOK_SECRET,
300, // 5min tolerance
);
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get("stripe-signature");
if (!signature) return apiError(400, "VALIDATION_ERROR", "Missing stripe-signature");
let event: Stripe.Event;
try {
event = await stripe.webhooks.constructEventAsync(
rawBody,
signature,
env.STRIPE_WEBHOOK_SECRET,
300, // 5min tolerance
);
} catch (err) {
logEvent("stripe_webhook_bad_signature", { err: String(err) });
return apiError(400, "INVALID_SIGNATURE", "Signature verification failed");
}
// Transactional dedup: G1 + G9 pattern
const dedup = await db.insert(stripeProcessedEvents)
.values({ eventId: event.id, type: event.type })
.onConflictDoNothing({ target: stripeProcessedEvents.eventId })
.returning({ id: stripeProcessedEvents.id });
if (dedup.length === 0) return apiSuccess({ deduped: true });
await dispatchEvent(event); // handlers apply G1-G9 per rules above
return apiSuccess({ ok: true });
}
const [user] = await tx.select(/* ... */).from(users).where(eq(users.id, userId)).for("update");
// snapshot is safe from concurrent invoice/dispute writes
const prev = user.stripeSubscriptionStatus;
await tx.insert(creditTransactions).values({ /* ... */ metadata: { previousStatus: prev } });
await tx.update(users).set({ stripeSubscriptionStatus: "past_due" });
development
Use for Clerk sessions, tokens, webhooks, orgs, and security. PROACTIVELY activate for session tokens, JWT templates, getToken(), custom claims, pending sessions, multi-session UX, organizations, roles, permissions, system vs custom permissions, features/plans, MFA/passkeys/password policy/bot protection, Clerk webhooks, Svix signatures, verifyWebhook(), user/org sync, retries/replays, environment variables, custom domains, secret rotation, logs, and auth security reviews. Provides token semantics, webhook idempotency, authorization defaults, and hardening checklist.
tools
Use for Clerk in Next.js. PROACTIVELY activate for @clerk/nextjs setup, App Router auth()/currentUser(), clerkMiddleware(), proxy.ts/middleware.ts, createRouteMatcher(), protected pages/layouts/Route Handlers/Server Actions/API routes/tRPC, auth.protect() role/permission/token checks, ClerkProvider placement, server-only clerkClient, Link prefetch, redirects, 401/404 auth failures, custom domains, __clerk proxy paths, and deployment gotchas. Provides file patterns, server/client boundary rules, matcher templates, and production checks.
development
Use for Clerk frontend auth flows. PROACTIVELY activate for React, JavaScript, Vue, Nuxt, Astro, Expo, React Router, TanStack React Start, or SPA setup; ClerkProvider and publishable-key wiring; SignIn/SignUp/UserButton/UserProfile/OrganizationSwitcher; custom useUser/useAuth/useClerk/useSignIn/useSignUp/useSession/useOrganization flows; multi-session UX; cross-origin getToken() fetches; loading states, redirects, routing, CORS/cookies, or hydration bugs. Provides SDK selection, UI patterns, token-fetch templates, and frontend gotchas.
development
Use for Clerk dev/prod readiness, deployment, and multi-language implementation planning. PROACTIVELY activate for environment variables, pk_test/sk_test vs pk_live/sk_live, local dev, preview/staging/prod instances, domains/DNS, redirects, OAuth credentials, custom domains/proxy, authorizedParties, CSP, CORS/cookies, webhooks/tunnels, Vercel/Netlify/Cloudflare/API gateways, monitoring/troubleshooting, and backends in Node/Express/Fastify, Python/FastAPI/Django/Flask, Go, Ruby/Rails, Java/Spring, .NET, PHP/Laravel. Provides checklists, rollout plans, and language-portable patterns.