Skip to content
Indiana
Dashboard
Docs
Browse docs / Server API key authentication

Server API key authentication

Create server keys, configure permissions, and handle revocation and rotation.

API v1Updated

Start with three identities

Server API calls use a gg_sk_ API key. The key is bound to one App/environment, and Indiana checks its permissions on every request. Server reads and writes do not require issuing a customer token first.

Who holds each credential
CallerCredentialPurpose
Your backendINDIANA_SECRET_KEY / gg_sk_Call Indiana within the App/environment and permissions
Your clientcustomerToken / gg_ct_Call allowed customer endpoints directly; issued by your backend
Indiana console memberSupabase AuthSign in to the console and create Apps and keys

Who holds each credential

  • Your backend

    Credential
    INDIANA_SECRET_KEY / gg_sk_
    Purpose
    Call Indiana within the App/environment and permissions
  • Your client

    Credential
    customerToken / gg_ct_
    Purpose
    Call allowed customer endpoints directly; issued by your backend
  • Indiana console member

    Credential
    Supabase Auth
    Purpose
    Sign in to the console and create Apps and keys

A Customer is an Indiana business record mapped to your product's user by App, environment and externalUserId. It does not share console login credentials. Your authentication system verifies end users; an Indiana key does not prove user login. Complete the server-key-only flow below before considering client sessions.

1. Create a Sandbox server key

  • Sign in as an Owner, Admin or Developer with App access and apiKeys:write, then select your App and Sandbox in SDK setup.
  • Create a key with customers:write, the only permission required for the first request below. Plaintext is shown once; save it in your backend configuration.
  • Copy the base URL and App ID from the same page. Use the site root as base URL; do not append /api/v1. Server-only requests do not require a browser origin.

A key is bound to one App/environment and expires in 1–365 days. Create a replacement to change permissions or expiry. Each App/environment supports up to 20 active keys.

2. Configure and verify your Key

Use Node.js 22+ and native fetch to verify the Key without an SDK. This verifies server authentication; the quickstart continues with Customer sessions and Web reads.

Save a Git-ignored .env.local in your app root and replace the site root, App ID and key below with the values from SDK setup. Read INDIANA_SECRET_KEY on the backend only; never prefix it with NEXT_PUBLIC_.

Create indiana-server-check.mjs in the same directory and copy the complete script below. It creates and retains one Sandbox test customer. It does not issue a customer token. Eligible free default Products can initialize; paid Products/trials require independent billing.

First request complete

Success includes requestId, Sandbox scope and customerId. Rerunning returns the same customerId with created: false, indicating an existing Customer was found.

In your application, replace the fixed test externalUserId with a stable user ID verified by your backend or authorized for your business task. A browser-supplied customerId or user ID is not authorization.

.env.localShell
# Copy Sandbox values from SDK setup; ignore this file in Git.INDIANA_BASE_URL=https://your-indiana-host.exampleINDIANA_PROJECT_ID=your-app-idINDIANA_SECRET_KEY=your-sandbox-server-key
indiana-server-check.mjsJavaScript
class ConfigurationError extends Error {}class RequestError extends Error {  constructor(code, status, requestId) { super(code); this.code = code; this.status = status; this.requestId = requestId; }}function required(name) {  const value = process.env[name];  if (!value) throw new ConfigurationError("Missing environment variable: " + name);  return value;}try {  const baseUrl = required("INDIANA_BASE_URL");  const key = required("INDIANA_SECRET_KEY");  const projectId = required("INDIANA_PROJECT_ID");  const response = await fetch(new URL("/api/v1/customers", baseUrl).href, {    method: "POST", redirect: "error", signal: AbortSignal.timeout(10000),    headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json",      "X-Indiana-Project": projectId, "X-Indiana-Environment": "sandbox" },    body: JSON.stringify({ externalUserId: "docs-server-auth-check" }),  });  const body = await response.json();  if (!response.ok) throw new RequestError(body.error?.code ?? "HTTP_ERROR", response.status, body.requestId);  const { requestId, scope, customer, created } = body;  console.log(JSON.stringify({ requestId, scope, customerId: customer.id, created }, null, 2));} catch (error) {  if (error instanceof RequestError) console.error(JSON.stringify({ code: error.code, status: error.status, requestId: error.requestId }));  else if (error instanceof ConfigurationError) console.error(error.message);  else console.error("The request did not complete. Check Node, the environment file and network configuration.");  process.exitCode = 1;}
TerminalShell
node --env-file=.env.local indiana-server-check.mjs
Example success outputJSON
{  "requestId": "<request-id>",  "scope": {    "projectId": "<your-app-id>",    "environment": "sandbox"  },  "customerId": "<customer-id>",  "created": true}

Verify the same request with curl

HTTP uses the same key and permission as the SDK. Set INDIANA_BASE_URL, INDIANA_PROJECT_ID and INDIANA_SECRET_KEY in your terminal environment before this command; Node's --env-file does not export variables to your shell.

Success returns HTTP 200 with requestId, scope, created and customer. Repeating the same App, environment and externalUserId returns the existing customer.

TerminalShell
curl -X POST "$INDIANA_BASE_URL/api/v1/customers" \  -H "Authorization: Bearer $INDIANA_SECRET_KEY" \  -H "X-Indiana-Project: $INDIANA_PROJECT_ID" \  -H "X-Indiana-Environment: sandbox" \  -H "Content-Type: application/json" \  -d '{"externalUserId":"docs-server-auth-check"}'

3. Add permissions as needed

Common SDK methods and permissions
SDK methodHTTPCredentialPermission
getOrCreateCustomerPOST /api/v1/customersServer keycustomers:write
getCustomerPOST /api/v1/customers/getServer keycustomers:read
readHoldingsPOST /api/v1/customer-holdingsServer key / customer tokenentitlements:read
mutateBalancePOST /api/v1/balances/executeServer keycredits:consume
createCustomerSessionPOST /api/v1/customer-sessionsServer keysessions:create

Common SDK methods and permissions

  • getOrCreateCustomer

    HTTP
    POST /api/v1/customers
    Credential
    Server key
    Permission
    customers:write
  • getCustomer

    HTTP
    POST /api/v1/customers/get
    Credential
    Server key
    Permission
    customers:read
  • readHoldings

    HTTP
    POST /api/v1/customer-holdings
    Credential
    Server key / customer token
    Permission
    entitlements:read
  • mutateBalance

    HTTP
    POST /api/v1/balances/execute
    Credential
    Server key
    Permission
    credits:consume
  • createCustomerSession

    HTTP
    POST /api/v1/customer-sessions
    Credential
    Server key
    Permission
    sessions:create

SDK scope must match the key's App/environment. X-Indiana-Project and X-Indiana-Environment assert the expected scope; changing them cannot authorize another App. Direct HTTP may omit them, but must send both when used.

Refunds additionally require credits:manage alongside credits:consume. Funnel start requires sessions:create and funnels:run. Check each endpoint for additional conditions.

Related:All endpoints and permissions

Optional: let your client access Indiana directly

If your application only calls Indiana from its backend, the flow above is sufficient. Add customer sessions when the browser needs current-customer data or Funnel presentation.

  • Your backend verifies login through your authentication system, then maps the user's Customer.
  • Add sessions:create to the key plus the client's required permissions, such as entitlements:read.
  • Your backend calls POST /api/v1/customer-sessions with { customerId }, returns customerToken/expiresAt only to the verified user, and sets no-store.
  • The client sends Authorization: Bearer <customerToken> to Indiana. Add the browser origin to the current App/environment allowlist.

Customer tokens normally last 10 minutes, capped by key expiry. Only entitlements:read, funnels:run, billing:read, billing:write, offers:resolve and credits:read are delegated. Every request rechecks the issuing key and permissions. Guest sessions receive funnels:run only.

There is no Public SDK Key plus arbitrary user-ID bootstrap. Ordinary SDK handlers reject server-key requests carrying Origin; keep secret keys out of browsers. See the Funnel reference for its separate gateway's Origin rules.

Related:Complete backend session endpoint and browser integration files

Related:Funnel credentials and execution

Troubleshoot your first request

Common failures and next steps
HTTP / codeCheck first
401 / UNAUTHENTICATEDCheck that the full key was copied and loaded by your backend and is not revoked or expired. Refreshing a customer token cannot fix a server-key failure.
403 / FORBIDDENThe first request requires customers:write. Ordinary server requests must not carry a browser Origin.
403 / SCOPE_MISMATCHCheck the App ID and sandbox/live against the key's scope.
400 / INVALID_REQUESTCheck the DTO, paired scope headers and JSON body.
500 / INTERNAL_ERROR; 503 / SERVICE_UNAVAILABLERecord code, status and requestId to investigate dependencies/network failures. Preserve the original identity and input for write retries.
501 / NOT_CONFIGUREDA required Indiana service is unconfigured; its operator must resolve it.

Common failures and next steps

  • 401 / UNAUTHENTICATED

    Check first
    Check that the full key was copied and loaded by your backend and is not revoked or expired. Refreshing a customer token cannot fix a server-key failure.
  • 403 / FORBIDDEN

    Check first
    The first request requires customers:write. Ordinary server requests must not carry a browser Origin.
  • 403 / SCOPE_MISMATCH

    Check first
    Check the App ID and sandbox/live against the key's scope.
  • 400 / INVALID_REQUEST

    Check first
    Check the DTO, paired scope headers and JSON body.
  • 500 / INTERNAL_ERROR; 503 / SERVICE_UNAVAILABLE

    Check first
    Record code, status and requestId to investigate dependencies/network failures. Preserve the original identity and input for write retries.
  • 501 / NOT_CONFIGURED

    Check first
    A required Indiana service is unconfigured; its operator must resolve it.

Record code, status and requestId, then follow retryable/recovery. Recover the original business operation after a timeout or lost response. See the error reference for fields and version requirements. Never log keys or tokens.

Related:Full error reference

Revoke and rotate keys

  • Server-only rotation: create a replacement key for the same App/environment with required permissions, update backend configuration, confirm requests, then revoke the old key.
  • For client integration, also issue customer sessions with the new key and update clients before revoking the old key. renewCustomerSession requires the same issuer; it cannot renew across keys.
  • After revocation or expiry, the key and customer tokens it issued fail subsequent authentication. Committed operations are not rolled back.
  • The SDK does not renew automatically. Reauthenticate through the host before issuing or renewing customer tokens. grow.reset clears memory only; revoke sessions explicitly through the backend's revokeCustomerSessions.

Related:Customer-session lifecycle and sign-out