API & MCP reference
Drive LinkedMinds in plain language from Claude - or from any MCP client - over one HTTPS endpoint. Twenty-two tools, the same safety engine as the dashboard, and your own credentials that you can rotate or revoke at any time.
Overview
LinkedMinds exposes a Model Context Protocol (MCP) server over HTTPS. Any MCP client can use it - Claude Desktop, claude.ai, Claude Code or your own - by sending a Bearer token. Generate credentials from Settings → API Key inside your dashboard.
Connect to Claude (MCP)
Add the server to Claude in three steps.
- 1Go to Settings → API Key, open Claude MCP Credentials, and click Generate. Copy the Client Secret - it's shown once.
- 2In claude.ai → Settings → Connectors (or Claude Desktop → MCP Servers), choose Add custom MCP server.
- 3Enter the URL below and your credentials, then save.
https://mcp.linkedminds.in/mcpClaude Desktop config.json alternative:
{
"mcpServers": {
"linkedminds": {
"type": "streamable-http",
"url": "https://mcp.linkedminds.in/mcp",
"headers": { "Authorization": "Bearer <YOUR_CLIENT_SECRET>" }
}
}
}MCP tools
Once connected, ask Claude in plain language - it calls these under the hood.
list_campaigns()Every campaign with its ID, status and targeting. Start here - most tools need a campaign ID.
get_status()Real activity for the last 7 days: invites sent, accepted, replies received.
list_accounts()Your connected LinkedIn accounts and what each one is allowed to do. Ask before any live search.
create_campaign()Create a campaign from a plain-English brief. Always starts paused.
pause_campaign()Stop all automation for a campaign immediately.
resume_campaign()Resume a paused campaign within its working hours.
delete_campaign()Cancel a campaign for good. Requires an explicit confirmation.
update_icp()Adjust the ideal-customer profile a campaign searches against.
search_leads()Find leads instantly in our database, or scrape LinkedIn live. The one search tool to reach for.
search_people_db()Database-only search: instant, and it never touches LinkedIn.
import_leads_from_db()Add people found in the database straight into a campaign.
add_leads_to_campaign()Push your own list of LinkedIn profile URLs into a campaign, up to 200 a call.
remove_leads_from_campaign()Take leads back out of a campaign so nothing further is sent to them from it.
get_search_results()Check a running live search: which page it is on, what it has found, and the results once it finishes.
find_leads()Live LinkedIn people search, qualified by AI. Falls back to the campaign's saved ICP.
find_leads_advanced()Pro+Sales Navigator search with company-size and seniority filters.
find_recruiter_leads()Enterprise+LinkedIn Recruiter candidate search by job function and experience.
find_companies()Pro+Find companies on Sales Navigator, then search the people inside the ones worth pursuing.
set_campaign_workflow()Build the outreach sequence itself - warm-up, connection request, follow-ups, branches.
get_campaign_workflow()Read a campaign's current sequence in plain English before changing it.
get_post_campaign()Read a Post campaign's rules and the people waiting for messages, with the post and their comment.
set_post_messages()Save the messages your Claude wrote for a Post campaign. Checked, then approved by you before sending.
get_inbox()Sync replies and read the last 20 messages inline.
reply_message()Message a lead who accepted your request, in your own voice.
withdraw_connection()Withdraw a connection request that is still pending.
create_post()Standard+Draft, and optionally schedule, a LinkedIn post.
REST API
The REST API is for your own code - a CRM sync, a nightly report, an internal tool. It reads and writes the same data as the dashboard and enforces the same rules (plan limits, content policy, account safety), so nothing you can do here is something the product would not let you do anywhere else.
403 plan_required. The MCP server in section 1 is included on every plan and does the same work.Base URL and authentication
curl https://mcp.linkedminds.in/v1/me \ -H "Authorization: Bearer lm_YOUR_REST_API_KEY"
Generate the key in Settings → API Key under REST API Key. It is shown once. Rotating or revoking it takes effect immediately. GET /v1 needs no key and lists every endpoint.
| Endpoint | What it does |
|---|---|
| GET /v1/me | Your client record, plan, and subscription period. |
| GET /v1/accounts | Every connected LinkedIn account with its session status, warm-up phase, today's counters and ban-risk score. |
| GET /v1/campaigns | Your campaigns. Optional ?status=running|paused|cancelled, ?limit (max 200), ?offset. |
| POST /v1/campaigns | Creates a campaign. Requires name, target_titles[], target_locations[]. Optional target_industries[], product_description, target_objective, daily_connect_limit (1-20), account_id. Always created paused - an API call never starts sending by itself. |
| GET /v1/campaigns/{id} | One campaign. |
| PATCH /v1/campaigns/{id} | Updates name, targets, daily_connect_limit, or status ("running" / "paused"). Setting running is what starts the automation. |
| DELETE /v1/campaigns/{id} | Cancels a campaign and stops its pending work. It must be paused first. Leads are preserved. |
| GET /v1/campaigns/{id}/leads | The campaign's leads with their state, connect message, profile fields and the custom_variables stored for each. Optional ?state=CONNECTED etc. |
| POST /v1/campaigns/{id}/leads | Adds your own leads to a campaign. Body: {"leads":[{"linkedin_url":"..."}]} or {"urls":["..."]}. Max 200 per call. Optional per lead: first_name, last_name, full_name, headline, current_title, current_company, location, and custom_variables - your own merge fields (see below). Re-sending the same list is safe - duplicates are skipped, never restarted. Returns 202. |
| DELETE /v1/campaigns/{id}/leads | Removes leads from a campaign so nothing further is sent to them from it. Body: {"urls":["..."]} and/or {"deal_ids":["..."]}, max 200 per call. A lead that already has a conversation is stopped rather than deleted, so its messages stay in your inbox. Does not withdraw an invitation already sent on LinkedIn - withdraw first if you need that. Same call as POST /v1/campaigns/{id}/leads/remove, for tools that cannot send a body with DELETE. |
| GET /v1/campaigns/{id}/post-leads | Post campaigns only. The campaign's rules (your company profile and campaign rules) plus which messages the user wants written and how (their own text, AI lines, or written in full), and each person with the LinkedIn post and their comment, so your own AI or integration can write them. ?view=to_write (default), review, approved, waiting or all; limit up to 50. |
| POST /v1/campaigns/{id}/post-leads/messages | Post campaigns only. Save messages you wrote: {"messages":[{"deal_id":"...","invite_note":"...","opener":"...","followup_1":"...","followup_2":"..."}]}, max 50. followup_3 and followup_4 are accepted when the campaign's workflow has those messages. The invite note goes with the connection request (200 characters, no links). Each is checked like an AI-written message (length, no long dashes, no placeholders, only your booking link, content rules); rejected ones come back with the reason. Saved messages wait for approval in the dashboard unless the campaign sends automatically, and never use your AI allowance. |
| GET /v1/conversations | Your inbox: one entry per lead with the latest message, unread count and who it is with. |
| GET /v1/conversations/{deal_id} | The full message thread, oldest first. |
| POST /v1/conversations/{deal_id}/reply | Queues a LinkedIn message. Body: {"message": "..."}, max 2000 characters. The lead must have accepted your connection request. Returns 202. |
| POST /v1/deals/{deal_id}/withdraw | Withdraws a connection request that has not been accepted. Returns 202. |
Example: pause a campaign and read its leads
# Pause it
curl -X PATCH https://mcp.linkedminds.in/v1/campaigns/$CAMPAIGN_ID \
-H "Authorization: Bearer $LM_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"paused"}'
# Read the leads that accepted
curl "https://mcp.linkedminds.in/v1/campaigns/$CAMPAIGN_ID/leads?state=CONNECTED&limit=100" \
-H "Authorization: Bearer $LM_KEY"Example: push your own leads in
This is how a sheet, a CRM or an n8n / Zapier / Make workflow feeds a campaign. Send LinkedIn profile URLs; anything you know about the person is optional and only used until we enrich the profile ourselves. Adding a lead never sends anything on its own - the campaign has to be running, and every message still passes the same daily caps, warm-up phase and ban-risk checks.
# The short form - just URLs
curl -X POST https://mcp.linkedminds.in/v1/campaigns/$CAMPAIGN_ID/leads \
-H "Authorization: Bearer $LM_KEY" \
-H "Content-Type: application/json" \
-d '{"urls":[
"https://www.linkedin.com/in/some-person/",
"https://www.linkedin.com/in/another-person/"
]}'
# The long form - carry what you already know
curl -X POST https://mcp.linkedminds.in/v1/campaigns/$CAMPAIGN_ID/leads \
-H "Authorization: Bearer $LM_KEY" \
-H "Content-Type: application/json" \
-d '{"leads":[
{"linkedin_url":"https://www.linkedin.com/in/some-person/",
"first_name":"Priya","current_company":"Acme","current_title":"Head of Growth"}
]}'
# With your own merge fields - used in messages as {{c_first_line}} and {{c_role}}
curl -X POST https://mcp.linkedminds.in/v1/campaigns/$CAMPAIGN_ID/leads \
-H "Authorization: Bearer $LM_KEY" \
-H "Content-Type: application/json" \
-d '{"leads":[
{"linkedin_url":"https://www.linkedin.com/in/some-person/",
"custom_variables":{
"first_line":"Congrats on the $12M round",
"role":"Senior Backend Engineer"}}
]}'
# 202 Accepted
# {"data":{"added":1,"duplicates":0,"invalid":[],"invalid_count":0,
# "campaign_leads":42,"plan_limit":10000,
# "custom_variables_saved":1,"custom_variables_failed":0,
# "custom_variables_rejected":[]}}- 200 leads per call. Split a bigger list across calls; the writes-per-minute budget below paces them.
- Safe to retry. A lead already on the campaign is reported under
duplicatesand left exactly as it is - re-importing never restarts someone mid-conversation. - Bad URLs do not fail the call. Anything that is not a LinkedIn profile URL comes back in
invalidwhile the rest are added. Only a request where every row is invalid returns 422. - Leads per campaign are capped at 10,000 on Pro and 50,000 on Enterprise. Past that the call returns 422
campaign_lead_limitrather than importing part of the batch.
Custom variables
The API version of the extra columns in a CSV import. Anything you know that LinkedIn does not - an opening line, the role you are hiring for, a renewal month - goes in custom_variables on the lead, and your messages reference it with a c_ prefix: {"role": "..."} is {{c_role}} in a template. The prefix is what stops a variable called company from changing what {{company}} means.
- Names are lowercase letters, digits and underscores, up to 64 characters, sent without the
c_. Up to 20 per lead, 500 characters per value. Numbers are stored as text. - Nothing is dropped silently. A name we cannot store, or a value that is an object, a list or true/false, is skipped and listed in
custom_variables_rejectedwhile the lead is still added. - Sending custom_variables replaces the lead's whole set, the same as re-importing a CSV with those columns. Leaving the field out keeps what the lead already has, so a plain retry never erases anything.
- Check what was stored with
GET /v1/campaigns/{id}/leads- each lead carries itscustom_variables. - A variable a lead does not have renders as blank, exactly like a built-in with no value. They are private to your workspace and never shared with anyone else targeting the same person.
Rate limits
Three separate budgets, because the three kinds of call cost very different things. A read is free and carries no LinkedIn risk. A write changes your workspace. An action - a reply or a withdrawal - puts real work on a real LinkedIn account, so it is measured over an hour rather than a minute.
| Budget | Pro | Enterprise | Counts |
|---|---|---|---|
| Requests / minute | 120 | 300 | Every call, reads included. |
| Writes / minute | 30 | 60 | POST, PATCH and DELETE. A write also spends a request. |
| LinkedIn actions / hour | 60 | 150 | Replies and withdrawals. These also spend a write and a request. |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and the same pair prefixed X-RateLimit-Actions- for the hourly budget, so you can pace yourself rather than discover the ceiling by hitting it. A 429 carries Retry-After in seconds - wait that long rather than retrying in a tight loop.
Retries and duplicate sends
A 202 means the work is queued, not delivered. If a call times out you can safely retry it: an identical message to the same conversation within two minutes is treated as the retry it almost certainly is and comes back "deduplicated": true without sending again. A message a recipient has already seen cannot be unsent, so this errs towards sending once.
If the action cannot be queued at all - the account is disconnected, has no working proxy, or is not usable for that job - you get a 409 with the reason, and nothing is recorded as sent. A 502 means the queue itself was unreachable and nothing was queued; retry it.
Responses, paging and errors
List endpoints return { "data": [...], "limit": n, "offset": n } and single objects return { "data": {...} }. Anything that queues LinkedIn work returns 202 - queued, not yet sent. Errors are always { "error": { "code": "...", "message": "..." } }, and the message is written to be shown to a human as-is.
| Status | code | When |
|---|---|---|
| 400 | invalid_query / invalid_id / invalid_json | A query filter is not one of the accepted values, an ID is not a UUID, or the body is not JSON. The message lists what is valid. |
| 401 | missing_credentials / invalid_key | No Authorization header, or the key is wrong, rotated or revoked. |
| 402 | subscription_inactive / proxy_required | The subscription lapsed, or a trial account tried to start a campaign without a proxy. |
| 403 | plan_required | Your plan does not include the REST API. |
| 404 | not_found | The campaign or conversation does not exist, or is not yours. The two are deliberately indistinguishable. |
| 409 | campaign_running / account_disconnected / not_connected / not_pending / campaign_limit_reached | The request is valid but the current state does not allow it. The message says which. |
| 409 | no_proxy / account_unusable / account_not_found | The action could not be put on your LinkedIn account - usually no working proxy, or the account is circuit-broken or the wrong tier. Nothing was recorded as sent. |
| 422 | invalid_field / content_blocked / message_too_long | A field is missing or malformed, or the text hit the content policy. |
| 413 | body_too_large | The request body is over 64 KB. Nothing legitimate comes close - split the work up. |
| 429 | rate_limited | One of the three budgets is spent. The body names which (requests, writes or actions) and Retry-After says how long to wait. |
| 429 | too_many_attempts | Too many rejected keys from your IP in five minutes. Fix the key rather than retrying - a correct key is refused too while the block lasts. |
| 401 | key_disabled | We disabled this key after detecting an attack on it, and told you why. Generate a new one in Settings - nothing else about your account changed. |
| 502 / 503 | queue_unavailable / unavailable | A dependency is briefly unavailable. Retry with backoff; nothing was queued twice. |
How the API is protected
- Keys are stored only as bcrypt hashes (rounds 12) - we cannot read yours back, and a rotate or revoke kills the old one immediately, not after a cache expires.
- Twenty rejected keys from one IP in five minutes blocks that IP for the rest of the window, so guessing a key is not something you can do at speed.
- Filter values are checked against a fixed list, IDs must be UUIDs, arrays are capped, paging is clamped and bodies over 64 KB are refused.
- Every request is scoped to your own client ID in the query itself. Another customer's campaign or conversation returns 404 - the same answer as one that does not exist, so the API cannot be used to find out who else is on the platform.
- Responses carry an explicit field list. Credentials, cookies, TOTP seeds and internal audit fields are never in a payload, on any endpoint.
- Every write is recorded to your audit log with the endpoint and the campaign it touched.
If we think a key has leaked, we disable it and tell you
The same watch runs on both credentials - the REST key and the Claude MCP client secret. It reacts to two different things, in two deliberately different ways:
- Repeated wrong secrets against your key (twelve in fifteen minutes). Neither key can realistically be guessed - they are 32 random bytes - so this means someone has seen the first part of a real key, in a screenshot, a log or a committed file. If that leaked, the rest may have. We disable the key, email you and post it to the bell, and Settings shows what happened. Generating a new key clears it. Nothing else is touched: campaigns, LinkedIn accounts and data carry on.
- One valid key used from an unusual number of places (25+ IP addresses in an hour). We only tell you - we do not disable it. That pattern is what a leaked key looks like, but it is also just what a serverless integration looks like, and killing a working production automation on that evidence would be the worse mistake. You decide whether to rotate.
Rotating is always safe and always available - it takes effect immediately rather than when a cache expires.
Using it from n8n
Yes - the REST key is exactly what n8n needs, and no LinkedMinds-specific node is required. Add an HTTP Request node, set Authentication to Generic Credential Type → Header Auth, and store the key as a credential so it never sits in the workflow JSON:
Name: Authorization Value: Bearer lm_your_key_here Method: GET URL: https://mcp.linkedminds.in/v1/conversations?unread=true
The same applies to Zapier (Webhooks by Zapier → Custom Request) and Make (HTTP module). Three things worth setting up front, because they are what breaks these workflows:
- Do not poll every minute. A schedule of five minutes or more sits comfortably inside the per-minute budget even with several workflows running.
- Turn on retry-on-fail with a wait, and honour
Retry-After. n8n's default retry is immediate, which just spends the rest of your budget. - A 202 is queued, not sent. If a branch depends on the message having actually gone out, read it back from
/v1/conversations/{deal_id}rather than assuming.
Authentication
Secrets are stored as bcrypt hashes (rounds=12). We can't recover the raw value - rotate to get a new one.
Verified credentials are cached briefly so repeated calls don't hit the database every time.
Rotate or revoke from Settings; the cache is flushed immediately.
If your subscription lapses, credentials stop working until you renew.
Rate limits & safety
Driving LinkedMinds from Claude changes nothing about how carefully it acts. Every action runs through the same pacing engine as the dashboard: randomised delays of 2-10 minutes, burst windows with cool-down breaks, a four-week warm-up on every new account (5 requests a day in week 1, rising to 20 by week 4), and per-account caps set at or below LinkedIn's own.
| Limit | Window | Applies to |
|---|---|---|
| 60 calls | 1 minute | Every MCP tool |
| 10 calls | 1 hour | Live LinkedIn searches only |
| 5 - 200 searches | 1 hour | Database searches, by plan |
| 20 / day | Per account | Connection requests at full warm-up |
Database searches never contact LinkedIn, so they carry no ban risk and do not spend your LinkedIn search quota. Full parameter-level reference for all 26 tools lives in the dashboard, under Docs.
See plan limits