Skip to content

API

Add leads, run campaigns, work the inbox and read reports for one workspace from your own tools. The API never does more on LinkedIn than the console would: every action still waits for its sender’s limits.

Authentication

Make a key in the workspace’s Settings → API keys; it is shown once. Send it with every request:

curl https://api.reachpool.ai/v1/campaigns \
  -H "Authorization: Bearer rp_live_…"
  • A key works in its workspace only, as the person who made it, with their role there.
  • It stops working when it is revoked, or when that person leaves the workspace: then every call answers 401.
  • The workspace’s id, for workspaceId, is shown beside its keys in Settings.

Limits

600 requests a minute per key. Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining; over the limit, 429 rate_limited with Retry-After in seconds.

Answers and errors

Answers are JSON under data. Lists come a page at a time: pass the meta.nextCursor you got as ?cursor= for the next page, until it is null. Errors look like this:

{ "error": { "code": "validation_error", "message": "request failed validation",
             "issues": [{ "path": "profileUrl", "message": "Required" }] } }
StatusCodeMeans
400validation_errorThe request does not match; issues say where.
401unauthorizedNo key, or one revoked or no longer valid.
403forbiddenThis route is for the console, not for API keys.
403forbidden_roleYour role in the workspace does not allow it.
404not_foundNothing by that id in the key’s workspace.
409conflictIt clashes with how things are, e.g. a campaign running.
429rate_limitedOver 600 requests a minute: wait Retry-After seconds.

Adding leads from other tools

POST /v1/leads takes one lead, or up to 500 under leads. Only profileUrl is required; a lead already in the workspace is updated, not added twice, and an excluded one is left out.

curl -X POST https://api.reachpool.ai/v1/leads \
  -H "Authorization: Bearer rp_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "workspaceId": "…", "profileUrl": "https://www.linkedin.com/in/ada-lovelace/",
        "firstName": "Ada", "company": "Analytical Engines" }'

The answer counts what happened: created, deduped, excluded and invalid, with each row’s lead id in ids. Attach them to a campaign with POST /v1/campaigns/{id}/leads.

Zapier

  1. Add an action step: Webhooks by Zapier → POST.
  2. URL https://api.reachpool.ai/v1/leads, Payload Type Json.
  3. Data: workspaceId, profileUrl and any of firstName, lastName, company, title, email, mapped from the trigger.
  4. Headers: Authorization = Bearer rp_live_….

Make

  1. Add HTTP → Make a request: URL https://api.reachpool.ai/v1/leads, Method POST.
  2. Headers: Authorization = Bearer rp_live_….
  3. Body type Raw, Content type JSON (application/json), Request content: the JSON above, with values mapped from earlier modules.

Webhooks

Add a webhook in the workspace’s Settings → Webhooks (or with POST /v1/webhooks): an https URL and the events it takes. Each event is POSTed as JSON:

{ "id": "4f0c…",            // the event's id: the same on every retry
  "type": "lead.replied",
  "createdAt": "2026-11-02T09:14:03.120Z",
  "workspaceId": "…",
  "data": { "senderId": "…", "leadId": "…", "campaignId": "…", … } }
  • data holds ids and states, never the words of a message; fetch the rest with the API, e.g. GET /v1/conversations/{id}.
  • Answer with any 2xx within 10 seconds. Anything else, a redirect or no answer is tried again after 1 minute, 10 minutes, 1 hour, 6 hours and 24 hours, then marked failed: it can be sent again from the webhook’s page.
  • An event can arrive more than once (when your answer was lost): skip an id you have seen.
  • Send test event on the webhook’s page sends webhook.test.

Checking the signature

Every request carries X-Signature: sha256=…, the HMAC-SHA256 of the raw body with the webhook’s secret (shown once, when it was made). Compare it before trusting the body:

import { createHmac, timingSafeEqual } from 'node:crypto';

function signed(rawBody, header, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  return header?.length === expected.length &&
    timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

Events

TypeWhendata
invite.sentAn invitation went out from a sender.workspaceId, senderId, leadId, campaignId, campaignLeadId, actionId
invite.acceptedA lead accepted a sender’s invitation.workspaceId, senderId, leadId, campaignId, campaignLeadId
message.sentA message or InMail went out: a campaign step, or a reply from the inbox.workspaceId, senderId, leadId, campaignId?, campaignLeadId?, conversationId?, actionId, channel
message.receivedA lead wrote to a sender.workspaceId, senderId, leadId, conversationId, messageId
lead.repliedA lead replied to a campaign: its sequence stops.workspaceId, senderId, leadId, campaignId, campaignLeadId, conversationId
lead.completedA lead reached the end of a campaign’s sequence.workspaceId, senderId?, leadId, campaignId, campaignLeadId
sender.warningLinkedIn showed a sender a check: the sender is paused.senderId, reason
sender.restrictedLinkedIn restricted a sender.senderId, reason
conversation.labeledA conversation’s AI label changed.workspaceId, senderId, leadId, conversationId, label

Zapier: Catch Hook

  1. Start a Zap with the trigger Webhooks by Zapier → Catch Hook and copy its URL. Keep it private: anyone with it can post to your Zap.
  2. In Settings → Webhooks, add that URL with the events you want.
  3. On the webhook’s page, Send test event; in Zapier, Test trigger finds it, and later steps can use its fields.

Make: Custom webhook

  1. Start a scenario with Webhooks → Custom webhook, add a webhook and copy its address.
  2. In Settings → Webhooks, add that address with the events you want.
  3. Click Redetermine data structure in Make, then Send test event on the webhook’s page: Make learns the fields from it.

Reference

Every route an API key may call, from the api’s OpenAPI 3.1 document at https://api.reachpool.ai/v1/openapi.json, which tools such as Postman can import. * marks what is required.

campaigns

GET/v1/campaigns→ 200

ParameterInType
workspaceIdqueryuuid
statusquery"DRAFT" | "RUNNING" | "PAUSED" | "COMPLETED" | "ARCHIVED"
cursorquerystring ≤ 200
limitqueryinteger 1–200

POST/v1/campaigns→ 201

JSON body

workspaceId *uuid
name *string ≤ 200
senderPoolobject[]
settingsobject

GET/v1/campaigns/{id}→ 200

ParameterInType
id *pathuuid

PATCH/v1/campaigns/{id}→ 200

ParameterInType
id *pathuuid

JSON body

namestring ≤ 200
senderPoolobject[]
settingsobject

DELETE/v1/campaigns/{id}→ 200

ParameterInType
id *pathuuid

POST/v1/campaigns/{id}/find-emails→ 202

ParameterInType
id *pathuuid

GET/v1/campaigns/{id}/find-emails→ 200

ParameterInType
id *pathuuid

GET/v1/campaigns/{id}/first-lines→ 200

ParameterInType
statusquery"PENDING" | "REVIEW" | "READY" | "REJECTED" | "SKIPPED" | "FAILED"
cursorquerystring ≤ 200
limitqueryinteger 1–200
id *pathuuid

POST/v1/campaigns/{id}/first-lines/{leadId}/approve→ 200

ParameterInType
leadId *pathuuid
id *pathuuid

JSON body

textstring ≤ 1000

POST/v1/campaigns/{id}/first-lines/{leadId}/reject→ 200

ParameterInType
leadId *pathuuid
id *pathuuid

POST/v1/campaigns/{id}/leads→ 200

ParameterInType
id *pathuuid

JSON body, form 1 of 2

leadIds *uuid[]

JSON body, form 2 of 2

importId *uuid

GET/v1/campaigns/{id}/leads→ 200

ParameterInType
statusquery"QUEUED" | "IN_PROGRESS" | "PAUSED" | "REPLIED" | "COMPLETED" | "FAILED" | "EXCLUDED"
qquerystring ≤ 100
cursorquerystring ≤ 200
limitqueryinteger 1–200
id *pathuuid

POST/v1/campaigns/{id}/leads/{leadId}/pause→ 200

ParameterInType
leadId *pathuuid
id *pathuuid

POST/v1/campaigns/{id}/leads/{leadId}/remove→ 200

ParameterInType
leadId *pathuuid
id *pathuuid

POST/v1/campaigns/{id}/leads/{leadId}/resume→ 200

ParameterInType
leadId *pathuuid
id *pathuuid

POST/v1/campaigns/{id}/pause→ 200

ParameterInType
id *pathuuid

POST/v1/campaigns/{id}/start→ 200

ParameterInType
id *pathuuid

GET/v1/campaigns/{id}/stats→ 200

ParameterInType
id *pathuuid

PUT/v1/campaigns/{id}/steps→ 200

ParameterInType
id *pathuuid

JSON body

steps *__schema0[]

POST/v1/campaigns/{id}/stop→ 200

ParameterInType
id *pathuuid

GET/v1/campaigns/{id}/timeline→ 200

ParameterInType
qquerystring ≤ 100
cursorquerystring ≤ 200
limitqueryinteger 1–100
id *pathuuid

conversations

GET/v1/conversations→ 200

ParameterInType
workspaceIdqueryuuid
labelquery"INTERESTED" | "NOT_NOW" | "NOT_INTERESTED" | "OOO" | "WRONG_PERSON" | "QUESTION" | "UNLABELED"
assigneequeryuuid | "me" | "none"
unreadquery"true" | "false"
senderIdqueryuuid
campaignIdqueryuuid
tagquerystring ≤ 40
snoozedquery"true" | "false"
includeSnoozedquery"true" | "false"
closedquery"true" | "false"
qquerystring ≤ 200
cursorquerystring ≤ 200
limitqueryinteger 1–100

GET/v1/conversations/{id}→ 200

ParameterInType
id *pathuuid

PATCH/v1/conversations/{id}→ 200

ParameterInType
id *pathuuid

JSON body

assigneeUserIduuid | null
labelsstring ≤ 40[]
snoozedUntildate-time | null
aiLabel"INTERESTED" | "NOT_NOW" | "NOT_INTERESTED" | "OOO" | "WRONG_PERSON" | "QUESTION" | "UNLABELED" | null
unreadboolean
closedboolean

POST/v1/conversations/{id}/meeting→ 200

ParameterInType
id *pathuuid

JSON body

booked *boolean

POST/v1/conversations/{id}/messages→ 202

ParameterInType
id *pathuuid

JSON body

body *string ≤ 8000
requestId *uuid

credits

GET/v1/credits→ 200

exclusions

GET/v1/exclusions→ 200

ParameterInType
workspaceIdqueryuuid
qquerystring ≤ 200
cursorquerystring ≤ 200
limitqueryinteger 1–200

POST/v1/exclusions→ 201

JSON body

workspaceId *uuid | null
entriesstring ≤ 500[]
csvstring ≤ 15000000
columnstring ≤ 200
reasonstring ≤ 200

DELETE/v1/exclusions/{id}→ 200

ParameterInType
id *pathuuid

imports

GET/v1/imports→ 200

ParameterInType
campaignId *queryuuid

GET/v1/imports/{id}→ 200

ParameterInType
id *pathuuid

POST/v1/imports/{id}/discard→ 201

ParameterInType
id *pathuuid

leads

GET/v1/leads→ 200

ParameterInType
workspaceIdqueryuuid
qquerystring ≤ 200
cursorquerystring ≤ 200
limitqueryinteger 1–200

POST/v1/leads→ 201

JSON body, form 1 of 2

workspaceId *uuid
leads *object[]

JSON body, form 2 of 2

firstNamestring ≤ 100
lastNamestring ≤ 100
companystring ≤ 200
titlestring ≤ 300
locationstring ≤ 200
emailemail
companyDomainstring ≤ 300
customobject
profileUrl *string ≤ 500
workspaceId *uuid

GET/v1/leads/{id}→ 200

ParameterInType
id *pathuuid

PATCH/v1/leads/{id}→ 200

ParameterInType
id *pathuuid

JSON body

firstNamestring ≤ 100 | null
lastNamestring ≤ 100 | null
companystring ≤ 200 | null
titlestring ≤ 300 | null
locationstring ≤ 200 | null
emailemail | null
companyDomainstring ≤ 300 | null
customobject

POST/v1/leads/{id}/find-email→ 200

ParameterInType
id *pathuuid

POST/v1/leads/import→ 202

JSON body, form 1 of 6

workspaceId *uuid
campaignIduuid
source *string
payload *object

JSON body, form 2 of 6

workspaceId *uuid
campaignId *uuid
source *string
payload *object

JSON body, form 3 of 6

workspaceId *uuid
campaignIduuid
source *string
payload *object

JSON body, form 4 of 6

workspaceId *uuid
campaignIduuid
source *string
payload *object

JSON body, form 5 of 6

workspaceId *uuid
campaignIduuid
source *string
payload *object

JSON body, form 6 of 6

workspaceId *uuid
campaignIduuid
source *string
payload *object

POST/v1/leads/import/preview→ 200

JSON body

csv *string ≤ 15000000

reports

GET/v1/reports/campaign/{id}→ 200

ParameterInType
fromquerydate
toquerydate
daysqueryinteger 1–366
id *pathuuid

GET/v1/reports/export.csv→ 200

ParameterInType
workspaceId *queryuuid
fromquerydate
toquerydate
daysqueryinteger 1–366
byquery"day" | "sender"

GET/v1/reports/senders→ 200

ParameterInType
workspaceId *queryuuid
fromquerydate
toquerydate
daysqueryinteger 1–366

GET/v1/reports/workspace/{id}→ 200

ParameterInType
fromquerydate
toquerydate
daysqueryinteger 1–366
id *pathuuid

saved replies

GET/v1/saved-replies→ 200

ParameterInType
workspaceId *queryuuid

POST/v1/saved-replies→ 201

JSON body

workspaceId *uuid
name *string ≤ 80
body *string ≤ 8000

PATCH/v1/saved-replies/{id}→ 200

ParameterInType
id *pathuuid

JSON body

namestring ≤ 80
bodystring ≤ 8000

DELETE/v1/saved-replies/{id}→ 200

ParameterInType
id *pathuuid

senders

GET/v1/senders→ 200

ParameterInType
workspaceIdqueryuuid

GET/v1/senders/{id}/events→ 200

ParameterInType
cursorquerystring ≤ 200
limitqueryinteger 1–200
id *pathuuid

GET/v1/senders/{id}/health→ 200

ParameterInType
id *pathuuid

webhooks

GET/v1/webhooks→ 200

ParameterInType
workspaceId *queryuuid

POST/v1/webhooks→ 201

JSON body

workspaceId *uuid
url *uri
events *"invite.sent" | "invite.accepted" | "message.sent" | "message.received" | "lead.replied" | "lead.completed" | "sender.warning" | "sender.restricted" | "conversation.labeled"[]

PATCH/v1/webhooks/{id}→ 200

ParameterInType
id *pathuuid

JSON body

urluri
events"invite.sent" | "invite.accepted" | "message.sent" | "message.received" | "lead.replied" | "lead.completed" | "sender.warning" | "sender.restricted" | "conversation.labeled"[]
activeboolean

DELETE/v1/webhooks/{id}→ 200

ParameterInType
id *pathuuid

GET/v1/webhooks/{id}/deliveries→ 200

ParameterInType
cursorquerystring ≤ 200
limitqueryinteger 1–100
id *pathuuid

POST/v1/webhooks/{id}/deliveries/{deliveryId}/retry→ 202

ParameterInType
deliveryId *pathuuid
id *pathuuid

POST/v1/webhooks/{id}/test→ 202

ParameterInType
id *pathuuid