Skip to content

TypeScript — relay (@paylera/server)

@paylera/server exposes a /api/paylera/* relay that holds the secret pl_live_* token on the server and forwards identity-injected calls to api.paylera.dev. Four framework adapters ship in the same package:

  • @paylera/server/express
  • @paylera/server/next — Next.js App Router
  • @paylera/server/hono
  • @paylera/server/fastify

Inbound-webhook receivers (HMAC-SHA-256 verification + per-event-type handler dispatch) ship under @paylera/server/webhook — see the webhooks page.

Install

Terminal window
npm install @paylera/server @paylera/sdk
# plus whichever framework you use:
npm install express # or next / hono / fastify

Framework peer-deps are all optional — install only what you use.

Express

import express from "express";
import { payleraExpress } from "@paylera/server/express";
const app = express();
app.use(express.json());
app.use("/api/paylera", requireAuth, payleraExpress({
apiToken: process.env.PAYLERA_API_TOKEN!,
identify: (req) => ({
customerId: req.user.payleraCustomerId, // null = auto-create
email: req.user.email,
name: req.user.name,
}),
autoCreateCustomer: true,
csrf: true,
}));

Mount the middleware after your session-auth layer (requireAuth here). The relay only forwards once identify() has resolved a customer.

Next.js App Router

app/api/paylera/[...path]/route.ts
import { payleraNextRouteHandler } from "@paylera/server/next";
export const { GET, POST, DELETE, PATCH } = payleraNextRouteHandler({
apiToken: process.env.PAYLERA_API_TOKEN!,
identify: async (req) => {
const session = await getSession(req);
return {
customerId: session.user.payleraCustomerId,
email: session.user.email,
};
},
autoCreateCustomer: true,
csrf: true,
});

The catch-all segment [...path] lets one route file serve every relay subpath. Be sure to mark the file export const dynamic = "force-dynamic" so Next doesn’t try to cache it.

Hono

import { Hono } from "hono";
import { payleraHonoHandler } from "@paylera/server/hono";
const app = new Hono();
app.all("/api/paylera/*", payleraHonoHandler({
apiToken: process.env.PAYLERA_API_TOKEN!,
identify: (c) => ({
customerId: c.get("user").payleraCustomerId,
email: c.get("user").email,
}),
autoCreateCustomer: true,
csrf: true,
}));

Fastify

import Fastify from "fastify";
import { payleraFastify } from "@paylera/server/fastify";
const app = Fastify();
await app.register(payleraFastify, {
prefix: "/api/paylera",
apiToken: process.env.PAYLERA_API_TOKEN!,
identify: (req) => ({
customerId: (req as never)["user"]?.payleraCustomerId,
email: (req as never)["user"]?.email,
}),
autoCreateCustomer: true,
csrf: true,
});

The ten relay routes

Every adapter exposes the same routes defined in the SDK relay protocol. The frontend never supplies a customer_id directly; the relay injects it from identify().

MethodPathNotes
POST/attachOne-shot signup. Idempotency required.
POST/checkRuntime gate.
GET/check?feature_code=...Read-only gate.
POST/trackUsage event. Idempotency auto-stamped.
GET/entitlementsBulk entitlement read.
GET/meResolved customer (auto-creates when configured).
GET/plansPublic plan catalog.
GET/invoicesCustomer invoices.
POST/billing-portalMint a billing-portal URL.
POST/subscriptions/{id}/upgradeChange plan.
POST/subscriptions/{id}/cancelCancel.
GET/csrf-tokenMint + cookie. Browser-only.

Configuration

payleraExpress({
apiToken: string; // pl_live_* or pl_test_*
apiVersion?: string; // pin a specific date
baseUrl?: string; // self-hosted / staging override
identify: (req) => Identity | Promise<Identity>;
autoCreateCustomer?: boolean; // default false
csrf?: boolean; // default true
cookiePath?: string; // default "/api/paylera"
});

The Identity envelope:

interface PayleraCustomerIdentity {
customerId: string | null; // null + autoCreate=true → POST /v1/customers
email?: string;
name?: string;
}

CSRF

The relay mints a token at GET /api/paylera/csrf-token, sets the paylera_csrf cookie (Path=/api/paylera, SameSite=Lax, Secure in production), and validates the Paylera-CSRF-Token header against the cookie on every state-changing route via crypto.timingSafeEqual. Mismatch → 403 with a problem-detail of type paylera.csrf_mismatch.

@paylera/react handles the browser side; you only have to mount this middleware.

Idempotency

The relay forwards a caller-supplied Idempotency-Key header verbatim. When the caller omits one on a required-idempotency route (attach, track, billing-portal, subscriptions/{id}/upgrade, subscriptions/{id}/cancel), the relay auto-stamps a UUID v7. track also accepts a dedup_key body field that becomes the idempotency key.

Auto-create customer

When identify() returns customerId: null and autoCreateCustomer: true, the relay calls POST /v1/customers with the rest of the identity envelope. The auto-create idempotency key is paylera-relay-autocreate:{tenantId}:{email}, so two concurrent first-time useFeature() calls for the same user resolve to the same customer.

Persist the returned id back to your user table so subsequent identify() calls return it directly and skip the lookup.

Cross-package compatibility

@paylera/server@X.Y.Z peer-depends on @paylera/sdk@^X.Y.Z (matching major).

Production checklist

  • Mount your auth middleware before the relay.
  • Serve over HTTPS in production; the CSRF cookie is Secure outside dev.
  • Pin apiVersion so SDK upgrades don’t silently change semantics.
  • Front the route with rate-limiting; the relay does not.
  • Keep the API token in a real secret manager.

Example

A minimal Express + relay example lives at examples/typescript/express/ in the monorepo. A full Next.js example with SSR entitlements lives at examples/typescript/nextjs/.