Browser SDK
Use @perkamo/browser only with short-lived client tokens returned by your own backend. Never put a Perkamo server API key in browser, mobile or embedded widget code. The browser package is preview.
Browser integrations are always a backend plus frontend implementation. The frontend package calls your backend first; your backend verifies the user's application session and returns a short-lived client token. /api/perkamo/token below is your application route, not a Perkamo API route.
Install with a bundler
bash
npm install @perkamo/browserts
import {
createPerkamoBrowserClient,
mountPerkamoProgressWidget,
} from "@perkamo/browser";
const client = createPerkamoBrowserClient({
getToken: async () =>
(await fetch("/api/perkamo/token", { method: "POST" }))
.json()
.then((body) => body.token),
});
const customer = await client.getCustomerJson();
document.querySelector("#points").textContent = String(customer.wallets.points ?? 0);
mountPerkamoProgressWidget({ client, target: "#perkamo-progress" });Load from CDN
For storefronts or widgets without a bundler, load the standalone browser build from the free jsDelivr npm CDN:
The CDN build exposes window.PerkamoBrowser. Pin the package version so production pages load a reviewed browser bundle. UNPKG serves the same npm package as an alternative: https://unpkg.com/@perkamo/browser@0.10.0/dist/perkamo-browser.global.min.js.
html
<script src="https://cdn.jsdelivr.net/npm/@perkamo/browser@0.10.0/dist/perkamo-browser.global.min.js"></script>
<script>
const client = PerkamoBrowser.createPerkamoBrowserClient({
getToken: () =>
fetch("/api/perkamo/token", { method: "POST", credentials: "include" })
.then((response) => response.json())
.then((body) => body.token),
});
PerkamoBrowser.mountPerkamoProgressWidget({
client,
target: "#perkamo-progress",
});
</script>Backend token route
The browser package never creates tokens itself. Your backend route verifies the logged-in user and returns a short-lived client token that the frontend sends as a bearer credential. /api/perkamo/token in these examples is your own application route, not a Perkamo endpoint.
First create a signing key in the Perkamo console under Settings → Security → Signing keys. Creating one gives you two values:
- a KID — shown in the creation dialog and then listed in the signing keys table
- a one-time secret — shown once on creation; store it backend-only
Allowed websites are configured once for the Space in Security; signing keys are not scoped per website.
Token values
Every client token is built from these fields:
| Field | What it is | Where it comes from |
|---|---|---|
kid | KID written into the token header so Perkamo knows which signing key to verify against. Safe to expose. | Your signing key in Security. |
secret | The signing key secret that signs the token. Sensitive — backend-only. | Shown once when you create the signing key. |
subject | The user the token authorizes — your stable user id. | Your authenticated application session. |
scope | Permissions the token may use: customer:read, events:write, stream:read. Clamped to the key policy at verify. | You choose per route. |
events | Optional allow-list of event names the token may emit. Clamped to the key policy. | You choose per route. |
ttlSeconds | Token lifetime in seconds (default 600). Clamped to the key's maximum at verify. Keep it short. | You choose per route. |
Only the secret is sensitive; the kid is public and travels in every token header.
Self-sign on your backend (recommended)
Sign the token locally with the signing key secret — no round-trip to Perkamo. Read the kid and secret from your backend environment (the variable names below are your own choice; any secret manager works the same way):
ts
import { signClientToken } from "@perkamo/sdk";
// Your backend route handler, after you have verified the user's session.
export async function issuePerkamoToken(userId: string) {
return signClientToken({
kid: process.env.PERKAMO_SIGNING_KID!, // KID
secret: process.env.PERKAMO_SIGNING_SECRET!, // one-time secret, backend-only
subject: userId,
scope: ["customer:read", "events:write"],
events: ["page.view", "cart.add"],
ttlSeconds: 600,
});
}Return { token } from the route; the browser SDK reads it in getToken.
Next.js apps can mount a ready-made route that does the same thing:
ts
// app/api/perkamo/token/route.ts
import { createPerkamoClientTokenRoute } from "@perkamo/nextjs/server";
import { auth } from "@/auth";
export const POST = createPerkamoClientTokenRoute({
kid: process.env.PERKAMO_SIGNING_KID,
signingSecret: process.env.PERKAMO_SIGNING_SECRET,
getUserId: async () => (await auth())?.user?.id ?? null,
scope: ["customer:read", "events:write"],
});kid and signingSecret also accept an async function, for loading them from a secret manager instead of environment variables.
Or let Perkamo mint it
If you prefer not to handle signing, build a server client with your server API key and let Perkamo sign the token with the same signing key:
ts
import { createPerkamoClient } from "@perkamo/sdk";
const perkamo = createPerkamoClient({ apiKey: process.env.PERKAMO_SECRET_KEY });
const { token } = await perkamo.createClientToken(userId, {
scope: ["customer:read", "events:write"],
});Validate any token during development with perkamo.verifyClientToken(token), which reports signature, type, lifetime and scope. The console also has a token tester under Security.
Customer data
getCustomerJson() returns a JSON-safe customer snapshot for storefront or app UI code. It omits traits by default; pass { includeTraits: true } only for pages that explicitly need customer traits.
Customer streams require a separate getStreamToken provider so regular bearer tokens are not written into EventSource URLs. Customer streaming remains preview; see Realtime streaming.
Handle API errors
Non-2xx client-route responses throw PerkamoApiError. The error includes the HTTP status, parsed body and operational metadata when available:
ts
try {
await client.emit("page.viewed", { path: location.pathname });
} catch (error) {
if (error instanceof PerkamoBrowser.PerkamoApiError) {
console.error(error.status, error.requestId, error.retryAfter, error.rateLimit);
}
}Security defaults
The browser package accepts only getToken, never a server API key, and rejects secret-shaped runtime options before sending requests. It also rejects server-authoritative event context fields such as xp, wallet, wallets, level, perks, rewards and achievements.
The browser client defaults to the hosted Perkamo API. Set baseUrl only when Perkamo support supplies a different API URL.
Read Security and keys before shipping a browser or mobile integration.