tool

Stripe

created 2026-05-10 updated 2026-05-25 payments · checkout · webhooks · ecommerce

Stripe

Payments platform. Two integration surfaces matter most for typical app work:

  • Checkout Sessions — hosted, redirect-based. Stripe owns the form. ~5 min to integrate.
  • PaymentIntent + Payment Element — embedded, client-side <PaymentElement />. App owns the form. More control, more code.

stripe-best-practices skill is loaded in this harness — invoke it for integration-specific guidance.

Checkout Sessions vs PaymentIntents — pick one

NeedPick
Fastest path, hosted page is fineCheckout Sessions
Custom form on your own domainPayment Element + PaymentIntent
Subscription management UICheckout Sessions + Customer Portal
Marketplace / ConnectPaymentIntent (more control over flow)
One-time payment, low complexityCheckout Sessions

kulevents uses PaymentIntents with the embedded Payment Element — kept the checkout form in-app for design consistency.

Webhooks are non-negotiable

The client-side success redirect lies. The user can close the tab between Stripe’s callback and your server. Always create the database order from the webhook (payment_intent.succeeded or checkout.session.completed), never from the redirect.

Pattern:

  1. Server creates PaymentIntent (or Checkout Session) → returns client_secret to client
  2. Client confirms payment → Stripe redirects to /payment-success?payment_intent=...
  3. Webhook fires → server creates Order in DB, sends confirmation email
  4. /payment-success page reads order via the verified webhook-created record

Webhook idempotency

Stripe retries on 5xx. Your handler must be idempotent — typically by checking whether an Order with paymentIntentId === event.data.object.id already exists before creating one. kuleventswebhookHandlers.ts is the reference shape: central event router, one handler per event type, bail-early on already-processed.

This is a textbook application of idempotency (event-ID dedup pattern). Stripe also exposes an Idempotency-Key header for outbound API calls (e.g. retry-safe PaymentIntent creation) — the same pattern but at the request level.

Promotion codes

Stripe-native promo codes (promotion_codes API) > rolling your own. Validate via stripe.promotionCodes.list({ code, active: true }) server-side, attach to PaymentIntent via discounts: [{ promotion_code: id }]. Stripe handles % vs amount, expiry, redemption limits, and surfaces them in Dashboard analytics.

kuleventspromoCodeService.ts does exactly this — validation server-side, discount applied during PaymentIntent creation, the discount ID + amount stored on the Order for receipt rendering.

Restricted keys

Don’t use the secret key for everything. For server processes that only do specific things (read-only reporting, webhook handling, refunds), use a restricted key scoped to just those resources. Limits blast radius if a key leaks.

Testing

  • Test mode — entirely separate pk_test_* / sk_test_* keys. 4242 4242 4242 4242 is the magic card.
  • stripe listen CLI forwards live webhooks to localhost; signs them with a temp secret you paste into your env.
  • Stripe Test Clocks — for subscription/billing flows, jump the clock forward to test renewal/expiry.

Used in

  • kulevents — PaymentIntents (one-off photo booth orders), promotion codes, webhook → Firestore order creation, email confirmation. SDK versions: stripe@17.6 (server), @stripe/stripe-js@5.10 + @stripe/react-stripe-js@3.6 (client).
  • stripe-best-practices — official skill for integration decisions, security, migration
  • upgrade-stripe — guide for SDK + API version upgrades

See also

  • kulevents — production usage
  • firebase — storage backend in the same kulevents stack
  • idempotency — the underlying pattern for webhook handlers and outbound retries
  • delivery-guarantees — Stripe webhooks are at-least-once; idempotency makes it exactly-once effect