# Copy these values from your App → Sandbox → SDK setupGROOVE_BASE_URL=https://your-grow-host.exampleGROOVE_PROJECT_ID=your-project-idGROOVE_SECRET_KEY=your-sandbox-server-key # Public browser configuration; no secret keyNEXT_PUBLIC_GROOVE_BASE_URL=https://your-grow-host.exampleNEXT_PUBLIC_GROOVE_PROJECT_ID=your-project-idCode examples
Complete integration files and common recipes. Connect your own authentication and configuration.
Download integration exampleNext.js: identity and customer sessions
Use these files in quickstart order. The host integration seam is your existing getSignedInUser function; it must verify the actual login session.
Refresh sessions and sign out
Add this module alongside your grow client. Refresh once only on UNAUTHENTICATED and propagate failures. Call disconnectSession on sign-out or user change to abort the host session fetch and old SDK requests, then call your sign-out endpoint. Write retries retain their original requestId/idempotency key.
import { GrooveError } from "@tinkerlandai/indiana-sdk";import { grow } from "./groove-client"; // Get a fresh token from your own authenticated session endpointlet generation = 0;let pending: { controller: AbortController; promise: Promise<void> } | undefined; function refreshSession(): Promise<void> { if (pending) return pending.promise; const expected = generation; const controller = new AbortController(); const promise = (async () => { const response = await fetch("/api/groove-session", { signal: controller.signal, cache: "no-store", credentials: "same-origin", }); if (!response.ok) throw new Error("User is not signed in"); const { customerToken } = await response.json(); if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed during refresh"); if (typeof customerToken !== "string") throw new Error("Invalid session"); grow.identify(customerToken); })(); pending = { controller, promise }; const clear = () => { if (pending?.promise === promise) pending = undefined; }; void promise.then(clear, clear); return promise;} // Refresh once and retry when the token expires (10 minutes by default)export async function withSession<T>(call: () => Promise<T>) { const expected = generation; try { return await call(); } catch (error) { if (!(error instanceof GrooveError) || error.code !== "UNAUTHENTICATED") throw error; if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed"); await refreshSession(); if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed"); return call(); }} // Discard the token before signing the user out of your appexport function disconnectSession() { generation++; pending?.controller.abort(); pending = undefined; grow.reset(); // Discard the customer token and abort pending requests} export async function signOut() { disconnectSession(); await fetch("/api/logout", { method: "POST" });}Create checkout from a published plan
This is a browser display fragment. First implement /api/groove-funnel and the host callbacks from the Funnel integration guide. Publish the current product and configure its Sandbox payment binding, then publish a Funnel checkout node referencing the fixed product version. Persist the original start input and resume the same run after payment return. Retry the original command after a timeout instead of creating another checkout.
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", { method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({ baseUrl: "https://grow.your-domain.example", scope: { projectId: "your-app-uuid", environment: "sandbox" }, runToken,});const handle = await showFunnelRun(container, { client, onAuthenticate: async () => obtainCurrentUserCustomerToken(), onCheckout: ({ url }) => window.location.assign(url), onOpenUrl: ({ url }) => window.location.assign(url), onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().Server: consume credits before running a job
Accept a customerId already verified and mapped by your backend. jobId is your stable job ID; retries reuse the same idempotency key. Set the server environment variable GROOVE_GENERATIONS_ENTITLEMENT_ID to the stable generations feature ID, not its key or a product version ID. Run the protected job only when accepted is true. See the credit guide for refunds and long-running reservations.
import { groove } from "./groove-server"; const configuredEntitlementId = process.env.GROOVE_GENERATIONS_ENTITLEMENT_ID;if (!configuredEntitlementId) throw new Error("Missing GROOVE_GENERATIONS_ENTITLEMENT_ID"); export async function authorizeGeneration(customerId: string, jobId: string) { const { receipt } = await groove.mutateBalance({ customerId, command: { operation: "consume", entitlementId: configuredEntitlementId, quantity: "1", properties: {}, idempotencyKey: `job:${jobId}:consume`, }, }); if (!receipt.accepted) throw new Error(receipt.reason ?? "Credits unavailable"); return receipt;}Browser: display a published Funnel
Publish the Funnel and its entry configuration. Start on the host backend and display with the run credential. reportError handles failures; destroy and discard old runs on navigation or identity changes.
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", { method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({ baseUrl: "https://grow.your-domain.example", scope: { projectId: "your-app-uuid", environment: "sandbox" }, runToken,});const handle = await showFunnelRun(container, { client, onAuthenticate: async () => obtainCurrentUserCustomerToken(), onCheckout: ({ url }) => window.location.assign(url), onOpenUrl: ({ url }) => window.location.assign(url), onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().