Developer documentation

Everything your LinkedIn agent does in the dashboard, it can do over an API. Search for people, add them to campaigns, read and send messages, and get told the moment something happens — plus a hosted MCP server so an AI assistant can drive all of it directly.

REST API

Base URL

https://api.connectzly.com/public/v1

MCP server

Endpoint

https://mcp.connectzly.com

What you can build

Before you start. API access requires a paid Connectzly plan. Keys cannot be created on a trial.

Quickstart

Three steps: create a key, confirm it works, then add a lead.

1 · Create a key

Open app.connectzly.com/api-mcp and create a key. Give it a name you'll recognise later — the tool it belongs to is usually the most useful label.

The secret is displayed once. Copy it when it appears. If you lose it, delete the key and create another — there is no way to reveal it again.

2 · Confirm the key works

Checking your credit balance is the cheapest possible test. It costs nothing and returns instantly.

export CONNECTZLY_API_KEY="sk_live_..."

curl -s https://api.connectzly.com/public/v1/credits \
  -H "X-API-Key: $CONNECTZLY_API_KEY"
{
  "credits": 842,
  "topUpCredits": 200,
  "planId": "pro"
}

3 · Add a lead

You'll need a campaign ID. Fetch one with GET /campaigns, or copy it from the campaign's page in the dashboard.

curl -s https://api.connectzly.com/public/v1/campaigns/CAMPAIGN_ID/leads \
  -H "X-API-Key: $CONNECTZLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
    "fullName": "Jane Doe",
    "jobTitle": "VP Sales",
    "companyName": "Acme"
  }'

A lead that is genuinely new returns "status": "created" and costs one credit. Duplicates and excluded profiles are free — see Credits.

Authentication

Every request carries your key in a header. There is no OAuth flow and no bearer token.

X-API-Key: sk_live_xxxxxxxx
Content-Type: application/json
Use X-API-Key, not Authorization: Bearer. A bearer header is ignored and the request is rejected as unauthenticated.

Key format and limits

PropertyValue
Prefixsk_live_
Keys per workspace5 by default
EligibilityPaid plans only — not available on trial
ScopeA key acts as the workspace owner

Managing keys

The same dashboard page lists every key with the date it was last used, which makes it easy to spot ones nothing is calling any more. Deleting a key revokes it immediately — rotate by creating the replacement first, switching your integration over, then deleting the old one.

A missing or invalid key returns 401 unauthorized.

Credits and rate limits

What costs credits

Almost nothing does. Only creating a new lead consumes a credit — everything else is free to call.

ActionCredits
POST/campaigns/:campaignId/leads when a lead is created1
Duplicate or excluded profile — nothing is inserted0
POST/linkedin/searchPOST/linkedin/profiles/lookupPOST/inbox/messagesstart, pause and every GET
0

Running out returns 403 insufficient_credits.

Request limits

All /public/v1/* routes share a ceiling of 120 requests per minute. Some routes are tighter, because they touch LinkedIn rather than only our own systems.

RouteLimit
POST …/leads60 / minute
PATCH …/leads/:id60 / minute
POST /inbox/messages10 / minute
POST /linkedin/profiles/lookup5 / minute per key · 1 per 20s per account · 80/day, or 150/day with Sales Navigator
POST /linkedin/search6 / minute per key · 1 page per 15s per account · daily people quota shared with dashboard search (1,000, or 1,500 with Sales Navigator)
The LinkedIn-facing limits are deliberate. They are the same pacing rules the agent follows in the dashboard, and they exist to protect the account you're automating. Treat a 429 as the system doing its job, not as a failure to route around.

Backing off correctly

A rate-limited response tells you exactly how long to wait, in two places — the standard Retry-After header and error.details.retryAfterSeconds in the body. Honour whichever your HTTP client makes easier to read.

Errors

Every failure returns the same shape, so you can parse errors once and reuse that code everywhere.

{
  "error": {
    "code": "insufficient_credits",
    "message": "Not enough credits to add this lead.",
    "details": {}
  }
}
Branch on error.code, not on the HTTP status. Several distinct situations share status 403 — an ineligible plan, an empty credit balance, a disconnected LinkedIn account. Only the code tells you which, and only the code tells you whether retrying could ever help.

Status codes

StatusMeaning
200Success. Bodies are top-level JSON with no wrapper object.
400The request body or query failed validation
401Key missing or invalid
403Plan ineligible, out of credits, Sales Navigator required, or account disconnected
404No such campaign or lead
429Rate limit or LinkedIn daily quota reached
502 / 503Temporary upstream problem — safe to retry

Error codes

CodeWhat happenedRetry?
unauthorizedKey missing or wrongNo
plan_requiredPlan doesn't include API accessNo
validation_errorUnknown field, malformed URL or bad cursorNo
not_foundCampaign or lead doesn't existNo
insufficient_creditsNo credits left to insert a leadAfter topping up
account_disconnectedThe LinkedIn account can't be used right nowAfter reconnecting
sales_nav_requiredSales Navigator search attempted without an SN accountNo
rate_limitedRequests-per-minute or burst limit hitYes, after the stated delay
linkedin_daily_quotaLinkedIn's daily allowance is spentTomorrow
send_failedThe message could not be deliveredInvestigate first
upstream_unavailableTransient failure downstreamYes

Endpoints

Base URL https://api.connectzly.com/public/v1. Every request needs the X-API-Key header.

MethodPathDoesCreditsDocs
GET/creditsCurrent balance and plan0Open
GET/accountsConnected LinkedIn accounts and their IDs0Open
GET/campaignsList campaigns0Open
GET/campaigns/:idA single campaign in detail0Open
POST/campaigns/:id/startResume a campaign0Open
POST/campaigns/:id/pausePause a campaign0Open
GET/campaigns/:id/leadsLeads in a campaign0Open
GET/campaigns/:id/leads/:leadIdOne lead0Open
POST/campaigns/:id/leadsAdd a lead1 if createdOpen
PATCH/campaigns/:id/leads/:leadIdSet a tag or exclude the lead0Open
POST/inbox/messagesSend a LinkedIn message to a lead0Open
POST/linkedin/profiles/lookupLook up a profile0Open
POST/linkedin/searchSearch LinkedIn0Open

Sending a message

The lead must already exist in the campaign. The call is synchronous — you'll know whether it sent before the response returns.

curl -s https://api.connectzly.com/public/v1/inbox/messages \
  -H "X-API-Key: $CONNECTZLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaignId": "search_abc",
    "leadId": "lead_xyz",
    "text": "Hi Jane — following up on our conversation."
  }'

text accepts up to 8,000 characters. accountId is optional; leave it out and the account already associated with the lead is used. Once a message is sent, the conversation follows the same agent and human-handoff rules as the dashboard inbox.

Tagging a lead

Tags are a fixed set — anything outside it is rejected with validation_error.

PATCH /campaigns/search_abc/leads/lead_xyz

{ "tag": "Interested", "excluded": false }

Accepted values: Meeting request, Interested, Information request, Not interested, Wrong person, To be defined. Pass null or an empty string to clear a tag. Tagging may fire a webhook — see below.

Webhooks

Rather than polling, let Connectzly tell you. Point us at an HTTPS URL and we'll POST JSON to it when something happens.

Setting one up

  1. Go to app.connectzly.com/api-mcp → Webhooks.
  2. Give it a name, an HTTPS URL, and the events you want. You can scope it to a single campaign.
  3. Optionally set a signing secret so your endpoint can verify the call came from us.

Five webhooks per workspace by default. Subscriptions are managed in the dashboard rather than over the API.

Events

EventFires when
reply.receivedA prospect replies
connection.acceptedA connection request is accepted
lead.taggedA lead is tagged, by a human or by the agent
account.disconnectedA LinkedIn account drops out or fails

Payload

{
  "id": "evt_...",
  "type": "reply.received",
  "createdAt": "2026-07-24T05:00:00.000Z",
  "data": {
    "campaignId": "search_abc",
    "leadId": "lead_xyz",
    "accountId": "acc_...",
    "linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
    "fullName": "Jane Doe",
    "messageText": "Thanks, interested in learning more.",
    "tag": null
  }
}

Which data fields are populated depends on the event — messageText appears on replies, tag on tag events, and so on. Very long messages may be truncated at around 4,000 characters.

Verifying the sender

If you set a signing secret, every delivery carries it in a header:

X-Webhook-Signature: your-secret-value
This is a shared secret, not an HMAC. The header contains the exact string you entered — there is no hashing or sha256= prefix to compute. Compare it to your stored value and reject anything that doesn't match. In n8n, use Header Auth with the header name and that same value.

Leave the secret blank and deliveries arrive unsigned.

Delivery guarantees

We attempt each delivery up to three times with a growing gap between tries. Delivery is at-most-once: if the worker fails mid-flight an event can be lost, so treat webhooks as a fast path rather than the only path. If an event absolutely must not be missed, reconcile periodically against GET /campaigns/:id/leads.

A failing endpoint on your side never blocks anything inside Connectzly — the inbox and tagging keep working regardless.

MCP server

MCP — the Model Context Protocol — is how an AI assistant discovers and calls tools. We host a server for you, so Claude, ChatGPT or Cursor can operate LinkedIn through your Connectzly account without you writing an integration.

It is a thin one-to-one wrapper over /public/v1 for a single workspace. Every tool maps to a REST route you can already see above — there are no extra product, billing or documentation tools hiding in it.

URL

https://mcp.connectzly.com

Use the origin URL in client config rather than /mcp. The /mcp path is kept as a compatibility alias for the same transport, but the bare origin is what you want.

MCP clients must send Accept: application/json, text/event-stream — that is what the Streamable HTTP transport requires. Any compliant client does this for you.

Which client are you using?

This is the fork in the road, and getting it wrong is the most common setup failure.

ClientHow you authenticate
Cursor and most IDE MCP configsPut X-API-Key in the headers of mcp.json
Claude.ai, Claude Desktop, ChatGPTOAuth — you paste your key on our consent page after clicking Connect. See Connect Claude / ChatGPT

Connect Cursor

{
  "mcpServers": {
    "connectzly": {
      "url": "https://mcp.connectzly.com",
      "headers": {
        "X-API-Key": "sk_live_..."
      }
    }
  }
}
Keys belong in client config, never in a chat message. Anything you type into a conversation may be stored or logged by that provider. The header is the only place a key should appear.

For search, lookup and send you'll also need an accountId — copy it from Accounts in the dashboard, or call GET /accounts.

Look before you connect

You can inspect the server with no key at all. Open the URL in a browser, or paste it into an AI assistant, and you'll get a short overview of what the server is and what its tools do.

URLReturns
https://mcp.connectzly.com/Short overview as JSON
https://mcp.connectzly.com/.well-known/mcp.jsonThe same overview
https://mcp.connectzly.com/llms.txtThe same overview as plain text
https://mcp.connectzly.com/healthLiveness check

Public discovery is deliberately thin — tool names and one-line summaries only. It does not expose full JSON schemas, REST paths or request bodies, and it never returns workspace data. Full argument schemas arrive only once a real client connects and runs initialize then tools/list.

Tools mapped to REST

Thirteen tools, each a direct equivalent of a route. If you know the API, you already know the tools.

MCP toolREST equivalent
get_creditsGET /credits
list_accountsGET /accounts
list_campaignsGET /campaigns
get_campaignGET /campaigns/:campaignId
start_campaignPOST /campaigns/:campaignId/start
pause_campaignPOST /campaigns/:campaignId/pause
list_leadsGET /campaigns/:campaignId/leads
get_leadGET /campaigns/:campaignId/leads/:leadId
add_leadPOST /campaigns/:campaignId/leads
update_leadPATCH /campaigns/:campaignId/leads/:leadId
send_messagePOST /inbox/messages
lookup_profilePOST /linkedin/profiles/lookup
search_linkedinPOST /linkedin/search

Cost and safety

Read this row by row before letting an agent run unsupervised. Some tools are free and reversible; two are neither.

ToolCostWhat to watch
Reads — get_credits, list_*, get_*0 creditsMarked read-only, so clients may run them without asking you first
lookup_profile, search_linkedin0 creditsRead-only, but each call spends part of your LinkedIn daily quota
add_lead1 credit per successful insertDuplicates and rejects cost nothing
update_lead0 credits
start_campaign0 creditsBegins real outbound activity. Your client should confirm before calling it
pause_campaign0 creditsHalts outreach already in progress
send_message0 creditsSends a real LinkedIn message. It cannot be recalled. Always confirm first

What needs a key

ActionAPI key
Browsing the catalog — GET /, /llms.txt, /.well-known/mcp.jsonNot needed
Discovering tools — initialize, tools/listOptional
Calling a tool against your dataRequired — header or OAuth token

The same plan requirements and rate limits apply as on REST. MCP is a different door into the same building, not a way around the locks.

Connect Claude or ChatGPT

Claude.ai, Claude Desktop and ChatGPT all use OAuth. Your API key is not entered in Claude's Advanced settings — you paste it on our consent page after clicking Connect.

Before you start

What to type in each field

In Claude: Customize → Connectors → Add custom connector.

FieldWhat to enter
NameConnectzly — any label works
Remote MCP server URLhttps://mcp.connectzly.com
Advanced → OAuth Client IDLeave blank. Skip Advanced entirely
Advanced → OAuth Client SecretLeave blank. Skip Advanced entirely

Click Add, then Connect.

After you click Connect

  1. A Connectzly page opens asking you to connect your AI assistant.
  2. Paste your sk_live_… key.
  3. Click Authorize. You're returned to Claude and the connector is live.

What not to do

Nothing belongs in the Advanced fields. They exist for a rare pre-registered OAuth app identity and are not request headers. Putting a key there breaks the connection rather than securing it.
Don't put this……in this field
Your API key sk_live_…OAuth Client ID or Secret
The text X-API-KeyOAuth Client ID
Authorization or Bearer …OAuth Client ID

Cursor is different

Cursor does not use this OAuth flow at all. Use the mcp.json snippet above with headers.X-API-Key instead.

If something goes wrong

SymptomFix
Claude keeps asking for a Client IDClear the Advanced fields completely and connect again
The consent page rejects your keyCreate a fresh key on the API & MCP page and paste that one
Tools fail after connecting successfullyCheck the connector is enabled inside the chat — the + menu, then Connectors

Changelog

Public API v1 Current

How to read future entries. Additive changes — new endpoints, new optional fields, new event types — can ship at any time without warning, so write clients that ignore anything they don't recognise. Anything that could break an existing integration will be listed here before it lands.
← Back to Connectzly Pricing Get an API key