TypeScript — getting started
The TypeScript SDK family is four packages that share a versioning policy and snap together:
| Package | What it is | Runtime |
|---|---|---|
@paylera/sdk | Typed HTTP client for the Paylera REST API. | Node ≥ 18, Bun, Deno, Cloudflare Workers, Vercel Edge, browsers. |
@paylera/react | <PayleraProvider> + 12 hooks. | React 18 / 19. |
@paylera/server | Merchant-backend relay + webhook receivers. | Node ≥ 18. Express, Next.js, Hono, Fastify. |
@paylera/cli | Declarative paylera.config.ts deploy CLI. | Node ≥ 18. |
If you only need to make server-side calls (a worker job, a CRON, a CI script), @paylera/sdk alone is enough. Browser-facing apps need @paylera/server (to hide the secret token) plus, if React, @paylera/react.
Install
npm install @paylera/sdk# or, all four:npm install @paylera/sdk @paylera/react @paylera/server @paylera/cliOpenTelemetry instrumentation is opt-in — install the peer dep only if you want spans + metrics:
npm install @opentelemetry/api@paylera/sdk — core HTTP client
import { PayleraClient } from "@paylera/sdk";
const paylera = new PayleraClient(process.env.PAYLERA_API_TOKEN!);
const customer = await paylera.customers.create({ type: "business", name: "Acme", currency: "USD",});
const decision = await paylera.check({ feature_code: "api_calls", customer_id: customer.id, required_usage: 1,});
if (decision.allowed) { // serve the request await paylera.track({ feature_code: "api_calls", value: 1 });}Token-prefix base-URL routing
The client derives the base URL from the token prefix:
| Token prefix | Base URL |
|---|---|
pl_live_* | https://api.paylera.dev |
pl_test_* | https://api.test.paylera.dev |
| anything else | explicit opts.baseUrl required |
An explicit baseUrl always wins (self-hosted Paylera, staging environments).
Construction options
new PayleraClient(apiToken, { baseUrl, // override token-prefix routing fetch, // (input, init) => Promise<Response> — defaults to global fetch tracer, // OpenTelemetry Tracer; spans emitted as `Paylera.<Op>` meter, // OpenTelemetry Meter; emits `paylera.requests.{total,duration}` retry, // RetryConfig | false (see below) defaultHeaders, // attached to every request apiVersion, // default "2026-05-01" userAgent, // appended to outgoing User-Agent sleep, // (ms) => Promise<void> — override for tests});Idempotency-Key auto-stamping
Every endpoint flagged with a required Idempotency-Key in the OpenAPI spec gets one auto-stamped (UUID v7). Override per call:
await paylera.track( { feature_code: "api_calls", value: 1 }, { idempotencyKey: "req-abc123" },);Auto-stamped endpoints: POST /v1/customers, POST /v1/subscriptions, POST /v1/subscriptions/{id}/cancel-at-period-end, POST /v1/subscriptions/{id}/change-plan, POST /v1/payment-methods, POST /v1/payment-methods/{id}/set-default, POST /v1/coupons/{code}/redeem, POST /v1/attach, POST /v1/track.
Retry policy
Default: 5 attempts, exponential backoff (200 ms → 400 ms → 800 ms → 1.6 s → 3.2 s), only on 5xx + 429 (4xx is never retried). On 429 the Retry-After response header overrides the computed backoff.
new PayleraClient(token, { retry: false }); // disablenew PayleraClient(token, { // tune retry: { maxAttempts: 3, initialDelayMs: 100, multiplier: 3, maxDelayMs: 5_000, jitter: 0.2, },});await paylera.check({ feature_code: "x" }, { retry: false }); // per callTyped errors (RFC 9457)
Every non-2xx response is parsed as RFC 9457 problem-details and thrown as the matching subclass:
import { PayleraError, PayleraAuthenticationError, PayleraAuthorizationError, PayleraNotFoundError, PayleraPreconditionFailedError, PayleraRateLimitError, PayleraIdempotencyConflictError, PayleraIdempotencyInProgressError, PayleraServerError,} from "@paylera/sdk";
try { await paylera.subscriptions.changePlan(id, { new_plan_id: "..." });} catch (err) { if (err instanceof PayleraRateLimitError) { await sleep(err.retryAfter ?? 1); } else if (err instanceof PayleraError) { console.error(err.code, err.requestId, err.problem); }}The type URI is the primary discriminator; HTTP status is the fallback.
Endpoint surface
The client exposes one method per public API endpoint (36 operations):
paylera.customers.*— create, get, list, update, listInvoices, listPaymentMethodspaylera.products.*— create, get, list, updatepaylera.plans.*— create, get, list, update, addComponentpaylera.subscriptions.*— create, get, list, update, cancel, pause, resume, changePlan, entitlementspaylera.paymentMethods.*— attach, get, detach, setDefaultpaylera.coupons.*— redeempaylera.payoutBatches.*— list, get, listItemspaylera.attach(…),paylera.check(…),paylera.track(…)
Types for every request body and response payload are generated from the OpenAPI spec — import type { Schemas } from "@paylera/sdk".
OpenTelemetry
Spans are named Paylera.<Op> (e.g. Paylera.Check, Paylera.Attach). Attribute keys conform to the cross-language SDK protocol:
| Key | Value |
|---|---|
paylera.endpoint | path template, e.g. /v1/check |
paylera.api_mode | "live" / "test" / "custom" |
paylera.idempotency_key | first 8 chars of the key |
paylera.problem.type | RFC 9457 type on error |
Metrics:
paylera.requests.total(counter,{request}) labelled byendpoint,status_code,api_modepaylera.requests.duration(histogram,s) same labels
Both span + metric emission are no-op when no tracer/meter is supplied.
import { trace, metrics } from "@opentelemetry/api";
const paylera = new PayleraClient(token, { tracer: trace.getTracer("my-app"), meter: metrics.getMeter("my-app"),});Next steps
- React app? Mount
<PayleraProvider>and consume the granular hooks. - Browser-facing app? Mount the merchant relay so the secret token stays server-side.
- Receiving webhooks? Wire up
@paylera/server/webhook. - Want declarative config? Drive your tenant from
paylera.config.ts.
Examples
Three end-to-end example apps live under examples/typescript/ in the monorepo:
examples/typescript/nextjs/— Next.js 15 App Router with relay route, SSR entitlements, gated feature.examples/typescript/express/— Express server mounting the relay; exercise withcurl.examples/typescript/webhook-receiver/— standalone Express webhook receiver.