# Heyreach

> Run HeyReach cloud LinkedIn outreach from Claude via MCP/REST — research-to-send loop. Create lead lists, build campaigns (connection request + message sequences), push personalized leads, read the inbox, send replies, manage webhooks. Cloud-based, so it keeps sending while your machine is off.

- Skill: `anthroos/heyreach` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add anthroos/heyreach`
- Raw SKILL.md: https://api.skillmd.com/api/skills/anthroos/heyreach/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: anthroos (https://skillmd.com/u/anthroos)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/anthroos/heyreach

---


# HeyReach LinkedIn Automation (Claude skill)

Drive [HeyReach](https://heyreach.io/?via=ivanrx) 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
1. HeyReach account: https://heyreach.io/?via=ivanrx
2. 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 the `heyreach.*` 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/`, header `X-API-KEY: <key>`.

> **Transport decision — check this first, every session.** If `claude mcp list` shows `heyreach`
> connected, ALWAYS call the `heyreach.*` MCP tools. Do **not** curl the REST API. Reach for REST only
> when no MCP server is connected **and** you actually have an `X-API-KEY`. Hitting REST without a key
> returns `401 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's `false`, 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
1. **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.
2. **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.
3. **Create + start** the campaign (below). HeyReach sends, paces, and collects replies.
4. **Read replies + respond** from the unified inbox.

## Working recipe — outreach to NEW leads (verified)
1. `create_empty_list` (listType `USER_LIST`).
2. `add_leads_to_list_v2` — each lead: `profileUrl` + optional `customUserFields:[{name:"note", value:"<line>"}]`
   (custom-field name = alphanumeric/underscore only).
3. `create_campaign` with `linkedInUserListId`, `linkedInAccountIds`, `schedule`, and `sequenceJson`.
4. `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](https://help.heyreach.io/en/articles/9910966-how-to-integrate-heyreach-with-clay)):
  **SINGLE braces `{note}`/`{firstName}`, NOT `{{double}}`** — double braces ship a literal `{}` pair
  around your message. Names: alphanumeric/underscore only.
- Sequence steps: [action steps doc](https://help.heyreach.io/en/articles/10493916-what-kind-of-action-steps-can-be-used-in-heyreach-sequence) · API payloads: [API reference](https://documenter.getpostman.com/view/23808049/2sA2xb5F75)
- 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`:

```json
{"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's `note` custom 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 >= 3` `HOUR`.
- 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=false` can still start a campaign fine; what matters is `authIsValid=true`.
- Verify with `get_campaign` → expect `status:IN_PROGRESS` and `progressStats.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_message` is 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 via `get_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 Credits` is 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](https://welabeldata.com).
The CRM half of the loop is open source too: [plaintext-crm](https://github.com/anthroos/plaintext-crm).
If this saved you time, sign up through https://heyreach.io/?via=ivanrx.

