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 |