Skip to main content
UserKit speaks Model Context Protocol, so an assistant that supports MCP — Claude Desktop, Claude Code, Cursor, and anything else that reads an MCP config — can answer questions about your contacts, customers, subscriptions and pricing without leaving the tool you were already in. There is nothing to install. The server is an endpoint on the API:
It is authenticated with an ordinary API key (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.
An API key is a full-access credential for everything else. The MCP surface reads everything and writes only in the nine places listed below, but the same string works against every write endpoint on /v1. Treat the config file you paste it into like any other place a secret lives, and use a key minted for this rather than the one your backend runs on — keys are free, and a key per purpose is a key you can revoke on its own.

Connect a client

In the project you want the assistant to work in:
That writes the config for you — and an 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.
Restart the client, and it will list ninety tools. Ask it something — “how many contacts signed up in the test environment?” — and it will pick one.

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.
Three things follow from how the help centre is built, and an assistant will tell you about all three because the tools say so in their answers. Every save is a new version, and nothing is ever overwritten. The history is the whole record of what an article has said, “restore” is saving an old body again, and even deleting an article is a different act from taking it off the site — 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.
This is your pricing page. Unlike the help centre, these tools change what real customers are quoted — in the environment your key belongs to, so a test key can only ever touch your test catalogue. Ask the assistant to read get_catalogue and tell you what it is about to do before you let it write.
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 published version is frozen at the database and cannot be edited or withdrawn. Publishing is the act: it can be dated ahead, and who is asked to agree again is decided by the highest in-force version that declared a material change. delete_legal_document works only while nothing was ever published — after that, taking a document out of circulation is archiving it.
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:
There is no unpublish. publish_changelog_post puts the post on your public changelog page — a document a CDN may keep — makes the widget’s unread badge count it, and delivers changelog_post.published to your webhooks. The only way back is delete_changelog_post, which recalls none of that and takes everybody’s read state with it. An assistant should publish because you asked for that release to be announced, never to tidy up a list of drafts.
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.
Changing the steps rewrites the past. With no stored history to disagree with the new definition, reordering the steps or swapping an event re-answers the earlier months too: a chart somebody screenshotted last week will not reproduce. 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.
An 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.
None of them gives points to anybody. Granting is POST /v1/points, a deliberate act by your backend with a required request_key. No tool here takes contact_id, external_id or amount — configuring what a product rewards is a draft somebody reviews; paying somebody is not.
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.
Two edits are refused with a sentence. The 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:
Connect it with transport Streamable HTTP, the URL 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 answers 401 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.