One surface, two transports
Everything below is generated from the capability table this server dispatches on, so it cannot describe a capability the endpoint does not serve. The table's fingerprint today is 7b1c2fd4 — it changes whenever a capability's name, arguments, scope or routing change, and the generated client carries a copy so a client can tell whether it was built for the server it is talking to.
43 of the 65 reach LinkedIn through a sender account you connected. The rest read this server's own database — your campaigns, leads, inbox, the product catalogue, your library and projects. The split decides everything that follows: whether a call spends the LinkedIn budget, whether it lands in the execution ledger, and whether an unconfigured provider can make it fail.
Authenticate
One credential for both transports: a workspace API key, minted on the Developer page in the app. Send it as Authorization: Bearer tcz_live_…. The header X-Api-Key works too, and ?key= exists for clients that accept only a URL — it ends up in access logs, so prefer a header.
Scopes belong to the key, not to the workspace: two keys are how you give a third-party client read access to LinkedIn without also handing it the ability to install products. The scopes in use are credits:read, linkedin:propose, linkedin:read, products:install, products:read, projects:read, usage:read, workspace:read, workspace:write. A key minted before scopes existed carries none and is allowed everywhere; that is deliberate, so nobody's integration broke the day scopes shipped.
A key is shown once
Call it over HTTP
GET /api/v1/capabilities lists the surface and needs no credential — it describes the server, not a workspace.
curl -s https://topclozer.com/api/v1/capabilitiesPOST /api/v1/capabilities/{name} runs one. The body is the arguments object — the same arguments the MCP tool takes — or no body at all.
curl -s -X POST https://topclozer.com/api/v1/capabilities/search_linkedin_people \
-H "Authorization: Bearer tcz_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"keywords":"VP Engineering","industry":"Financial Services","limit":25}'A success is the capability name and its payload, so a log line says what produced the result:
{
"capability": "search_linkedin_people",
"result": {
"searchedAs": "Marcus Feld",
"count": 25,
"people": [ … ]
}
}Field naming
/api/v1/campaigns, /api/v1/leads, /api/v1/accounts, /api/v1/inbox — return the workspace document as the app stores it, which is camelCase. That inconsistency is real; it is written down here rather than hidden by translating one of them at the edge, and it will be resolved by a v2, not by changing a response somebody is already parsing.Call it from an AI client
The same capabilities, spoken as JSON-RPC at https://topclozer.com/api/mcp. This is the transport Claude, Cursor, VS Code, ChatGPT, n8n and Make understand.
claude mcp add --transport http topclozer https://topclozer.com/api/mcp \
--header "Authorization: Bearer tcz_live_YOUR_KEY"Per-client setup, the tool reference and the response envelopes are in the MCP documentation; the endpoint itself is described at /hosted-mcp.
The CLI
Zero dependencies and no build step. It asks the server what exists rather than carrying its own copy, so it describes the server you point it at — a capability shipped this morning is callable from a CLI installed last year.
export TOPCLOZER_API_KEY=tcz_live_YOUR_KEY
topclozer capabilities # what this server can do
topclozer describe get_linkedin_profile
topclozer call get_stats
topclozer call search_linkedin_people --keywords "VP Engineering" --limit 25
topclozer usage # LinkedIn calls this month, and the limitExit codes are 0 success, 1 usage error, 2 the server refused or failed — so a shell script can branch without parsing output.
The TypeScript client
Capability names and argument names are typed, generated from the server's own registry. Results are typed unknown: each payload is documented in prose and an example on its reference page, and inventing interfaces from an illustrative example would produce types that look authoritative and are not.
import { createClient, TopclozerError } from "@topclozer/sdk";
const topclozer = createClient({ apiKey: process.env.TOPCLOZER_API_KEY });
try {
const people = await topclozer.call("search_linkedin_people", {
keywords: "VP Engineering",
limit: 25,
});
} catch (e) {
if (e instanceof TopclozerError && e.errorClass === "rate_limited") {
// e.retryAfter is in seconds. Backing off is your decision, not the library's.
}
}Not on npm yet
npm --prefix packages/sdk run build.Neither of them retries. A retry loop against the LinkedIn budget is the exact request pattern that gets a LinkedIn account restricted, and a library that hid a 429 behind three silent attempts would be doing that to your account on your behalf.
Budgets
Two limits, and conflating them is how a job gets planned against the wrong number.
| Limit | Budget | Applies to |
|---|---|---|
| requests | 120 per 60s per workspace | Every call on the HTTP surface. |
| 30 per 300s per workspace | The 43 capabilities that reach LinkedIn, on either transport. |
Both are per workspace, not per key: a second key does not buy a second allowance, and splitting traffic between the two transports does not either. The LinkedIn number is published because it is enforced.
Errors
Every failure carries a stable class, so a client can branch without parsing English: { "error": { "class": "rate_limited", "message": "…", "retry_after": 42 } }. On MCP the same classes become JSON-RPC codes, and the ones a model can act on come back as a readable tool result rather than a protocol fault.
| Class | HTTP | Means |
|---|---|---|
| invalid_request | 400 | The body was not a JSON object. |
| unauthorized | 401 | No valid credential was presented. |
| forbidden_scope | 403 | A valid key that is not scoped for this capability. |
| unknown_capability | 404 | No capability by that name. |
| capability_refused | 422 | It ran and said no, for a reason you can act on. |
| rate_limited | 429 | A budget is spent. Carries retry_after. |
| upstream_failed | 502 | Anything else. A bug here, or a provider that failed in a way we did not model. |
| provider_unconfigured | 503 | Our problem: the provider behind it is not configured on this server. |
Every capability
All 65, with the scope each one needs and what it costs. The name is both the MCP tool name and the HTTP path segment.
| Capability | Scope | Provider | Notes |
|---|---|---|---|
| search_linkedin_peopleSearch LinkedIn for people matching an ICP, through a connected sender account | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_linkedin_profileRead one LinkedIn profile: identity, current role, followers, job-change signal and latest post | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_linkedin_postsRecent posts by a LinkedIn member or company page, newest first | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_linkedin_connectionsFirst-degree connections of a connected sender account | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_account_seatWhat the connected LinkedIn seat includes: Premium, Sales Navigator, Recruiter, Open Profile, InMail credits left, company pages it can message as | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_my_profileThe connected account's own LinkedIn profile: name, headline, location, public URL, email | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_profile_viewersWho viewed the connected account's profile recently, as LinkedIn lists it | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_ssiThe connected account's LinkedIn Social Selling Index: overall score and the four pillars | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_profile_sectionsOne LinkedIn profile in depth: experience, education, skills (with endorsement ids), languages, certifications, projects, recommendations, contact info | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| check_reachabilityBefore reaching out: degree of connection, pending invitation, whether a connection request, a message or an InMail can go to this person — and whether the do-not-contact list or another account of the workspace already covers them | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_companyA LinkedIn company page: description, industry, size, headquarters, followers, website, employee growth | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_postOne LinkedIn post: text, author, reactions, comments, reposts, and whether the account can react or comment | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_post_commentsComments on a LinkedIn post, or replies to one comment — with each commenter's headline and degree | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_post_reactionsWho reacted to a LinkedIn post (or one of its comments), with each person's headline and degree — the warmest leads a post produces | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_profile_activityWhat a member has been doing on LinkedIn: the comments they wrote or the posts they reacted to | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_sent_invitationsConnection requests the account sent that are still pending — the ones to withdraw before they hurt the accept rate | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_received_invitationsConnection requests the account received and has not answered, with each inviter's headline and note | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_followersFollowers of the connected account, or of another member or company | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_followingMembers and pages the connected account follows | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_conversationsThe account's LinkedIn conversations, newest first — classic, Sales Navigator, Recruiter and company-page inboxes | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| get_conversationOne LinkedIn conversation: who is in it (headline, degree) and its messages, oldest first | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_recent_messagesThe latest LinkedIn messages across all of the account's conversations, newest first | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| search_linkedin_companiesSearch LinkedIn companies by keywords, location, industry and headcount — classic or Sales Navigator | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| search_linkedin_postsSearch LinkedIn posts by keywords — the conversations a market is having this week | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| search_linkedin_jobsSearch LinkedIn job postings — the companies hiring for a role are the companies with the budget | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| search_linkedin_urlRun a search a person built in LinkedIn or Sales Navigator and copied the URL of — people, companies, posts or jobs, filters intact | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| find_company_peopleThe people at one company who match a role — decision makers, a department, a title — through the account's search | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| lookup_search_filterThe ids LinkedIn's typed search filters take, for a name: a location, industry, company, school, job title, skill… | linkedin:read | unipile | read-only · LinkedIn budget · recorded as a run |
| list_sender_accountsThe LinkedIn accounts this workspace can act as, with their status, warm-up stage and remaining daily budget | workspace:read | internal | read-only |
| connect_linkedin_accountConnect a LinkedIn account to this workspace. Returns a sign-in link, valid for 24 hours, for the PERSON to open and sign in to LinkedIn on the provider's hosted page; the account is added when they finish. Give the person the URL. Never ask them for their LinkedIn password, a verification code or a session cookie — they enter those only on that page | workspace:write | internal | writes |
| propose_connection_requestPropose a LinkedIn connection request. It is NOT sent: it waits in the workspace's Approvals queue until a person approves it, then goes out under the account's Smart Limits. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_messagePropose a LinkedIn message to a 1st-degree connection. It is NOT sent: it waits in Approvals until a person approves (and may edit) it. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_profile_visitPropose visiting a LinkedIn profile as the account — the person will see it in "Who viewed your profile". NOT done until approved. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_inmailPropose an InMail to someone outside the account's network. NOT sent: it waits in Approvals. Spends an InMail credit when sent, unless the person has an Open Profile. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_replyPropose a reply in an existing LinkedIn conversation. NOT sent: it waits in Approvals. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_withdraw_invitationPropose withdrawing a pending connection request. NOT done until approved. LinkedIn then blocks re-inviting that person for about three weeks. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_accept_invitationPropose accepting a connection request the account received. NOT done until approved. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_ignore_invitationPropose ignoring a connection request the account received. NOT done until approved. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_followPropose following a LinkedIn member — a softer first touch than an invitation. NOT done until approved. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_unfollowPropose unfollowing a LinkedIn member. NOT done until approved. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_endorsementPropose endorsing one of a member's skills. NOT done until approved. The person is notified. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_post_reactionPropose reacting to a LinkedIn post or one of its comments. NOT done until approved. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_commentPropose a comment on a LinkedIn post, or a reply to a comment on it. NOT posted until approved. Public and permanent. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_postPropose a post on the account's own feed, optionally scheduled. NOT published until approved — and not before publish_at. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| propose_message_reactionPropose reacting to a LinkedIn message with an emoji. NOT done until approved. | linkedin:propose | unipile | writes · LinkedIn budget · recorded as a run |
| get_approvalThe status of a proposal made with any propose_* tool: pending, approved, sent, rejected, failed or expired | linkedin:propose | internal | read-only |
| list_approvalsProposals in this workspace and what became of them: pending, approved, sent, rejected, failed or expired | linkedin:propose | internal | read-only |
| list_do_not_contactThe workspace's do-not-contact list: people and companies no account may invite, message or InMail | workspace:read | internal | read-only |
| add_do_not_contactAdd a person or a company to the do-not-contact list. It can only stop outreach, never start it. | workspace:write | internal | writes |
| get_usageHow many LinkedIn tool calls this workspace has made this month, and the rate limit in force | usage:read | internal | read-only |
| list_campaignsList all campaigns with performance stats | workspace:read | internal | read-only |
| get_statsGet workspace-wide outreach stats | workspace:read | internal | read-only |
| list_leadsList lead lists and leads | workspace:read | internal | read-only |
| get_inboxGet unified inbox conversations | workspace:read | internal | read-only |
| list_tasksList drafts waiting for approval | workspace:read | internal | read-only |
| pause_campaignPause a campaign by id | workspace:write | internal | writes |
| search_capabilitiesSearch what TopClozer products can actually do, and what performs each capability today | products:read | internal | read-only |
| search_productsSearch the TopClozer catalogue by outcome, kind, tier, price, risk or capability | products:read | internal | read-only |
| get_productOne product in full: capabilities, risk, price, and whether this workspace has bought or installed it | products:read | internal | read-only |
| install_productRecord an installation of a product into an AI client or the hosted runner. Writes a row; runs nothing. | products:install | internal | writes |
| list_installationsWhat this workspace has installed, where, and what it paid for | products:read | internal | read-only |
| list_projectsThe projects in this workspace, with how complete each one's context is | projects:read | internal | read-only |
| get_project_contextThe reusable company / ICP / offer / voice record an agent should read before it writes anything | projects:read | internal | read-only |
| check_creditsThe workspace credit balance and the ledger entries behind it | credits:read | internal | read-only |
| get_platform_usageWhat this workspace owns and has run: installs, purchases, the hosted seat, credits and the execution ledger | usage:read | internal | read-only |
What this API does not do
Stated here because finding out from a 404 wastes a developer's afternoon, and because an API that only advertises its strengths is how integrations get built on assumptions.
- No sending. Nothing here invites, messages or posts on LinkedIn, and there is no plan for a capability that does. Outbound actions are released by the product under the workspace sending policy, which by default waits for a person.
- No webhooks. There is no way to be told that a reply arrived; you poll
get_inbox. - No paging on the workspace reads.
list_leadsandget_inboxreturn everything in one payload. On a large workspace that response is big; filter on your side. - No creating campaigns, importing leads or approving drafts. Those are in the app. An agent can read the approval queue and summarise it; it cannot press send.
- No idempotency keys. The two write capabilities are effectively idempotent — pausing a paused campaign changes nothing, and an install records a row — but there is no header that makes a retry safe in general, so do not build one that assumes it.