uk_sk_live_… /
uk_sk_test_…), which means two things worth knowing before you paste one in.
The key decides the environment, and nothing else can. No tool takes an
environment argument. A
uk_sk_test_… key can only ever show you test data — so
connecting your test key is the safe way to try this, and it is not a setting
somebody can talk the model out of.Connect a client
In the project you want the assistant to work in:AGENTS.md carrying the handful of
UserKit rules that compile when broken, picked for what the project turned out
to be. It writes no key: the config names the USERKIT_API_KEY variable and
reads it from the environment, so the file is safe to commit. Re-running it is a
no-op, and it owns a delimited block rather than the file, so your own guidelines
and your other MCP servers are left alone.
Pick the clients with --mcp: claude writes .mcp.json, cursor writes
.cursor/mcp.json and vscode writes .vscode/mcp.json.
To do it by hand instead, every client stores the same three facts: a name, the
URL, and the header. Two details differ between them, and both fail silently: VS
Code keeps its servers under servers where the others use mcpServers, and it
expands only its own ${env:NAME} syntax — a bare ${NAME} is written through
literally, so the header would carry the characters instead of the key.
What it can do
tools/list is the contract, and it is answered by the running server — if a
tool takes an argument this page does not mention, the client already knows
about it. Each tool also declares the shape of its answer, so a client can hand
the result to code instead of re-parsing it.
The nine things it can change
An assistant can write in exactly nine places: your help centre, the catalogue you sell, your terms, your changelog, your funnels, your onboarding, your points, your message templates and the roles inside your customers. Everything else answers questions.The help centre
Nine tools:create_help_collection, update_help_collection,
delete_help_collection, create_help_article, save_help_article_version,
update_help_article, publish_help_article, unpublish_help_article and
delete_help_article.
Writing an article and publishing it are separate calls. A new or edited
article is a draft — nobody outside your organization can read it until
publish_help_article names a version to put live. So an assistant that has
written something wrong has not yet said anything wrong to your customers, and
the review step is a real one rather than a convention.unpublish_help_article keeps every word and publish_help_article puts
them back.
The help centre belongs to the organization, not to an environment. There is
one of it, shared by live and test — so unlike every other tool here, these
answer scope: "organization" and carry no environment. There is no second
copy to go and fix.
Publishing and unpublishing are recorded in your audit trail, against the API
key that did it rather than against a person. Settings → Audit shows the entry
as API key · <the key's name>, which is the reason to mint a key for the
assistant instead of lending it one your backend uses: the trail can only be as
specific as the credential is.
The catalogue
Nine more tools, for the plans, prices and features you sell — not the plan you are on with UserKit:create_catalogue_plan, update_catalogue_plan,
create_catalogue_price, reprice_catalogue_price, archive_catalogue_price,
create_catalogue_feature, update_catalogue_feature,
set_catalogue_plan_feature and remove_catalogue_plan_feature.
A price is never edited, and that is what makes this safe to hand to an
assistant. A subscription points at the price row and that row is the record of
what somebody agreed to pay, so the database refuses any change to it. Changing
what a plan costs is reprice_catalogue_price, which archives the old offer and
creates the new one in a single step — and existing subscribers are not moved
onto the new amount. They keep the price they agreed to; moving somebody is a
plan change they consent to at your payment gateway.
Nothing is deleted. A plan or a feature is archived, which stops it being
sold and is undone by the same call with archived: false. A price is archived,
and putting the offer back means creating a new one. The single exception is
taking a feature out of a plan, which removes that cell of the matrix — putting
it back means stating its terms again.
Two things are permanent and asking is refused rather than ignored: a plan’s
and a feature’s key (the string your own code gates on), and how a feature is
measured. A feature measured differently is a different feature.
What it cannot reach is your payment gateway. No tool stores, reads or uses
the credential that charges a card — connecting a provider is owner-only and
lives in the panel — and nothing here creates a charge, issues a refund or
touches somebody’s subscription.
Every catalogue write lands in your audit trail against the key, exactly as the
help centre’s do.
The terms
Five tools:create_legal_document, update_legal_document,
save_legal_version, publish_legal_version and delete_legal_document, plus
list_legal_documents, get_legal_document and get_legal_version.
Writing and publishing are separate here too, and the separation matters more
than anywhere else on this surface: a new document and every save_legal_version
are drafts, and nothing changes for anybody until publish_legal_version.
A document marked required is reported as blocking wherever the person’s
agreements are read, and nothing more — it never signs anybody out and never
blocks /v1/contact/token; what to do about it is your product’s decision. Publishing terms must mean “everybody is asked”, never
“everybody is locked out”.
The changelog
Four tools:create_changelog_post, update_changelog_post,
publish_changelog_post and delete_changelog_post, plus the two reads in the
table above.
Writing a post and announcing it are separate calls, as in the help centre —
a new post is a draft nobody outside your organization can see. What is different
here, and it is the sharpest edge on this whole surface:
A targeted post is not on the public page. segment_id (or
feedback_post_id, for the people who voted for a request) narrows the audience,
and a narrowed post is reachable only by somebody signed in to your product —
the public document has no reader to resolve, and answering one per caller-named
identity would turn it into an oracle for who is in which segment. So an
assistant that aims a post at a segment and then checks the public page will
correctly see nothing there.
list_segments is where those ids come from, and it carries each segment’s rule
as well as its name — so an assistant picks an audience by what it means rather
than by what it is called.
Everything else on this surface still cannot change anything — see below.
The funnels
Three tools:create_funnel, update_funnel and delete_funnel, plus the two
reads in the table above.
A funnel is a saved question — a name, an ordered list of event names and a
window in days — rather than a measurement. It stores no number at all: the
drop-off from one step to the next is recomputed out of the raw events every
time somebody opens the chart. That is why this is the mildest of the four
exceptions: a wrong delete_funnel destroys the question, which you state
again, and nothing anybody measured.
Two things are worth knowing before you ask for an edit.
A funnel belongs to the organization, not to an environment. There is one of
it, live and test share the definition, and only the numbers are per
environment. A test key editing a funnel is editing the same funnel live reads —
which is the opposite of the changelog, where a post belongs to one environment
and the other’s key cannot reach it.
update_funnel replaces the step list wholesale rather than
patching it, so read the funnel first with get_funnel.
The numbers themselves are not here: they need a range and an environment, and
they are the panel’s chart. No tool on this surface computes one.
The onboarding
Six tools:create_checklist, update_checklist, delete_checklist,
add_checklist_step, update_checklist_step and delete_checklist_step, plus
the two reads above.
This is the exception whose failure is quietest, and the tools are deliberately
narrower than the panel because of it. A changelog post written wrong is wrong on
a page somebody reads; a step written wrong produces nothing at all — an
event type nobody publishes is a step that never goes green, and you find out
weeks later as a completion rate you cannot explain.
Nothing here turns a checklist on.
create_checklist makes one that is off,
and no tool takes active. Activating is the moment measurement starts for the
whole audience at once, and it stays a thing a person does in the panel after
looking at the steps.event step names one of our facts and a wrong name is refused. A
product_event step names one of yours — whatever your product sends through
track() — and that vocabulary is open, so nothing can refuse a name for not
existing. list_checklists carries sightings for those: a zero means this
environment has never received that event, which is almost always a typo.
Steps are added and removed, never repointed. What satisfies a step — the
kind, the event type, the feature key — is not editable here or in the panel:
people already hold progress against it, and changing what it means would leave
rows saying somebody did something they were never asked to do. A step waiting on
the wrong fact is deleted and added again, which loses exactly the progress it
should.
Nothing satisfies a step, either. No tool on this surface can mark anybody’s
onboarding done, because none exists anywhere: a step is satisfied by a fact
landing on the bus or by an entitlement being held.
update_checklist also carries the guide’s shape — unlocks_after puts one
checklist behind another, and selectable makes one a choice the person makes
for themselves. Two checklists waiting on each other are refused: neither would
ever open.
snooze_days is how long a close holds for: seven by default, 0 for the
checklist that really is a one-time offer. Somebody dismissing a setup guide
usually means “not now”, so the card comes back — and changing the number never
moves a snooze already running.
Both the checklist and the step take hint_pending and hint_done: what the
line says on hover before and after, because the same line means different
things in the two states. A checklist takes a third, hint_locked — a padlock
tells the reader which door, not why it is there. A step has no locked state, so
it has no third.
A step also takes action_url, a path in the tenant’s app or an absolute
http(s) URL, which makes the whole row a link. Setting one on almost every
step is the cheapest improvement available to a guide: a checklist that names
the work and hides the door makes the reader navigate twice.
Points
Twelve tools:create_point_rule, update_point_rule, delete_point_rule,
create_point_level, update_point_level, delete_point_level,
create_achievement, update_achievement, delete_achievement,
create_gamification_metric, update_gamification_metric and
delete_gamification_metric, plus the six reads in the table above.
A metric is a unit of your own beside the points balance — courses finished,
emails sent — credited by the same rules through the same ledger. Creating one
changes nothing on its own: a metric no rule credits stays at zero for everybody,
so the order that works is metric, then rule, then level.
award_limit is required and refused at the tool rather than only at the
database. That is what separates this exception from the other eight: “reward
people for using the app” is a sentence that paraphrases straight into a rule
pointed at the busiest event your product sends, and a rule with no ceiling
writes a ledger line per occurrence, forever.
A rule and a badge are created OFF — but unlike a checklist,
update_point_rule and update_achievement can turn them on. The asymmetry is
retroactivity: turning a checklist on starts measuring an audience against steps
some of them may already have satisfied, while a rule pays nobody retroactively —
switching one on begins counting facts that have not happened yet, and switching
it off costs nothing already earned. A level has no switch at all: it does not
fire, it is what a balance means.
What a rule listens to and what a badge requires do not change. key, kind
and event_type on one; kind, rule_id, threshold and segment_id on the
other — refused with a sentence, never silently ignored. The lines already
written say “you earned this for X”, and the badge on somebody’s shelf says “you
did this”. Re-pricing is allowed and is never retroactive.
The three deletes differ from each other, and each answer says which is which.
Deleting a rule keeps the ledger — every line carries the name the rule had at
the time — and the answer carries ledger_kept; it is refused while a badge
counts that rule, and the error says how many. Deleting a level only loses the
record of who was congratulated for reaching it. Deleting a badge is the one
that takes something from people: the unlocks go with it, and the answer carries
unlocks_removed with the count — read before the delete, because afterwards
there is nothing left to count.
Message templates
Six tools:list_message_templates, get_message_template,
create_message_template, update_message_template,
restore_message_template_version and archive_message_template.
These are the words your product sends — the text a customer reads. They are
only here because an edit became a version: what was live stays on the
shelf, and restore_message_template_version puts it back. An unfortunate
paraphrase is one restore away rather than a sentence you lost.
Writing a template sends nothing. A notification goes out when your backend
posts to /v1/notifications naming the key; a campaign goes out when somebody
arms it. No tool here does both, and that is the door kept shut: an assistant
that could write the words and mail them would put a paraphrase in front of an
audience.
update_message_template replaces the body wholesale, so read it with
get_message_template first — an edit made without reading drops whatever it
did not know about. Omitted fields carry the live version’s value forward, so
changing only the subject keeps the body.
There is no delete. A template names what people were sent, so leaving the
picker is archive_message_template and the row keeps resolving the history.
And the platform’s own templates — the dunning sequence — are refused: that
sequence cannot be paused, and its wording is not yours to rewrite from here.
The roles inside your customers
Six tools:list_customer_roles, list_customer_role_versions,
create_customer_role, update_customer_role,
restore_customer_role_version and delete_customer_role.
These are the roles the people on your customers’ teams hold, per environment.
owner and member are seeded into every environment and cannot be deleted or
renamed; everything beside them is your own vocabulary.
A role’s permission list holds two of those vocabularies. A permission beginning
with $ must be one of the four reserved capabilities UserKit evaluates —
$team.manage, $billing.manage, $keys.manage and $webhooks.manage — and
anything else in that namespace is refused rather than stored. Everything unprefixed is yours: stored,
carried in the contact’s JWT, and never read by us.
An edit is a new version, never an overwrite, which is what makes this
admissible here at all.
update_customer_role replaces the whole permission set
and moves the role’s head to a new immutable version, so the set you replaced is
still in list_customer_role_versions and restore_customer_role_version puts
it back with one call.owner role may not give up
$team.manage — the guard that keeps a team administrable runs when a membership
moves, so stripping it here would leave every team in the environment with nobody
who can administer it. And a role somebody currently holds cannot be deleted.
key is immutable for the reason a plan’s key is: it is the string your own code
compares and the one the JWT carries, so renaming it would break your gate with
no error raised anywhere. Renaming for display is name, which changes freely.
Prompts, which are the work rather than the data
The server also serves prompts: the jobs people actually bring to this API, written down once. Clients that support them render them as something you pick — Claude Code shows them as slash commands.
They are rendered per call rather than being fixed text, which is the reason
they live on the server instead of in a plugin you install.
integrate_userkit
already knows which keys the environment holds and whether its identity secret
has been read, so the assistant is told to wire a signed boot rather than to
invent a sign-in page this API does not answer. plan_go_live says
out loud whether the key you connected can actually see the environment you are
asking about.
None of them carries a key, for the reason in the section below.
What it deliberately cannot do
It cannot change anything outside the nine places above. No tool creates, updates, deletes or merges a contact, a customer or a subscription, and none changes a configuration. That is not a phase of the work: the caller is a model acting on a sentence somebody typed, and “tidy up the duplicate contacts” is one paraphrase away from a merge that cannot be undone. The acts are all still there on the regular API, where a person wrote the call. Every exception is admitted by what a wrong call can destroy, one at a time. The help centre touches prose and a pointer: every edit is a new version rather than an overwrite, and the worst outcome reachable is text on a help page. The catalogue describes what you sell without being able to rewrite it: prices are immutable, plans and features archive rather than delete, and what a customer agreed to pay is out of reach by construction. The changelog is prose again, with one act that has no opposite — which is why publishing says so in the tool itself and why creating always writes a draft. A funnel is a saved question about data it cannot touch: deleting one costs the definition, never the measurement. The message templates and the customer roles are here for the same reason as each other: an edit writes a new immutable version and moves a pointer, so what was live is one restore away rather than a thing you lost. The terms are the strictest of the nine, and it is publishing that carries the weight — writing is always a draft, a published version can never be edited or withdrawn, and the tool says so before it acts. All of them pass the same permission test, which is what keeps this honest: minting an API key needs owner or admin, and every one of these writes was already within reach of those roles. A tool that gave a key-holder something a session would not does not belong here — which is exactly why the audit trail, which is owner-only, stays out. It sends nothing to anybody’s inbox. Publishing a release note does not mail your users: the changelog page and the widget’s badge are things they come and read. No tool on this surface sends email. It cannot move money. No charge, no refund, no plan change on somebody’s subscription, and no access to your payment gateway’s credentials — connecting a provider is owner-only and stays in the panel, where a person pasted the key. It cannot read your staff audit log.audit:read is owner-only — the record
of what an administrator did is not the administrator’s to curate — and an API
key carries no role at all. Serving the audit trail here would route owner-only
records out through a credential anyone who can mint keys already holds.
It cannot answer with a credential. No secret keys, no publishable keys, no
sessions, no one-time tokens. A conversation with an assistant is stored,
summarised and rendered wherever the client runs, which makes it the last place a
credential should be printed.
Reading the answers
Three things surprise people, and all three are deliberate.search_docs answers sections, not pages, and answers an empty list when it
finds nothing. That empty list is a real answer — it means this documentation
does not cover the question, which is worth hearing rather than having filled in
from an assistant’s memory of some other product’s auth API. It searches one
language tree at a time (en by default, pt-br on request), and the index it
reads ships with the deployment, so what it says is current for the API you are
talking to.
run_doctor has a third status, and it is not a pass. Every check answers
ok, attention or unknown, and unknown means the check could not tell —
the key set is served from a CDN, so a fetch a cache answered never reached us,
and “never seen by us” is not “never fetched”. A check that does not apply to
this environment is left out of the list rather than reported as ok. Assistants
summarise, and the summary to watch for is the one that turns four oks and two
unknowns into “everything is working”.
active_contacts_usage answers zero in a test environment, with
counted: false beside it. Test contacts are never metered, so that zero is a
statement about billing rather than about your users. Point the assistant at a
live key when the question is “how many active users do we have”.
Amounts are minor units. A price carries amount_minor and
currency_exponent, so 1000 is R$10.00 and also ¥1000. The assistant has both
numbers; ask it to format, not to divide by a hundred.
Checking the connection without a client
The MCP Inspector talks to the endpoint directly, so it answers “is it me or is it them” before you go looking through an editor’s logs:https://api.userkit.dev/mcp, and an Authorization header of
Bearer uk_sk_test_…. If the tools list there and not in your editor, the
problem is the editor’s config file; if they list in neither, it is the key.
Troubleshooting
The client connects and lists no tools. Almost always the header. The endpoint answers401 with the usual error envelope when the credential is
missing or is not a uk_sk_… key — a publishable key or a panel session is
refused here, and the message says which one you sent.
Everything is empty. Check which key you pasted. An empty answer from a test
key is the environment boundary doing its job, not a broken read; the tools
report the environment they answered for in every response.
The assistant says it hit a rate limit. The MCP endpoint carries the same
per-key limit as the rest of the API — 1000 requests a minute per key, with a
3000-a-minute ceiling across an environment. An agent walking a large list in a
loop is exactly the caller that finds it. Ask for a narrower question, or give
the agent its own key so a long crawl cannot spend your backend’s allowance.