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" }] } }| Status | Code | Means |
|---|---|---|
| 400 | validation_error | The request does not match; issues say where. |
| 401 | unauthorized | No key, or one revoked or no longer valid. |
| 403 | forbidden | This route is for the console, not for API keys. |
| 403 | forbidden_role | Your role in the workspace does not allow it. |
| 404 | not_found | Nothing by that id in the key’s workspace. |
| 409 | conflict | It clashes with how things are, e.g. a campaign running. |
| 429 | rate_limited | Over 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
- Add an action step: Webhooks by Zapier → POST.
- URL
https://api.reachpool.ai/v1/leads, Payload Type Json. - Data:
workspaceId,profileUrland any offirstName,lastName,company,title,email, mapped from the trigger. - Headers:
Authorization=Bearer rp_live_….
Make
- Add HTTP → Make a request: URL
https://api.reachpool.ai/v1/leads, Method POST. - Headers:
Authorization=Bearer rp_live_…. - 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": "…", … } }dataholds 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
idyou 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
| Type | When | data |
|---|---|---|
| invite.sent | An invitation went out from a sender. | workspaceId, senderId, leadId, campaignId, campaignLeadId, actionId |
| invite.accepted | A lead accepted a sender’s invitation. | workspaceId, senderId, leadId, campaignId, campaignLeadId |
| message.sent | A message or InMail went out: a campaign step, or a reply from the inbox. | workspaceId, senderId, leadId, campaignId?, campaignLeadId?, conversationId?, actionId, channel |
| message.received | A lead wrote to a sender. | workspaceId, senderId, leadId, conversationId, messageId |
| lead.replied | A lead replied to a campaign: its sequence stops. | workspaceId, senderId, leadId, campaignId, campaignLeadId, conversationId |
| lead.completed | A lead reached the end of a campaign’s sequence. | workspaceId, senderId?, leadId, campaignId, campaignLeadId |
| sender.warning | LinkedIn showed a sender a check: the sender is paused. | senderId, reason |
| sender.restricted | LinkedIn restricted a sender. | senderId, reason |
| conversation.labeled | A conversation’s AI label changed. | workspaceId, senderId, leadId, conversationId, label |
Zapier: Catch Hook
- 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.
- In Settings → Webhooks, add that URL with the events you want.
- On the webhook’s page, Send test event; in Zapier, Test trigger finds it, and later steps can use its fields.
Make: Custom webhook
- Start a scenario with Webhooks → Custom webhook, add a webhook and copy its address.
- In Settings → Webhooks, add that address with the events you want.
- 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
| Parameter | In | Type |
|---|---|---|
| workspaceId | query | uuid |
| status | query | "DRAFT" | "RUNNING" | "PAUSED" | "COMPLETED" | "ARCHIVED" |
| cursor | query | string ≤ 200 |
| limit | query | integer 1–200 |
POST/v1/campaigns→ 201
JSON body
| workspaceId * | uuid |
| name * | string ≤ 200 |
| senderPool | object[] |
| settings | object |
GET/v1/campaigns/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
PATCH/v1/campaigns/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| name | string ≤ 200 |
| senderPool | object[] |
| settings | object |
DELETE/v1/campaigns/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
POST/v1/campaigns/{id}/find-emails→ 202
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
GET/v1/campaigns/{id}/find-emails→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
GET/v1/campaigns/{id}/first-lines→ 200
| Parameter | In | Type |
|---|---|---|
| status | query | "PENDING" | "REVIEW" | "READY" | "REJECTED" | "SKIPPED" | "FAILED" |
| cursor | query | string ≤ 200 |
| limit | query | integer 1–200 |
| id * | path | uuid |
POST/v1/campaigns/{id}/first-lines/{leadId}/approve→ 200
| Parameter | In | Type |
|---|---|---|
| leadId * | path | uuid |
| id * | path | uuid |
JSON body
| text | string ≤ 1000 |
POST/v1/campaigns/{id}/first-lines/{leadId}/reject→ 200
| Parameter | In | Type |
|---|---|---|
| leadId * | path | uuid |
| id * | path | uuid |
POST/v1/campaigns/{id}/leads→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body, form 1 of 2
| leadIds * | uuid[] |
JSON body, form 2 of 2
| importId * | uuid |
GET/v1/campaigns/{id}/leads→ 200
| Parameter | In | Type |
|---|---|---|
| status | query | "QUEUED" | "IN_PROGRESS" | "PAUSED" | "REPLIED" | "COMPLETED" | "FAILED" | "EXCLUDED" |
| q | query | string ≤ 100 |
| cursor | query | string ≤ 200 |
| limit | query | integer 1–200 |
| id * | path | uuid |
POST/v1/campaigns/{id}/leads/{leadId}/pause→ 200
| Parameter | In | Type |
|---|---|---|
| leadId * | path | uuid |
| id * | path | uuid |
POST/v1/campaigns/{id}/leads/{leadId}/remove→ 200
| Parameter | In | Type |
|---|---|---|
| leadId * | path | uuid |
| id * | path | uuid |
POST/v1/campaigns/{id}/leads/{leadId}/resume→ 200
| Parameter | In | Type |
|---|---|---|
| leadId * | path | uuid |
| id * | path | uuid |
POST/v1/campaigns/{id}/pause→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
POST/v1/campaigns/{id}/start→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
GET/v1/campaigns/{id}/stats→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
PUT/v1/campaigns/{id}/steps→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| steps * | __schema0[] |
POST/v1/campaigns/{id}/stop→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
GET/v1/campaigns/{id}/timeline→ 200
| Parameter | In | Type |
|---|---|---|
| q | query | string ≤ 100 |
| cursor | query | string ≤ 200 |
| limit | query | integer 1–100 |
| id * | path | uuid |
conversations
GET/v1/conversations→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId | query | uuid |
| label | query | "INTERESTED" | "NOT_NOW" | "NOT_INTERESTED" | "OOO" | "WRONG_PERSON" | "QUESTION" | "UNLABELED" |
| assignee | query | uuid | "me" | "none" |
| unread | query | "true" | "false" |
| senderId | query | uuid |
| campaignId | query | uuid |
| tag | query | string ≤ 40 |
| snoozed | query | "true" | "false" |
| includeSnoozed | query | "true" | "false" |
| closed | query | "true" | "false" |
| q | query | string ≤ 200 |
| cursor | query | string ≤ 200 |
| limit | query | integer 1–100 |
GET/v1/conversations/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
PATCH/v1/conversations/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| assigneeUserId | uuid | null |
| labels | string ≤ 40[] |
| snoozedUntil | date-time | null |
| aiLabel | "INTERESTED" | "NOT_NOW" | "NOT_INTERESTED" | "OOO" | "WRONG_PERSON" | "QUESTION" | "UNLABELED" | null |
| unread | boolean |
| closed | boolean |
POST/v1/conversations/{id}/meeting→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| booked * | boolean |
POST/v1/conversations/{id}/messages→ 202
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| body * | string ≤ 8000 |
| requestId * | uuid |
credits
GET/v1/credits→ 200
exclusions
GET/v1/exclusions→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId | query | uuid |
| q | query | string ≤ 200 |
| cursor | query | string ≤ 200 |
| limit | query | integer 1–200 |
POST/v1/exclusions→ 201
JSON body
| workspaceId * | uuid | null |
| entries | string ≤ 500[] |
| csv | string ≤ 15000000 |
| column | string ≤ 200 |
| reason | string ≤ 200 |
DELETE/v1/exclusions/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
imports
GET/v1/imports→ 200
| Parameter | In | Type |
|---|---|---|
| campaignId * | query | uuid |
GET/v1/imports/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
POST/v1/imports/{id}/discard→ 201
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
leads
GET/v1/leads→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId | query | uuid |
| q | query | string ≤ 200 |
| cursor | query | string ≤ 200 |
| limit | query | integer 1–200 |
POST/v1/leads→ 201
JSON body, form 1 of 2
| workspaceId * | uuid |
| leads * | object[] |
JSON body, form 2 of 2
| firstName | string ≤ 100 |
| lastName | string ≤ 100 |
| company | string ≤ 200 |
| title | string ≤ 300 |
| location | string ≤ 200 |
| companyDomain | string ≤ 300 |
| custom | object |
| profileUrl * | string ≤ 500 |
| workspaceId * | uuid |
GET/v1/leads/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
PATCH/v1/leads/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| firstName | string ≤ 100 | null |
| lastName | string ≤ 100 | null |
| company | string ≤ 200 | null |
| title | string ≤ 300 | null |
| location | string ≤ 200 | null |
| email | null | |
| companyDomain | string ≤ 300 | null |
| custom | object |
POST/v1/leads/{id}/find-email→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
POST/v1/leads/import→ 202
JSON body, form 1 of 6
| workspaceId * | uuid |
| campaignId | uuid |
| 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 |
| campaignId | uuid |
| source * | string |
| payload * | object |
JSON body, form 4 of 6
| workspaceId * | uuid |
| campaignId | uuid |
| source * | string |
| payload * | object |
JSON body, form 5 of 6
| workspaceId * | uuid |
| campaignId | uuid |
| source * | string |
| payload * | object |
JSON body, form 6 of 6
| workspaceId * | uuid |
| campaignId | uuid |
| source * | string |
| payload * | object |
POST/v1/leads/import/preview→ 200
JSON body
| csv * | string ≤ 15000000 |
reports
GET/v1/reports/campaign/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| from | query | date |
| to | query | date |
| days | query | integer 1–366 |
| id * | path | uuid |
GET/v1/reports/export.csv→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId * | query | uuid |
| from | query | date |
| to | query | date |
| days | query | integer 1–366 |
| by | query | "day" | "sender" |
GET/v1/reports/senders→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId * | query | uuid |
| from | query | date |
| to | query | date |
| days | query | integer 1–366 |
GET/v1/reports/workspace/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| from | query | date |
| to | query | date |
| days | query | integer 1–366 |
| id * | path | uuid |
saved replies
GET/v1/saved-replies→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId * | query | uuid |
POST/v1/saved-replies→ 201
JSON body
| workspaceId * | uuid |
| name * | string ≤ 80 |
| body * | string ≤ 8000 |
PATCH/v1/saved-replies/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| name | string ≤ 80 |
| body | string ≤ 8000 |
DELETE/v1/saved-replies/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
senders
GET/v1/senders→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId | query | uuid |
GET/v1/senders/{id}/events→ 200
| Parameter | In | Type |
|---|---|---|
| cursor | query | string ≤ 200 |
| limit | query | integer 1–200 |
| id * | path | uuid |
GET/v1/senders/{id}/health→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
webhooks
GET/v1/webhooks→ 200
| Parameter | In | Type |
|---|---|---|
| workspaceId * | query | uuid |
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
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
JSON body
| url | uri |
| events | "invite.sent" | "invite.accepted" | "message.sent" | "message.received" | "lead.replied" | "lead.completed" | "sender.warning" | "sender.restricted" | "conversation.labeled"[] |
| active | boolean |
DELETE/v1/webhooks/{id}→ 200
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |
GET/v1/webhooks/{id}/deliveries→ 200
| Parameter | In | Type |
|---|---|---|
| cursor | query | string ≤ 200 |
| limit | query | integer 1–100 |
| id * | path | uuid |
POST/v1/webhooks/{id}/deliveries/{deliveryId}/retry→ 202
| Parameter | In | Type |
|---|---|---|
| deliveryId * | path | uuid |
| id * | path | uuid |
POST/v1/webhooks/{id}/test→ 202
| Parameter | In | Type |
|---|---|---|
| id * | path | uuid |