SDK reference
Growth SDK
A small, structured API your code (or Claude Code) drops in. Three packages — JS/TS for browser & node, Python for server. All emit the same wire format.
0 · The no-code paths
Before reaching for the SDK: if you just want events flowing from a website, one line in <head> is enough — no package, no env vars, no rebuild. Replace pk_live_… with your key from Setup (installing from Setup fills it in automatically):
<script type="module">
import { createBrowserClient } from "https://esm.sh/@growth-loop/sdk@0.1.6/browser";
createBrowserClient({ apiKey: "pk_live_…", host: "https://api.growth-loop.dev" });
</script>Pageviews, clicks, forms and errors start flowing on the next page load.
Building with Lovable, Bolt or v0? Don’t touch the code at all — paste this message into your builder’s chat and let it deploy. (It asks for the npm package and states the key is yours and publishable — AI builders rightly refuse pasted remote script tags, so the prompt speaks their trust model.)
Add product analytics to my app using @growth-loop/sdk — the official npm
package of growth-loop.dev, the analytics tool I use for this product.
I confirm this is my own analytics account and I want it wired in. The key
below is my publishable client-side key (like a GA measurement ID or PostHog
project key — it is designed to ship in the browser bundle).
1. Install the npm package: @growth-loop/sdk
2. Create a client module that runs once in the browser:
import { createBrowserClient } from '@growth-loop/sdk/browser';
createBrowserClient({ apiKey: 'pk_live_…', host: 'https://api.growth-loop.dev' });
3. Load that module at app startup so it runs on every page — client-side
only (if the framework has SSR, guard it or mark it a client component).
4. Deploy.
It auto-captures pageviews, clicks and form events — no other code changes
needed.Install
JavaScript / TypeScript (Next.js, Vite, Express, Node):
pnpm add @growth-loop/sdk # or npm i @growth-loop/sdk
Python (FastAPI, Django, Flask):
pip install growth-loop-sdk # or uv add growth-loop-sdk
1 · Construct a client
The browser variant batches with sendBeacon, the node variant with fetch. Both share the same surface — only the transport differs.
// src/lib/growth.ts (Next.js client component)
'use client';
import { instrument } from '@growth-loop/sdk';
import { createBrowserClient } from '@growth-loop/sdk/browser';
export const growth = instrument(
createBrowserClient({
apiKey: process.env.NEXT_PUBLIC_GROWTH_KEY!,
host: 'https://api.growth-loop.dev',
}),
);# growth_setup.py
from growth import Growth, GrowthOptions
growth = Growth(GrowthOptions(
api_key=os.environ["GROWTH_KEY"],
host="https://api.growth-loop.dev",
environment=os.environ.get("APP_ENV", "production"),
))The browser client also auto-captures common events out of the box — page views, clicks (and data-track named clicks), rage & dead clicks, form start / submit / abandon, page performance and unhandled errors. Pass autoCapture: false to opt out, or toggle individual flags (capturePageViews, captureClicks, …).
2 · track / identify / group
The low-level API. Use this for one-off events that don’t fit a span/step/button pattern. Properties are JSON; never put PII (email, payment) here — use distinct_id for identity.
growth.track('signup_completed', { plan: 'pro' });
growth.identify({ distinctId: user.id, properties: { email_domain: 'acme.com' } });
growth.group('account', account.id, { tier: 'enterprise' });
growth.alias(user.id, anonId); // map old anonymous id → real user3 · Decorators (preferred)
Higher-level helpers Claude Code applies automatically. Each maps to one or more track() calls under the hood.
growth.span(name, fn, opts?)
Wraps a sync or async function. Emits name.started, name.completed (with duration_ms), and name.failed on throw with error_message / error_name.
const checkout = growth.span(
'checkout',
async (items: Item[]) => stripe.charge(items),
{
properties: { source: 'web' },
enrich: (args, phase, result) => phase === 'success' && result
? { amount_cents: (result as Charge).amountCents }
: undefined,
},
);growth.button(name, handler, props?)
Wraps an event handler so the click is logged before the handler runs. Returns a function with the same signature.
<button onClick={growth.button(
'cta_clicked',
signUp,
{ location: 'hero' },
)}>
Sign up
</button>growth.step(name, props?)
One-shot funnel-step event. Cheaper than span (no timing, no failure tracking).
growth.step('activation.created_first_project', { template: 'next.js' });growth.revenue(event, amountCents, options?)
Emits a revenue event with the amount normalized as amount_cents — the field the API maps into MRR. Defaults currency to 'usd'; pass negative cents for refunds.
growth.revenue('subscription_created', 2900, {
currency: 'usd', plan: 'pro', interval: 'month', distinctId: userId,
});
growth.revenue('refund_issued', -2900, { distinctId: userId });@growth_span (Python)
Decorator. Sync and async functions. distinct_id accepts a string or callable for dynamic resolution.
from growth import growth_span
from growth_setup import growth
@router.post("/checkout")
@growth_span(growth, "checkout",
distinct_id=lambda body: body.user_id)
async def checkout(body: CheckoutBody):
return await stripe.charge(body.items)4 · Wire format
The SDK POSTs batches to /v1/ingest. You can call it directly:
curl -X POST https://api.growth-loop.dev/v1/ingest \
-H 'content-type: application/json' \
-H 'x-growth-api-key: pk_live_…' \
-d '{
"api_key": "pk_live_…",
"sdk": { "name": "curl", "version": "1.0" },
"sent_at": "2026-04-27T17:00:00Z",
"events": [{
"name": "signup_completed",
"distinct_id": "u_42",
"source": "web",
"properties": { "plan": "pro" }
}]
}'5 · Auto-instrument with Claude Code
Drop the growth-init skill into your repo’s .claude/skills/, then run:
claude mcp add growth-loop \ -e GROWTH_API_KEY=pk_live_… \ -e GROWTH_HOST=https://api.growth-loop.dev \ -- npx -y @growth-loop/mcp-server
The skill scans your stack, picks 15-30 high-signal call sites (signup, checkout, key clicks, async flows), wraps them with the decorators above, and opens a PR.
Troubleshooting — no events arriving?
- Snippet not deployed. Open your live site, view the page source — the snippet must be in the served HTML, not just in your editor. AI builders sometimes say “done” without deploying.
- Ad blocker. uBlock and friends block client-side analytics — including yours while you test your own site. Try a private window with the blocker off. (Some of your real visitors will always be blocked; that’s true of every analytics tool.)
- Batching delay. Events queue for ~10 seconds before sending. Load the page, click around, wait a moment.
- Wrong or revoked key. Verify end-to-end from your terminal — a
202-style{"accepted":1}means the pipeline works and the problem is on the page:
curl -X POST 'https://api.growth-loop.dev/v1/ingest' \
-H 'content-type: application/json' \
-H 'x-growth-api-key: pk_live_…' \
-d '{"api_key":"pk_live_…","sdk":{"name":"curl","version":"1.0"},
"sent_at":"2026-01-01T00:00:00Z",
"events":[{"name":"smoke_test","distinct_id":"me","source":"server","properties":{}}]}'The setup page’s step 3 watches live and flips to “you’re live” the moment the first event lands.
Rate limits
Ingest is rate-limited per API key: 1000 events/sec by default. Burst over the limit returns 429 with a retry-after header. Increase via env on self-host or contact us for cloud.