Skip to content

Documentation

Add Kicklace to a Next.js SaaS

The one page to copy from — install, the two keys, the tag, the events that move somebody through the lifecycle, the Stripe and Clerk webhooks that need no code, and the MCP server for whatever agent is doing the wiring, in the order to do them.

This is the one page to copy from: everything a Next.js SaaS needs to send Kicklace the facts that build a customer record — who signed up, what they did, what they paid — in the order to do it. Fifteen minutes, most of it optional.

1. Install

Already have a Kicklace workspace and a setup token from its setup screen?

npx kicklace init --setup <token>

One command: it adds the tag to your root layout, adds kicklace to package.json, exchanges the token for your two keys and writes them into .env.local, and writes a short Kicklace section into this project's own AGENTS.md so a coding agent working here later already knows the wiring is done. Nothing it writes is printed back — the secret key is named, never shown.

No token yet, or setting this up before a workspace exists:

npx kicklace init             # add the tag, the package, and the four env names
# or, by hand:
npm install kicklace

Run npx kicklace init --dry-run first to see every change it would make without writing anything. It refuses cleanly, with one sentence, if this is not a Next.js project or has no root layout.

2. The two keys

Key Shape Belongs
Public key wk_… in the tag, in a page's source — it is public by design, and can only add people
Secret key sk_live_… on your server, in an environment variable — never in browser code

Both are minted under Connections → Your website → For your developers, and the same screen gathers the tag, a heartbeat of what has arrived, and the outgoing delivery log. A secret key is shown once and stored as a hash; lose it, and mint another.

KICKLACE_SECRET_KEY=sk_live_…
NEXT_PUBLIC_KICKLACE_KEY=wk_…
KICKLACE_URL=https://www.kicklace.com
NEXT_PUBLIC_KICKLACE_URL=https://www.kicklace.com

3. The tag

One script element, in <head>, or — in a Next.js app — one component in the root layout:

import { KicklaceTag } from "kicklace/next";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <KicklaceTag />
      </body>
    </html>
  );
}

It records page reads, remembers how each visitor arrived — the campaign, the referrer — and hears a signup form marked data-kl-list="<your-list>" with no handler of yours. No cookies. npx kicklace init adds this for you; by hand it is:

<script async src="https://www.kicklace.com/api/in/YOUR_PUBLIC_KEY/track.js"></script>

4. The events that move somebody

Kicklace's lifecycle — visitor → signed up → activated → first value → paying → expanding → retained — is derived from events, never set by hand. Post these four, exactly by name, from your own server code:

import { Kicklace } from "kicklace";

const kl = new Kicklace(process.env.KICKLACE_SECRET_KEY!);

await kl.track("signup", { email });                                   // moves them to Signed up
await kl.track("activated", { email });                                // moves them to Activated
await kl.track("value", { email });                                    // moves them to First value
await kl.track("purchase", { email, value: 49, idempotencyKey: invoice.id }); // moves them to Paying
  • signup — an account was created.
  • activated — the first real thing in your product happened. You decide what that is: the first project created, the first integration connected.
  • value — the product delivered the outcome somebody came for. Post it every time it happens, not only the first, because a product that never posts one has nobody reaching First value.
  • purchase — somebody paid, with value in your currency's main unit (49 for $49.00) and an idempotencyKey so a retried webhook lands once.
  • churned or subscription_cancelled — a paying customer stopped. Neither moves anybody directly: Kicklace reads Churned off the money itself, the same as it reads Paying.

Any other name is yours: 2–40 characters of lowercase letters, digits and underscores (credit_added), landing on the person's timeline for an automation to answer, and moving nobody, because Kicklace cannot know what a name it has never seen means.

Not on Node, or not through the package: every one of these is POST https://www.kicklace.com/api/v1/events with the secret key as a bearer token, so any language that can make an HTTP request can post them.

curl -X POST https://www.kicklace.com/api/v1/events \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"type":"purchase","email":"ada@example.com","value":49,"idempotency_key":"in_123"}'

5. Let Stripe and your auth provider do it for you

A payment and a sign-up often already happen somewhere Kicklace can hear directly — no code, no deploy.

Stripe. Connections → Stripe → Connect Stripe gives an address shaped https://www.kicklace.com/api/webhooks/stripe/<token>. Paste it into Stripe (Developers → Webhooks → Add endpoint), choose the events, bring the signing secret back and paste it in. A paid invoice becomes the same purchase event your own server would have posted.

Clerk. Connections → Clerk → Connect Clerk gives https://www.kicklace.com/api/webhooks/clerk/<token>. Register it in Clerk's dashboard under Webhooks, subscribed to user.created, user.updated, session.created and user.deleted, and paste the signing secret back. A new user becomes a signup, moving them to Signed up, exactly as if your own server had posted it.

Kinde. The same shape at https://www.kicklace.com/api/webhooks/kinde/<token>, subscribed to user.created, user.updated, user.authenticated and user.deleted.

Read your history at the same time — Stripe's, Clerk's or Kinde's own read-only key, pasted into the third step of that connection's card, turns two years of customers and payments into the same records the webhook would have written, dated the day the money or the sign-up actually happened.

6. The MCP server, for the agent doing this

Kicklace also answers as an MCP server, useful for setting up lists, templates and automations, or for asking a question about a workspace, rather than for tracking events:

https://www.kicklace.com/api/mcp

A member's own key is minted at Connections → Claude; a read-only server key, for a machine rather than a person, is minted at Connections → Your website → For your developers. From Claude Code:

claude mcp add --transport http kicklace https://www.kicklace.com/api/mcp \
  --header "Authorization: Bearer sk_live_…"

It never sends an email, never deletes a record, and never puts anyone on a list without the words they agreed to; a person in the workspace approves those three. Everything else — looking a customer up, writing a note, saving a view, drafting an automation — happens at once, in the key-holder's name.

Reads

The graph is readable directly: GET /api/v1/people, /organizations, /subscriptions, /events, /releases, /journey and /populations (and one by id — /people/{id}, /organizations/{id}, /releases/{sha}), with the same secret key in Authorization: Bearer that posts events; a list answers { data, next } and takes limit (up to 100) and after. The kicklace package reads the same way (kl.people.list({ stage: "paying" }), kl.journey(), kl.populations(), .iterate() where a list pages), and the MCP server offers the same ten as resources beside its tools. Every address and field is on Read the graph.

What Kicklace knows once this is done

One record per person, merged across an anonymous visit, a signed-up account and a purchase, on whichever device each happened on. Where they stand on the lifecycle, worked out from the events above rather than set by anyone. Who is paying, who is trialling, and who paid once and stopped. And, the moment an automation is switched on, the obvious next action — a welcome email when somebody signs up, a check-in when they never activate, a note when a payment lapses — sent only to people who agreed to hear from you, with the exact words they agreed to stored beside every send.