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
MCP server
Endpoint
What you can build
- Push leads into a campaign from wherever they already live — your CRM, your own enrichment tooling, or a spreadsheet.
- Mirror replies into Slack, a helpdesk or your own database as they arrive.
- Let an AI assistant search LinkedIn and draft outreach without any glue code.
- Trigger downstream automation — a video, an invoice, a task — when a lead is tagged.
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.
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/jsonX-API-Key, not Authorization: Bearer. A bearer header is ignored and the request is rejected as unauthenticated.Key format and limits
| Property | Value |
|---|---|
| Prefix | sk_live_ |
| Keys per workspace | 5 by default |
| Eligibility | Paid plans only — not available on trial |
| Scope | A 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.
| Action | Credits |
|---|---|
POST/campaigns/:campaignId/leads when a lead is created | 1 |
| Duplicate or excluded profile — nothing is inserted | 0 |
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.
| Route | Limit |
|---|---|
POST …/leads | 60 / minute |
PATCH …/leads/:id | 60 / minute |
POST /inbox/messages | 10 / minute |
POST /linkedin/profiles/lookup | 5 / minute per key · 1 per 20s per account · 80/day, or 150/day with Sales Navigator |
POST /linkedin/search | 6 / minute per key · 1 page per 15s per account · daily people quota shared with dashboard search (1,000, or 1,500 with Sales Navigator) |
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": {}
}
}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
| Status | Meaning |
|---|---|
| 200 | Success. Bodies are top-level JSON with no wrapper object. |
| 400 | The request body or query failed validation |
| 401 | Key missing or invalid |
| 403 | Plan ineligible, out of credits, Sales Navigator required, or account disconnected |
| 404 | No such campaign or lead |
| 429 | Rate limit or LinkedIn daily quota reached |
| 502 / 503 | Temporary upstream problem — safe to retry |
Error codes
| Code | What happened | Retry? |
|---|---|---|
unauthorized | Key missing or wrong | No |
plan_required | Plan doesn't include API access | No |
validation_error | Unknown field, malformed URL or bad cursor | No |
not_found | Campaign or lead doesn't exist | No |
insufficient_credits | No credits left to insert a lead | After topping up |
account_disconnected | The LinkedIn account can't be used right now | After reconnecting |
sales_nav_required | Sales Navigator search attempted without an SN account | No |
rate_limited | Requests-per-minute or burst limit hit | Yes, after the stated delay |
linkedin_daily_quota | LinkedIn's daily allowance is spent | Tomorrow |
send_failed | The message could not be delivered | Investigate first |
upstream_unavailable | Transient failure downstream | Yes |
Endpoints
Base URL https://api.connectzly.com/public/v1. Every request needs the X-API-Key header.
| Method | Path | Does | Credits | Docs |
|---|---|---|---|---|
| GET | /credits | Current balance and plan | 0 | Open |
| GET | /accounts | Connected LinkedIn accounts and their IDs | 0 | Open |
| GET | /campaigns | List campaigns | 0 | Open |
| GET | /campaigns/:id | A single campaign in detail | 0 | Open |
| POST | /campaigns/:id/start | Resume a campaign | 0 | Open |
| POST | /campaigns/:id/pause | Pause a campaign | 0 | Open |
| GET | /campaigns/:id/leads | Leads in a campaign | 0 | Open |
| GET | /campaigns/:id/leads/:leadId | One lead | 0 | Open |
| POST | /campaigns/:id/leads | Add a lead | 1 if created | Open |
| PATCH | /campaigns/:id/leads/:leadId | Set a tag or exclude the lead | 0 | Open |
| POST | /inbox/messages | Send a LinkedIn message to a lead | 0 | Open |
| POST | /linkedin/profiles/lookup | Look up a profile | 0 | Open |
| POST | /linkedin/search | Search LinkedIn | 0 | Open |
/creditsRemaining credit balance and the plan the workspace is on. 0 credits.
GET /credits
X-API-Key: sk_live_...| FIELD | TYPE | NOTES |
|---|---|---|
credits | number | Main balance |
topupCredits | number | Top-up balance |
planId | string | Plan identifier |
Response
{
"credits": 642,
"topupCredits": 200,
"planId": "pro"
}/accountsConnected LinkedIn accounts. Use the returned accountId for search, lookup and send. 0 credits.
GET /accounts
X-API-Key: sk_live_...| STATUS | MEANING |
|---|---|
connected | Usable |
connecting | In progress |
failed | Error state |
disconnected | Not usable |
The today counters cover the current UTC day.
Response
{
"accounts": [
{
"id": "acc_abc",
"name": "Jane Doe",
"status": "connected",
"active": true,
"hasSalesNavigator": false,
"today": {
"connectionRequestsSent": 12,
"connectionRequestsAccepted": 5,
"connectionRequestsLimit": 20,
"messagesSent": 8,
"messageLimit": 30
}
}
]
}/campaignsPaginated list of campaigns in the workspace. 0 credits.
GET /campaigns?limit=50&cursor=
X-API-Key: sk_live_...| QUERY | NOTES |
|---|---|
status | Optional filter |
limit | Default 50, max 100 |
cursor | Opaque id from the previous page |
acceptanceRate and replyRate are percentages to one decimal, or null when the denominator is 0.
Response
{
"campaigns": [
{
"id": "search_abc",
"name": "Q3 outbound",
"status": "CAMPAIGN_IN_PROGRESS",
"totalLeads": 120,
"assignedTemplateId": "tpl_...",
"stats": {
"connectionRequestsSent": 80,
"connectionRequestsAccepted": 20,
"acceptanceRate": 25.0,
"messagesSent": 40,
"repliesReceived": 8,
"replyRate": 10.0
}
}
],
"cursor": null
}/campaigns/:campaignIdA single campaign, with the same fields as a list item. 0 credits.
GET /campaigns/search_abc
X-API-Key: sk_live_...An unknown id returns 404 not_found.
Response
{
"id": "search_abc",
"name": "Q3 outbound",
"status": "CAMPAIGN_IN_PROGRESS",
"totalLeads": 120,
"assignedTemplateId": "tpl_...",
"stats": {
"connectionRequestsSent": 80,
"connectionRequestsAccepted": 20,
"acceptanceRate": 25.0,
"messagesSent": 40,
"repliesReceived": 8,
"replyRate": 10.0
}
}/campaigns/:campaignId/startStart outreach on a ready campaign, or resume a paused one — the same behaviour as the Start and Resume controls in the dashboard. 0 credits.
This begins real LinkedIn outbound. Confirm with the user before calling it. The campaign must already have an agent assigned (assignedTemplateId); agents are created and LinkedIn is connected in the dashboard.
POST /campaigns/search_abc/start
X-API-Key: sk_live_...
Content-Type: application/json{}| CURRENT STATUS | BEHAVIOUR |
|---|---|
READY_TO_SEND | Starts → CAMPAIGN_IN_PROGRESS (action: started) |
| FAILED, with leads | Starts → CAMPAIGN_IN_PROGRESS (action: started) |
CAMPAIGN_PAUSED | Resumes → CAMPAIGN_IN_PROGRESS (action: resumed) |
CAMPAIGN_IN_PROGRESS | No-op success (action: noop) |
| COMPLETED | 409 conflict |
| FAILED, no leads | 409 conflict — add leads first |
| Anything else | 409 conflict |
The body must be empty or {}. Any other field returns 400 validation_error. Via MCP this is start_campaign(campaignId).
Response
{
"campaignId": "search_abc",
"action": "started",
"status": "CAMPAIGN_IN_PROGRESS",
"campaign": {
"id": "search_abc",
"name": "Q3 outbound",
"status": "CAMPAIGN_IN_PROGRESS",
"totalLeads": 120,
"assignedTemplateId": "tpl_...",
"stats": {
"connectionRequestsSent": 80,
"connectionRequestsAccepted": 20,
"acceptanceRate": 25.0,
"messagesSent": 40,
"repliesReceived": 8,
"replyRate": 10.0
}
}
}| FIELD | VALUES |
|---|---|
campaignId | Same as the path |
action | started | resumed | noop |
status | Status after the call — normally CAMPAIGN_IN_PROGRESS |
campaign | Same shape as GET/campaigns/:campaignId |
| ERROR CODE | WHEN |
|---|---|
validation_error | Body carries extra fields, or no agent is assigned |
not_found | Unknown campaign or template |
account_disconnected | The agent's LinkedIn account is not connected |
plan_required | Plan inactive — usually blocked at auth |
conflict | Wrong status, no leads, or already completed |
rate_limited | Over the start/pause limit of 30 per minute |
upstream_unavailable | Temporary failure while resuming — retry |
/campaigns/:campaignId/pausePause a campaign that is in progress. Queued outreach stops; nothing further is sent. 0 credits.
POST /campaigns/search_abc/pause
X-API-Key: sk_live_...
Content-Type: application/json{}| CURRENT STATUS | BEHAVIOUR |
|---|---|
CAMPAIGN_IN_PROGRESS | Pauses → CAMPAIGN_PAUSED (action: paused) |
CAMPAIGN_PAUSED | No-op success (action: noop) |
| Anything else | 409 conflict |
You will also get conflict if leads are still being added — let that finish, then retry. To resume, call POST /campaigns/:campaignId/start. Via MCP this is pause_campaign(campaignId).
Response
{
"campaignId": "search_abc",
"action": "paused",
"status": "CAMPAIGN_PAUSED",
"campaign": {
"id": "search_abc",
"name": "Q3 outbound",
"status": "CAMPAIGN_PAUSED",
"totalLeads": 120,
"assignedTemplateId": "tpl_...",
"stats": {
"connectionRequestsSent": 80,
"connectionRequestsAccepted": 20,
"acceptanceRate": 25.0,
"messagesSent": 40,
"repliesReceived": 8,
"replyRate": 10.0
}
}
}| FIELD | VALUES |
|---|---|
campaignId | Same as the path |
action | paused | noop |
status | Always CAMPAIGN_PAUSED on success |
campaign | Same shape as GET/campaigns/:campaignId |
| ERROR CODE | WHEN |
|---|---|
validation_error | Body carries extra fields |
not_found | Unknown campaign |
conflict | Not in progress, or leads are still being added |
rate_limited | Over the start/pause limit of 30 per minute |
upstream_unavailable | Temporary failure — retry |
/campaigns/:campaignId/leadsPaginated leads in a campaign. Filter by exactly one of tag, replied or needsReply. 0 credits.
GET /campaigns/search_abc/leads?limit=50
X-API-Key: sk_live_...| QUERY | NOTES |
|---|---|
tag | Exact tag label — see PATCH/campaigns/:campaignId/leads/:leadId |
replied | true or false |
needsReply | true or false |
limit | Default 50, max 100 |
cursor | Next page |
stage is one of new, connection_sent, connected, messaged, replied, excluded, failed, handed_off.
Response
{
"leads": [
{
"id": "lead_xyz",
"fullName": "Jane Doe",
"firstName": "Jane",
"lastName": "Doe",
"linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
"jobTitle": "VP Sales",
"location": "Austin, TX",
"companyName": "Acme",
"companyLinkedin": null,
"website": null,
"email": null,
"tag": null,
"replied": false,
"needsReply": false,
"stage": "connection_sent"
}
],
"cursor": null
}/campaigns/:campaignId/leads/:leadIdOne lead in full — every list field plus headline, work history and engagement flags. 0 credits.
GET /campaigns/search_abc/leads/lead_xyz
X-API-Key: sk_live_...Response
{
"id": "lead_xyz",
"fullName": "Jane Doe",
"linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
"jobTitle": "VP Sales",
"companyName": "Acme",
"tag": "interested",
"replied": true,
"needsReply": false,
"stage": "engaged",
"headline": "VP Sales at Acme",
"workExperience": [
{
"company": "Acme",
"position": "VP Sales",
"location": null,
"description": null,
"start": "2022-01",
"end": null
}
],
"connectionRequestAccepted": true,
"messageSent": true
}/campaigns/:campaignId/leadsAdd a LinkedIn profile to an existing campaign. 1 credit when the status comes back as created.
POST /campaigns/search_abc/leads
X-API-Key: sk_live_...
Content-Type: application/json{
"linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
"fullName": "Jane Doe",
"jobTitle": "VP Sales",
"companyName": "Acme"
}| FIELD | REQUIRED | MAX |
|---|---|---|
linkedinUrl | Yes | Personal /in/… URL |
fullName | No | 100 |
firstName | No | 100 |
lastName | No | 100 |
companyName | No | 200 |
jobTitle | No | 200 |
email | No | 320 |
location | No | 200 |
headline | No | 500 |
personalization | No | 2000 |
If the campaign is still FAILED — after a run that found no leads, for example — a successful insert promotes it to READY_TO_SEND, so /start works without creating a new campaign. Unknown fields return validation_error. No live profile fetch happens on this call, and HTTP is always 200 for the three outcomes above, so branch on status rather than the status code.
Response
{
"leadId": "lead_xyz",
"status": "created",
"creditsCharged": 1
}| STATUS | CREDITS |
|---|---|
created | 1 |
duplicate | 0 |
excluded | 0 |
/campaigns/:campaignId/leads/:leadIdSet the inbox tag, the excluded flag, or both. At least one field is required. 0 credits.
PATCH /campaigns/search_abc/leads/lead_xyz
X-API-Key: sk_live_...
Content-Type: application/json{
"tag": "interested",
"excluded": false
}| FIELD | NOTES |
|---|---|
tag | An exact label from the list below, or null / "" to clear it |
excluded | Boolean |
Allowed tags: Meeting request, Interested, Information request, Not interested, Wrong person, To be defined. An unrecognised tag returns validation_error. A successful call may emit the lead.tagged webhook.
Response
{
"leadId": "lead_xyz",
"tag": "interested",
"excluded": false
}/inbox/messagesSend a LinkedIn DM to a lead that already exists in a campaign. The call is synchronous — you know whether it sent before the response returns. 0 credits.
POST /inbox/messages
X-API-Key: sk_live_...
Content-Type: application/json{
"campaignId": "search_abc",
"leadId": "lead_xyz",
"text": "Hi Jane — following up on our conversation.",
"accountId": "acc_abc"
}| FIELD | REQUIRED | NOTES |
|---|---|---|
campaignId | Yes | |
leadId | Yes | |
text | Yes | Max 8000 characters |
accountId | No | Defaults from the lead when omitted |
Response
{
"ok": true,
"sentAt": "2026-08-13T09:31:00Z",
"messageId": "msg_..."
}| ERROR CODE | WHEN |
|---|---|
validation_error | Missing or invalid fields |
not_found | Unknown campaign or lead |
account_disconnected | Account not usable |
rate_limited / linkedin_daily_quota | Daily caps reached |
send_failed | The send itself failed |
/linkedin/profiles/lookupLive profile lookup by LinkedIn URL. 0 credits. Strict rate limits apply.
POST /linkedin/profiles/lookup
X-API-Key: sk_live_...
Content-Type: application/json{
"accountId": "acc_abc",
"linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
"depth": "basic"
}| FIELD | REQUIRED | NOTES |
|---|---|---|
accountId | Yes | From GET/accounts |
linkedinUrl | Yes | Personal /in/… URL |
depth | No | basic (default) or full |
Prefer basic. Both depths count toward the daily cap. With depth: full the response also carries about and experience[].
Response
{
"fullName": "Jane Doe",
"firstName": "Jane",
"lastName": "Doe",
"headline": "VP Sales at Acme",
"jobTitle": "VP Sales",
"companyName": "Acme",
"companyLinkedin": "https://www.linkedin.com/company/acme",
"website": null,
"location": "Austin, TX",
"linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
"providerId": "...",
"profilePictureUrl": "https://..."
}/linkedin/searchOne page of LinkedIn people search, returned synchronously. 0 credits. Strict rate limits apply.
POST /linkedin/search
X-API-Key: sk_live_...
Content-Type: application/json{
"accountId": "acc_abc",
"type": "classic",
"query": "VP Sales SaaS Austin",
"limit": 25
}| FIELD | REQUIRED | NOTES |
|---|---|---|
accountId | Yes | |
type | No | classic (default) or sales_nav |
query | One of query / url | Natural language or keywords |
url | One of query / url | A LinkedIn search URL |
limit | No | Max 50 classic, 100 Sales Navigator |
cursor | No | Next page, from the previous response |
Supply exactly one of query or url. Sales Navigator search requires hasSalesNavigator on the account.
Response
{
"results": [
{
"fullName": "Jane Doe",
"headline": "VP Sales at Acme",
"jobTitle": "VP Sales",
"companyName": "Acme",
"location": "Austin, TX",
"linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
"providerId": "...",
"profilePictureUrl": "https://..."
}
],
"cursor": "opaque-next-page-cursor-or-null"
}| ERROR CODE | WHEN |
|---|---|
validation_error | Missing query and url, or a bad type |
sales_nav_required | Sales Navigator search without Sales Navigator |
account_disconnected | Account not usable |
rate_limited / linkedin_daily_quota | Daily caps reached |
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
- Go to app.connectzly.com/api-mcp → Webhooks.
- Give it a name, an HTTPS URL, and the events you want. You can scope it to a single campaign.
- 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
| Event | Fires when |
|---|---|
reply.received | A prospect replies |
connection.accepted | A connection request is accepted |
lead.tagged | A lead is tagged, by a human or by the agent |
account.disconnected | A 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-valuesha256= 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
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.
| Client | How you authenticate |
|---|---|
| Cursor and most IDE MCP configs | Put X-API-Key in the headers of mcp.json |
| Claude.ai, Claude Desktop, ChatGPT | OAuth — 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_..."
}
}
}
}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.
| URL | Returns |
|---|---|
https://mcp.connectzly.com/ | Short overview as JSON |
https://mcp.connectzly.com/.well-known/mcp.json | The same overview |
https://mcp.connectzly.com/llms.txt | The same overview as plain text |
https://mcp.connectzly.com/health | Liveness 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 tool | REST equivalent |
|---|---|
get_credits | GET /credits |
list_accounts | GET /accounts |
list_campaigns | GET /campaigns |
get_campaign | GET /campaigns/:campaignId |
start_campaign | POST /campaigns/:campaignId/start |
pause_campaign | POST /campaigns/:campaignId/pause |
list_leads | GET /campaigns/:campaignId/leads |
get_lead | GET /campaigns/:campaignId/leads/:leadId |
add_lead | POST /campaigns/:campaignId/leads |
update_lead | PATCH /campaigns/:campaignId/leads/:leadId |
send_message | POST /inbox/messages |
lookup_profile | POST /linkedin/profiles/lookup |
search_linkedin | POST /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.
| Tool | Cost | What to watch |
|---|---|---|
Reads — get_credits, list_*, get_* | 0 credits | Marked read-only, so clients may run them without asking you first |
lookup_profile, search_linkedin | 0 credits | Read-only, but each call spends part of your LinkedIn daily quota |
add_lead | 1 credit per successful insert | Duplicates and rejects cost nothing |
update_lead | 0 credits | — |
start_campaign | 0 credits | Begins real outbound activity. Your client should confirm before calling it |
pause_campaign | 0 credits | Halts outreach already in progress |
send_message | 0 credits | Sends a real LinkedIn message. It cannot be recalled. Always confirm first |
What needs a key
| Action | API key |
|---|---|
Browsing the catalog — GET /, /llms.txt, /.well-known/mcp.json | Not needed |
Discovering tools — initialize, tools/list | Optional |
| Calling a tool against your data | Required — 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
- Create a key at app.connectzly.com/api-mcp and keep it to hand — it is shown once.
- The MCP URL is
https://mcp.connectzly.com.
What to type in each field
In Claude: Customize → Connectors → Add custom connector.
| Field | What to enter |
|---|---|
| Name | Connectzly — any label works |
| Remote MCP server URL | https://mcp.connectzly.com |
| Advanced → OAuth Client ID | Leave blank. Skip Advanced entirely |
| Advanced → OAuth Client Secret | Leave blank. Skip Advanced entirely |
Click Add, then Connect.
After you click Connect
- A Connectzly page opens asking you to connect your AI assistant.
- Paste your
sk_live_…key. - Click Authorize. You're returned to Claude and the connector is live.
What not to do
| Don't put this… | …in this field |
|---|---|
Your API key sk_live_… | OAuth Client ID or Secret |
The text X-API-Key | OAuth 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
| Symptom | Fix |
|---|---|
| Claude keeps asking for a Client ID | Clear the Advanced fields completely and connect again |
| The consent page rejects your key | Create a fresh key on the API & MCP page and paste that one |
| Tools fail after connecting successfully | Check the connector is enabled inside the chat — the + menu, then Connectors |
Changelog
Public API v1 Current
- REST under
/public/v1— credits, accounts, campaigns, leads, messaging, and LinkedIn lookup and search. - Outbound webhooks covering four events, with subscriptions managed on the API & MCP page.
- MCP tools mapped one-to-one with REST at
https://mcp.connectzly.com. - Key-free discovery on the MCP host at
GET /,/.well-known/mcp.jsonand/llms.txt— tool names and summaries only; full schemas come fromtools/list. - Optional webhook signing. Set a secret and it is sent back verbatim in
X-Webhook-Signature, which works directly with n8n Header Auth.