SDKs and the CLI
npm packages for the browser, your server and React Native, plus a command-line tool.
Which one do you need
The script tag is still the fastest way to start and it is not a lesser option: it is the same tracker, the same wire format and the same numbers. Reach for a package when a tag is the wrong tool rather than as an upgrade.
| Package | Use it when |
|---|---|
| (none — script tag) | An ordinary website. One line of HTML, no build step. |
| @termind/browser | A bundled app that wants types, or a consent flow that must not load a third-party script until the visitor agrees, or a CSP that forbids external scripts. |
| @termind/node | Server-side events an ad blocker cannot eat, revenue reported from the handler that took the payment, and AI crawler tracking. |
| @termind/react-native | A React Native or Expo app. Same identity as your website, so a web-to-app funnel is one journey rather than two. |
| @termind/cli | Checking an install from the machine that deployed it, and pulling numbers into scripts. |
Browser
npm install @termind/browserimport { createTermind } from "@termind/browser";
export const termind = createTermind({
projectKey: "pk_your_project_key",
// Page views fire automatically, including on client-side navigation.
});
termind.track("signed_up", { plan: "pro" });
termind.identify("user_123", { email: "maya@example.com" });Localhost is ignored unless you pass trackLocalhost: true, so development never pollutes real numbers.
For a consent banner, construct it with disabled: true and create it properly once the visitor agrees — or simply defer the import. Nothing is sent and no identifier is stored while disabled.
Your server
import { createTermindServer } from "@termind/node";
const termind = createTermindServer({ apiKey: process.env.TERMIND_API_KEY! });
// An event that an ad blocker can never remove.
termind.track("trial_started", { personId: "user_123" }, { plan: "pro" });
// Money, reported by the same code that charged the card.
await termind.revenue({
personId: "user_123",
amountMinor: 4900,
currency: "EUR",
plan: "Growth",
});
// Serverless functions exit fast; flush before returning.
await termind.flush();Use the same pk_ key the tracking script uses — Termind issues one key per website, found under Settings, then Tracking. It is safe in public HTML, which is why the browser package uses it too.
Seeing AI crawlers
An assistant fetches your HTML and leaves without running any JavaScript, so no browser SDK will ever see one. Call this from your middleware and Termind decides what is worth keeping.
// middleware.ts
import { createTermindServer } from "@termind/node";
const termind = createTermindServer({
apiKey: process.env.TERMIND_API_KEY!,
projectKey: process.env.NEXT_PUBLIC_TERMIND_KEY!,
});
export async function middleware(request: Request) {
const url = new URL(request.url);
await termind.crawl({
path: url.pathname,
userAgent: request.headers.get("user-agent"),
// Forward the IP so the claim can be confirmed. A user-agent is a string
// anyone can set; without an address it cannot be verified.
ip: request.headers.get("x-forwarded-for")?.split(",")[0] ?? null,
});
}React Native and Expo
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createTermindNative } from "@termind/react-native";
export const termind = createTermindNative({
projectKey: "pk_your_project_key",
storage: AsyncStorage,
appVersion: "2.1.0",
platform: "ios",
});
termind.screen("Pricing");
termind.track("subscribed", { plan: "pro" });
await termind.identify("user_123");Storage is your choice — AsyncStorage, MMKV, expo-secure-store — because the package will not force a dependency on an app that already made that decision. Anything with getItem, setItem and removeItem works.
Call identify with the same id your website uses and the two are one person. That is what makes a web-to-app funnel readable.
Command line
npx @termind/cli check yoursite.com
https://yoursite.com → 200
✓ Installed via the script tag
✓ Key pk_abc123It fetches the page and reads the served HTML rather than asking our API, because "the dashboard is empty" is usually a deploy that dropped the tag, and only the page itself can settle that. It exits non-zero when the snippet is missing, so it works as a deploy check.
export TERMIND_API_KEY=pk_your_project_key
termind stats --days 7
termind events --limit 20
termind track deploy_completed
termind stats --json | jq .visitors