Skip to content

Kicklace, an AI-powered CRM for micro SaaS

CRM for developers

One line in your server sends what a person did. The record, the lifecycle, the consent and the follow-up are what you stop maintaining.

One line in your server

Install the package, pass it a secret key, and post what a person did. It has no dependencies, it runs anywhere fetch does, and it never throws.

Install
npm install kicklace
Your server
import { Kicklace } from "kicklace";

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

await kl.track("purchase", { email, value: 49, idempotencyKey: invoice.id });

Kicklace's own names are pageview, signup, download, activated and purchase. A name of your own, like credit_added, lands on the person's timeline and an automation can answer it; it moves nobody between stages, because Kicklace cannot know what a name it has never seen means.

Who they are, and what they agreed to
await kl.track("signup", { email });
await kl.track({ type: "credit_added", accountId, properties: { credits: 10 } });

await kl.identify({ email, anonId, name, fields: { phone } });

await kl.subscribe({
  email,
  list: "product-updates",
  consent: "Send me product updates. I can unsubscribe with one click.",
});

await kl.support({ from: email, who: name, topic: "Billing", message: "…" });

subscribe takes the sentence the person actually read and stores it verbatim as the proof. Without one it is refused. The browser id from the tag is anonId, which is how the visitor who read your pricing page last week and the customer who paid today are one record.

A refusal is an answer

Nothing this package does can throw inside your checkout. Kicklace saying no is data, and the fix is usually inside it.

Every method resolves to one of two shapes
type Outcome =
  | { ok: true; status: string; id?: string }
  | { ok: false; error: string; message: string; httpStatus: number };
Reading one
const sent = await kl.messages.send({ to: email, template: "Welcome" });
if (!sent.ok) log.warn(sent.message);   // "Kicklace only emails a person who…"
  • error is Kicklace's own code and message is its own sentence, handed on untouched. TypeScript makes you look at ok.
  • A 5xx and a network failure are tried again — three attempts, a short wait between them. A 4xx never is, because a refusal will not change.
  • Give anything that must land exactly once an idempotencyKey. It is what makes a retried purchase — ours, or your own webhook's — land once.
  • A network failure that survived the retries is unreachable, with an HTTP status of 0.
  • The only exceptions are thrown when you build the client, and only for the two mistakes worth stopping a program for: no key, and a public key where a secret one belongs.

The tag, for the traffic side

The events API is what your server knows. The tag is what your website knows: page reads, where somebody came from, and the signup forms it can hear.

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

It takes your public key, wk_…, which is meant to be in a page; your secret key belongs on a server and never in browser code. It keeps one random id in the browser's own storage, reads the campaign a visitor arrived on, and can hear your signup forms without touching them — no preventDefault, no markup of ours, so your form's own handler and success state are what they were. No cookies.

What arrives without you writing anything

Some of what a person does, nobody has to send. Connect the door and it lands as the same event your own code would have posted.

  • Stripe. Payments, refunds, subscriptions and trials. A paid invoice becomes a purchase with its amount, and the lifecycle moves to Purchaser. One address pasted into Stripe, and the signing secret.
  • Clerk and Kinde. A new account becomes a person with the lifecycle moved to Account, and a later event with no address still finds them, because the provider's own id is written down beside the address.
  • Vercel. A production deployment becomes a Release on a board of its own, which is what makes “when production deploys, draft the changelog email” an ordinary automation.
  • GitHub. A sponsor becomes a purchase, an issue becomes a support message you can answer from their record, and a star is recorded only if you ask for it. An issue's body is never read.
  • Your own servers. The package is a wrapper over POST /api/v1/events, so anything it does you can post by hand with a secret key — single or a batch of up to a hundred, each with an idempotency key and an expiry date if you promise one.

What Kicklace tells your code

The way back out. Every trigger can be posted to an address of yours, signed, and tried again when your server is down.

A stage entered, a subscription, a reply arriving — each one is posted to your address with an X-Kicklace-Signature header, an HMAC-SHA256 of the exact body under a secret shown to you once. A non-2xx or a network error is tried again five more times over about fifteen hours, at widening intervals, and a retry sends the same bytes, so the signature still holds. The address must be a public https one; Kicklace refuses to knock on a private host.

An automation can also post to your own API as one of its steps, signed the same way, so “when somebody cancels, tell my billing service” is a sentence rather than a job.

Configure it with Claude

The other half of Kicklace is the configuration layer: point Claude at your workspace and set it up by asking.

Kicklace speaks the Model Context Protocol. Add it as a connector in Claude and sign in — or take a key for a client that wants one — and Claude can look people up, run a report, write notes and tasks, make the lists, templates, stages, fields, views and charts your events then feed, and draft an automation for you to read and turn on.

It never sends an email, never deletes a record, never puts anybody on a list without the words they agreed to, and never mints or reads a key. Sending, destroying and cutting a key loose are the three acts it can only propose: they land on an Approvals page for you to press. Everything it does is in your name, and written down on that same screen.

What you stop maintaining

The parts a small team wires together by hand, and what each one becomes.

A subscribe endpoint
One public address takes a signup from a static site: idempotent on the email and the list, a honeypot field, rate limited per IP, CORS for the origins you allow, and an answer in under a second. Subscribed and already subscribed both answer 200 and look the same, so a form cannot be used to find out who is on your list.
A mailing list table
Lists live on the record. The person who paid you and the person on your newsletter are the same row, so there is nothing to keep in step and no export to reconcile.
A consent log
The sentence each person read is stored word for word beside their subscription, with the day, the source and their IP if you send it. Taking somebody off a list keeps the consent record: it is the proof they once agreed.
A lifecycle you recompute
Stages come from events as they arrive, and are never recalculated afterwards. A purchase moves somebody to Purchaser; a second one to Repeat; a cancellation is the one move down.
A Stripe handler that emails people
Kicklace takes Stripe's webhook itself — one address pasted in, the signing secret, no code — and the follow-up is an automation you can read, not a branch in a payment handler.

Questions

Does the package throw when Kicklace refuses something?
No. Every method resolves to an answer with an ok on it, and TypeScript makes you look at it: a refusal carries Kicklace's own code and its own sentence. The only exceptions are thrown when you build the client, and only for no key at all and a key of the wrong kind.
What does it install?
Nothing else. The package has no runtime dependencies, and neither entry point imports anything from node:, so it runs on Node 18 and up, on edge runtimes, in workers, in Bun and in Deno.
Do I have to send events at all?
Not for most of it. The tag is what your website knows, and Stripe, Clerk, Kinde, Vercel and GitHub post what they know once you have connected them. The package is for the part only your own servers know — that somebody activated, that a credit was added, that an order shipped.
How do I make sure a purchase lands exactly once?
Give it an idempotency key: the invoice id, or your own order id. A 5xx and a network failure are tried again three times, a 4xx never is, and the key is what makes any of those retries land once.
Can I use Kicklace without writing any code?
Yes. Put the tag on your site, connect Stripe or your auth provider, and point Claude at the workspace to set the lists, emails and automations up by asking. The API is the runtime layer, not the way in.
How does my code hear what Kicklace knows?
Outgoing webhooks. Every trigger — a stage entered, a subscription, a reply arriving — is posted to an address of yours with a signed body, and tried again when your server is down. An automation can also post to your own API as one of its steps.

Read next

Join the waitlist

We are onboarding one company at a time. Leave your email and we will let you know when your workspace is ready.

Optional.

Optional. Tell us about your product and where your leads come from.

Which plan would you want?

Prices are not final and nothing is charged. Pick a plan now and you will get that price when billing opens.

By joining, you agree to receive waitlist updates from Kicklace: Put me on the Kicklace waitlist and tell me when a workspace can open for my company. We store this wording with the date and your email address. Every email has a one-click unsubscribe link. Privacy policy.

We reply to every signup.