stripe-simplifier
v0.4.1
Published
A developer-friendly SDK that simplifies common Stripe integration tasks.
Maintainers
Readme
stripe-simplifier
Add Stripe checkout to your Next.js app without becoming a Stripe expert.
stripe-simplifier is a thin layer over the official stripe SDK. It doesn't
replace Stripe or reproduce its API. It collapses the handful of things every
Checkout integration repeats (session shape, cents conversion, webhook
signature verification, error triage) into four small functions.
Application
|
v
stripe-simplifier
|
v
StripeTable of contents
- Status
- Install
- Quick start
- API reference
- Scope & limitations
- Starter kit
- Demo
- Design documents
- Development approach
- Product principle
- License
Status
Published on npm as [email protected],
including Checkout, multi-product Cart, PaymentIntent, refunds, expanded
webhooks, and a stripe-simplifier init CLI that installs a starter into an
existing Next.js project. The full flow has been exercised end-to-end
against a real Stripe test account (session creation, hosted payment,
PaymentIntent confirmation, refunds, redirect confirmation, and webhook
callbacks all firing correctly), and a fresh npm install from the public
registry has been verified to work. See demo/ for the working
Next.js app all of this was verified in, or Starter kit
below for the CLI.
Install
npm install stripe-simplifier stripestripe is a peerDependency you already control the version of, and
stripe-simplifier re-exports its types rather than hiding them.
Requires Node.js >= 20. The SDK uses the global crypto.randomUUID()
(Web Crypto), only guaranteed available without flags from Node 19+;
Node 20 (the current LTS) is the declared and tested minimum.
Quick start
This is the full golden path, matching what's actually running in demo/.
lib/payments.ts
import { createPayments } from "stripe-simplifier";
export const payments = createPayments({
secretKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
});app/api/checkout/route.ts
import { payments } from "@/lib/payments";
export async function POST() {
const session = await payments.checkout.create({
amount: 20,
currency: "usd",
successUrl: "https://app.example.com/success",
cancelUrl: "https://app.example.com/cancel",
});
return Response.json({ url: session.url });
}app/api/webhook/route.ts
import { payments } from "@/lib/payments";
export const POST = payments.webhooks.handler({
onPaymentSucceeded: async (event) => {
// your fulfillment logic, not the SDK's job
await markOrderPaid(event.session.id);
},
});API reference
createPayments(config: {
secretKey: string;
webhookSecret?: string; // required only if you use webhooks.handler()
}): PaymentsOne call, one config object. Throws ConfigurationError immediately if
secretKey is missing, before any request is ever made, not mid-checkout.
payments.checkout.create(options: {
amount: number; // major unit, e.g. dollars, not cents
currency: string; // ISO code, e.g. "usd"
successUrl: string;
cancelUrl: string;
productName?: string; // shown on Stripe's page, defaults to "Payment"
} | {
items: Array<{
name: string;
unitAmount: number; // major unit, per item
quantity?: number; // defaults to 1
}>;
currency: string; // one currency for the whole session, Stripe requires it
successUrl: string;
cancelUrl: string;
}): Promise<{ id: string; url: string }>Converts every amount to the integer cents Stripe expects internally, and
builds the line_items/price_data shape for you. Pass amount for a
single flat charge, or items for a real multi-product cart, each item
becomes its own Stripe line item with its own quantity. Mixing amount and
items in the same call isn't a valid shape, TypeScript rejects it at
compile time.
payments.checkout.retrieve(sessionId: string): Promise<{
id: string;
status: "paid" | "unpaid" | "no_payment_required";
raw: Stripe.Checkout.Session;
}>payments.paymentIntents.create(options: {
amount: number; // major unit, e.g. dollars, not cents
currency: string;
orderId?: string; // becomes the idempotency key basis and metadata.orderId
idempotencyKey?: string; // explicit override
metadata?: Record<string, string>;
}): Promise<{
id: string;
clientSecret: string | null;
status: "pending" | "processing" | "succeeded" | "failed" | "canceled";
stripeStatus: Stripe.PaymentIntent.Status; // the exact original status, never collapsed away
amount: number;
currency: string;
metadata: Record<string, string>;
raw: Stripe.PaymentIntent;
}>
payments.paymentIntents.retrieve(paymentIntentId: string): Promise<PaymentIntentResult>For the browser side (<PaymentElement />, stripe.confirmPayment()), use
@stripe/react-stripe-js directly, this SDK doesn't wrap it. See
demo/app/pay/page.tsx and
demo/components/CheckoutForm.tsx for
the full working example.
payments.refunds.create(options: {
paymentIntentId: string;
amount?: number; // major unit; omit for a full refund, provide for a partial one
}): Promise<{ id: string; status: string | null; amount: number; raw: Stripe.Refund }>One function for both full and partial refunds. Idempotency is handled for
you, derived from paymentIntentId + amount, so retrying the same refund
request never double-refunds.
payments.webhooks.handler(handlers: {
onPaymentSucceeded?: (event: CheckoutWebhookEvent | PaymentWebhookEvent) => Promise<void> | void;
onPaymentFailed?: (event: CheckoutWebhookEvent | PaymentWebhookEvent) => Promise<void> | void;
onRefundCreated?: (event: RefundWebhookEvent) => Promise<void> | void;
onDisputeCreated?: (event: DisputeWebhookEvent) => Promise<void> | void;
onUnhandledEvent?: (event: Stripe.Event) => Promise<void> | void;
}): (req: Request) => Promise<Response>Returns a plain (Request) => Promise<Response>. Drop it straight into a
Next.js Route Handler. It reads the raw body, verifies the Stripe signature,
and routes checkout.session.completed / checkout.session.async_payment_failed
/ payment_intent.succeeded / payment_intent.payment_failed /
charge.refunded / charge.dispute.created to your callbacks. An invalid or
missing signature returns 400 automatically, so you never write that check
yourself.
onPaymentSucceeded/onPaymentFailed fire for both Checkout and
PaymentIntent events, discriminate with "session" in event:
onPaymentSucceeded: async (event) => {
if ("session" in event) {
// a Checkout Session completed
} else {
// a PaymentIntent succeeded
}
}No automatic deduplication. Stripe can deliver the same event more than once. This SDK does not dedupe for you: that would mean owning storage, which is more than a checkout helper should take on. Every event exposes
event.raw.idso your app can dedupe if it needs to.
Thrown by checkout.create(), checkout.retrieve(), paymentIntents.create(),
paymentIntents.retrieve(), refunds.create(), and webhooks.handler():
| Class | Thrown when |
|---|---|
| ConfigurationError | Missing/invalid secretKey or webhookSecret, or Stripe rejects your API key |
| ValidationError | Bad input: non-positive amount, missing currency, missing paymentIntentId, or Stripe rejects the request shape |
| WebhookError | Signature verification fails inside webhooks.handler() |
| PaymentError | A wrapped call receives a card decline from Stripe |
Every error keeps the original Stripe error on .raw. Nothing is discarded,
only re-labeled into something you can branch on without knowing Stripe's
internal error taxonomy.
Note on PaymentError: card declines happen at confirmation time
(stripe.confirmPayment() in the browser), and confirmation isn't wrapped by
this SDK (see paymentIntents.create() above), so in normal usage a decline
surfaces as Stripe's own error client-side, not PaymentError. The class
exists and is tested for when a wrapped call does receive one.
payments.raw: Stripe // the underlying official Stripe clientNothing about your Stripe account, your API key, or Stripe's full API is
hidden. Anything this SDK doesn't cover, payments.raw does.
Scope & limitations
Written down on purpose, not left for you to discover:
- No wrapped
<PaymentElement />. Card/Apple Pay/Google Pay/SEPA UI stays a direct@stripe/react-stripe-jsintegration you control, this SDK only wraps the backend (session/PaymentIntent creation, refunds, webhooks). - No webhook deduplication storage. See the note under
webhooks.handler()above. - Next.js App Router only. The webhook handler assumes the Fetch API
Request/Responseshape. - One-time payments only. No subscriptions, invoicing, or tax handling.
- No
paymentIntents.confirm()wrapper. Confirmation is designed to stay client-side (Stripe.js) or, if you need it server-side, viapayments.raw. paymentIntents.create()always usesautomatic_payment_methods: { enabled: true }. Not configurable: no way to disable redirect-based methods or opt intocapture_method: "manual"(pre-authorization) yet. A deliberate decision to keep the surface small, not an oversight.refunds.create()accepts apaymentIntentIdonly, not achargeId, even though Stripe's own API accepts either. This SDK is built around PaymentIntents throughout; add achargeIdpath only if you have a real need Stripe'spaymentIntentId-based refund can't cover.
Starter kit
npx create-next-app@latest my-app # if you don't have a Next.js project yet
cd my-app
npx stripe-simplifier initInstalls a ready-to-customize Next.js starter (Checkout, multi-product Cart,
PaymentIntent + PaymentElement, Refunds) into your project, and installs its
dependencies automatically (detects npm/yarn/pnpm/bun from your lockfile,
--skip-install to do it yourself). It never creates a Next.js project
itself, and it never overwrites an existing file without asking first
(--dry-run to preview, --yes to skip prompts). Source lives in
starter/, see starter/README.md.
Demo
A working Next.js app using this exact API also lives in demo/:
Checkout (/checkout), the PaymentIntent + PaymentElement flow (/pay), and
a webhook handler covering both, all verified against a real Stripe test
account. demo/ stays the minimal technical reference; starter/ above is
the polished, ready-to-customize version, both call the same API. See
demo/README.md for setup.
Design documents
How this scope was decided, for anyone who wants the reasoning, not just the API:
docs/STRIPE_PROBLEMS.md: the original problem framingdocs/DISCOVERY_REPORT.md: Stripe concepts, pain points, what should and shouldn't be abstracted (includes the later "Real Project Specification Impact" section that opened v-next)docs/STRIPE_FLOW_COMPARISON.md: Checkout vs. PaymentIntent+Elements, measureddocs/MVP_DEFINITION.md: the v0.1.1 scope decision and whydocs/API_DESIGN.md: v0.1.1 function signaturesdocs/ARCHITECTURE.md: v0.1.1 package structure, build, testing strategydocs/MVP_VNEXT.md,docs/API_DESIGN_VNEXT.md,docs/ARCHITECTURE_VNEXT.md: the same, for PaymentIntent/refunds/expanded webhooksdocs/STRIPE_FLOW_COMPARISON_VNEXT.md: real, measured complexity comparison for v-next
Development approach
Discovery done
|
v
MVP Definition done
|
v
API Design done
|
v
Architecture done
|
v
Implementation done
|
v
Demo Application done
|
v
Testing done (unit tests + verified live demo)
|
v
Documentation this file
|
v
Publication done (npm + GitHub) for v0.1.1
|
v
v-next (PaymentIntent/refunds/webhooks) done, published as v0.2.0Product principle
The SDK should not simply be a wrapper around the official Stripe SDK. Every abstraction should provide a clear benefit to the developer. The main question is:
What can this SDK make significantly easier than integrating Stripe directly?
License
MIT
