Stripe is one of the most common starting points for SaaS, subscriptions, marketplaces, and card payments. This guide uses the official stripe Node SDK on the server, @stripe/stripe-js in the browser, and Stripe-hosted Checkout Sessions so card details never touch your DaloyJS app.
What you should know up front
Start with Checkout Sessions.Stripe Checkout is the fastest secure path: your server creates a session, the browser redirects to Stripe, and Stripe handles the hosted payment page, payment method collection, SCA, wallets, localization, and receipts.
Use Stripe.js on the client, not raw card forms.@stripe/stripe-jsis a small loader for Stripe's hosted https://js.stripe.com script. Stripe says this is required for PCI compliance; do not bundle or self-host Stripe.js.
Webhook verification needs the raw body.stripe.webhooks.constructEvent() requires the exact raw request body, the Stripe-Signature header, and the endpoint secret. JSON parsing before verification will break the signature check.
Send idempotency keys on mutating retries. Stripe accepts idempotency keys on every POST. Generate a UUID per logical attempt and reuse it when retrying the same create or update call.
Stripe is separate from PayPal. Keep this guide next to Braintree, not under it. Braintree is PayPal's gateway; Stripe is a separate provider.
Install and authenticate the Stripe CLI so you can create test products, forward webhooks, and run local integration checks.
Copy your test Secret key and Publishable key from Developers - API keys. Keep the secret key server-side only.
Add a webhook endpoint for your app and copy its Signing secret, which starts with whsec_. For local development, stripe listen prints a different secret than the Dashboard endpoint.
2. Install
ts
pnpm add stripe @stripe/stripe-js
stripe belongs in your server application. @stripe/stripe-js belongs in browser code that redirects to Checkout or renders Elements. Neither package is a runtime dependency of@daloyjs/core.
If your route returns a session.url, a plainwindow.location.assign(url) is enough. If you prefer redirecting by Session ID, load Stripe.js from @stripe/stripe-jsand call redirectToCheckout:
CSP note: include https://js.stripe.comand Stripe's required frame/connect endpoints in your content security policy when you render Stripe.js or Elements.
7. Receive and verify webhooks
Webhook verification
StripeDaloyJS routeYour queue
01requestStripeDaloyJS routePOST /webhooks/stripeStripe-Signature over raw body
02noteDaloyJS routeDaloyJS routewebhooks.constructEventraw body + signature + whsec_ secret
03responseDaloyJS routeStripe400 when verification fails{ error: 'invalid signature' }
04asyncDaloyJS routeYour queueDedupe on event.id, then enqueue and ack200 fast, fulfill asynchronously
Stripe webhook verification fails if the body was parsed or re-serialized first. Verify the raw bytes, dedupe on event.id, and handle fulfillment from the signed event or by refetching the Checkout Session.
ts
import { z } from "zod";import type Stripe from "stripe";import { readRawBody } from "@daloyjs/core/raw";app.route({ method: "POST", path: "/webhooks/stripe", operationId: "stripeWebhook", responses: { 200: { description: "ack", body: z.object({ ok: z.literal(true) }) }, 400: { description: "bad signature", body: z.object({ error: z.string() }) }, }, handler: async ({ request, state }) => { const raw = await readRawBody(request); const signature = request.headers.get("stripe-signature"); let event: Stripe.Event; try { event = state.stripe.constructWebhookEvent(raw, signature); } catch { return { status: 400, body: { error: "invalid signature" } }; } if (await seenStripeEvent(event.id)) { return { status: 200, body: { ok: true as const } }; } switch (event.type) { case "checkout.session.completed": { const session = event.data.object as Stripe.Checkout.Session; // Fulfill idempotently on session.id or session.payment_intent. await enqueuePaidOrder(session.id, session.client_reference_id); break; } case "checkout.session.expired": // Release cart reservation, if you hold one. break; case "charge.refunded": case "refund.updated": // Reconcile refund state. break; } return { status: 200, body: { ok: true as const } }; },});
During local development, forward events with the Stripe CLI:
The CLI prints a temporary whsec_ secret. Use that local secret for forwarded events; do not mix it with the Dashboard endpoint secret.
8. Refunds
ts
// Full refund by PaymentIntent id.await state.stripe.refund({ paymentIntent: "pi_xxx", idempotencyKey: "refund:order_123",});// Partial refund, amount is in the smallest currency unit.await state.stripe.refund({ paymentIntent: "pi_xxx", amount: 250, // $2.50 for USD idempotencyKey: "refund:order_123:partial_1",});
Errors
The SDK throws structured Stripe.errors.StripeErrorsubclasses for API, card, authentication, rate-limit, and connection failures. Preserve requestId, code, decline_code, and payment_intent in internal logs, but return a stable problem+jsonshape to clients.
Runtimes
The official stripepackage is a server-side SDK and currently supports Node.js LTS versions 18+. It fits DaloyJS Node, serverless, and AWS Lambda deployments. The same SDK also documents a Deno npm import path. For strict edge workers, keep Checkout creation on a Node route or call Stripe's REST API with fetch from an isolated plugin instead of assuming every Node SDK feature works in the worker runtime.
Modernisation notes
Do not make Stripe a framework dependency. Keep it in the application, behind a plugin, so apps that use Braintree, Adyen, Square, or no payments at all keep a dependency-free DaloyJS core.
Use Checkout first, Payment Intents when you need control.Payment Intents are the lower-level API for custom payment forms and advanced flows. Start there only when hosted Checkout cannot model the experience.
Pin your API behavior deliberately. Stripe SDK types track the latest API shape. If your account uses an older pinned API version, test upgrades carefully and keep type suppressions rare and local.
Never use real card details in test mode. Use Stripe test cards or test PaymentMethod IDs. Real payment method details belong only in live mode through Stripe-hosted collection.