Skip to content
Cipay
Esc
navigateopen⌘Jpreview
On this page

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

Adapt your existing server auth to the customer shape Cipay needs.

Configure the handler

Create the server client and fixed storefront locator.

Mount the complete route

Validate, rate-limit, bound, forward, and return the request.

Call from the storefront

Use the same-origin mounted route.

Verify payment signals

Test protected status and signed webhooks separately.

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.