Express
Convert an Express request to a bounded Web Request for the Cipay buyer handler.
Express uses Node request and response objects. Adapt them only at the route boundary; the Cipay handler itself remains a standard Request → Response function.
Connect your application session
Configure the handler
Mount the complete route
Call from the storefront
Verify payment signals
Step 1: Connect your application session
readVerifiedSession belongs to your application; it is not a Cipay SDK helper. This example uses a Better Auth instance exported from ./auth and reads only a server-verified session.
import { auth } from "./auth";
export async function readVerifiedSession(request: Request) {
const session = await auth.api.getSession({ headers: request.headers });
const cipayCustomerId = session?.user.cipayCustomerId;
if (!session || typeof cipayCustomerId !== "string") return null;
return { userId: session.user.id, cipayCustomerId };
}
If your session does not include the mapping, retrieve it from your database after verifying session.user.id. Never trust a customer ID sent by the browser.
Step 2: Configure the handler
Keep the key and fixed storefront locator in server configuration. Resolve customer ownership from your verified Express session, never browser input.
import {
createCipayBuyerHandler,
createCipayClient,
} from "@cipay/client-sdk/api";
import { readVerifiedSession } from "./cipay-session";
const client = createCipayClient({
apiKey: process.env.CIPAY_SANDBOX_API_KEY!,
mode: "sandbox",
});
export const cipayBuyerHandler = createCipayBuyerHandler({
client,
storefront: { locator: process.env.CIPAY_STOREFRONT_LOCATOR! },
resolveCustomer: async (request) => {
// Resolve ownership after server authentication succeeds.
const session = await readVerifiedSession(request);
return session
? { subject: session.userId, customerId: session.cipayCustomerId }
: null;
},
});
Step 3: Mount the complete Express route
The Web Request constructor needs an absolute URL. This complete adapter builds it from trusted deployment configuration, rate-limits before reading the body, rejects oversized writes, calls Cipay, and copies the Web Response back to Express.
import express, { type Request as ExpressRequest } from "express";
import { cipayBuyerHandler } from "./cipay-buyer";
import { allowCipayRequest, rateLimited } from "./rate-limit";
const app = express();
app.use("/api/cipay", async (request, response) => {
// Never derive the public origin from an untrusted Host header.
const incoming = new Request(
`${process.env.PUBLIC_APP_ORIGIN}${request.originalUrl}`,
{
method: request.method,
headers: request.headers as HeadersInit,
},
);
if (!allowCipayRequest(incoming)) {
const result = rateLimited();
response.status(result.status);
result.headers.forEach((value, name) => response.setHeader(name, value));
return response.send(Buffer.from(await result.arrayBuffer()));
}
// Buffer only writes, and stop before an oversized body reaches Cipay.
const body = request.method === "GET" || request.method === "HEAD"
? undefined
: await readBody(request, 16_384);
if (body === null) return response.status(413).send("Request body too large");
const result = await cipayBuyerHandler(new Request(incoming, {
body: body?.byteLength ? body : undefined,
}));
response.status(result.status);
result.headers.forEach((value, name) => response.setHeader(name, value));
return response.send(Buffer.from(await result.arrayBuffer()));
});
async function readBody(
request: ExpressRequest,
limit: number,
): Promise<Buffer | null> {
const chunks: Buffer[] = [];
let size = 0;
for await (const chunk of request) {
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
size += bytes.byteLength;
if (size > limit) return null;
chunks.push(bytes);
}
return Buffer.concat(chunks);
}
Mount this route before any JSON body parser so Cipay receives the original request bytes.
Step 4: Call the same-origin route
This plain browser example uses SDK-exported contract types instead of re-declaring the response. React applications can use CipayProvider and ProductsList for the same request.
import type { BuyerProduct, Page } from "@cipay/client-sdk/api";
export async function loadCatalog(): Promise<Page<BuyerProduct>> {
const response = await fetch("/api/cipay/v1/products?pageSize=8", {
credentials: "same-origin",
cache: "no-store",
});
if (!response.ok) throw new Error("Could not load the catalog");
return await response.json() as Page<BuyerProduct>;
}
Step 5: Verify the journey
Verify session ownership, catalog loading, quote preview, and hosted checkout. After return, use capability-protected status for UI while a separate verified webhook establishes durable payment state.
Complete example
Use the complete Express example to see the handler, bounded body reader, response adapter, and limiter assembled in one file.