tool
Stripe
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
| Need | Pick |
|---|---|
| Fastest path, hosted page is fine | Checkout Sessions |
| Custom form on your own domain | Payment Element + PaymentIntent |
| Subscription management UI | Checkout Sessions + Customer Portal |
| Marketplace / Connect | PaymentIntent (more control over flow) |
| One-time payment, low complexity | Checkout 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:
- Server creates PaymentIntent (or Checkout Session) → returns
client_secretto client - Client confirms payment → Stripe redirects to
/payment-success?payment_intent=... - Webhook fires → server creates Order in DB, sends confirmation email
/payment-successpage 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. kulevents’ webhookHandlers.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.
kulevents’ promoCodeService.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 4242is the magic card. stripe listenCLI 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).
Related skills
stripe-best-practices— official skill for integration decisions, security, migrationupgrade-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