# Reach MCP — overview Reach MCP is a LinkedIn runtime for AI agents. It connects a real LinkedIn account (the owner's own browser session, captured by a Chrome extension) to Claude, ChatGPT, Cursor, n8n, Make or any code that speaks MCP or REST, and puts the controls an agent needs around every call: - **60 MCP tools** that act on the account (inbox, invitations, network, profiles, Sales Navigator search, posts, publishing, webhooks) — see [MCP tools](/docs/mcp-tools.md). - **8 playbooks** served as MCP prompts: bounded workflows that preview every write before it happens — see [Playbooks](/docs/playbooks.md). - **Daily quotas enforced server-side**, per account and per action family; the call is refused with a structured error and the reset time — see [Quotas](/docs/quotas.md). - **One error shape** with a stable code, whether a retry can help and what to do instead — see [Errors](/docs/errors.md). - **Idempotency keys** on every write, so a retry never sends twice — see [Idempotency](/docs/idempotency.md). - **Signed outbound webhooks** when a prospect replies, a connection is made, an account disconnects or a quota runs low — see [Webhooks](/docs/webhooks.md). - **Jobs**: hand Reach a batch of messages, invitations, visits or comments in one call; it runs them over working hours at a human pace, inside the quotas, and reports by webhook — see [Jobs](/docs/jobs.md). - **Natural-language Sales Navigator search**: describe who you want, get a real search URL with resolved filters and the first page of results. What it is not: a multi-channel API (LinkedIn only, on purpose) and not an official LinkedIn API (it relies on the account owner's authenticated session; automation carries real account risk, reduced by a residential proxy per account, enforced limits and human-like pacing, never removed). ## Who it is for - AI and automation agencies giving their clients' agents hands on LinkedIn, and builders shipping agents or products on the API and MCP. - People running their own LinkedIn from Claude or ChatGPT. One plan, every feature: $20 for the first account, $7 each up to 10, then $5 — see [Pricing](/docs/pricing.md). ## Endpoints | What | URL | |------|-----| | MCP server (OAuth or API key) | `https://app.reachmcp.com/mcp` | | REST base | `https://app.reachmcp.com/api` | | Interactive REST reference | https://app.reachmcp.com/scalar | | OpenAPI document | https://app.reachmcp.com/openapi.json | | App (connect accounts, keys, webhooks, billing) | https://app.reachmcp.com | Built by the team behind Kanbox (2,000+ teams, 2 million LinkedIn messages sent). --- # Reach MCP — quickstart ## 1. Connect a LinkedIn account Sign up at https://app.reachmcp.com/signup (7-day trial, no card). Install the Reach MCP Chrome extension and click **Connect** while logged into LinkedIn in that browser. The extension hands Reach the current session; no LinkedIn password is ever typed into Reach. Removing the extension revokes that path. Each account gets its own residential proxy. For a client's account: generate a branded invitation link from the app and send it; the account owner connects their own session. ## 2. Plug an MCP client Add `https://app.reachmcp.com/mcp` as a remote MCP server. Claude, ChatGPT and Cursor authenticate with OAuth (a Reach login page opens; nothing to paste). Clients without OAuth can append an API key: `https://app.reachmcp.com/mcp?api_key=mcp_…` or send it as the `X-API-Key` header. ```json { "mcpServers": { "reach": { "url": "https://app.reachmcp.com/mcp" } } } ``` Then ask the assistant what it can do: the `reach_playbooks` tool returns the catalogue of ready-made workflows, and `list_accounts` returns the connected accounts with their ids. ## 3. Or call the REST API Create an API key in the app (**API & MCP**). Every request carries `X-API-Key`. Reference: https://app.reachmcp.com/scalar. ```bash curl -H "X-API-Key: $KEY" https://app.reachmcp.com/api/linkedin-accounts curl -H "X-API-Key: $KEY" "https://app.reachmcp.com/api/linkedin/$ACCOUNT_ID/conversations?count=10" curl -X POST -H "X-API-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"recipient_linkedin_id":"ACoAA…","text":"Thanks for connecting, Ana."}' \ https://app.reachmcp.com/api/linkedin/$ACCOUNT_ID/messages ``` ## 4. React to events instead of polling Register a webhook (app → **Webhooks**, or `POST https://app.reachmcp.com/api/webhooks/endpoints`) for `message.received` and `connection.new`. Reach posts a signed JSON body to your URL within a few minutes of the event. See [Webhooks](/docs/webhooks.md). ## Things an agent should know - Every write is subject to the account's daily quotas. A refused call returns `daily_quota_reached` with `retry_at`; act on the code, not the wording. - Pass an `idempotency_key` (MCP) or `Idempotency-Key` header (REST) on writes you might retry. - Playbooks always preview before writing and wait for the user to name what to send. Direct tool calls do not; the API key holder is responsible. - Reads are free of quota; writes to comments, reactions and posts count too. --- # Reach MCP — MCP tools (60) Server: `https://app.reachmcp.com/mcp` (JSON-RPC over HTTP, `tools/list`, `tools/call`, `prompts/list`, `prompts/get`). Each tool carries MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), summarised after its name. Tools marked **write** change something on LinkedIn or in the account; they accept an optional `idempotency_key` and count against the account's daily quotas where a family applies. Failures come back as `isError` results carrying the [error object](/docs/errors.md). **Tool profiles.** `tools/list` returns all 60 tools by default. `https://app.reachmcp.com/mcp?tools=core` lists the **core** profile (33 tools, marked *core* below): what the playbooks and everyday use need, lighter on the model's context. Every tool is callable whatever the listing. Former names (`connect`, `follow`, `invitation_status`, `user_posts`, and the `proxy_` prefix) keep working as aliases. ## Start here ### `reach_playbooks` · read-only · Reach only · *core* START HERE. Return the catalogue of ready-made LinkedIn playbooks — what this server can actually accomplish, as named workflows rather than raw endpoints. Each entry carries its tool sequence, its prerequisites, and the full instructions to run it. Call this first when the user asks what you can do with their LinkedIn account, or when a request is vague. Pass playbook_id to get one playbook's instructions and then follow them. Use first for a vague request or 'what can you do'; not needed when the user names a precise action. | Parameter | Type | Notes | |---|---|---| | `playbook_id` | string | Id of one playbook from the catalogue, to get its full instructions. | | `include_instructions` | boolean | Also return the full instruction text of every playbook (longer output). | ## Accounts, quotas and logs ### `list_accounts` · read-only · Reach only · *core* Return LinkedIn accounts accessible with the provided API key. If the key is restricted, only allowed account IDs are returned. Use first in every session to get the account_id the other tools need; ask which account when several are listed. ### `delete_account` · **write** · destructive · idempotent · Reach only Delete a LinkedIn account by account_id. Also clears invitation references before deletion. Removes the account from Reach only (LinkedIn is untouched); irreversible, requires the user's explicit confirmation. Not for disconnecting temporarily. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | ### `get_account_quotas` · read-only · Reach only · *core* Get the quotas configuration and usage counters for a LinkedIn account. Use before a batch to know what is left today; use update_account_quotas to change limits or the activity window. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | ### `update_account_quotas` · **write** · additive · idempotent · Reach only Update one or more quota configuration fields for a LinkedIn account, and its activity window (the hours, days and timezone jobs run in when a job does not set its own). Only provided fields are updated. Changes limits and the activity window for jobs; does not reset today's counters. Raising a limit is the user's decision, ask first. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `daily_invitations_conf` | integer | Daily limit for 'invitations' on this account; 0 means unlimited. | | `daily_messages_conf` | integer | Daily limit for 'messages' on this account; 0 means unlimited. | | `daily_visits_conf` | integer | Daily limit for 'visits' on this account; 0 means unlimited. | | `daily_imports_standard_conf` | integer | Daily limit for 'imports_standard' on this account; 0 means unlimited. | | `daily_imports_salesnav_conf` | integer | Daily limit for 'imports_salesnav' on this account; 0 means unlimited. | | `daily_imports_recruiter_conf` | integer | Daily limit for 'imports_recruiter' on this account; 0 means unlimited. | | `daily_posts_conf` | integer | Daily limit for 'posts' on this account; 0 means unlimited. | | `daily_comments_conf` | integer | Daily limit for 'comments' on this account; 0 means unlimited. | | `daily_reactions_conf` | integer | Daily limit for 'reactions' on this account; 0 means unlimited. | | `daily_job_searches_conf` | integer | Pages of search_job_postings (up to 25 postings each) per UTC day. Default 50; 0 = not limited. | | `daily_linkedin_requests_conf` | integer | Ceiling on all LinkedIn requests (reads included) per UTC day; protects the account from a LinkedIn restriction. 1 to 2000, default 1000. Raising it is the user's decision. | | `daily_messaging_reads_conf` | integer | Inbox reads (conversations, messages, Sales Navigator threads) per UTC day. Default 300; 0 = not limited. | | `messaging_min_interval_seconds` | integer | Seconds before the same inbox page or conversation can be read again. Default 60; 0 = off. | | `window_start` | string | HH:MM, in the account's timezone: jobs on this account start no earlier. Empty string resets to 09:00. | | `window_end` | string | HH:MM: jobs on this account stop at this time. Empty string resets to 18:00. | | `window_days` | array | Weekdays jobs may run: mon, tue, wed, thu, fri, sat, sun. Empty list resets to Monday–Friday. | | `timezone` | string | IANA zone for the window, e.g. Europe/Paris. Empty string resets to the zone of the account's proxy country. | ### `get_account_request_logs` · read-only · Reach only Get request logs for a LinkedIn account with optional filtering by request_type and pagination using limit/offset. Use to debug a failing account (each LinkedIn call with its status); for totals use get_account_request_logs_stats. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `request_type` | string | Only rows of this request type (for example send_message, list_conversations, connect). | | `limit` | integer | Maximum number of rows to return. | | `offset` | integer | Number of rows to skip. | ### `get_account_request_logs_stats` · read-only · Reach only Get aggregated request-log statistics for a LinkedIn account, grouped by day, week, or month, with optional action and date-range filters. Use for the weekly review or to size an account's activity; for individual calls use get_account_request_logs. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `group_by` | string (day, week, month) | Bucket size for the statistics: day, week or month. | | `request_type` | string | Only rows of this request type (for example send_message, list_conversations, connect). | | `start_date` | string | Start of the period, ISO 8601 (default: 30 days ago). | | `end_date` | string | End of the period, ISO 8601 (default: now). | ### `get_me` · read-only · calls LinkedIn · *core* Call LinkedIn Voyager /me for an account through the stored proxy/cookies and return the normalized profile (same fields as Kanbox uses from /me). Use to confirm which LinkedIn profile an account is (name, headline, premium); one LinkedIn call. Not needed before every action. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | ## Inbox and messaging ### `list_conversations` · read-only · calls LinkedIn · *core* List LinkedIn messenger conversations (Voyager Messaging GraphQL), with optional archived, unread, starred filters and next_cursor paging — same as GET /api/linkedin/{account_id}/conversations. Each item returns conversation_id (the thread id — use this as conversation_linkedin_id when fetching messages or replying) and recipient_linkedin_id (the peer's fsd_profile id — use this as recipient_linkedin_id when sending a new message). Do not call this in a loop to wait for replies: the same read is refused if repeated within 60s, inbox reads are capped at 300 per account per day, and polling gets LinkedIn accounts restricted. Subscribe a webhook to message.received instead. Use for the classic LinkedIn inbox (this is the one that matters in 95% of cases); Sales Navigator threads are separate, see salesnav_list_messaging_threads. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `archived` | boolean | Only archived conversations. | | `unread` | boolean | Only conversations with unread messages. | | `starred` | boolean | Only starred conversations. | | `count` | integer | Page size. | | `next_cursor` | string | Cursor returned by the previous page; omit for the first page. | ### `list_conversation_messages` · read-only · calls LinkedIn · *core* List messages in a LinkedIn conversation via Voyager ``/messaging/conversations/{id}/events`` (Kanbox ``get_conversation_messages``), with optional created_before paging — same as GET /api/linkedin/{account_id}/conversations/{conversation_linkedin_id}/messages. Do not call this in a loop to wait for replies: the same read is refused if repeated within 60s, inbox reads are capped at 300 per account per day, and polling gets LinkedIn accounts restricted. Subscribe a webhook to message.received instead. Use after list_conversations to read a thread before replying; the conversation_linkedin_id comes from list_conversations. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `conversation_linkedin_id` | string | required; Conversation (thread) id, as returned by list_conversations. | | `created_before` | string | Only messages created before this Unix timestamp in milliseconds, to page back in time. | ### `send_message` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn · *core* Send a LinkedIn DM via Voyager createMessage: use conversation_linkedin_id for an existing thread or recipient_linkedin_id for a new DM. Use for the classic inbox: conversation_linkedin_id to reply in a thread, recipient_linkedin_id for a new one (1st-degree connections, or InMail). Counts against the daily messages quota; for a Sales Navigator thread use salesnav_send_message; for more than ten messages use create_job. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `text` | string | Plain text to send or post. | | `conversation_linkedin_id` | string | Conversation (thread) id, as returned by list_conversations. | | `recipient_linkedin_id` | string | Recipient profile id (fsd_profile id) to start a new conversation; from list_conversations or scrape_profile. Omit when replying in an existing conversation. | | `in_mail_premium` | boolean | Send as InMail to someone outside the network (premium accounts); requires recipient_linkedin_id. | ### `react_message` · **write** · additive · idempotent · calls LinkedIn Add or remove an emoji reaction on a LinkedIn message. Set react=false to remove the reaction. message_urn is the message_urn returned by list_conversation_messages (e.g. urn:li:msg_message:(urn:li:fsd_profile:…,123456789)). emoji is a Unicode emoji character (e.g. 👍, ❤️). Pass conversation_linkedin_id to also mark the conversation as read. Use for an emoji reaction on one message inside a thread (message_urn from list_conversation_messages); to reply in words use send_message. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `message_urn` | string | required; Full message URN to react to. | | `emoji` | string | required; Unicode emoji character (e.g. 👍, ❤️). | | `react` | boolean | default `True`; True to add the reaction, False to remove it. | | `conversation_linkedin_id` | string | Conversation ID — when provided the conversation is marked as read. | ### `star_conversation` · **write** · additive · idempotent · calls LinkedIn Star or unstar a LinkedIn conversation. Set star=false to remove the star. conversation_linkedin_id is the conversation_id returned by list_conversations. Housekeeping in the classic inbox; reversible (star=false). No quota. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `conversation_linkedin_id` | string | required; Conversation (thread) id, as returned by list_conversations. | | `star` | boolean | default `True`; True to star, False to unstar. | ### `archive_conversation` · **write** · additive · idempotent · calls LinkedIn Archive or unarchive a LinkedIn conversation. Set archive=false to unarchive. conversation_linkedin_id is the conversation_id returned by list_conversations. Housekeeping in the classic inbox; reversible (archive=false). Not a delete, see delete_conversation. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `conversation_linkedin_id` | string | required; Conversation (thread) id, as returned by list_conversations. | | `archive` | boolean | default `True`; True to archive, False to unarchive. | ### `delete_conversation` · **write** · destructive · idempotent · calls LinkedIn Permanently delete a LinkedIn conversation. conversation_linkedin_id is the conversation_id returned by list_conversations. Irreversible for this account; confirm with the user. To hide a thread without losing it use archive_conversation. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `conversation_linkedin_id` | string | required; Conversation (thread) id, as returned by list_conversations. | ## Sales Navigator inbox ### `salesnav_list_messaging_threads` · read-only · calls LinkedIn List Sales Navigator messaging threads (salesApiMessagingThreads). Requires the account to have a Sales Navigator subscription (has_sales_nav). Returns threads with messages and participant profiles. Paginate by passing next_page_starts_at from the previous response as page_starts_at. Use only when the account has Sales Navigator and the user asks about that inbox; the classic inbox is list_conversations. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `filter` | string (INBOX, ARCHIVE, UNREAD, INMAIL_PENDING, INMAIL_ACCEPTED, INMAIL_DECLINED) | default `INBOX`; Conversation folder. | | `count` | integer | default `20`; Threads per page. | | `page_starts_at` | integer | Pagination cursor — next_page_starts_at from the previous response. | ### `salesnav_list_thread_messages` · read-only · calls LinkedIn Fetch a Sales Navigator thread with all its messages and participant profiles (salesApiMessagingThreads/{id}). Requires a Sales Navigator subscription. Use the thread id returned by salesnav_list_messaging_threads. message_count controls how many (most recent) messages are returned; compare with total_message_count in the response to know if more exist. Use to read one Sales Navigator thread (thread id from salesnav_list_messaging_threads); classic inbox threads use list_conversation_messages. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `thread_id` | string | required; Sales Navigator thread ID. | | `message_count` | integer | default `20`; Number of most-recent messages to return. | ### `salesnav_send_message` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn Send a message in an existing Sales Navigator thread (salesApiMessageActions). Requires the account to have a Sales Navigator subscription. Use the thread id returned by salesnav_list_messaging_threads. Use only to reply inside an existing Sales Navigator thread; everything else (new messages, classic inbox) goes through send_message. Counts against the daily messages quota. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `thread_id` | string | required; Sales Navigator thread ID. | | `text` | string | required; Message body (plain text). | | `copy_to_crm` | boolean | default `False`; Copy message to CRM. | ## Network and invitations ### `list_connections` · read-only · calls LinkedIn · *core* List LinkedIn 1st-degree connections for an account (Voyager dash connections), paged with start/count — same payload as GET /api/linkedin/{account_id}/connections. Use to walk the account's own 1st-degree network; for pending invitations use list_sent_invitations or list_received_invitations; for search use scrape_search. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `start` | integer | Pagination offset, 0-based. | | `count` | integer | Page size. | ### `send_invitation` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn · *core* Send a connection request to a LinkedIn member. Optional message capped at 300 chars (premium) or 200 chars. Use to connect with someone not yet a 1st-degree connection; check get_invitation_status first to avoid duplicates. Counts against the daily invitations quota; for more than ten use create_job. To follow without connecting use follow_member. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; LinkedIn member ID, vanity name, or profile URL. | | `message` | string | Optional personalised invitation note. | ### `get_invitation_status` · read-only · calls LinkedIn · *core* Get the connection and invitation status between the connected account and a LinkedIn member. Returns is_connection, invitation_type (SENT/PENDING/WITHDRAWN/REFUSED). Use before send_invitation or send_message to know the relationship (connected, invitation pending, refused). Read-only, cheap. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; The LinkedIn member: profile URL, vanity name (the part after /in/), Sales Navigator URL, or internal member id. | ### `withdraw_invitation` · **write** · destructive · idempotent · calls LinkedIn Withdraw a pending sent connection request. Use to cancel a pending sent invitation (from list_sent_invitations), for example one older than three weeks. Not for received invitations, see decline_invitation. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; The LinkedIn member: profile URL, vanity name (the part after /in/), Sales Navigator URL, or internal member id. | ### `accept_invitation` · **write** · additive · idempotent · calls LinkedIn Accept a pending received connection request. Use to accept a received invitation (invitation_secret from list_received_invitations). Ask the user which ones. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; The LinkedIn member: profile URL, vanity name (the part after /in/), Sales Navigator URL, or internal member id. | ### `decline_invitation` · **write** · destructive · idempotent · calls LinkedIn Decline/ignore a pending received connection request. Use to ignore a received invitation; the sender is not notified. Not for invitations the account sent, see withdraw_invitation. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; The LinkedIn member: profile URL, vanity name (the part after /in/), Sales Navigator URL, or internal member id. | ### `remove_connection` · **write** · destructive · idempotent · calls LinkedIn Remove an existing 1st-degree connection. Irreversible on LinkedIn (the person is not notified); confirm with the user. Not for pending invitations, see withdraw_invitation. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; The LinkedIn member: profile URL, vanity name (the part after /in/), Sales Navigator URL, or internal member id. | ### `follow_member` · **write** · additive · idempotent · calls LinkedIn Follow or unfollow a LinkedIn member without connecting. Set follow=false to unfollow. Use to follow someone's posts without a connection request (no quota); to connect use send_invitation. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; The LinkedIn member: profile URL, vanity name (the part after /in/), Sales Navigator URL, or internal member id. | | `follow` | boolean | default `True`; True to follow, False to unfollow. | ### `list_received_invitations` · read-only · calls LinkedIn · *core* List pending connection invitations received by the account (with invitation_secret to accept them). Use to triage invitations waiting for the account (then accept_invitation or decline_invitation); for those the account sent use list_sent_invitations. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `start` | integer | default `0`; Pagination offset, 0-based. | | `count` | integer | default `20`; Page size (max 100). | ### `list_sent_invitations` · read-only · calls LinkedIn · *core* List pending connection invitations sent by the account that have not yet been accepted. Use to find invitations the account sent that are still pending (then withdraw_invitation if stale); for received ones use list_received_invitations. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `start` | integer | default `0`; Pagination offset, 0-based. | | `count` | integer | default `20`; Page size (max 100). | ## Profiles and search ### `scrape_profile` · read-only · calls LinkedIn · *core* Fetch a complete LinkedIn profile (name, headline, company, experience, skills…). Pass a LinkedIn URL, vanity name, internal member ID, or Sales Navigator lead URL. Uses SalesNav API when available for richer data. Use to read one person in depth (experience, skills, company); costly, one LinkedIn call per profile. For a list of people use scrape_search; to just check the relationship use get_invitation_status. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; LinkedIn profile URL, vanity name, Sales Navigator URL, or internal member ID. | ### `scrape_search` · read-only · calls LinkedIn · *core* Run one page of a LinkedIn, Sales Navigator, or Recruiter search and return normalized profile rows. Pass the full search URL. Use start+count to paginate (count ignored for standard LinkedIn, fixed ~10/page). Use to run a search URL you have (LinkedIn, Sales Navigator, Recruiter) one page at a time; build the Sales Navigator URL with salesnav_build_search_url first. Counts against the imports quota. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `url` | string | required; Full LinkedIn / SalesNav / Recruiter search URL. | | `start` | integer | default `0`; Pagination offset. | | `count` | integer | default `25`; Page size (max 25). Ignored for standard LinkedIn. | ### `visit_profile` · **write** · additive · idempotent · calls LinkedIn · *core* Simulate a profile view, triggering a 'viewed your profile' notification for the target member. Use to leave a 'viewed your profile' trace as a soft touch before an invitation; counts against the daily visits quota. Not needed to read a profile, use scrape_profile. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; LinkedIn member ID, vanity name, or profile URL. | ### `profile_viewers` · read-only · calls LinkedIn · *core* Get the list of members who recently viewed the account's profile (requires LinkedIn Premium). Use for the 'who viewed my profile' list (needs LinkedIn Premium; empty or an error otherwise). Read-only. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | ### `salesnav_resolve_industry` · read-only · Reach only · *core* Resolve an industry name (any language) to its LinkedIn Industry Code(s) using the bundled Industry Codes V2 taxonomy (offline, no LinkedIn call). Returns ranked candidates with id, label and hierarchy path. Use the returned id(s) in salesnav_build_search_url's `industry` filter. Call once per industry term; if several candidates look plausible, pick by the hierarchy path. Use for industries only (offline taxonomy, any language, no LinkedIn call); for locations, companies, schools and titles use salesnav_typeahead. | Parameter | Type | Notes | |---|---|---| | `query` | string | required; Industry name to look up, e.g. 'software', 'banque', 'real estate'. | | `limit` | integer | default `8`; Maximum number of rows to return. | ### `salesnav_typeahead` · read-only · calls LinkedIn · *core* Live Sales Navigator autocomplete for facets whose ids are dynamic. Use type='geo' to resolve a location to its REGION id (the only reliable way — LinkedIn region ids are not enumerable), and type='company' / 'school' / 'title' for those entity facets. Returns [{id, displayValue, headline}]. Feed the chosen id into salesnav_build_search_url (region / current_company / school / current_title). Requires a connected account. Use for facets whose ids are dynamic (geo, company, school, title…); for industries use salesnav_resolve_industry. One LinkedIn call per lookup. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `type` | string (geo, company, school, title, group, industry) | default `geo`; Facet to autocomplete. 'geo' -> region ids. | | `query` | string | required; Text to autocomplete, e.g. 'Paris', 'Google', 'HEC'. | | `count` | integer | default `10`; Page size. | ### `salesnav_build_search_url` · read-only · calls LinkedIn · *core* Build a Sales Navigator people-search URL from structured lead-search filters, using as many filters as the request implies. RESOLUTION: `industry` accepts numeric ids OR names (auto-resolved via the taxonomy); `region` accepts numeric REGION ids OR location names (auto-resolved via geo typeahead — needs account_id). If a name is ambiguous, the tool returns {needs_disambiguation:[...]} with candidates instead of a URL — re-call with the chosen id. BOOLEAN: `keywords` and `current_title` accept LinkedIn boolean syntax (AND/OR/NOT, quotes, parentheses). EXCLUSION: any list value may be an object {id|name, exclude:true}. Set execute=true to also run the first page of results (reuses the live search) and return leads. Use after resolving the ids to get the Sales Navigator search URL, then scrape_search on it. Builds a URL only, no LinkedIn call. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | Required when passing location/industry NAMES or execute=true. | | `keywords` | string | Global keyword search; supports boolean operators. | | `current_title` | string | Current job title; free text + boolean (e.g. '(CTO OR "VP Engineering") NOT interim'). | | `past_title` | string | Boolean search on past job titles. | | `first_name` | string | First name filter. | | `last_name` | string | Last name filter. | | `company_headcount` | array | COMPANY_HEADCOUNT codes: A=Self-employed B=1-10 C=11-50 D=51-200 E=201-500 F=501-1000 G=1001-5000 H=5001-10000 I=10001+. | | `company_type` | array | COMPANY_TYPE: C=Public P=Privately Held N=Non Profit D=Educational S=Partnership E=Self-Employed O=Self Owned G=Government. | | `function` | array | FUNCTION 1-26: 1 Accounting 4 Business Dev 8 Engineering 10 Finance 12 HR 13 IT 15 Marketing 18 Operations 19 Product 25 Sales 26 Support (etc.). | | `seniority_level` | array | SENIORITY: 320 Owner/Partner 310 CXO 300 VP 220 Director 210 Exp. Manager 200 Entry Manager 130 Strategic 120 Senior 110 Entry 100 Trainee. | | `years_at_current_company` | array | 1=<1y 2=1-2y 3=3-5y 4=6-10y 5=>10y. | | `years_in_current_position` | array | 1=<1y 2=1-2y 3=3-5y 4=6-10y 5=>10y. | | `years_of_experience` | array | 1=<1y 2=1-2y 3=3-5y 4=6-10y 5=>10y. | | `relationship` | array | RELATIONSHIP: F=1st S=2nd A=Group members O=3rd+. | | `profile_language` | array | ISO 639-1 codes: en fr es de it pt nl ... | | `industry` | array | Industry ids (use salesnav_resolve_industry) or names to auto-resolve. Items may be {id\|name, exclude}. | | `region` | array | REGION ids (use salesnav_typeahead type=geo) or location names to auto-resolve. Items may be {id\|name, exclude}. | | `current_company` | array | Resolved COMPANY ids (salesnav_typeahead type=company). | | `past_company` | array | Past companies: names or ids; add exclude:true to exclude one. | | `school` | array | Resolved SCHOOL ids. | | `group` | array | LinkedIn groups: names or ids. | | `follows_your_company` | boolean | Only people who follow your company page. | | `viewed_your_profile` | boolean | Only people who viewed your profile recently. | | `past_colleague` | boolean | Only past colleagues. | | `with_shared_experiences` | boolean | Only people with shared experiences (school, company, group). | | `recently_changed_jobs` | boolean | Only people who changed jobs in the last 90 days. | | `posted_on_linkedin` | boolean | Only people who posted on LinkedIn in the last 30 days. | | `execute` | boolean | default `False`; Also run the first results page and return leads. | | `start` | integer | default `0`; Pagination offset, 0-based. | | `count` | integer | default `25`; Page size. | ### `search_job_postings` · read-only · calls LinkedIn · *core* Search LinkedIn job postings, like the Jobs tab of LinkedIn: which companies are hiring for a role right now. Returns up to 25 postings per page with the title, the hiring company (name and numeric company_id), location, workplace type, salary when shown and listing date. Keywords take LinkedIn's syntax: "exact phrase", OR, AND, NOT. Filter by location, recency, workplace type, job type, experience level and company; paginate with start (0, 25, 50…). These are LinkedIn job offers, not Reach jobs (create_job / list_jobs). Use to find companies hiring for a role (a buying signal: a company hiring SDRs needs sales tooling, one hiring a Head of Data will buy data tools); then salesnav_build_search_url with current_company=[company_id] for the people who decide, or scrape_search on a classic people search filtered on that company. Not for Reach batch jobs (list_jobs). One LinkedIn call per page of up to 25 postings, each counted against the daily job_searches quota (default 50 pages); keep to a few pages. Read-only. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `keywords` | string | The role or skills, e.g. "SDR" OR "Sales Development Representative". Optional when company_ids is given. | | `location` | string | Place name as typed on LinkedIn, e.g. France or Paris. Ignored when geo_id is given. Without location or geo_id, LinkedIn searches the account's own country only. | | `geo_id` | string | Numeric LinkedIn geo id (salesnav_typeahead type=geo); more precise than location. 92000000 is worldwide. | | `date_posted` | string (past-24h, past-week, past-month) | Only postings from this period. Omit for any time. | | `workplace_types` | array | Only these workplace types. | | `job_types` | array | Only these contract types. | | `experience` | array | Only these seniority levels. | | `company_ids` | array | Numeric LinkedIn company ids (salesnav_typeahead type=company): only their openings. | | `sort_by` | string (date_posted, relevance) | date_posted for the newest first (best for signals); relevance is LinkedIn's default. | | `start` | integer | default `0`; Pagination offset: 0, 25, 50… | ## Posts and engagement ### `list_user_posts` · read-only · calls LinkedIn · *core* Get recent posts published by a LinkedIn member (via profileUpdatesV2). Paginate with start/count. Use to read what a member published recently (their content, not who engaged); to get likers and commenters of a post use scrape_post. Read-only. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `linkedin_id_or_url` | string | required; LinkedIn member ID, vanity name, or profile URL. | | `count` | integer | default `10`; Number of posts (max 50). | | `start` | integer | default `0`; Pagination offset. | ### `scrape_post` · read-only · calls LinkedIn · *core* Fetch likers and/or commenters for a LinkedIn post. Pass the full post URL or slug. Set liked=true and/or comments=true. Use to get the people who liked or commented on one post you have the URL of; for the account's own recent posts use scrape_my_posts. Counts against the imports quota. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `post_id_or_url` | string | required; LinkedIn post URL (incl. lnkd.in short links) or slug. | | `liked` | boolean | default `False`; Include likers. | | `comments` | boolean | default `False`; Include commenters. | ### `scrape_my_posts` · read-only · calls LinkedIn · *core* Fetch likers and/or commenters for the account's own recent posts. since_hours limits to posts published in the last N hours (default 24). Use for engagement on the account's own posts (likers, commenters, comment URNs for reply_comment); for someone else's post use scrape_post. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `liked` | boolean | default `False`; Include the people who liked. | | `comments` | boolean | default `False`; Include the people who commented, with their comment text. | | `since_hours` | integer | default `24`; Only posts from the last N hours (max 720). | | `max_posts` | integer | default `10`; Max posts to process (max 20). | ### `like_post` · **write** · additive · idempotent · calls LinkedIn · *core* Like a LinkedIn post on behalf of the connected account. Pass the post URL, slug, or URN. Use for a light touch on a post; idempotent on LinkedIn's side. Counts against the daily reactions quota. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `post_url_or_urn` | string | required; LinkedIn post URL, slug, or urn:li:activity:…. | ### `comment_post` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn · *core* Post a comment on a LinkedIn post on behalf of the connected account. Use to write a new top-level comment on a post; to answer an existing comment use reply_comment. Counts against the daily comments quota; the text is public, preview it with the user. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `post_url_or_urn` | string | required; LinkedIn post URL, slug, or URN. | | `text` | string | required; Comment text. | ### `reply_comment` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn · *core* Post a reply to an existing LinkedIn comment. Pass the comment_id (URN) returned by scrape_post as comment_urn — it is used directly as threadUrn. Use to answer a specific comment (comment_urn from scrape_my_posts or scrape_post); for a new comment on the post itself use comment_post. Counts against the daily comments quota. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `comment_urn` | string | required; URN of the comment to reply to (comment_id from scrape-post). | | `text` | string | required; Reply text. | ## Publishing ### `create_post` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn · *core* Create or schedule a LinkedIn post, optionally with a media attachment. For a single image pass a urn:li:digitalmediaAsset:… URN; for multiple images pass a urn:li:fsd_multiPhoto:… URN from create_multi_photo. Set scheduled_at (Unix ms) to schedule; omit it to publish immediately. Publishes now or schedules (scheduled_time); counts against the daily posts quota. Show the user the full text before calling. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `text` | string | required; Plain text to send or post. | | `media_urn` | string | Optional media URN to attach. | | `scheduled_at` | integer | Scheduled publish time in Unix milliseconds. Omit to publish now. | | `visibility` | string | default `ANYONE`; 'ANYONE' or 'CONNECTIONS_ONLY'. | | `allowed_commenters_scope` | string | default `ALL`; 'ALL' or 'CONNECTIONS_ONLY'. | ### `list_scheduled_posts` · read-only · calls LinkedIn List scheduled LinkedIn posts for an account, paged. Returns post URNs, text content, and scheduled publish times. Use to see what is queued to publish; published posts are not listed here, use list_user_posts for those. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `start` | integer | default `0`; Pagination offset, 0-based. | | `count` | integer | default `10`; Page size. | ### `update_scheduled_post` · **write** · additive · idempotent · calls LinkedIn Update the text and/or scheduled time of an existing scheduled LinkedIn post. post_urn is the URN returned by list_scheduled_posts (e.g. urn:li:ugcPost:…). At least one of text or scheduled_at must be provided. Only for posts still scheduled (post_urn from list_scheduled_posts); a published post cannot be edited through Reach. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `post_urn` | string | required; URN of the scheduled post (urn:li:…), from list_scheduled_posts. | | `text` | string | Plain text to send or post. | | `scheduled_at` | integer | Unix timestamp in milliseconds. | ### `delete_scheduled_post` · **write** · destructive · idempotent · calls LinkedIn Delete a scheduled LinkedIn post. post_urn is the URN returned by list_scheduled_posts (e.g. urn:li:ugcPost:…). Only for posts still scheduled; irreversible, confirm with the user. A published post cannot be deleted through Reach. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `post_urn` | string | required; URN of the scheduled post (urn:li:…), from list_scheduled_posts. | ### `upload_media_from_url` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn Upload an image to LinkedIn by fetching it from a public URL. Returns a digitalmediaAsset URN (urn:li:digitalmediaAsset:…) that can be passed to create_post (single image) or create_multi_photo (multi-photo). Use this instead of upload-media when working as an agent — no binary file upload needed. Use before create_post when the post needs an image; returns the URN create_post expects. For several images, then create_multi_photo. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `image_url` | string | required; Publicly accessible URL of the image to upload. | | `media_upload_type` | string | default `IMAGE_SHARING`; LinkedIn media upload type (default: IMAGE_SHARING). | ### `create_multi_photo` · **write** · additive · not idempotent, use idempotency_key · calls LinkedIn Create a LinkedIn multi-photo object from a list of digitalmediaAsset URNs. Returns an identifierUrn (urn:li:fsd_multiPhoto:…) to pass to create_post. author_urn is the urn:li:fsd_profile:… of the connected account (get it from get_me). Use after uploading two or more images with upload_media_from_url; returns the single URN create_post takes. Not for one image. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `author_urn` | string | required; Only messages from this author URN. | | `media_urns` | array | required; List of urn:li:digitalmediaAsset:… URNs. | | `alt_texts` | array | Optional alt text per image (same order as media_urns). | ## Webhooks ### `list_webhook_endpoints` · read-only · Reach only · *core* List the webhooks registered for this user: the URLs Reach calls when a conversation gets a new message (message.received), the account gains a 1st-degree connection (connection.new), an account changes connection state (account.status_changed) or a daily quota is close to or at its limit (quota.threshold_reached, quota.reached). Read-only. Use to see where Reach already posts events and which events; also returns the event catalogue. ### `create_webhook_endpoint` · **write** · additive · not idempotent, use idempotency_key · Reach only · *core* Register a URL to be called when something happens on LinkedIn, so an agent or an n8n/Make workflow can react instead of polling. Returns the signing secret ONCE; deliveries carry 'Reach-Signature: t=,v1='. Events: message.received, connection.new, account.status_changed, quota.threshold_reached, quota.reached, or '*' for all. Omit account_ids to subscribe for every account of the user. https only. Use when the user wants to react to a reply, a new connection, a quota alert or a job event from n8n, Make or their own code; needs an https URL they control. Not for the model to call itself. | Parameter | Type | Notes | |---|---|---| | `url` | string | required; https URL that accepts a JSON POST. | | `events` | array | required; Event types, or ['*']. | | `account_ids` | array | Optional: only these LinkedIn account ids. | | `description` | string | Free-text label for your own reference. | ### `update_webhook_endpoint` · **write** · additive · idempotent · Reach only Change a webhook's url, events, account_ids, description or is_active. Only provided fields change. Re-enabling clears the failure counter. Use to change events, accounts or url, or to re-enable an endpoint disabled after failures; to stop deliveries temporarily set is_active=false rather than deleting. | Parameter | Type | Notes | |---|---|---| | `endpoint_id` | integer | required; Webhook endpoint id, from list_webhook_endpoints. | | `url` | string | https URL that receives the signed JSON POST (http accepted for localhost only). | | `events` | array | Event types to subscribe to (message.received, connection.new, account.status_changed, quota.threshold_reached, quota.reached) or ['*']. | | `account_ids` | array | Only these LinkedIn account ids; omit for every account of the user. | | `description` | string | Free-text label for your own reference. | | `is_active` | boolean | Pause (false) or resume (true) deliveries. | ### `delete_webhook_endpoint` · **write** · destructive · idempotent · Reach only Delete a webhook endpoint and its delivery history. Irreversible (the delivery history goes too); confirm with the user. To pause deliveries use update_webhook_endpoint with is_active=false. | Parameter | Type | Notes | |---|---|---| | `endpoint_id` | integer | required; Webhook endpoint id, from list_webhook_endpoints. | ### `test_webhook_endpoint` · read-only · calls LinkedIn Send a signed 'ping' event to a webhook endpoint right now and report the HTTP status it answered. Use to check a receiver end to end (signed ping, HTTP status back); does not deliver real events. | Parameter | Type | Notes | |---|---|---| | `endpoint_id` | integer | required; Webhook endpoint id, from list_webhook_endpoints. | ## Jobs (durable batches) ### `create_job` · **write** · additive · not idempotent, use idempotency_key · Reach only · *core* Queue a batch of LinkedIn actions (messages, invitations, profile visits or comments) that Reach executes on its own over the following hours or days: spread across the account's activity window (set per account with update_account_quotas; default 09:00-18:00, Monday to Friday, in the account's timezone; overridable per job) with human-like gaps, never beyond the daily quota of the action. Returns immediately with a job_id and the planned schedule; the agent does not have to stay alive. The job pauses by itself when a quota is reached or the account disconnects and resumes when it can; webhooks job.started / job.progress / job.paused / job.completed report what happens. Use it for anything above a handful of actions instead of calling send_message or send_invitation in a loop. Always show the user the items before queuing them. Use for any batch above a handful of actions; returns at once with the schedule. Show the items to the user first. Not for a single action, call the direct tool. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `action` | string (send_message, send_invitation, visit_profile, comment_post) | required; What every item does. send_message: text + conversation_linkedin_id (reply) or recipient_linkedin_id (new thread). send_invitation: linkedin_id_or_url + optional message (<200 chars). visit_profile: linkedin_id_or_url. comment_post: post_url_or_urn + text. | | `items` | array | required; One object per action, in the order to execute them; fields depend on the action (see action). | | `label` | string | Short free text shown in the dashboard and in webhook events, e.g. 'Follow-ups week 40'. | | `window_start` | string | HH:MM in the job's timezone; default: the account's window (09:00 unless changed). | | `window_end` | string | HH:MM in the job's timezone; default: the account's window (18:00 unless changed). | | `days` | array | Weekdays the job may run: mon, tue, wed, thu, fri, sat, sun. Default: the account's window (Monday to Friday unless changed). | | `timezone` | string | IANA zone such as Europe/Paris. Default: the account's window setting, else the zone of its proxy country. | ### `get_job` · read-only · Reach only · *core* One job with its status, totals, next execution time and every item (scheduled time, result or error). Use to report progress or to see why an item failed; for an overview of all jobs use list_jobs. | Parameter | Type | Notes | |---|---|---| | `job_id` | integer | required; Job id, as returned by create_job or list_jobs. | ### `list_jobs` · read-only · Reach only · *core* Jobs of this user, newest first, without their items. Filter by account or status (scheduled, running, paused, completed, cancelled). Use to find running or paused jobs before creating a new one on the same account; details and items are in get_job. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | Reach id of the LinkedIn account to act on, from list_accounts. | | `status` | string (scheduled, running, paused, completed, cancelled) | Only jobs in this status. | | `limit` | integer | Maximum number of jobs; default 50. | ### `cancel_job` · **write** · destructive · idempotent · Reach only Cancel a job: its pending items will never run. Items already executed are unaffected. Cannot be undone. Irreversible for the pending items (executed ones stay); confirm with the user. To stop temporarily use pause_job. | Parameter | Type | Notes | |---|---|---| | `job_id` | integer | required; Job id, as returned by create_job or list_jobs. | ### `pause_job` · **write** · additive · idempotent · Reach only Pause a job: nothing more runs until resume_job is called. Reach also pauses a job by itself when a quota is reached or the account disconnects, and resumes those on its own. Use to hold a job the user is unsure about; resume_job re-plans it. Reach pauses on quota or disconnection by itself. | Parameter | Type | Notes | |---|---|---| | `job_id` | integer | required; Job id, as returned by create_job or list_jobs. | ### `resume_job` · **write** · additive · idempotent · Reach only Resume a paused job. Its pending items are re-planned from now, inside the window and the remaining quota. Use after a manual pause; jobs paused for quota or disconnection resume on their own and need no call. | Parameter | Type | Notes | |---|---|---| | `job_id` | integer | required; Job id, as returned by create_job or list_jobs. | ## Other ### `search_posts` · read-only · calls LinkedIn · *core* Search LinkedIn posts by keywords, like the Posts tab of LinkedIn search: who is talking about a topic right now. Returns about 10 posts per page with the text, the engagement and the author (name, headline, member id, relationship degree). Keywords take LinkedIn's syntax: "exact phrase", OR, AND, NOT. Filter by recency, author job title, author company or industry, and content type; paginate with start (0, 10, 20…). Use to find who is posting about a topic (buying signals, hiring, pain points, competitors) and reach the authors; then scrape_post on a result for the people who engaged, scrape_profile on an author to qualify, send_invitation with author_linkedin_id. One LinkedIn call per page of ~10 posts; keep to a few pages. Read-only. | Parameter | Type | Notes | |---|---|---| | `account_id` | integer | required; Reach id of the LinkedIn account to act on, from list_accounts. | | `keywords` | string | required; What the posts talk about, e.g. "hiring SDR" OR "SDR position". | | `date_posted` | string (past-24h, past-week, past-month) | Only posts from this period. Omit for any time. | | `sort_by` | string (date_posted, relevance) | date_posted for the newest first (best for signals); relevance is LinkedIn's default. | | `content_type` | string (videos, photos, documents, jobs, liveVideos, collaborativeArticles) | Only posts of this type. | | `author_job_title` | string | Only authors whose job title matches, e.g. CEO or Head of Sales. | | `author_company_ids` | array | Numeric LinkedIn company ids of the authors' current company (from salesnav_typeahead type=company). | | `author_industry_ids` | array | Numeric LinkedIn industry ids of the authors (from salesnav_resolve_industry). | | `start` | integer | default `0`; Pagination offset: 0, 10, 20… | --- # Reach MCP — playbooks (8) A playbook is a named, bounded workflow served as an MCP prompt (`prompts/list` / `prompts/get`) and through the `reach_playbooks` tool for clients that do not surface prompts. Every playbook that writes shows a numbered preview (person, context, exact text) and waits for the user to name what to send; only those go out, one at a time, inside the account's daily limits — or, when more than ten are approved, as one job that Reach runs over working hours (see [Jobs](/docs/jobs.md)). ## My LinkedIn week in review (`weekly_report`) What your account did this week, which invitations are rotting, and which conversations are waiting on you. **Say to your assistant:** “Give me my LinkedIn week in review.” - Writes to LinkedIn: no (read-only) - Arguments: `days` (How many days back to report on. default `7`) - Tools used, in order: `get_account_request_logs_stats`, `list_sent_invitations`, `list_received_invitations`, `list_conversations` - Suggested next step: Suggest running the 'Wake up cold conversations' playbook on the unread threads you just listed. ## Wake up cold conversations (`wake_cold_conversations`) Finds the threads where you spoke last and nobody answered, and drafts a follow-up for each one. **Say to your assistant:** “Find my LinkedIn conversations that went cold and draft a follow-up for each.” - Writes to LinkedIn: yes, after preview and confirmation - Arguments: `days` (Minimum days of silence before a thread counts as cold. default `7`), `limit` (Maximum number of people to handle in one run. default `20`) - Tools used, in order: `list_conversations`, `list_conversation_messages`, `send_message` - Suggested next step: Offer to star the threads that got a reply so they are easy to find tomorrow (star_conversation). ## Turn post reactions into leads (`reactions_to_leads`) Everyone who liked or commented on your recent posts, qualified and messaged — the warm audience you already earned. **Say to your assistant:** “Who reacted to my last LinkedIn posts? Draft a message for each of them.” - Writes to LinkedIn: yes, after preview and confirmation - Requires: recent_own_posts - Arguments: `hours` (How far back to look for your own posts, in hours. default `168`), `limit` (Maximum number of people to handle in one run. default `25`) - Tools used, in order: `scrape_my_posts`, `get_invitation_status`, `send_message`, `send_invitation` - Suggested next step: Offer to reply to the comments on the same posts with the 'Reply to every comment' playbook. ## Turn profile viewers into leads (`viewers_to_leads`) The people who checked you out and will never message first — scored, ranked and given an opener. **Say to your assistant:** “Who viewed my LinkedIn profile recently, and which ones are worth contacting?” - Writes to LinkedIn: yes, after preview and confirmation - Requires: linkedin_premium - Arguments: `limit` (Maximum number of viewers to handle in one run. default `20`) - Tools used, in order: `profile_viewers`, `scrape_profile`, `get_invitation_status`, `send_invitation`, `send_message` - Suggested next step: Offer to visit the profiles you did not contact (visit_profile) so you appear in their own viewer list. ## Reply to every comment (`reply_to_comments`) Drafts a real answer to each comment on your recent posts, in context, in the commenter's language. **Say to your assistant:** “Draft a reply to every comment on my recent LinkedIn posts.” - Writes to LinkedIn: yes, after preview and confirmation - Requires: recent_own_posts - Arguments: `hours` (How far back to look for your own posts, in hours. default `48`) - Tools used, in order: `scrape_my_posts`, `reply_comment` - Suggested next step: Offer to message the commenters who are not connections yet with the 'Turn post reactions into leads' playbook. ## A prospect list from one sentence (`prospect_list`) Describe who you want in plain words; get a real Sales Navigator search, the rows, and a personalised invitation each. **Say to your assistant:** “Find me heads of sales at 50-200 person SaaS companies in France on LinkedIn.” - Writes to LinkedIn: yes, after preview and confirmation - Requires: sales_navigator - Arguments: `query` (Who you are looking for, in plain words — role, industry, size, location. Ask the user for this; do not invent it. default `e.g. heads of sales at 50-200 person SaaS companies in France`), `limit` (Maximum number of invitations in one run. default `20`) - Tools used, in order: `salesnav_resolve_industry`, `salesnav_typeahead`, `salesnav_build_search_url`, `scrape_search`, `send_invitation` - Suggested next step: Offer to schedule the same search as a weekly habit, and to follow up on the invitations that get accepted. ## People posting about a topic, turned into conversations (`post_signals_to_leads`) Name a topic — a pain, a hire, a tool, a competitor — and get the people who posted about it this week, qualified, with an opener that answers their post. **Say to your assistant:** “Find people who posted on LinkedIn this week about hiring SDRs and draft a message for each.” - Writes to LinkedIn: yes, after preview and confirmation - Arguments: `topic` (What the people should be posting about, in plain words. Ask the user for this; do not invent it. default `e.g. struggling to hire SDRs, or moving off HubSpot`), `date_posted` (How recent the posts must be: past-24h, past-week or past-month. default `past-week`), `limit` (Maximum number of people to contact in one run. default `15`) - Tools used, in order: `search_posts`, `get_invitation_status`, `send_invitation`, `send_message` - Suggested next step: Offer to run scrape_post on the most engaged post found, to reach the people who liked or commented on it too. ## Companies hiring for a role, turned into conversations (`hiring_signals_to_leads`) Name a role — SDRs, a Head of Data, a first marketer — and get the companies that opened it on LinkedIn, the people who decide there, and an opener for each. **Say to your assistant:** “Find companies in France hiring SDRs on LinkedIn this week and draft a message to their head of sales.” - Writes to LinkedIn: yes, after preview and confirmation - Arguments: `role` (The role the companies should be hiring for, in plain words. Ask the user for this; do not invent it. default `e.g. SDRs or sales development representatives`), `decision_maker` (Who to contact at each hiring company, by title. default `the head of the team that is hiring (e.g. VP Sales for an SDR opening)`), `location` (Where the openings must be, as a place name. default `anywhere`), `date_posted` (How recent the openings must be: past-24h, past-week or past-month. default `past-week`), `limit` (Maximum number of companies to handle in one run. default `10`) - Tools used, in order: `search_job_postings`, `salesnav_build_search_url`, `scrape_search`, `get_invitation_status`, `send_invitation`, `send_message` - Suggested next step: Offer to rerun the same search next week with date_posted=past-week, to catch the new openings only. --- # Reach MCP — REST API Base URL `https://app.reachmcp.com/api`. Authenticate with `X-API-Key: mcp_…` (create keys in the app). Interactive reference with request and response schemas: https://app.reachmcp.com/scalar; machine-readable: https://app.reachmcp.com/openapi.json. Conventions: - Errors: `detail` (legacy) plus an `error` object with `code`, `message`, `status`, `safe_to_retry`, `retry_at`, `remediation` — see [Errors](/docs/errors.md). - Writes accept `Idempotency-Key` — see [Idempotency](/docs/idempotency.md). - Daily quotas apply to writes — see [Quotas](/docs/quotas.md). - Webhooks are managed under `/webhooks` — see [Webhooks](/docs/webhooks.md). - Batches of actions run over time are `/jobs` — see [Jobs](/docs/jobs.md). ## Endpoints (61) | Method | Path | Summary | |---|---|---| | POST | `/api/jobs` | Create a job: a batch of actions spread over the account's working window | | GET | `/api/jobs` | List jobs | | GET | `/api/jobs/actions` | Actions a job can run, with their payload fields | | GET | `/api/jobs/{job_id}` | One job with its items | | POST | `/api/jobs/{job_id}/cancel` | Cancel the pending items of a job | | POST | `/api/jobs/{job_id}/pause` | Pause a job | | POST | `/api/jobs/{job_id}/resume` | Resume a paused job; pending items are re-planned from now | | GET | `/api/linkedin-accounts` | List LinkedIn accounts | | DELETE | `/api/linkedin-accounts/{account_id}` | Delete a LinkedIn account | | GET | `/api/linkedin-accounts/{account_id}/logs` | Get request logs | | GET | `/api/linkedin-accounts/{account_id}/logs/stats` | Get request logs statistics | | GET | `/api/linkedin-accounts/{account_id}/quotas` | Get account quotas | | PATCH | `/api/linkedin-accounts/{account_id}/quotas` | Update account quotas | | POST | `/api/linkedin/{account_id}/accept-invitation` | LinkedIn proxy: accept a received invitation | | POST | `/api/linkedin/{account_id}/comment-post` | LinkedIn proxy: comment on a post | | POST | `/api/linkedin/{account_id}/connect` | LinkedIn proxy: send a connection request | | GET | `/api/linkedin/{account_id}/connections` | LinkedIn proxy: list connections (paged) | | GET | `/api/linkedin/{account_id}/conversations` | LinkedIn proxy: list messenger conversations (paged) | | DELETE | `/api/linkedin/{account_id}/conversations/{conversation_linkedin_id}` | LinkedIn proxy: delete a conversation | | POST | `/api/linkedin/{account_id}/conversations/{conversation_linkedin_id}/archive` | LinkedIn proxy: archive or unarchive a conversation | | GET | `/api/linkedin/{account_id}/conversations/{conversation_linkedin_id}/messages` | LinkedIn proxy: list messages in a conversation | | POST | `/api/linkedin/{account_id}/conversations/{conversation_linkedin_id}/star` | LinkedIn proxy: star or unstar a conversation | | POST | `/api/linkedin/{account_id}/create-multi-photo` | LinkedIn proxy: create a multi-photo object | | POST | `/api/linkedin/{account_id}/create-post` | LinkedIn proxy: create or schedule a post | | POST | `/api/linkedin/{account_id}/decline-invitation` | LinkedIn proxy: decline a received invitation | | POST | `/api/linkedin/{account_id}/follow` | LinkedIn proxy: follow or unfollow a member | | GET | `/api/linkedin/{account_id}/invitation-status` | LinkedIn proxy: get invitation/connection status with a member | | POST | `/api/linkedin/{account_id}/like-post` | LinkedIn proxy: like a post | | GET | `/api/linkedin/{account_id}/me` | LinkedIn proxy: get /me | | POST | `/api/linkedin/{account_id}/messages` | LinkedIn proxy: send a message | | GET | `/api/linkedin/{account_id}/profile-viewers` | LinkedIn proxy: get recent profile viewers (premium only) | | POST | `/api/linkedin/{account_id}/react-message` | LinkedIn proxy: react to a message with an emoji | | GET | `/api/linkedin/{account_id}/received-invitations` | LinkedIn proxy: list received pending invitations | | POST | `/api/linkedin/{account_id}/remove-connection` | LinkedIn proxy: remove a 1st-degree connection | | POST | `/api/linkedin/{account_id}/reply-comment` | LinkedIn proxy: reply to a comment | | GET | `/api/linkedin/{account_id}/salesnav/messaging/threads` | Sales Navigator proxy: list messaging threads (paged) | | POST | `/api/linkedin/{account_id}/salesnav/messaging/threads/{thread_id}/messages` | Sales Navigator proxy: send a message in a thread | | GET | `/api/linkedin/{account_id}/salesnav/messaging/threads/{thread_id}/messages` | Sales Navigator proxy: list messages in a thread | | GET | `/api/linkedin/{account_id}/scheduled-posts` | LinkedIn proxy: list scheduled posts | | PATCH | `/api/linkedin/{account_id}/scheduled-posts` | LinkedIn proxy: update a scheduled post | | DELETE | `/api/linkedin/{account_id}/scheduled-posts` | LinkedIn proxy: delete a scheduled post | | GET | `/api/linkedin/{account_id}/scrape-my-posts` | LinkedIn proxy: scrape likers and/or commenters of the account's recent posts | | GET | `/api/linkedin/{account_id}/scrape-post` | LinkedIn proxy: scrape likers and/or commenters of a post | | GET | `/api/linkedin/{account_id}/scrape-profile` | LinkedIn proxy: scrape a single profile | | GET | `/api/linkedin/{account_id}/scrape-search` | LinkedIn proxy: scrape a search page | | GET | `/api/linkedin/{account_id}/sent-invitations` | LinkedIn proxy: list sent pending invitations | | POST | `/api/linkedin/{account_id}/upload-media` | LinkedIn proxy: upload a media file | | POST | `/api/linkedin/{account_id}/upload-media-url` | LinkedIn proxy: upload a media file from a URL | | GET | `/api/linkedin/{account_id}/user-posts` | LinkedIn proxy: get recent posts of a LinkedIn member | | POST | `/api/linkedin/{account_id}/visit-profile` | LinkedIn proxy: simulate a profile visit | | POST | `/api/linkedin/{account_id}/withdraw-invitation` | LinkedIn proxy: withdraw a sent invitation | | POST | `/api/webhooks/deliveries/{delivery_id}/replay` | Send this event again, now | | GET | `/api/webhooks/endpoints` | List webhook endpoints | | POST | `/api/webhooks/endpoints` | Create a webhook endpoint (returns the secret once) | | PATCH | `/api/webhooks/endpoints/{endpoint_id}` | Update url, events, accounts or active state | | DELETE | `/api/webhooks/endpoints/{endpoint_id}` | Delete a webhook endpoint | | GET | `/api/webhooks/endpoints/{endpoint_id}/deliveries` | Recent deliveries to this endpoint | | POST | `/api/webhooks/endpoints/{endpoint_id}/rotate-secret` | Replace the signing secret | | GET | `/api/webhooks/endpoints/{endpoint_id}/secret` | Reveal the signing secret | | POST | `/api/webhooks/endpoints/{endpoint_id}/test` | Send a signed ping now | | GET | `/api/webhooks/events` | Event types Reach can send | --- # Reach MCP — errors Every failure, on REST and on MCP, carries the same object: | Field | Meaning | |---|---| | `code` | Stable identifier. Act on this, never on the wording of `message`. | | `message` | Human sentence. | | `status` | HTTP status the server chose. | | `safe_to_retry` | `true` if repeating the same call unchanged can succeed. | | `retry_at` | ISO 8601 UTC instant from which a retry makes sense, when applicable. | | `remediation` | What to do instead, when a retry cannot help. | - **REST**: the body keeps `detail` (unchanged for older clients) and adds `error` beside it. - **MCP**: a failed tool call is a result with `isError: true`; `content[0].text` and `structuredContent` both carry the object. Authentication failures are JSON-RPC errors (`error.code -32000`) with the object under `error.data`. ## Codes | Code | Retry? | What to do | |---|---|---| | `account_forbidden` | no | This API key is restricted to other accounts. Call list_accounts to see which ones, or use a key that covers this account. | | `account_not_found` | no | Call list_accounts and use one of the returned account ids. | | `account_profile_incomplete` | no | Reconnect the account or refresh its profile from the dashboard, then retry. | | `daily_quota_reached` | later — `retry_at` is the next 00:00 UTC | Stop this action on this account for today; continue with other actions or accounts, or raise the limit with update_account_quotas if the account can take it. | | `idempotency_in_progress` | in a few seconds | Wait a few seconds and repeat the same request; you will get its result. | | `idempotency_key_reused` | no | A key names one exact request. Send a new key for a new action, or repeat the original request unchanged to get its stored result. | | `internal_error` | yes | Unexpected server error; retry once, then report it with the request id. | | `invalid_idempotency_key` | no | Send a UUID or any string of 1 to 200 characters as the key. | | `invalid_job_action` | no | Use one of the job actions: send_message, connect, visit_profile, comment_post. | | `invalid_job_items` | no | Fix the item named in the message; GET /api/jobs/actions lists the fields of each action. | | `invalid_request` | no | Fix the arguments named in the message and call again; do not retry unchanged. | | `invalid_timezone` | no | Use an IANA zone such as Europe/Paris or America/New_York. | | `invalid_webhook_events` | no | Use event types from GET /api/webhooks/events, or '*'. | | `invalid_webhook_url` | no | Use a publicly reachable https URL (http is accepted for localhost only). | | `invalid_window` | no | Times are HH:MM, days are mon…sun. | | `job_exceeds_capacity` | no | The batch does not fit in the next 30 days of quota and window: raise the quota, widen the window or split the batch. | | `job_not_found` | no | Call list_jobs and use one of the returned ids. | | `linkedin_request_failed` | once, after a short pause; then check the account status | Check the account with list_accounts (GET /api/linkedin-accounts). If its status is 'disconnected' or 'proxy_error', reconnect it from the dashboard; if it is 'connected', retry once after a short pause, then stop and report. | | `missing_argument` | no | Add the missing argument and call the tool again. | | `not_found` | no | The identifier does not exist or is not visible to this account; do not retry unchanged. | | `payment_required` | no | Subscribe and assign a seat to this account to continue. | | `plan_locked_by_subscription` | no | A live subscription is billed on its plan; write to us to move it. | | `recruiter_required` | no | Use a LinkedIn account with an active Recruiter subscription, or the standard LinkedIn search tools. | | `sales_navigator_required` | no | Use a LinkedIn account with an active Sales Navigator subscription, or the standard LinkedIn tools for the same task. | | `seat_required` | no | The 7-day trial has ended and the account has no paid seat. Subscribe and assign a seat (Subscription page). | | `unauthorized` | no | Check the X-API-Key header or the OAuth token. If the connector was re-authorised or the key rotated, reconnect the MCP server. | | `unknown_plan` | no | Use the self-serve plan id: standard (or omit it). | | `webhook_limit_reached` | no | Delete an unused endpoint or subscribe one endpoint to several event types (20 per user). | | `webhook_not_found` | no | List your endpoints and use one of the returned ids. | Example: ```json { "detail": "...", "error": { "code": "daily_quota_reached", "status": 429, "safe_to_retry": true, "retry_at": "2026-09-11T00:00:00Z", "action": "messages", "used": 150, "limit": 150, "remediation": "Stop this action on this account for today…" } } ``` --- # Reach MCP — idempotency A retried write must not reach LinkedIn twice. The contract is Stripe's: - **MCP**: every write tool accepts an optional `idempotency_key` (string ≤ 200 chars; a UUID is fine). - **REST**: send an `Idempotency-Key` header on any `POST` / `PATCH` / `DELETE` under `https://app.reachmcp.com/api/linkedin/`. - Same key + same request → the stored response is returned (REST adds `Idempotent-Replayed: true`; MCP adds `structuredContent.idempotent_replay: true`) and nothing is done again. - Same key + different request → `409 idempotency_key_reused`. - Same key while the first attempt is still running → `409 idempotency_in_progress` (retry in a few seconds). - Only successful responses are kept; a failed write releases the key, so retrying with the same key is exactly right. - Keys are scoped to the credential that sent them and expire after 24 hours. Write tools: `accept_invitation`, `archive_conversation`, `cancel_job`, `comment_post`, `create_job`, `create_multi_photo`, `create_post`, `create_webhook_endpoint`, `decline_invitation`, `delete_account`, `delete_conversation`, `delete_scheduled_post`, `delete_webhook_endpoint`, `follow_member`, `like_post`, `pause_job`, `react_message`, `remove_connection`, `reply_comment`, `resume_job`, `salesnav_send_message`, `send_invitation`, `send_message`, `star_conversation`, `update_account_quotas`, `update_scheduled_post`, `update_webhook_endpoint`, `upload_media_from_url`, `visit_profile`, `withdraw_invitation`. --- # Reach MCP — webhooks Register a URL and Reach calls it, instead of your agent polling LinkedIn. | Event | When | |---|---| | `message.received` | A conversation on the account has a new unread message from the other person (polled every few minutes; one event per conversation per new activity). | | `connection.new` | The account has a new 1st-degree connection: someone accepted the account's invitation, or the account accepted theirs (polled every few minutes; one event per new connection). | | `job.started` | A job executed its first item (data: job_id, action, label, total). | | `job.progress` | A job item was executed: done or failed, with its result or error and the running totals. | | `job.paused` | A job stopped by itself: the daily quota of its action was reached (resume_at tells when it resumes), the account disconnected (resumes when it is back), or billing blocks writes. | | `job.completed` | Every item of a job has been executed (data: done, failed, skipped, total). | | `account.status_changed` | The account's connection state changed: connected, disconnected (session expired) or proxy_error. | | `quota.threshold_reached` | A daily quota of the account passed 80% of its limit. | | `quota.reached` | A daily quota of the account is spent; further calls of that kind are refused until 00:00 UTC. | | `*` | Everything above. | ## Managing endpoints - App: **Webhooks** page (create, secret shown once, test ping, deliveries, replay, pause, rotate). - REST: `GET/POST https://app.reachmcp.com/api/webhooks/endpoints`, `PATCH/DELETE …/endpoints/{id}`, `GET …/endpoints/{id}/secret`, `POST …/endpoints/{id}/rotate-secret`, `POST …/endpoints/{id}/test`, `GET …/endpoints/{id}/deliveries`, `POST https://app.reachmcp.com/api/webhooks/deliveries/{id}/replay`, `GET https://app.reachmcp.com/api/webhooks/events`. - MCP tools: `list_webhook_endpoints`, `create_webhook_endpoint`, `update_webhook_endpoint`, `delete_webhook_endpoint`, `test_webhook_endpoint`. - `account_ids` limits an endpoint to some accounts; omitted, it covers every account of the user. URLs must be https (http accepted for localhost only). Up to 20 endpoints per user. ## Delivery ```json { "id": "evt_123", "type": "message.received", "created_at": "2026-09-10T12:41:00+00:00", "account_id": 12, "data": { ... }, "delivery": { "id": "dlv_456", "attempt": 1 } } ``` Headers: `Reach-Signature`, `Reach-Event-Id`, `Reach-Event-Type`, `Reach-Delivery-Attempt`, `User-Agent: ReachMCP-Webhooks/1.0`. **Signature**: `Reach-Signature: t=,v1=` where `v1 = HMAC-SHA256(secret, ".")`. Reject if `|now − t| > 300 s`. ```python import hmac, hashlib, time def verify(secret, header, body: bytes, tolerance=300): parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts["t"]) if abs(time.time() - t) > tolerance: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"]) ``` **Retries**: anything but a 2xx within 10 s is retried after 1 min, 5 min, 30 min, 2 h, 12 h; then the delivery is `dead` and can be replayed. An endpoint failing 50 times in a row is disabled with a reason; re-enable it once fixed. **Latency**: `account.status_changed` and the quota events are queued immediately and sent by a dispatcher that runs every minute. `message.received` and `connection.new` come from a poller that runs every 5 minutes and reads, for each account with a subscriber, the inbox and/or the connection list (one LinkedIn request per list). Expect up to about six minutes. The first poll for an account only sets its cursor and reports nothing. ## Payloads - `message.received` → `conversation_id`, `recipient_linkedin_id`, `participant {firstname, lastname, headline, linkedin_id}`, `last_message_text`, `unread_count`, `last_activity_at`, `is_groupchat`. - `connection.new` → `connection {linkedin_id, linkedin_public_id, profile_url, firstname, lastname, headline, picture}`, `connected_at`. - `account.status_changed` → `from`, `to` (`connected`, `disconnected`, `error`). - `quota.threshold_reached` / `quota.reached` → `action`, `used`, `limit` (and `threshold` = 0.8). --- # Reach MCP — jobs Every agent framework has a maximum runtime, and LinkedIn wants forty messages spread over a day, not fired in a minute. A **job** removes the choice between "send fast and risk the account" and "stay alive for hours calling one tool at a time": the agent deposits the batch in one call, gets a `job_id` back, and Reach does the rest. What Reach does with a job: - Spreads the items across the account's **activity window**: hours, days and timezone set per account (app → account → Quotas → Activity window, or `update_account_quotas` / `PATCH /api/linkedin-accounts/{id}/quotas` with `window_start`, `window_end`, `window_days`, `timezone`; default 09:00–18:00, Monday to Friday, timezone of the proxy country), overridable per job, with human-like, jittered gaps (never two items under 45 s apart). - Caps every day at the **remaining daily quota** of the action, counting what other active jobs already hold; the overflow goes to the next working day. A batch that does not fit within 30 days is refused (`job_exceeds_capacity`) rather than silently truncated. - Runs each item through the **same gate as a direct tool call** (billing, then quota, consumed one unit per item). A job can never do what a direct call could not. - **Pauses by itself** when the quota is reached (`pause_reason: quota_reached`, `resume_at` set), when the account disconnects (`account_disconnected`, resumes when it reconnects) or when billing blocks writes; **resumes by itself** and re-plans the pending items. A manual pause (`pause_job`) is never auto-resumed. - Retries a transient LinkedIn failure once, ten minutes later; a second failure marks the item `failed` and the job goes on. - Reports every step through **webhooks** (below), so the agent does not poll. ## Actions (4) | Action | Quota family | Required item fields | Optional | Does | |---|---|---|---|---| | `send_message` | messages | — | text, conversation_linkedin_id, recipient_linkedin_id, in_mail_premium | Send a LinkedIn message: text plus conversation_linkedin_id (reply) or recipient_linkedin_id (new). | | `send_invitation` | invitations | linkedin_id_or_url | message | Send a connection request, with an optional note under 200 characters. | | `visit_profile` | visits | linkedin_id_or_url | — | Visit a profile so the member gets a 'viewed your profile' notification. | | `comment_post` | comments | post_url_or_urn, text | — | Comment on a post. | Limits: 500 items per job. Item fields are the same as the direct tool's arguments (`send_message`, `send_invitation`, `visit_profile`, `comment_post`; `connect` is accepted as an alias). ## Where - MCP tools: `create_job`, `get_job`, `list_jobs`, `cancel_job`, `pause_job`, `resume_job` (`create_job` accepts `idempotency_key`). - REST: `POST https://app.reachmcp.com/api/jobs` (accepts `Idempotency-Key`), `GET https://app.reachmcp.com/api/jobs`, `GET https://app.reachmcp.com/api/jobs/{id}`, `POST …/jobs/{id}/cancel|pause|resume`, `GET https://app.reachmcp.com/api/jobs/actions`. - App: **Jobs** page (progress, items, pause, resume, cancel); the account's default window is under its **Quotas** tab. ## Example (MCP) ```json {"name": "create_job", "arguments": { "account_id": 12, "action": "send_message", "label": "Follow-ups week 40", "items": [ {"conversation_linkedin_id": "2-MTU3…", "text": "Hi Anna, following up on the pricing question…"}, {"recipient_linkedin_id": "ACoAAB…", "text": "Hi Marc, thanks for connecting…"} ], "window_start": "09:30", "window_end": "17:30", "days": ["mon", "tue", "wed", "thu"], "timezone": "Europe/Paris" }} ``` The answer carries `status: scheduled`, `next_item_at`, `totals` and every item with its planned time. The agent's work is done; it can end its run. ## Job object `id`, `account_id`, `action`, `label`, `status` (`scheduled` → `running` → `completed`; `paused`; `cancelled`), `pause_reason` (`quota_reached`, `account_disconnected`, `billing`, `manual`), `resume_at`, `window` (`start`, `end`, `days` 0 = Monday, `timezone`), `totals` (`total`, `done`, `failed`, `skipped`, `pending`), `next_item_at`, `source` (`mcp`, `rest`, `playbook`), `created_at`, `started_at`, `completed_at`, and on `get_job`/create `items[]` (`position`, `status` `pending|sending|done|failed|skipped|cancelled`, `scheduled_at`, `executed_at`, `attempt`, `payload`, `result`, `error`). ## Webhook events | Event | When | |---|---| | `job.started` | A job executed its first item (data: job_id, action, label, total). | | `job.progress` | A job item was executed: done or failed, with its result or error and the running totals. | | `job.paused` | A job stopped by itself: the daily quota of its action was reached (resume_at tells when it resumes), the account disconnected (resumes when it is back), or billing blocks writes. | | `job.completed` | Every item of a job has been executed (data: done, failed, skipped, total). | Payloads always carry `job_id`, `action`, `label` and the running totals; `job.progress` adds the item (`item_id`, `position`, `status`, `result` or `error`); `job.paused` adds `reason` and `resume_at`. Subscribe an endpoint to `job.*` events on the Webhooks page or with `create_webhook_endpoint` — see [Webhooks](/docs/webhooks.md). ## Playbooks The playbooks that write (`wake_cold_conversations`, `reactions_to_leads`, `viewers_to_leads`, `prospect_list`, `post_signals_to_leads`, `hiring_signals_to_leads`) tell the model to queue approved rows as one job when there are more than ten, so a "send these forty follow-ups" ends in one `create_job` call, not forty tool calls in a loop. --- # Reach MCP — daily quotas Every LinkedIn account has a daily limit per family of action, enforced by the server before the write, reset at 00:00 UTC. Exceeding one returns `429 daily_quota_reached` with `used`, `limit`, `requested` and `retry_at`; other families and other accounts keep working. A limit of 0 means unlimited. The owner can change limits in the app or with `update_account_quotas`. | Family | Default per day | Charged by | |---|---|---| | `invitations` | 20 | `send_invitation` | | `messages` | 150 | `send_message`, `salesnav_send_message` | | `visits` | 60 | `visit_profile` | | `imports_standard` | 30 | `scrape_profile`, `scrape_search` on standard LinkedIn URLs, by rows returned | | `imports_salesnav` | 2500 | same, on Sales Navigator URLs | | `imports_recruiter` | 2500 | same, on Recruiter URLs | | `posts` | 10 | `create_post` | | `comments` | 50 | `comment_post`, `reply_comment` | | `reactions` | 100 | `like_post`, `react_message` | | `job_searches` | 50 | `search_job_postings`, per page of up to 25 postings | Reads (listing conversations, connections, profiles pages already imported, scheduled posts…) do not consume quota. Webhook events `quota.threshold_reached` (80 %) and `quota.reached` (100 %) are emitted when a family crosses those marks. --- # Reach MCP — pricing One plan with every feature: tools, playbooks, quotas, residential proxy, webhooks, REST and MCP. Billed monthly, no commitment, 7-day trial without card. ## Standard Every feature: MCP, API, webhooks, playbooks and multi-account. Mode: graduated — each seat is billed at the rate of its band. | Band | Per account / month | |---|---| | 1st account | $20 | | Accounts 2 – 10 | $7 | | Accounts 11+ | $5 | Monthly totals: 1 → $20, 3 → $34, 10 → $83, 20 → $133, 50 → $283, 100 → $533. More than 200 accounts or white-label: write to us. Sign up: https://app.reachmcp.com/signup --- # Reach MCP — security, data and risk - **Session, not password.** The Chrome extension reads the LinkedIn session of the account owner's own browser. No LinkedIn password is typed into Reach. Removing the extension revokes that path; deleting the account in the app removes the stored session. - **Storage.** Session cookies are stored encrypted (Fernet, key held in Google Secret Manager). Every LinkedIn request is logged per account (type, method, URL, status) for the owner's dashboard and audit. Webhook signing secrets are stored encrypted and shown to the owner on request. - **Proxy.** Each account gets a residential proxy in a location chosen at connection time, with a session id rotated periodically. Proxy errors flip the account to `error` and emit a webhook event. - **Limits.** Daily quotas per family are enforced by the server before each write (see [Quotas](/docs/quotas.md)); playbooks add a numbered preview and explicit confirmation before any write. - **Authentication.** MCP clients use OAuth 2.1 with PKCE (dynamic client registration); REST and non-OAuth MCP clients use API keys, which can be restricted to specific accounts. Team members and clients connect their own sessions through invitation links. - **Hosting and jurisdiction.** Backend on Google Cloud Run and Cloud SQL in `europe-west1` (Belgium); app and site on Cloudflare Pages. Operated by Kanbox (France). GDPR page: https://www.reachmcp.com/gdpr. - **Risk, stated plainly.** Reach MCP is not an official LinkedIn API. LinkedIn automation carries real account risk; new or fake accounts are the most exposed. Proxy, enforced limits and pacing reduce it; nothing removes it, and Reach never claims zero risk. - **Security contact.** https://app.reachmcp.com/.well-known/security.txt. --- # Reach MCP — integrations | Client | How | Guide | |---|---|---| | Claude (web, desktop) | Settings → Connectors → Add custom connector → `https://app.reachmcp.com/mcp`; sign in on the Reach page that opens (OAuth). | https://www.reachmcp.com/integrations/claude | | ChatGPT | Add `https://app.reachmcp.com/mcp` as an MCP connector; OAuth. | https://www.reachmcp.com/integrations/chatgpt | | Cursor / any MCP client | `{ "mcpServers": { "reach": { "url": "https://app.reachmcp.com/mcp" } } }`; fall back to `?api_key=mcp_…` if the client has no OAuth. | — | | n8n | MCP Client node on `https://app.reachmcp.com/mcp` with an API key, or HTTP Request nodes on the REST API; Webhook node as a receiver for Reach webhooks. | https://www.reachmcp.com/integrations/n8n | | Make | HTTP modules on the REST API; Custom Webhook as a receiver. | https://www.reachmcp.com/integrations/make | | Any language | REST with `X-API-Key`; OpenAPI at https://app.reachmcp.com/openapi.json. | https://www.reachmcp.com/integrations/rest-api | Comparison pages: https://www.reachmcp.com/lp/vs-heyreach, https://www.reachmcp.com/lp/vs-unipile. Registry listings: Smithery https://smithery.ai/servers/jean-iudk/reachmcp.