API reference · updated 2026-09-20

One set of capabilities. Two ways to call them.

A capability is one thing this server does for you — search LinkedIn through an account you connected, read your inbox, install a product. There are 21. An AI client reaches them over MCP; everything else reaches them over plain HTTP. Same credential, same scopes, same limits, same executor.

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 4e1f93d2 — 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.

4 of the 21 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: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

Only its hash is stored, so it cannot be read back out of the database — by us or by anyone who gets a copy of it. Lost keys are replaced, not recovered.

Call it over HTTP

GET /api/v1/capabilities lists the surface and needs no credential — it describes the server, not a workspace.

shell
curl -s https://topclozer.com/api/v1/capabilities

POST /api/v1/capabilities/{name} runs one. The body is the arguments object — the same arguments the MCP tool takes — or no body at all.

shell
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:

json
{
  "capability": "search_linkedin_people",
  "result": {
    "searchedAs": "Marcus Feld",
    "count": 25,
    "people": [ … ]
  }
}

Field naming

This surface is snake_case, including the error body. The older resource routes — /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.

shell
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.

shell
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 limit

Exit 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.

typescript
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

The client and the CLI ship in the repository, not in a registry. Until they are published, run the CLI from a clone and build the library with 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.

LimitBudgetApplies to
requests120 per 60s per workspaceEvery call on the HTTP surface.
linkedin30 per 300s per workspaceThe 4 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.

ClassHTTPMeans
invalid_request400The body was not a JSON object.
unauthorized401No valid credential was presented.
forbidden_scope403A valid key that is not scoped for this capability.
unknown_capability404No capability by that name.
capability_refused422It ran and said no, for a reason you can act on.
rate_limited429A budget is spent. Carries retry_after.
upstream_failed502Anything else. A bug here, or a provider that failed in a way we did not model.
provider_unconfigured503Our problem: the provider behind it is not configured on this server.

Every capability

All 21, with the scope each one needs and what it costs. The name is both the MCP tool name and the HTTP path segment.

CapabilityScopeProviderNotes
search_linkedin_peopleSearch LinkedIn for people matching an ICP, through a connected sender accountlinkedin:readunipileread-only · LinkedIn budget · recorded as a run
get_linkedin_profileRead one LinkedIn profile: identity, current role, followers, job-change signal and latest postlinkedin:readunipileread-only · LinkedIn budget · recorded as a run
get_linkedin_postsRecent posts by a LinkedIn member, newest firstlinkedin:readunipileread-only · LinkedIn budget · recorded as a run
list_linkedin_connectionsFirst-degree connections of a connected sender accountlinkedin:readunipileread-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 budgetworkspace:readinternalread-only
get_usageHow many LinkedIn tool calls this workspace has made this month, and the rate limit in forceusage:readinternalread-only
list_campaignsList all campaigns with performance statsworkspace:readinternalread-only
get_statsGet workspace-wide outreach statsworkspace:readinternalread-only
list_leadsList lead lists and leadsworkspace:readinternalread-only
get_inboxGet unified inbox conversationsworkspace:readinternalread-only
list_tasksList drafts waiting for approvalworkspace:readinternalread-only
pause_campaignPause a campaign by idworkspace:writeinternalwrites
search_capabilitiesSearch what TopClozer products can actually do, and what performs each capability todayproducts:readinternalread-only
search_productsSearch the TopClozer catalogue by outcome, kind, tier, price, risk or capabilityproducts:readinternalread-only
get_productOne product in full: capabilities, risk, price, and whether this workspace has bought or installed itproducts:readinternalread-only
install_productRecord an installation of a product into an AI client or the hosted runner. Writes a row; runs nothing.products:installinternalwrites
list_installationsWhat this workspace has installed, where, and what it paid forproducts:readinternalread-only
list_projectsThe projects in this workspace, with how complete each one's context isprojects:readinternalread-only
get_project_contextThe reusable company / ICP / offer / voice record an agent should read before it writes anythingprojects:readinternalread-only
check_creditsThe workspace credit balance and the ledger entries behind itcredits:readinternalread-only
get_platform_usageWhat this workspace owns and has run: installs, purchases, the hosted seat, credits and the execution ledgerusage:readinternalread-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_leads and get_inbox return 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.

FAQ

Is the HTTP API a different product from the MCP endpoint?
No. They are two transports over one executor. The same 21 capabilities, the same API key, the same scopes, the same LinkedIn budget, the same execution ledger. Whichever one you call, the code that resolves the sender account and talks to LinkedIn is the same code — which is why the two cannot disagree about what a capability does or what it refuses.
Can an agent send a LinkedIn invite or a message through this API?
No, and that is a product decision rather than a gap. 2 of 21 capabilities write anything at all (pause_campaign and install_product), and neither writes to LinkedIn. Outbound actions are released by the product under the workspace sending policy, which by default waits for a person to approve them.
What happens if I hit the LinkedIn rate limit?
You get HTTP 429 with error class rate_limited and retry_after in seconds. Nothing is queued and nothing is retried for you. The limit — 30 calls per 5 minutes per workspace — exists because a retry loop against a live LinkedIn session is the request pattern that gets accounts restricted, and the account at risk is yours.
Do I need an API key to read the documentation endpoint?
No. GET /api/v1/capabilities and GET /api/v1/capabilities/{name} are unauthenticated: they describe the server, not a workspace. Only POST needs a credential.