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

Server API key authentication

Create server keys and understand per-request verification, scope, permissions, revocation and rotation.

SDK 0.0.1Updated

Start with three identities

Ordinary server SDK 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 backendGROOVE_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
    GROOVE_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. Install, configure and make your first request

Use Node.js 22 or later. This guide pins the published npm 0.0.1 beta; install it directly.

Save a Git-ignored .env.local in your app root and replace the App ID and key below. For self-hosted Indiana, use your own site root. Read GROOVE_SECRET_KEY on the backend only; never prefix it with NEXT_PUBLIC_.

Create groove-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. Customer creation can assign an eligible configured default plan or automatic trial.

First request complete

requestId, Sandbox scope and customerId confirm that Indiana verified the key and completed the database operation. Rerunning returns the same customerId with created: false, which is also success. Constructing the SDK makes no authentication request; verification occurs when an API is called.

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.

TerminalShell
npm install @tinkerlandai/indiana-sdk@0.0.1
.env.localShell
# Copy Sandbox values from SDK setup; ignore this file in Git.GROOVE_BASE_URL=https://groove-grow.vercel.appGROOVE_PROJECT_ID=your-app-idGROOVE_SECRET_KEY=your-sandbox-server-key
groove-server-check.mjsJavaScript
import { createGrooveServer, GrooveError } from "@tinkerlandai/indiana-sdk/server"; class ConfigurationError extends Error {} function required(name) {  const value = process.env[name];  if (!value) throw new ConfigurationError("Missing environment variable: " + name);  return value;} try {  const groove = createGrooveServer({    baseUrl: required("GROOVE_BASE_URL"),    apiKey: required("GROOVE_SECRET_KEY"),    scope: {      projectId: required("GROOVE_PROJECT_ID"),      environment: "sandbox",    },  });   // A fixed Sandbox test identity; reruns return the same Customer.  const { requestId, scope, customer, created } =    await groove.getOrCreateCustomer({      externalUserId: "docs-server-auth-check",    });   console.log(JSON.stringify({    requestId, scope, customerId: customer.id, created,  }, null, 2));} catch (error) {  if (error instanceof GrooveError) {    // Log diagnostic fields only, never keys, tokens or the full exception.    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 groove-server-check.mjs
Example success outputJSON
{  "requestId": "<request-id>",  "scope": {    "projectId": "<your-app-id>",    "environment": "sandbox"  },  "customerId": "<customer-id>",  "created": true}

You can also call HTTP directly

HTTP uses the same key and permission as the SDK. Set GROOVE_BASE_URL, GROOVE_PROJECT_ID and GROOVE_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 "$GROOVE_BASE_URL/api/v1/customers" \  -H "Authorization: Bearer $GROOVE_SECRET_KEY" \  -H "X-Groove-Project: $GROOVE_PROJECT_ID" \  -H "X-Groove-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-Groove-Project and X-Groove-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 createCustomerSession(customer.id), returns customerToken/expiresAt only to the verified user, and sets no-store.
  • After identify(customerToken), the client can call Indiana directly. 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.

HTTP errors use { requestId, error: { code, message } }. SDK GrooveError carries code, status and requestId; the SDK does not retry automatically. The script logs these diagnostic fields only, never 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

Appendix: how Indiana verifies keys

gg_sk_ is an opaque random Bearer credential sent over HTTPS. Plaintext is returned only at creation; the database stores a SHA-256 hash. It is not a Supabase user JWT or a per-request body signature.

  • Each request parses Bearer, hashes it with SHA-256 and looks it up through grow_authenticate_sdk.
  • Check revocation, expiry and App/environment records, build a verified principal, then validate endpoint permissions, scope, Origin and input.
  • Execute business and ownership checks with trusted scope, then validate the response. Successful authentication is not cached across requests; failed verification cannot execute the operation.

Appendix: internal credentials and database access

Integrating apps configure GROOVE_SECRET_KEY only. SUPABASE_SECRET_KEY is Indiana's internal database credential; you do not need it. The SDK does not expose a database connection. Customers live in customers, key hashes in api_keys, and session hashes in customer_sessions.

Ordinary console operations use member Supabase Auth, current membership and RLS, with writes through grow_console_writer. The SDK gateway and scoped business RPCs are separately documented service_role exceptions, relying on trusted scope and ownership/business checks rather than console-member RLS.

The current Redis request-limit policy is observe: record without blocking. A RATE_LIMITED error contract does not establish enforced rate limiting.

Related:Server SDK method reference