Skip to content

stripe-integration

<!-- Loaded by nextjs-ecommerce-engineer when task involves Stripe, payment processing, Payment Intents, webhooks, or checkout -->

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

<!-- Loaded by nextjs-ecommerce-engineer when task involves Stripe, payment processing, Payment Intents, webhooks, or checkout -->

Agent definition

stripe-integration.md

Stripe Integration Reference

<!-- Loaded by nextjs-ecommerce-engineer when task involves Stripe, payment processing, Payment Intents, webhooks, or checkout -->

Stripe's cardinal rule: the client never sees raw card data. The browser collects card details directly into Stripe's systems via Elements; your server only ever handles payment tokens and intents.

Stripe Elements Setup

**When to use:** Building a custom checkout form embedded in your UI (vs. redirecting to Stripe-hosted Checkout).

// app/checkout/page.tsx — server component wraps client checkout
import { stripe } from '@/lib/stripe'
import { CheckoutForm } from './CheckoutForm'
import { getCart } from '@/lib/cart'

export default async function CheckoutPage() {
  const cart = await getCart()
  if (!cart || cart.items.length === 0) redirect('/cart')

  // Create Payment Intent on the server before rendering
  const total = cart.items.reduce(
    (sum, item) => sum + item.product.price * item.quantity, 0
  )

  const paymentIntent = await stripe.paymentIntents.create({
    amount: Math.round(total * 100), // Stripe uses cents
    currency: 'usd',
    metadata: { cartId: cart.id },
    automatic_payment_methods: { enabled: true },
  })

  return (
    <CheckoutForm
      clientSecret={paymentIntent.client_secret!}
      total={total}
    />
  )
}
// app/checkout/CheckoutForm.tsx
'use client'
import { useState } from 'react'
import { loadStripe } from '@stripe/stripe-js'
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js'

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)

interface CheckoutFormProps {
  clientSecret: string
  total: number
}

export function CheckoutForm({ clientSecret, total }: CheckoutFormProps) {
  return (
    <Elements stripe={stripePromise} options={{ clientSecret }}>
      <CheckoutFormInner total={total} />
    </Elements>
  )
}

function CheckoutFormInner({ total }: { total: number }) {
  const stripe = useStripe()
  const elements = useElements()
  const [processing, setProcessing] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function handleSubmit(e: React.FormEvent): Promise<void> {
    e.preventDefault()
    if (!stripe || !elements) return

    setProcessing(true)
    setError(null)

    const { error: stripeError } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/checkout/success`,
      },
    })

    if (stripeError) {
      setError(stripeError.message ?? 'Payment failed')
      setProcessing(false)
    }
    // On success, Stripe redirects to return_url
  }

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      {error && <p role="alert" className="text-red-700 text-sm mt-2">{error}</p>}
      <button type="submit" disabled={processing || !stripe}>
        {processing ? 'Processing...' : `Pay ${new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(total)}`}
      </button>
    </form>
  )
}

---

Payment Intents

**When to use:** One-time payments. Create the intent server-side, confirm client-side via Elements.

// lib/stripe.ts — Stripe singleton
import Stripe from 'stripe'

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-06-20',
  typescript: true,
})
// app/api/payment-intent/route.ts — alternative: create via API route
import { NextResponse } from 'next/server'
import { stripe } from '@/lib/stripe'
import { z } from 'zod'

const CreateIntentSchema = z.object({
  cartId: z.string().cuid(),
})

export async function POST(req: Request) {
  const body = await req.json()
  const parsed = CreateIntentSchema.safeParse(body)

  if (!parsed.success) {
    return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
  }

  // Fetch cart and compute total server-side — never trust client-provided amounts
  const cart = await db.cart.findUniqueOrThrow({
    where: { id: parsed.data.cartId },
    include: { items: { include: { product: true } } },
  })

  const amount = cart.items.reduce(
    (sum, item) => sum + item.product.price * item.quantity, 0
  )

  const paymentIntent = await stripe.paymentIntents.create({
    amount: Math.round(amount * 100),
    currency: 'usd',
    metadata: { cartId: cart.id },
    automatic_payment_methods: { enabled: true },
  })

  return NextResponse.json({ clientSecret: paymentIntent.client_secret })
}

---

Checkout Sessions (Stripe-Hosted)

**When to use:** When you want Stripe to handle the entire checkout UI — faster to implement, less control.

// app/api/checkout-session/route.ts
import { stripe } from '@/lib/stripe'
import { getCart } from '@/lib/cart'

export async function POST() {
  const cart = await getCart()
  if (!cart) return new Response('No cart', { status: 400 })

  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: cart.items.map(item => ({
      price_data: {
        currency: 'usd',
        product_data: {
          name: item.product.name,
          images: [item.product.imageUrl],
        },
        unit_amount: Math.round(item.product.price * 100),
      },
      quantity: item.quantity,
    })),
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cart`,
    metadata: { cartId: cart.id },
  })

  return Response.json({ url: session.url })
}

---

Webhook Signature Verification

**When to use:** Every Stripe webhook handler, without exception. Unverified webhooks can be spoofed.

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe'

// CRITICAL: disable Next.js body parsing — Stripe needs the raw body string
export const config = { api: { bodyParser: false
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked