Save configuration in an uncommitted .env.local. Add INDIANA_VERIFIED_USER_ID using your verified test user ID. Browser configuration is used in the later Web integration.
.env.localShell
1# Copy these values from your App → Sandbox → SDK setup2INDIANA_BASE_URL=https://your-indiana-host.example3INDIANA_PROJECT_ID=your-project-id4INDIANA_SECRET_KEY=your-sandbox-server-key56# Public browser configuration; no secret key7NEXT_PUBLIC_INDIANA_BASE_URL=https://your-indiana-host.example8NEXT_PUBLIC_INDIANA_PROJECT_ID=your-project-id
2. Identify and read holdings
Save the complete script as quickstart.mjs and run it. All three requests use the same App/Sandbox. The final request uses the Customer Token to read its own data, without customerId.
Expected result
The script prints customerId and balances. Repeating an externalUserId in the same scope returns the same Customer. Empty balances still mean the connection worked; there is no eligible holding yet.
quickstart.mjsJavaScript
1// Node.js 22+. Run on your trusted backend. Never log keys or Customer Tokens.2const required = (name) => {3const value = process.env[name];4if (!value) thrownewError(`Missing ${name}`);5return value;6};7const baseUrl = required("INDIANA_BASE_URL");8const projectId = required("INDIANA_PROJECT_ID");9const serverKey = required("INDIANA_SECRET_KEY");10asyncfunctionpost(path, body, credential = serverKey) {11const response = awaitfetch(newURL(path, baseUrl), {12 method: "POST", redirect: "error", signal: AbortSignal.timeout(10000),13 headers: {14Authorization: `Bearer ${credential}`, "Content-Type": "application/json",15"X-Indiana-Project": projectId, "X-Indiana-Environment": "sandbox",16 },17 body: JSON.stringify(body),18 });19const result = await response.json();20if (!response.ok) thrownewError(`${response.status} ${result.error?.code ?? "HTTP_ERROR"} requestId=${result.requestId ?? "unknown"}`);21return result;22}23// Use an ID verified by your own login system.24const { customer } = awaitpost("/api/v1/customers", {25 externalUserId: required("INDIANA_VERIFIED_USER_ID"),26});27const { customerToken } = awaitpost("/api/v1/customer-sessions", { customerId: customer.id });28const { holdings } = awaitpost("/api/v1/customer-holdings", {}, customerToken);29console.log({ customerId: customer.id, balances: holdings.balances });
3. Connect your login system
In your app, put the first two requests in your authenticated backend: verify login → identify by stable user.id → return only customerToken/expiresAt. Return 401 anonymously and never trust a browser-supplied Customer ID.
Add the exact website Origin in SDK setup, including scheme, host and port. The browser gets a token from your session endpoint and reads customer-holdings. Keep the token in memory; cancel requests and clear prior state on sign-out or account changes.
Completion checklist
Anonymous session calls return 401; signed-in reads return the current Customer's holdings; no Server Key reaches the browser; account changes clear old data. Reads are for display; protected business work still requires backend authorization and atomic consumption.
Troubleshooting and next steps
What to check
Result
Action
UNAUTHENTICATED
Distinguish host login from Indiana credential failure. Check the Key or issue a fresh expired session.
FORBIDDEN
Check the operation permission and browser Origin.
SCOPE_MISMATCH
Match the Key, App ID, environment and server/browser configuration.
balances: []
The connection worked. Configure a Feature and free default Product, or purchase independently; no Funnel is required.
What to check
UNAUTHENTICATED
Action
Distinguish host login from Indiana credential failure. Check the Key or issue a fresh expired session.
FORBIDDEN
Action
Check the operation permission and browser Origin.
SCOPE_MISMATCH
Action
Match the Key, App ID, environment and server/browser configuration.
balances: []
Action
The connection worked. Configure a Feature and free default Product, or purchase independently; no Funnel is required.