Skip to content

Documentation

Read the graph

Every fact Kicklace holds about your customers — the people, the organizations, what they pay, what they did, your deployments, where everybody stands on the lifecycle, and the populations Kicklace works out on its own — answered three ways: REST addresses under /api/v1/, MCP resources, and the kicklace package's own reads. All three run through one reader, so the same question gets the same answer however you ask it.

Your customer data is an API an agent can operate, not a chat window bolted onto a database. Every fact Kicklace holds about your customers — the people, the organizations they belong to, what they pay, what they did, the deployments you have shipped, where everybody stands on the lifecycle, and the populations Kicklace works out on its own — is one reader, answered three ways: a REST address under /api/v1/, an MCP resource, and the kicklace package. All three run through the same code, so a person read through one is the same JSON a person read through another.

This is reads only. Writing — an event, a note, a subscription to a list — is the events API and the MCP server; this page is what comes back.

Authorization

Every address here takes the same two kinds of bearer token the events API does:

Authorization: Bearer sk_live_…    a secret key, pasted under Connections → Your website
Authorization: Bearer kla_…        an access token from Claude's own sign-in

A key stays one workspace's. A connection that reached "all your workspaces" through Claude's own sign-in reads whichever workspace you name with ?workspace=<slug or name> — left out, the one it last acted in, or its only one. A key made under Connections → Your website → For your developers for a customer's own backend may read; the setup token npx kicklace init --setup exchanges cannot — it lasts fifteen minutes and is spent by the one exchange it is for.

A request with no credential, or one Kicklace does not know, is refused in one sentence:

{ "ok": false, "error": "invalid_key", "message": "…" }

The addresses

Address Query Answers
GET /api/v1/people q, stage, population, organization, updated_since, after, limit a page of people
GET /api/v1/people/{id} one person
GET /api/v1/organizations q, updated_since, after, limit a page of organizations
GET /api/v1/organizations/{id} one organization
GET /api/v1/subscriptions after, limit a page of customer_subscriptions rows, each with its payer
GET /api/v1/events person, name, since, after, limit a page of what people did
GET /api/v1/releases after, limit a page of deployments, newest first
GET /api/v1/releases/{sha} one deployment, by its sha or the version string it was tagged with
GET /api/v1/journey the seven lifecycle rungs, and how many are stuck
GET /api/v1/populations every population, with its count and its formula

A list answers { "data": [ … ], "next": "…" }, where next is a cursor for the next page or null at the end — pass it back as after. limit is 1 to 100, 50 when left out. There is no total: read until next is null. A one-record address answers that record's own shape directly, with no envelope around it. Cache-Control: no-store on every answer, because what it says is true this second and nothing here is meant to be cached.

People

{
  "id": "rec_7f3a2c9e",
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "identities": [{ "kind": "email", "value": "ada@example.com" }],
  "stage": { "key": "paying", "name": "Paying" },
  "state": "paying",
  "organization": { "id": "rec_org_1", "name": "Analytical Engines" },
  "money": { "monthlyRecurring": 4900, "creditsRunRate": 0, "currency": "usd", "otherCurrencies": [] },
  "reading": "Paying for three months and slowing: 2 events in 14 days against 9 the 14 before.",
  "createdAt": "2026-01-04T09:12:00.000Z",
  "updatedAt": "2026-09-10T16:40:00.000Z",
  "url": "https://www.kicklace.com/app/north-shelf/directory/people/rec_7f3a2c9e"
}

money is Kicklace's own rule, kept here rather than added away: monthlyRecurring is a subscription that renews by itself, creditsRunRate is one-off purchases of the last ninety days averaged to a month, and the two are never printed as one figure — a screen that added them would be saying something neither half says on its own. Where somebody pays nothing themselves and their organization does, state and money are the organization's and via names it. reading is the same sentence a person reads over their name in the app — one line of interpretation, never a column read out loud — and is absent where Kicklace has nothing to say yet.

?population=<key> narrows to a population's members, ?stage=<key or name> to one lifecycle rung, ?organization=<id or name> to one organization's people, ?updated_since=<ISO 8601> to what changed since a moment. GET /api/v1/populations names every population key there is.

Organizations

The same shape, without stage or via, plus archived. An organization is a company or a team your people belong to — never one of a single person — made only by something real: a Clerk or Kinde organization, a GitHub sponsoring organization, or a Stripe customer somebody has said is an organization's.

Subscriptions

{
  "id": "sub_9k2m",
  "payer": { "id": "rec_org_1", "name": "Analytical Engines", "kind": "organization" },
  "provider": "stripe",
  "providerId": "sub_1Q2w3e",
  "status": "active",
  "plan": "Studio",
  "priceAmount": 4900,
  "currency": "usd",
  "interval": "month",
  "intervalCount": 1,
  "quantity": 3,
  "monthlyAmount": 14700,
  "currentPeriodStart": "2026-09-01T00:00:00.000Z",
  "currentPeriodEnd": "2026-10-01T00:00:00.000Z",
  "cancelAtPeriodEnd": false,
  "createdAt": "2026-03-01T00:00:00.000Z",
  "updatedAt": "2026-09-01T00:00:00.000Z"
}

payer is whoever the money sits on: a person, or an organization. monthlyAmount is the price already normalised to a month, in the price's own minor units — a year's price ÷ 12, a week's × 52 ÷ 12 — so summing a page is always a plain sum.

Events

{
  "id": "int_4h7q",
  "name": "purchase",
  "person": { "id": "rec_7f3a2c9e", "name": "Ada Lovelace" },
  "at": "2026-09-10T16:40:00.000Z",
  "properties": { "value": 49 }
}

What a person did: a built-in name (purchase, signup, download, activated, and the rest Events names) or one of the workspace's own, the same names kl.track() posts. ?person=<id> takes a record id from GET /api/v1/people — not an address; narrow by address with ?q= on the people list first. ?name= and ?since= narrow further, and paging is the same cursor every list uses.

Releases

{
  "id": "rec_rel_9",
  "name": "Ship the graph API",
  "sha": "a1b2c3d",
  "at": "2026-09-16T09:00:00.000Z",
  "changes": {
    "repo": "kicklace/kicklace",
    "base": "e5f6a7b",
    "head": "a1b2c3d",
    "totalCommits": 4,
    "totalFiles": 12
  },
  "url": "https://www.kicklace.com/app/north-shelf/directory/releases/rec_rel_9"
}

changes is GitHub's own compare of this deployment against the one before it, read once and kept — absent where no compare has been read yet, or where nothing can read your repository. GET /api/v1/releases/{sha} takes the sha Vercel deployed or the version string your own server sent as release on an event, whichever names one.

Journey

{
  "stages": [
    { "key": "signed_up", "name": "Signed up", "count": 214, "medianDays": 3.5, "stuck": 12, "arrived": 18, "wentFurther": 9 }
  ],
  "total": 940
}

One row per lifecycle rung, in order: how many stand on it, how long half of them have been there, how many arrived in the last month and how many of those went further, and how many are stuck — on the rung past its own usual wait, with nothing recorded since.

Populations

{ "data": [{ "key": "group:quiet", "name": "Quiet", "count": 6, "formula": "Nothing recorded for 30 days." }] }

A population is a set of people who satisfy one meaningful condition right now — a lifecycle rung, one of Kicklace's own groups (Likely to leave, Payment issue, Quiet, Slowing, High-value, Stuck on a rung), or a saved view. Kicklace works out who is in each; nothing here proposes a new one, and formula is the rule as the sentence a person reads on the Populations page. Membership is GET /api/v1/people?population=<key>, never a field on this list.

The MCP resources

The same ten reads, offered the way the MCP server expects data to be found rather than asked for: resources/list names the seven whole ones, resources/templates/list the three that take an id or a sha, and resources/read answers with application/json — the exact body a REST call to the matching address would answer.

kicklace://people                    kicklace://people/{id}
kicklace://organizations             kicklace://organizations/{id}
kicklace://subscriptions
kicklace://events
kicklace://releases                  kicklace://releases/{sha}
kicklace://journey
kicklace://populations

Every filter a REST address takes is a query parameter on the same uri — kicklace://people?stage=paying&limit=10workspace included, the same argument a tool call takes for a connection that reaches more than one.

The package's reads

kicklace.people, .organizations, .subscriptions, .events and .releases read; .journey() and .populations() have no page to be a namespace over, so they are plain methods:

const paying = await kl.people.list({ stage: "paying", limit: 20 });
if (paying.ok) for (const person of paying.data) console.log(person.name, person.money);

for await (const person of kl.people.iterate({ population: "group:likely-to-leave" })) {
  // every page, read on its own
}

list() answers one page and never throws, the same rule every write in the package keeps; iterate() is the same read as an async generator, and a page that comes back refused simply ends the loop rather than throwing into yours. See the package's own reference for every method.

Limits

What Limit
Rows in one page 1 to 100, 50 when limit is left out
Calls in ten minutes 3,000 a key or a signed-in connection — a separate, more generous count from the 600 calls a key gets on /api/mcp or on /api/v1/events, because paging through a workspace is ordinary here

Past the limit, every address answers the same way the rest of /api/v1/ does:

{ "ok": false, "error": "rate_limited", "message": "…" }

Errors

Every refusal is one sentence with the fix inside it, in the same shape as the rest of /api/v1/: { "ok": false, "error": "…", "message": "…" }.

error When
invalid_key No credential, or one Kicklace does not know — a setup token included, which cannot read
invalid_token An access token that has expired, been disconnected, or was issued for another server
no_longer_a_member The member behind the key or the connection has left the workspace
unknown_workspace / say_which_workspace A connection that reaches several workspaces was not told which, or named one it does not reach
not_found No record, release or resource matches the id, the sha or the uri
unknown_stage / unknown_population / unknown_organization / ambiguous_organization A filter named something this workspace does not have, or matched more than one organization
bad_argument updated_since or since was not ISO 8601
rate_limited Too many reads in ten minutes