HeyReach LinkedIn Automation (Claude skill)
Drive HeyReach entirely from Claude. HeyReach is API-first, so you never babysit browser automation or VM queues — campaigns run in HeyReach's cloud with built-in warmup.
Setup
- HeyReach account: https://heyreach.io/?via=ivanrx
- Pick ONE transport. MCP and REST use different credentials from different sub-sections of
Settings → Integrations — do not mix them up:
- MCP (recommended): Settings → Integrations → MCP Server → Get MCP key → copy the
MCP Connection URL (the key is already baked into the URL). Then:
claude mcp add --scope user --transport http heyreach '<MCP Connection URL>'Restart Claude so theheyreach.*tools load. Verify:claude mcp list→heyreach … connected. - REST (fallback): Settings → Integrations → API → copy the API key (a SEPARATE key from the
MCP key). Base
https://api.heyreach.io/api/public/, headerX-API-KEY: <key>.
- MCP (recommended): Settings → Integrations → MCP Server → Get MCP key → copy the
MCP Connection URL (the key is already baked into the URL). Then:
Transport decision — check this first, every session. If
claude mcp listshowsheyreachconnected, ALWAYS call theheyreach.*MCP tools. Do not curl the REST API. Reach for REST only when no MCP server is connected and you actually have anX-API-KEY. Hitting REST without a key returns401 Unauthorized— that means "wrong transport / missing key", not "service is down".
Core model
The API drives CAMPAIGNS (sequences), not individual clicks. You don't "send a connect to X" ad-hoc. You create a campaign with a sequence (connection request → wait → message → follow-up) and push leads into it; HeyReach executes from the cloud with warmup and rate limits.
Sourcing leads — the API does NOT search LinkedIn
There is no "find people" tool. HeyReach ingests leads you already have (add_leads_to_list /
add_leads_to_campaign take profileUrl / navigatorProfileId); it does not run a LinkedIn people-search
for you. So "find 10 X" is a research step you do first, then push the results in.
- Sales Navigator import needs a connected Sales Navigator seat. Check it:
get_all_linked_in_accounts→isValidNavigator: true. If it'sfalse, Sales-Nav sourcing is unavailable on that sender. - No Sales Navigator? Source by web research (recommended default): find real people via the web
(company leadership/about pages, LinkedIn, press, conference speaker lists), collect their real
LinkedIn profile URLs, then
add_leads_to_list. Never invent names or profile URLs — verify each. - Post-reactor and event-attendee harvesting work without a Sales Navigator seat.
Workflow with Claude: research → show the list (name, title, company, profile URL) for approval → only then create the list and push. Keep a human gate before anything touches the account.
The research-to-send loop
- Find/enrich leads (web research → real profile URLs; or post reactors / event attendees; or a Sales-Navigator search if you have a seat). The API does not search — see Sourcing leads above.
- Personalize: generate a custom first line per lead, store it as a lead custom field, and reference
it in the sequence template as
{custom_field}(e.g.{note}). HeyReach also supports{firstName}, etc. - Create + start the campaign (below). HeyReach sends, paces, and collects replies.
- Read replies + respond from the unified inbox.
Working recipe — outreach to NEW leads (verified)
create_empty_list(listTypeUSER_LIST).add_leads_to_list_v2— each lead:profileUrl+ optionalcustomUserFields:[{name:"note", value:"<line>"}](custom-field name = alphanumeric/underscore only).create_campaignwithlinkedInUserListId,linkedInAccountIds,schedule, andsequenceJson.start_campaign→ enrolls the list's leads, status DRAFT → IN_PROGRESS.
⚠️ Gotcha that costs you a day
Pre-launch check (mandatory): verify your campaign config against the official docs, element by element, BEFORE starting. Canary-testing doesn't work — sends queue inside a schedule window, so feedback arrives hours after you've committed the batch.
- Variables/custom fields (docs):
SINGLE braces
{note}/{firstName}, NOT{{double}}— double braces ship a literal{}pair around your message. Names: alphanumeric/underscore only. - Sequence steps: action steps doc · API payloads: API reference
- Check every message length, fallbacks, and the schedule window vs your targets' timezone.
Cross-check against a live working campaign via
get_campaign_sequence; never launch from memory.
The CONNECTION_REQUEST payload field is messages — an ARRAY of strings (max 300 chars each; one is
picked per lead; supports {firstName}/custom fields), NOT message (singular). The singular form
returns HTTP 500. Working sequenceJson:
{"nodeType":"CONNECTION_REQUEST","actionDelay":0,"actionDelayUnit":"HOUR",
"payload":{"messages":["Hi {firstName}, ... or {note}"]},
"conditionalNode":{"nodeType":"END","actionDelay":3,"actionDelayUnit":"HOUR"},
"unconditionalNode":{"nodeType":"END","actionDelay":3,"actionDelayUnit":"HOUR"}}
Rules that bite:
- Per-lead personalization →
messages:["{note}"]+ each lead'snotecustom field. - Batch pattern: ONE campaign per 10–15 leads, each carrying its own full personal text in the custom field. Don't create one campaign per person — it works, but you end up with dozens of single-lead campaigns and accept-tracking becomes painful.
- END nodes (and any node after CONNECTION_REQUEST/MESSAGE/INMAIL/VIEW_PROFILE/FOLLOW) need
actionDelay >= 3HOUR. - Connection-request note ≤ 300 chars (LinkedIn limit) — check EVERY note's length before launch.
- Default schedule = Mon–Fri 09:00–17:00 UTC; requests fire inside that window, not instantly. A campaign
launched after 17:00 UTC sits queued until the next window. Widen via
update_campaign_schedule(requires pause → update → resume) if you need it sooner. - A sender showing
isActive=falsecan still start a campaign fine; what matters isauthIsValid=true. - Verify with
get_campaign→ expectstatus:IN_PROGRESSandprogressStats.totalUsers > 0. - Progress stats right after launch are provisional — don't report "0 failed" until you re-check later.
⚠️ Connect notes silently stripped? Buy Premium FIRST, then connect
If the LinkedIn account was connected to HeyReach before buying LinkedIn Premium, HeyReach keeps the pre-Premium state and silently strips connection-request notes (even the fallback) — accepted leads end up with empty chats and you can't tell who accepted via the API. Support-confirmed behavior. Setup order: 1) buy LinkedIn Premium → 2) connect (or RE-connect) the account to HeyReach → 3) send one test connect with a note and verify it attaches before scaling.
First message to a 1st-degree contact with NO existing conversation
send_message requires an existing conversationId — there is no API to open a new conversation. To DM
someone who accepted a (noteless) connect, run a small MESSAGE campaign instead:
CHECK_IS_CONNECTION → (conditional) MESSAGE → END, with each lead's personal text in a note
custom field and messages:["{note}"].
Gotcha: a MESSAGE payload containing a {variable} REQUIRES fallbackMessage, or create_campaign
fails with "Fallback message must be specified in a Send Message action".
Who accepted a noteless connect? The API can't tell you
A noteless accept creates no conversation, get_lead has no connection-status field, and the network
endpoint is a 10k-row paginated list. The reliable check is outside the API: open each profile in a
logged-in browser and read the degree badge (1st = accepted, Pending = not yet). LinkedIn throttles
after ~25–30 rapid profile views — sweep in small batches. With notes enabled accepts create chats, so
the API sees them — one more reason to fix notes first.
Reply handling
get_conversations_v2— list conversations (filter by account/campaign/tags/seen).get_chatroom— full thread for one conversation.send_message— send a custom 1:1 reply into an existing conversation.
⚠️
send_messageis NOT idempotent — never retry it. An empty, no-error response usually means queued, not failed (inbox sync lags). Re-firing the same send creates a DUPLICATE. Send once → wait → re-check viaget_conversations_v2.
Limits (warmup)
- LinkedIn weekly cap ≈ 100–200 connection requests; on a fresh/non-premium account start low.
- Recommended warmup: ~15 connects/day + ~20–25 messages/day, ramp slowly.
- Lead-source harvesting from Sales Navigator needs a Sales Navigator seat; post-reactor and event-attendee harvesting do not.
0 Creditsis fine — credits are only for email-finding/enrichment, not LinkedIn messaging.
MCP tool map (high-level)
Campaigns: get_all_campaigns, create_campaign, get_campaign, get_campaign_sequence,
update_campaign_sequence, update_campaign_schedule, start_campaign, pause_campaign, resume_campaign,
stop_lead_in_campaign. Leads/lists: create_empty_list, add_leads_to_list_v2, add_leads_to_campaign_v2,
get_leads_from_list, get_leads_from_campaign, get_lead. Inbox: get_conversations_v2, get_chatroom,
send_message. Other: get_all_linked_in_accounts, get_my_network_for_sender, tags, webhooks, get_overall_stats.
Built while wiring HeyReach into a Claude + CRM outbound loop at WeLabelData. The CRM half of the loop is open source too: plaintext-crm. If this saved you time, sign up through https://heyreach.io/?via=ivanrx.