Skip to content

Documentation

Reading your data

Read your people, organizations and payments back, over REST, the package or MCP.

Everything Kicklace holds about your customers can be read back: the people, the organizations they belong to, what they pay, what they did, the deploys you shipped, where everybody stands, and the groups Kicklace works out by itself. It's 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 as through another.

This page is reads only. Writing (an event, a note, a sign-up to a list) is Events and the MCP server.

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>, or else the one it last acted in. A key made under Connections → Your website → Developers may read; the setup token npx kicklace init --setup exchanges can't, because it lasts fifteen minutes and is spent by that one exchange.

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 deploy, by its sha or the version string it was tagged with
GET /api/v1/journey the seven lifecycle stages, and how many are stuck
GET /api/v1/populations every group, with its count and its rule

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 added into one figure. 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, 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 deploy against the one before it, read once and kept. It's 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: at that stage 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 meet one condition right now: a lifecycle stage, 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, like kicklace://people?stage=paying&limit=10, workspace 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, counted apart from the 600 a key gets on /api/mcp or /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 doesn't know, a setup token included, which can't 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