SDKsPreview · 5 min read

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/browser

ts

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:

FieldWhat it isWhere it comes from
kidKID written into the token header so Perkamo knows which signing key to verify against. Safe to expose.Your signing key in Security.
secretThe signing key secret that signs the token. Sensitive — backend-only.Shown once when you create the signing key.
subjectThe user the token authorizes — your stable user id.Your authenticated application session.
scopePermissions the token may use: customer:read, events:write, stream:read. Clamped to the key policy at verify.You choose per route.
eventsOptional allow-list of event names the token may emit. Clamped to the key policy.You choose per route.
ttlSecondsToken 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.

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.