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
npm install @paylera/server @paylera/sdk# plus whichever framework you use:npm install express # or next / hono / fastifyFramework 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
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().
| Method | Path | Notes |
|---|---|---|
POST | /attach | One-shot signup. Idempotency required. |
POST | /check | Runtime gate. |
GET | /check?feature_code=... | Read-only gate. |
POST | /track | Usage event. Idempotency auto-stamped. |
GET | /entitlements | Bulk entitlement read. |
GET | /me | Resolved customer (auto-creates when configured). |
GET | /plans | Public plan catalog. |
GET | /invoices | Customer invoices. |
POST | /billing-portal | Mint a billing-portal URL. |
POST | /subscriptions/{id}/upgrade | Change plan. |
POST | /subscriptions/{id}/cancel | Cancel. |
GET | /csrf-token | Mint + 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
Secureoutside dev. - Pin
apiVersionso 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/.