Understand Customer identity, credentials, scope, sessions and account changes.
API v1Updated
Fix scope and identity first
Sandbox and Live hold separate Customers and business data for each App. Keys and Customer credentials stay bound to one scope; selecting UI or sending headers cannot switch authorization.
Your backend verifies App login, then identifies a Customer by stable externalUserId. Console members and Customers are different identities; a UUID or email is not authentication.
Your backend issues a short-lived Customer Token only for the signed-in user. The browser reads its own data without an arbitrary customerId.
Keep tokens in memory. Refresh through your authenticated backend after expiry. On sign-out or account changes, destroy old UI, cancel requests and clear previous data.
Guest login to an existing Customer switches identity without merging holdings or history. An existing Funnel retains its original Customer; use a new session and run after switching.
fingerprint is an optional stable deduplication signal supplied by your host, such as a browser installation identifier or a host-collected device signal. It is separate from login identity. A Product with unique_fingerprint also deduplicates trials across Customers sharing that signal; without one, trials still deduplicate by Customer. The SDK does not collect or generate it.
Use the same signal from the first Guest creation onward. Retain it through login, account changes and session renewal within the same installation.
A Customer records the first supplied signal; later values do not replace it. In-place Guest identification retains it. Switching to an existing Customer preserves each Customer's own record.
A business user ID differs per account and cannot provide cross-account deduplication. Customer Tokens, Guest Tokens, session tokens and request IDs have separate lifecycles and should not be used as fingerprints.
Web: retain an installation signal on the host
This HTTPS host example retains a random installation signal in a separate HttpOnly cookie. Call POST /api/installation before Guest/Customer initialization, and share that initialization request within the page. The cookie identifies browser storage, not attested hardware. Clearing cookies, changing browsers or expiration can produce a new value; signing out does not require resetting it.
After host authentication, pass readInstallationSignal(request) and the verified user ID separately to the backend function below. Keep the Secret Key on the backend; the browser cannot choose the signed-in user. Guest creation can use the same fingerprint, while its recoverable Guest Token is retained separately by the host and reused on creation retries.
server/installation.tsTypeScript
1import { randomUUID } from"node:crypto";23const cookieName = "__Host-indiana-install";4const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;56exportfunctionreadInstallationSignal(request: Request): string | undefined {7const value = request.headers.get("Cookie")?.split(";")8 .map((item) => item.trim())9 .find((item) => item.startsWith(cookieName + "="))10 ?.slice(cookieName.length + 1);11return value && uuid.test(value) ? value : undefined;12}1314// Mount on your HTTPS host as POST /api/installation.15// Initialize once before the first Guest or signed-in Customer request.16exportfunctioninitializeInstallation(request: Request): Response {17const headers = newHeaders({ "Cache-Control": "no-store" });18if (!readInstallationSignal(request)) {19 headers.set("Set-Cookie",20 cookieName + "=" + randomUUID() +21"; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=31536000");22 }23returnnewResponse(null, { status: 204, headers });24}
server/customer.tsTypeScript
1// Runs only on the trusted backend. The host verifies the user before calling.2exportasyncfunctioncustomerForVerifiedUser(3 verifiedUserId: string,4 installationSignal?: string,5) {6const response = awaitfetch(newURL("/api/v1/customers", process.env.INDIANA_BASE_URL!), {7 method: "POST",8 cache: "no-store",9 redirect: "error",10 headers: {11"Content-Type": "application/json",12Authorization: "Bearer " + process.env.INDIANA_SECRET_KEY!,13"X-Indiana-Project": process.env.INDIANA_PROJECT_ID!,14"X-Indiana-Environment": "sandbox",15 },16 body: JSON.stringify({17 externalUserId: verifiedUserId,18 ...(installationSignal === undefined ? {} : { fingerprint: installationSignal }),19 }),20 });21if (!response.ok) {22// Capture only these fields; never log the request body or credentials.23throwObject.assign(newError("Indiana Customer request failed"), {24 status: response.status,25 requestId: response.headers.get("X-Request-Id"),26 });27 }28return response.json();29}
Native: define installation storage and reset behavior
A native host can generate a random installation signal, persist it before use, and pass it as fingerprint through its backend. Reuse the stored value across launches, Guest sessions, login and account changes; do not regenerate it per request or session renewal. The host defines storage and behavior after reinstall, backup restoration or user reset. Do not assume reinstall always produces a new signal on every platform. Device attestation, if required, remains the host's responsibility.
Expected result
A second Customer with the same signal cannot obtain another trial of the same Product when signal uniqueness is enabled. A default paid trial falls through to a free default in its group, when present; an explicit start returns a conflict. Retain X-Request-Id for troubleshooting. Client messages do not expose another Customer, the signal or related records.