
Stripe
- 152 installs
- 1.5k repo stars
- Updated July 20, 2026
- vercel-labs/emulate
Local Stripe API emulator for testing payments, checkout, customers, and webhooks without real charges.
About
Emulated Stripe API for local development covering payment processing, checkout flows, customer and product management, and webhook testing. Starts with npx emulate --service stripe. Documents Bearer sk_test token auth, STRIPE_BASE_URL or stripe client baseUrl configuration, and Next.js adapter-next embedding. Provides REST endpoints mirroring Stripe resources with in-memory persistence. Enables inspecting payment intents, checkout sessions, and webhook event payloads during integration development without connecting to live Stripe accounts.
- Stateful Stripe API emulation for local payment flow testing
- sk_test Bearer tokens accepted by the emulator
- STRIPE_BASE_URL points official Stripe SDK at localhost
- Next.js withEmulate embeds Stripe emulator in app routes
- Webhook events captured for checkout and payment verification
Stripe by the numbers
- 152 all-time installs (skills.sh)
- +7 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #881 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
stripe capabilities & compatibility
- Capabilities
- start stripe emulator · test checkout flows · manage test customers · capture webhook events
- Works with
- stripe
- Use cases
- testing · api development
- Runs
- Runs locally
- Pricing
- Free
What stripe says it does
Emulated Stripe API for local development and testing.
npx skills add https://github.com/vercel-labs/emulate --skill stripeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 152 |
|---|---|
| repo stars | ★ 1.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 20, 2026 |
| Repository | vercel-labs/emulate ↗ |
How do I test Stripe checkout and payment flows locally?
Run a stateful local Stripe API emulator to test checkout, payments, customers, products, prices, and webhooks without real charges.
Who is it for?
Developers integrating Stripe payments who need local checkout and webhook testing.
Skip if: Live Stripe account administration or production payment processing.
When should I use this skill?
User needs to process payments locally, test checkout, or emulate Stripe API endpoints.
What you get
Stripe SDK or API calls hit local emulator with inspectable payment and webhook state.
Files
Stripe API Emulator
Fully stateful Stripe API emulation. Customers, products, prices, checkout sessions, payment intents, charges, and payment methods persist in memory. Webhooks fire on state changes. The hosted checkout UI lets you complete payments in the browser.
No real payments are processed. Every Stripe SDK call hits the emulator and produces realistic responses.
Start
# Stripe only
npx emulate --service stripe
# Default port (when run alone)
# http://localhost:4000Or programmatically:
import { createEmulator } from 'emulate'
const stripe = await createEmulator({ service: 'stripe', port: 4000 })
// stripe.url === 'http://localhost:4000'Pointing Your App at the Emulator
Stripe SDK
The Stripe Node.js SDK does not read an environment variable for the base URL. You must pass it when constructing the client:
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
host: 'localhost',
port: 4000,
protocol: 'http',
})Embedded in Next.js (adapter-next)
When using @emulators/adapter-next, the emulator runs inside your Next.js app at /emulate/stripe. The SDK needs to point at localhost with a proxy route to forward /v1/* calls to /emulate/stripe/v1/*:
// next.config.ts
import { withEmulate } from '@emulators/adapter-next'
export default withEmulate({
env: {
STRIPE_SECRET_KEY: 'sk_test_emulated',
},
})// lib/stripe.ts
import Stripe from 'stripe'
const port = process.env.PORT ?? '3000'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
host: 'localhost',
port: parseInt(port, 10),
protocol: 'http',
})// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as stripe from '@emulators/stripe'
export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
services: {
stripe: {
emulator: stripe,
seed: {
products: [
{ id: 'prod_widget', name: 'Widget', description: 'A useful widget' },
],
prices: [
{ id: 'price_widget', product_name: 'Widget', currency: 'usd', unit_amount: 1000 },
],
webhooks: [
{
url: `http://localhost:${process.env.PORT ?? '3000'}/api/webhooks/stripe`,
events: ['*'],
},
],
},
},
},
})// app/v1/[...path]/route.ts (proxy for Stripe SDK)
const STRIPE_URL = `http://localhost:${process.env.PORT ?? '3000'}/emulate/stripe`
async function handler(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
const { path } = await ctx.params
const url = new URL(req.url)
const target = `${STRIPE_URL}/v1/${path.join('/')}${url.search}`
const res = await fetch(target, {
method: req.method,
headers: req.headers,
body: req.body,
duplex: 'half',
} as any)
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: res.headers,
})
}
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }Direct fetch
curl http://localhost:4000/v1/customers \
-H "Authorization: Bearer sk_test_emulated"Seed Config
Seed data is optional. All entities support an optional id field for deterministic IDs that survive server restarts.
stripe:
customers:
- id: cus_demo
email: demo@example.com
name: Demo User
products:
- id: prod_tshirt
name: T-Shirt
description: A comfortable tee
prices:
- id: price_tshirt
product_name: T-Shirt
currency: usd
unit_amount: 2500
webhooks:
- url: http://localhost:3000/api/webhooks/stripe
events: ['*']
secret: whsec_testThe product_name field in prices links to the product by name. Use events: ['*'] to receive all webhook events, or specify individual event types.
API Endpoints
Customers
# Create customer
curl -X POST http://localhost:4000/v1/customers \
-d "email=user@example.com" -d "name=Jane Doe"
# Retrieve customer
curl http://localhost:4000/v1/customers/cus_xxx
# Update customer
curl -X POST http://localhost:4000/v1/customers/cus_xxx \
-d "name=Updated Name"
# Delete customer
curl -X DELETE http://localhost:4000/v1/customers/cus_xxx
# List customers
curl http://localhost:4000/v1/customersProducts
# Create product
curl -X POST http://localhost:4000/v1/products \
-d "name=Widget" -d "description=A useful widget"
# Retrieve product
curl http://localhost:4000/v1/products/prod_xxx
# List products
curl "http://localhost:4000/v1/products?active=true"Prices
# Create price
curl -X POST http://localhost:4000/v1/prices \
-d "product=prod_xxx" -d "currency=usd" -d "unit_amount=1000"
# Retrieve price
curl http://localhost:4000/v1/prices/price_xxx
# List prices
curl "http://localhost:4000/v1/prices?active=true"Checkout Sessions
# Create checkout session
curl -X POST http://localhost:4000/v1/checkout/sessions \
-d "mode=payment" \
-d "line_items[0][price]=price_xxx" \
-d "line_items[0][quantity]=1" \
-d "success_url=http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}" \
-d "cancel_url=http://localhost:3000/cart"
# Retrieve session
curl http://localhost:4000/v1/checkout/sessions/cs_xxx
# List sessions
curl http://localhost:4000/v1/checkout/sessions
# Expire a session
curl -X POST http://localhost:4000/v1/checkout/sessions/cs_xxx/expireThe session's url field points to a hosted checkout page at /checkout/cs_xxx. Clicking "Pay" on that page completes the session, fires the checkout.session.completed webhook, and redirects to success_url. The {CHECKOUT_SESSION_ID} template in success_url is replaced with the actual session ID.
Payment Intents
# Create payment intent
curl -X POST http://localhost:4000/v1/payment_intents \
-d "amount=2000" -d "currency=usd"
# Retrieve
curl http://localhost:4000/v1/payment_intents/pi_xxx
# Update
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx \
-d "amount=3000"
# Confirm (triggers payment_intent.succeeded + charge.succeeded webhooks)
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx/confirm
# Cancel
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx/cancel
# List
curl http://localhost:4000/v1/payment_intentsCharges
# Retrieve charge
curl http://localhost:4000/v1/charges/ch_xxx
# List charges
curl http://localhost:4000/v1/chargesCharges are created automatically when a payment intent is confirmed.
Customer Sessions
# Create customer session
curl -X POST http://localhost:4000/v1/customer_sessions \
-d "customer=cus_xxx"Payment Methods
# List payment methods
curl http://localhost:4000/v1/payment_methodsWebhooks
The emulator dispatches webhook events when state changes. Register webhooks via seed config or programmatically.
Events dispatched
| Event | Trigger |
|---|---|
customer.created | Customer created |
customer.updated | Customer updated |
customer.deleted | Customer deleted |
product.created | Product created |
price.created | Price created |
payment_intent.created | Payment intent created |
payment_intent.succeeded | Payment intent confirmed |
payment_intent.canceled | Payment intent canceled |
charge.succeeded | Payment intent confirmed (charge auto-created) |
checkout.session.completed | Checkout completed via hosted page |
checkout.session.expired | Checkout session expired |
Webhook handler example
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const body = await request.json()
const event = body.type as string
const obj = body.data?.object
switch (event) {
case 'customer.created':
console.log('Customer created:', obj.id, obj.email)
break
case 'checkout.session.completed':
console.log('Checkout completed:', obj.id)
break
case 'payment_intent.succeeded':
console.log('Payment succeeded:', obj.id)
break
case 'charge.succeeded':
console.log('Charge succeeded:', obj.id)
break
}
return NextResponse.json({ received: true })
}Common Patterns
Checkout Flow (embedded Next.js)
// Server action
const customer = await stripe.customers.create({
email: 'shopper@example.com',
name: 'Demo Shopper',
})
const session = await stripe.checkout.sessions.create({
mode: 'payment',
customer: customer.id,
line_items: [{ price: 'price_widget', quantity: 2 }],
success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/cart`,
})
redirect(session.url!)Retrieve Session on Success Page
const session = await stripe.checkout.sessions.retrieve(session_id)
const customer = await stripe.customers.retrieve(session.customer as string)
console.log(session.payment_status) // 'paid'
console.log(customer.name) // 'Demo Shopper'Payment Intent Flow (no checkout UI)
const pi = await stripe.paymentIntents.create({
amount: 5000,
currency: 'usd',
customer: 'cus_xxx',
})
// Confirm triggers payment_intent.succeeded + charge.succeeded webhooks
const confirmed = await stripe.paymentIntents.confirm(pi.id)
console.log(confirmed.status) // 'succeeded'Related skills
FAQ
How do I configure the Stripe SDK for the emulator?
Set STRIPE_BASE_URL to the emulator URL or pass baseUrl when constructing the Stripe client.
What auth does the emulator accept?
Bearer tokens with sk_test prefix are accepted like the Resend emulator pattern.
Are real charges processed?
No. The emulator stores state in memory for local inspection only.
Is Stripe safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.