<!-- skill: Stripe Payments | version: 1.0.0 | description: Accept Stripe payments with PaymentIntents, webhooks and SCA support | category: Web -->
## Instructions
- Keys: `pk_test_/sk_test_` for dev, `pk_live_/sk_live_` for prod — never ship secret keys to frontend; restrict keys in Dashboard, rotate on leak, use separate restricted keys per service.
- PaymentIntent flow: create intent server-side (`amount` in cents + `currency`), return `client_secret`, confirm with Stripe.js `confirmCardPayment`; never trust client-sent amounts — recompute server-side.
- Stripe.js: load via `@stripe/stripe-js` (`loadStripe(pk)`), mount `PaymentElement`, handle `requires_action` (3D Secure) automatically; style Element to match checkout theme.
- Webhooks: endpoint `/api/stripe/webhook` raw body (no JSON parse first), verify with `stripe.webhooks.constructEvent(body, sig, whsec)`, handle `payment_intent.succeeded/failed`, `charge.refunded`, `customer.subscription.*`.
- Idempotency: `Idempotency-Key` header on creates, reconcile via `payment_intent` id stored in orders table; webhook handler must be idempotent (upsert on event id).
- Refunds/disputes: `stripe.refunds.create({ payment_intent })` partial via `amount`, reason codes; disputes: respond with evidence within deadline, enable `radar` + 3DS to cut fraud.
- SCA (Europe): use PaymentIntents + `automatic_payment_methods`, handle `requires_action`, request `off_session` + exemptions only for subscriptions/MIT; test with `4000 0027 6000 3184` (3DS required).
- Products/prices: define in Dashboard or `stripe.prices.create`, use Price IDs (not ad-hoc amounts) for repeatables; coupons/tax via `Tax Rates` + Stripe Tax where needed.
- Subscriptions: `customer + price → subscription`, trial periods, `billing_cycle_anchor`, proration on plan change; listen `invoice.payment_failed` → dunning emails + grace period.
- Complements WooCommerce: Woo Stripe Gateway plugin for WP stores; this skill covers custom code (Next/Node/PHP) — share customer email/metadata so both report consistently.
- Testing: Stripe CLI `stripe listen --forward-to localhost:3000/api/stripe/webhook`, trigger `stripe trigger payment_intent.succeeded`; test cards `4242...`, `4000 0000 0000 0002` (declined).
- Security: TLS everywhere, webhook secret in env (`STRIPE_WEBHOOK_SECRET`), log event ids not card data (never touch PAN — Stripe.js tokenizes), PCI SAQ-A scope.
## Examples
- One-time checkout: Node creates $49 PaymentIntent, React PaymentElement confirms, webhook marks order paid, receipt email sent.
- SaaS subs: monthly Price, trial 14d, `invoice.payment_failed` dunning, customer portal (`billing_portal.sessions.create`) for self-serve cancel.
- Refund flow: admin dashboard issues partial refund, webhook `charge.refunded` updates order, dispute evidence checklist for card-not-present.
## Workflows
1. Create Product/Price, set test keys + webhook secret in `.env` (`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`).
2. Build server `POST /api/checkout` creating PaymentIntent with server-computed amount.
3. Build client Stripe.js PaymentElement confirm + 3DS handling + success page.
4. Add raw-body webhook route: verify signature, upsert order, send receipts.
5. Test with CLI triggers + 3DS/decline cards, then switch to live keys + live webhook.
6. Enable Radar, dunning emails, portal, refund/dispute runbook for support.
## Templates
- Create PaymentIntent (Node):
```js
// server/checkout.js
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function createCheckout(req, res) {
  const { orderId } = req.body;
  const order = await db.orders.get(orderId); // amount from DB, never client
  const intent = await stripe.paymentIntents.create({
    amount: order.totalCents, currency: 'usd',
    automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
    metadata: { orderId: order.id, email: order.email },
  }, { idempotencyKey: `order-${order.id}-v${order.version}` });
  res.json({ clientSecret: intent.client_secret });
}
```
- Confirm on client (React):
```jsx
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PK);
function Form() {
  const stripe = useStripe(), elements = useElements();
  return <form onSubmit={async (e) => {
    e.preventDefault();
    if (!stripe || !elements) return;
    const { error } = await stripe.confirmPayment({
      elements, confirmParams: { return_url: window.location.origin + '/success' },
    });
    if (error) alert(error.message);
  }}>
    <PaymentElement />
    <button disabled={!stripe}>Pay</button>
  </form>;
}
```
- Webhook verify (Next.js route):
```ts
// app/api/stripe/webhook/route.ts
import Stripe from 'stripe';
export const runtime = 'nodejs';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
  const sig = req.headers.get('stripe-signature')!;
  const body = await req.text(); // raw body required
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err: any) {
    return new Response(`bad sig: ${err.message}`, { status: 400 });
  }
  if (event.type === 'payment_intent.succeeded')
    await db.orders.markPaid(event.data.object.metadata.orderId, event.id);
  if (event.type === 'charge.refunded')
    await db.orders.markRefunded(event.data.object.payment_intent as string);
  return new Response('ok');
}
```
## Tools
- SARAH shell_run for Stripe CLI (`stripe listen/trigger/logs`), server boot, webhook forward; SARAH file_write for checkout/webhook scaffolds.
- Stripe Dashboard (test/live toggle), Workbench, Radar rules; WooCommerce Stripe Gateway when on WordPress.
## Advanced
- Connect/ marketplace: Express accounts, `transfer_data: { destination }`, application fees, `on_behalf_of`; verify payouts + 1099 handling per region.
- Dunning: `invoice.payment_failed` → 3 retries + email sequence, `customer.portal` for card update, Smart Retries + Radar `request_3d_secure: 'automatic'`.
- Migration: import customers via `stripe.customers.create({ email })` + PaymentMethods setup-intent flow; dual-run Woo + custom behind feature flag keyed on email.
## Documentation
- https://docs.stripe.com/payments/payment-intents
- https://docs.stripe.com/webhooks
- https://docs.stripe.com/js
- https://docs.stripe.com/testing
- https://docs.stripe.com/strong-customer-authentication
