# MCP Server
Source: https://docs.sendpilot.ai/ai/mcp
Connect your AI assistant to the SendPilot docs — Claude, Codex, Gemini, Cursor, or your own code
SendPilot hosts a **Model Context Protocol (MCP)** server that connects your AI assistant — Claude, OpenAI Codex, Gemini, Cursor, VS Code Copilot, or your own application — directly to the SendPilot documentation. Your assistant can look up endpoints, schemas, and webhook payloads in real time, so the integration code it writes against our API stays accurate.
**Beta.** This MCP server is in beta and currently provides **documentation search and reading** only. A full version that exposes the SendPilot API as **callable tools** — create campaigns, add leads, send messages, and more, all directly from your assistant — is coming soon. For now, your assistant reads the docs here and makes API calls itself using your key (see [Making API calls](#making-api-calls)).
## What is the MCP server?
The Model Context Protocol is an open standard that lets AI assistants connect to external tools and knowledge. SendPilot hosts its MCP server at:
```
https://docs.sendpilot.ai/mcp
```
It uses **streamable HTTP** transport and exposes two documentation tools:
| Tool | What it does |
| -------------------------------------- | ---------------------------------------------------------------------------------- |
| `search_send_pilot_api` | Searches the docs and returns relevant snippets with titles and direct links |
| `query_docs_filesystem_send_pilot_api` | Reads documentation pages directly (`head`, `cat`, `rg`, `tree`) for exact lookups |
The MCP server is **read-only**: it searches and reads your documentation. It does **not** call the SendPilot API. Connecting requires **no API key** — see [Making API calls](#making-api-calls) for how your assistant actually runs requests.
## Connect the MCP server
Pick your client below. Every client points at the same URL: `https://docs.sendpilot.ai/mcp`. No API key or header is needed to connect.
Run the following command in your terminal:
```bash theme={null}
claude mcp add --transport http sendpilot https://docs.sendpilot.ai/mcp
```
Verify with `claude mcp list`, then start a session and ask Claude to search the SendPilot docs.
Open **Settings → Developer → Edit Config** and add the server:
```json theme={null}
{
"mcpServers": {
"sendpilot": {
"url": "https://docs.sendpilot.ai/mcp"
}
}
}
```
Restart Claude Desktop. The SendPilot tools appear in the tools menu.
On **claude.ai**, **Claude Cowork**, and the mobile apps, the SendPilot server is added as a **custom connector** through the UI rather than a config file:
1. Open **Settings → Connectors** (on claude.ai: **Customize → Connectors**).
2. Click **Add custom connector**.
3. Name it `SendPilot` and enter the URL `https://docs.sendpilot.ai/mcp`.
4. Save. The SendPilot tools become available in your chats.
Custom connectors connect from Anthropic's cloud, so the server must be reachable over public HTTPS — SendPilot's MCP server is. Available on Free, Pro, Max, Team, and Enterprise plans (free users get one custom connector).
Edit `~/.codex/config.toml` and add the server:
```toml theme={null}
[mcp_servers.sendpilot]
url = "https://docs.sendpilot.ai/mcp"
```
Confirm it's registered with `codex mcp list`.
Add the server to `~/.gemini/settings.json` (or a project-level `.gemini/settings.json`). Note Gemini uses the `httpUrl` key for streamable HTTP servers:
```json theme={null}
{
"mcpServers": {
"sendpilot": {
"httpUrl": "https://docs.sendpilot.ai/mcp"
}
}
}
```
Restart the Gemini CLI and run `/mcp` to confirm SendPilot is connected.
Add the server to `~/.cursor/mcp.json` (or the project-level `.cursor/mcp.json`):
```json theme={null}
{
"mcpServers": {
"sendpilot": {
"url": "https://docs.sendpilot.ai/mcp"
}
}
}
```
Reload Cursor and confirm **SendPilot** shows up under **Settings → MCP**.
Create `.vscode/mcp.json` in your workspace:
```json theme={null}
{
"servers": {
"sendpilot": {
"type": "http",
"url": "https://docs.sendpilot.ai/mcp"
}
}
}
```
Open Copilot Chat in **Agent** mode and the SendPilot tools become available.
Any MCP-compatible client works. Provide these values wherever the client asks for server details:
| Setting | Value |
| -------------- | ------------------------------- |
| Transport | Streamable HTTP |
| URL | `https://docs.sendpilot.ai/mcp` |
| Authentication | None |
Most JSON-based clients use either a `url` (Claude, Cursor, VS Code) or `httpUrl` (Gemini) field under an `mcpServers` object.
### Verify the connection
Ask your assistant a question that requires the docs, for example:
> *"Using the SendPilot MCP, what's the request body for creating a lead extractor campaign?"*
If it returns the correct endpoint (`POST /v1/lead-extractor/campaigns`) with the `name`, `urls`, `limit`, `url_type`, and `mode` fields, the server is connected.
## Making API calls
This is the part people get wrong, so it's worth being explicit:
The MCP server only **searches your docs** — it never calls the SendPilot API. Putting your API key in the MCP config does **nothing**; the server ignores it. To let your assistant actually run API requests, give your key to the **assistant's environment**, not the MCP server.
The two pieces work as separate channels:
1. **The MCP server** teaches the assistant your API — it reads the docs to learn the right endpoint, fields, and auth.
2. **Your assistant** then writes and runs the request itself (a script, `curl`, etc.), authenticating with an API key from **its own runtime**.
So the setup is:
In the SendPilot dashboard, go to **Integrations → API Keys**, click **Create API Key**, and copy the `sp_live_...` value.
Keys are shown once at creation. Store yours immediately.
Set it as an environment variable wherever your assistant runs code — for example, in the terminal where you run Claude Code or Cursor:
```bash theme={null}
export SENDPILOT_API_KEY=sp_live_...
```
Then your prompts can say *"read my API key from `SENDPILOT_API_KEY`"* and the generated code will authenticate with the `X-API-Key` header. See [Authentication](/api-reference/authentication) for details.
Only give your API key to an assistant/environment you trust — that key can take real, billable actions (sending messages, spending credits). The key, not the MCP server, is the access boundary.
## Use it in code
You don't need a chat client — you can connect to the MCP server programmatically with the official MCP SDKs to build your own agents or tools on top of the SendPilot docs. No API key is needed to connect.
```typescript TypeScript theme={null}
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-app", version: "1.0.0" });
const transport = new StreamableHTTPClientTransport(
new URL("https://docs.sendpilot.ai/mcp"),
);
await client.connect(transport);
// List available tools
const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));
// Search the SendPilot docs
const result = await client.callTool({
name: "search_send_pilot_api",
arguments: { query: "create a lead extractor campaign" },
});
console.log(result.content);
await client.close();
```
```python Python theme={null}
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
async with streamablehttp_client("https://docs.sendpilot.ai/mcp") as (
read,
write,
_,
):
async with ClientSession(read, write) as session:
await session.initialize()
# List available tools
tools = await session.list_tools()
print([t.name for t in tools.tools])
# Search the SendPilot docs
result = await session.call_tool(
"search_send_pilot_api",
{"query": "create a lead extractor campaign"},
)
print(result.content)
asyncio.run(main())
```
Install the SDKs with `npm install @modelcontextprotocol/sdk` (TypeScript) or `pip install mcp` (Python).
## Scenarios & example prompts
Once the MCP server is connected (and your assistant has your API key in its environment), talk to it in plain language. The prompts below are copy-paste ready — they tell the assistant to use the SendPilot MCP so the output matches the current API.
> *"Use the SendPilot MCP to find the endpoint for creating a lead extractor campaign, then write a Node.js script that creates one from a LinkedIn people-search URL, polls the status endpoint until it completes, and prints the extracted leads. Read my API key from `SENDPILOT_API_KEY`."*
The assistant looks up `POST /v1/lead-extractor/campaigns`, the `/status` and `/results` endpoints, and the exact field names before writing code.
> *"Using the SendPilot MCP, list the available endpoint groups and give me a one-line summary of what each one is for."*
Great first prompt to understand campaigns, leads, inbox, lead database, lead extractor, and webhooks at a glance.
> *"Search the SendPilot docs for the `reply.received` webhook payload and generate a TypeScript interface for it. Include every field shown in the example payload."*
The assistant reads the webhook event page and produces types that match the real payload shape.
> *"Use the SendPilot MCP to find all fields returned by the lead extractor results endpoint, then map them to my HubSpot contact properties (first name, last name, company, job title, LinkedIn URL)."*
Useful for building sync jobs without guessing field names.
> *"I'm getting a 401 from `GET /v1/campaigns`. Use the SendPilot MCP to look up the authentication requirements and tell me what's wrong with this request: `curl https://api.sendpilot.ai/v1/campaigns -H 'Authorization: Bearer abc'`."*
The assistant finds that SendPilot expects an `X-API-Key` header, not a bearer token, and corrects the request.
> *"Write a Python function that adds a list of leads to a SendPilot campaign. Confirm the exact request body and required fields with the SendPilot MCP before writing the code."*
The assistant verifies `POST /v1/leads` (including the required `campaignId`) so the payload is correct the first time.
The pattern that works best: ask the assistant to **"confirm the endpoint and request body with the SendPilot MCP before writing code."** This keeps generated integrations aligned with the live API instead of relying on the model's memory.
## Troubleshooting
Confirm the URL is exactly `https://docs.sendpilot.ai/mcp` and that your client supports HTTP-transport MCP servers. Remember Gemini uses `httpUrl` while Claude, Cursor, and VS Code use `url`. Restart the client after editing its config file.
The MCP server only searches docs — it doesn't make API calls. Your assistant runs those itself, so make sure your API key is available in its environment (e.g. `SENDPILOT_API_KEY`), not in the MCP config. See [Making API calls](#making-api-calls).
Check that the request sends an `X-API-Key` header with a valid `sp_live_...` key (SendPilot does not use bearer tokens). See [Authentication → Error Responses](/api-reference/authentication#error-responses).
## Next steps
API key best practices, scoping, and error handling
Browse every available endpoint
# Authentication
Source: https://docs.sendpilot.ai/api-reference/authentication
Secure your API requests with API key authentication
## API Key Authentication
All requests to the SendPilot API must be authenticated using an API key. Include your API key in the `X-API-Key` header of every request:
```bash theme={null}
curl https://api.sendpilot.ai/v1/campaigns \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"
```
## Obtaining API Keys
API keys are created through the SendPilot dashboard:
1. Log in to your SendPilot account
2. Navigate to **Integrations** → **API Keys**
3. Click **Create API Key**
4. Give your key a descriptive name
5. Copy and securely store your API key
API keys are only shown once when created. Store them securely immediately.
## API Key Security
Your API keys carry sensitive privileges. Keep them secure at all times.
### Best Practices
* **Never commit API keys** to version control (use environment variables)
* **Rotate keys regularly** for enhanced security
* **Use descriptive names** to track which key is used where
* **Revoke unused keys** through the dashboard
* **Monitor usage** for unexpected patterns
### Storing Keys Securely
Store API keys in environment variables or a secure secrets management system:
```bash theme={null}
# .env file (never commit this)
SENDPILOT_API_KEY=sp_live_abc123xyz...
```
```javascript theme={null}
// Node.js example
const apiKey = process.env.SENDPILOT_API_KEY;
const response = await fetch('https://api.sendpilot.ai/v1/campaigns', {
method: 'GET',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
}
});
```
```python theme={null}
# Python example
import os
import requests
api_key = os.environ.get('SENDPILOT_API_KEY')
response = requests.get(
'https://api.sendpilot.ai/v1/campaigns',
headers={
'X-API-Key': api_key,
'Content-Type': 'application/json'
}
)
```
## Error Responses
### 401 Unauthorized
Returned when authentication fails:
```json theme={null}
{
"statusCode": 401,
"message": "Invalid API key",
"error": "Unauthorized"
}
```
**Common causes:**
* Missing `X-API-Key` header
* Invalid API key
* Revoked API key
### 403 Forbidden
Returned when authentication succeeds but you lack permission:
```json theme={null}
{
"statusCode": 403,
"message": "API key does not have permission to access this workspace",
"error": "Forbidden"
}
```
**Common causes:**
* API key belongs to a different workspace
* Trying to access another user's resources
## Scoping
Each API key is scoped to a specific workspace. You can only access campaigns and leads within the workspace associated with your API key.
# Get Campaign
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-campaign-by-id
GET /v1/campaigns/{id}
Retrieve a single campaign by ID
## Request
Your API key
The campaign ID
## Response
Unique campaign identifier
Campaign name
Current campaign status: `started`, `paused`, `draft`, `finished`
Campaign type (e.g., `regular`)
Total number of leads in the campaign
Number of connection requests sent
Number of messages sent
Number of replies received
Array of LinkedIn sender IDs associated with this campaign
ISO 8601 timestamp of when the campaign was created
ISO 8601 timestamp of when the campaign was last updated
```bash cURL theme={null}
curl https://api.sendpilot.ai/v1/campaigns/clxxx123456 \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/campaigns/clxxx123456', {
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
});
const campaign = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.sendpilot.ai/v1/campaigns/clxxx123456',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
campaign = response.json()
```
```json 200 theme={null}
{
"id": "clxxx123456",
"name": "Q1 Tech Founders Outreach",
"status": "started",
"type": "regular",
"totalLeads": 150,
"connectionsSent": 120,
"messagesSent": 85,
"repliesReceived": 15,
"linkedInSenderIds": ["sender_abc123", "sender_def456"],
"createdAt": "2024-02-20T10:00:00.000Z",
"updatedAt": "2024-02-24T15:30:00.000Z"
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "CAMPAIGN_NOT_FOUND",
"message": "Campaign with ID 'clxxx123456' not found in this workspace"
}
```
# List Campaigns
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-campaigns
GET /v1/campaigns
Retrieve all campaigns in your workspace
## Request
Your API key
Filter by campaign status. Options: `all`, `active`, `paused`, `draft`, `finished`
Page number for pagination
Number of items per page (max: 100)
## Response
Array of campaign objects
Unique campaign identifier
Campaign name
Current campaign status: `started`, `paused`, `draft`, `finished`
Total number of leads in the campaign
Number of connection requests sent
Number of messages sent
Number of replies received
ISO 8601 timestamp of when the campaign was created
ISO 8601 timestamp of when the campaign was last updated
Pagination metadata
Current page number
Items per page
Total number of campaigns
Total number of pages
```bash cURL theme={null}
curl https://api.sendpilot.ai/v1/campaigns \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/campaigns', {
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.sendpilot.ai/v1/campaigns',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
data = response.json()
```
```json 200 theme={null}
{
"campaigns": [
{
"id": "clxxx123456",
"name": "Q1 Tech Founders Outreach",
"status": "started",
"totalLeads": 150,
"connectionsSent": 120,
"messagesSent": 85,
"repliesReceived": 15,
"createdAt": "2024-02-20T10:00:00.000Z",
"updatedAt": "2024-02-24T15:30:00.000Z"
},
{
"id": "clxxx789012",
"name": "SaaS Decision Makers",
"status": "paused",
"totalLeads": 75,
"connectionsSent": 50,
"messagesSent": 30,
"repliesReceived": 5,
"createdAt": "2024-02-18T09:00:00.000Z",
"updatedAt": "2024-02-23T12:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 2,
"totalPages": 1
}
}
```
# List Conversations
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-conversations
GET /v1/inbox/conversations
Retrieve conversations from your LinkedIn accounts
Returns all conversations for LinkedIn accounts in your workspace. Optionally filter by a specific LinkedIn account.
## Request
Your API key
Filter by specific LinkedIn sender account ID. If not provided, returns conversations from all accounts.
Number of conversations per page (max: 100)
Token for fetching the next page of conversations. Returned in the previous response.
## Response
Array of conversation objects
Unique conversation/chat identifier
The LinkedIn sender account ID this conversation belongs to
Array of participants in the conversation
Participant's LinkedIn identifier
Participant's full name
Participant's LinkedIn profile URL
URL to participant's profile picture
The most recent message in the conversation
Message content (truncated to 100 characters)
ISO 8601 timestamp when the message was sent
Message direction: `sent` or `received`
ISO 8601 timestamp of the last activity in the conversation
Number of unread messages in the conversation
ISO 8601 timestamp when the conversation was created
ISO 8601 timestamp when the conversation was last updated
Pagination metadata
Token to use for fetching the next page of conversations
Items per page
Whether there are more conversations to fetch
```bash cURL theme={null}
curl "https://api.sendpilot.ai/v1/inbox/conversations?accountId=sender_abc123&limit=20" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const response = await fetch(
'https://api.sendpilot.ai/v1/inbox/conversations?accountId=sender_abc123&limit=20',
{
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.sendpilot.ai/v1/inbox/conversations',
params={
'accountId': 'sender_abc123',
'limit': 20
},
headers={'X-API-Key': 'YOUR_API_KEY'}
)
data = response.json()
```
```json 200 theme={null}
{
"conversations": [
{
"id": "2-OVp-y-UNyFXBYvx0FqmQ",
"accountId": "sender_abc123",
"participants": [
{
"id": "john-doe-12345",
"name": "John Doe",
"profileUrl": "https://www.linkedin.com/in/john-doe",
"profilePicture": "https://media.licdn.com/dms/image/..."
}
],
"lastMessage": {
"content": "Thanks for reaching out! I'd love to discuss...",
"sentAt": "2024-02-24T15:30:00.000Z",
"direction": "received"
},
"lastActivityAt": "2024-02-24T15:30:00.000Z",
"unreadCount": 1,
"createdAt": "2024-02-20T10:00:00.000Z",
"updatedAt": "2024-02-24T15:30:00.000Z"
}
],
"pagination": {
"continuationToken": "eyJjb250aW51YXRpb24iOiIxNzA4Nzg0MjAwMDAwIn0=",
"limit": 20,
"hasMore": true
}
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "SENDER_NOT_FOUND",
"message": "LinkedIn sender with ID 'sender_abc123' not found in this workspace"
}
```
Use the `continuationToken` from the response to fetch older response. Pass it as a query parameter in subsequent requests.
# Get Credits
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-credits
GET /v1/credits
Get your current credits balance and quota status
Returns your workspace's current credits balance, usage, and quota information.
## Request
Your API key
## Response
Total credits currently available across all buckets (subscription + purchased).
Remaining subscription/AppSumo credits for the current billing cycle.
These reset back to the plan allocation at the beginning of each billing cycle.
Remaining purchased credits.
Purchased credits never expire and roll over across billing cycles.
Credits consumed during the current billing cycle.
ISO 8601 timestamp indicating when the subscription credit bucket will next reset.
Returns `null` for workspaces without an active subscription or license.
```bash cURL theme={null}
curl https://api.sendpilot.ai/v1/credits \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/credits', {
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.sendpilot.ai/v1/credits',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
data = response.json()
```
```json 200 theme={null}
{
"available": 12500,
"subscription": 7500,
"purchased": 5000,
"used": 2500,
"nextResetDate": "2026-06-01T00:00:00.000Z"
}
```
Credits are tracked using two buckets:
* **Subscription credits** — Included with your active plan or AppSumo license and reset every billing cycle.
* **Purchased credits** — One-time purchased credits that never expire and roll over indefinitely.
The `available` field represents the combined total of both buckets.
# Get Lead
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-lead-by-id
GET /v1/leads/{id}
Retrieve a single lead by ID
## Request
Your API key
The lead ID
## Response
Unique lead identifier
LinkedIn profile URL of the lead
Current lead status
Lead's first name (if available)
Lead's last name (if available)
Lead's job title (if available)
Lead's company (if available)
The campaign this lead belongs to
ISO 8601 timestamp when the lead was added
ISO 8601 timestamp of last update
```bash cURL theme={null}
curl https://api.sendpilot.ai/v1/leads/lead_abc123 \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/leads/lead_abc123', {
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
});
const lead = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.sendpilot.ai/v1/leads/lead_abc123',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
lead = response.json()
```
```json 200 theme={null}
{
"id": "lead_abc123",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"status": "CONNECTION_ACCEPTED",
"firstName": "John",
"lastName": "Doe",
"title": "VP of Engineering",
"company": "TechCorp",
"campaignId": "camp_xyz789",
"createdAt": "2024-02-20T10:00:00.000Z",
"updatedAt": "2024-02-23T15:30:00.000Z"
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "LEAD_NOT_FOUND",
"message": "Lead with ID 'lead_abc123' not found in this workspace"
}
```
# Get Search Results
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-lead-database-search-results
GET /v1/lead-database/searches/{id}/results
Get the leads found by a lead database search
Returns the leads found by a completed lead database search.
## Request
Your API key
## Path Parameters
The search ID returned from the create search endpoint
## Query Parameters
Number of leads to skip (for pagination)
Maximum number of leads to return (1-1000)
## Response
Array of found leads
Total number of leads found
Current offset
Current limit
Whether there are more leads to fetch
```bash cURL theme={null}
curl -X GET "https://api.sendpilot.ai/v1/lead-database/searches/search_abc123xyz/results?limit=50" \
-H "X-API-Key: your-api-key"
```
```json Response theme={null}
{
"leads": [
{
"id": "lead_xyz789",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"email": "john.doe@example.com",
"phone": "+1-555-123-4567",
"linkedin_url": "https://www.linkedin.com/in/johndoe",
"job_title": "CEO",
"company": "TechCorp Inc",
"location": "San Francisco, CA",
"country": "United States",
"industry": "Technology",
"seniority": "C-Level",
"company_linkedin_url": "https://www.linkedin.com/company/techcorp",
"website": "https://techcorp.com",
"employees": "51-200"
}
],
"pagination": {
"total": 100,
"offset": 0,
"limit": 50,
"has_more": true
}
}
```
# Get Search Status
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-lead-database-search-status
GET /v1/lead-database/searches/{id}/status
Get the current status and progress of a lead database search
Returns the current status and progress of a lead database search.
## Request
Your API key
## Path Parameters
The search ID returned from the create search endpoint
## Response
Unique identifier for the search
Name of the search
Current status: `pending`, `processing`, `completed`, `failed`
Number of leads requested (your limit)
Number of leads found so far
Percentage complete (0-100)
ISO 8601 timestamp when the search was created
```bash cURL theme={null}
curl -X GET "https://api.sendpilot.ai/v1/lead-database/searches/search_abc123xyz/status" \
-H "X-API-Key: your-api-key"
```
```json Response theme={null}
{
"id": "search_abc123xyz",
"name": "Tech Founders Q1 2024",
"status": "processing",
"progress": {
"requested": 100,
"found": 45,
"percent_complete": 45
},
"created_at": "2024-02-24T10:30:00.000Z"
}
```
# Get Extractor Campaign Results
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-lead-extractor-campaign-results
GET /v1/lead-extractor/campaigns/{id}/results
Get the leads extracted by a lead extraction campaign
Returns the leads extracted by a completed lead extraction campaign.
## Request
Your API key
## Path Parameters
The campaign ID returned from the create campaign endpoint
## Query Parameters
Number of leads to skip (for pagination)
Maximum number of leads to return (1-1000)
## Response
Array of extracted leads with full profile data
Total number of leads extracted
Current offset
Current limit
Whether there are more leads to fetch
```bash cURL theme={null}
curl -X GET "https://api.sendpilot.ai/v1/lead-extractor/campaigns/camp_abc123xyz/results?limit=50" \
-H "X-API-Key: your-api-key"
```
```json Response theme={null}
{
"leads": [
{
"id": "lead_xyz789",
"linkedin_identifier": "johndoe",
"linkedin_url": "https://www.linkedin.com/in/johndoe",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"headline": "CEO at TechCorp | Building the future of AI",
"summary": "Experienced technology executive with 15+ years...",
"location": "San Francisco Bay Area",
"city": "San Francisco",
"country": "United States",
"profile_picture_url": "https://media.licdn.com/...",
"company": "TechCorp Inc",
"job_position": "CEO",
"email": "john.doe@techcorp.com",
"phone": "+1-555-123-4567",
"connections": 500,
"followers": 12500,
"experience": [
{
"title": "CEO",
"company": "TechCorp Inc",
"duration": "2020 - Present"
}
],
"education": [
{
"school": "Stanford University",
"degree": "MBA"
}
],
"skills": ["Leadership", "Strategy", "AI/ML"]
}
],
"pagination": {
"total": 100,
"offset": 0,
"limit": 50,
"has_more": true
}
}
```
# Get Extractor Campaign Status
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-lead-extractor-campaign-status
GET /v1/lead-extractor/campaigns/{id}/status
Get the current status and progress of a lead extraction campaign
Returns the current status and progress of a lead extraction campaign.
## Request
Your API key
## Path Parameters
The campaign ID returned from the create campaign endpoint
## Response
Unique identifier for the campaign
Name of the campaign
Current status: `PENDING`, `RUNNING`, `FINISHED`, `FAILED`
Number of leads extracted so far
Number of leads enriched (if enrichment enabled)
Number of leads requested (your limit)
Percentage complete (0-100)
ISO 8601 timestamp when the campaign was created
```bash cURL theme={null}
curl -X GET "https://api.sendpilot.ai/v1/lead-extractor/campaigns/camp_abc123xyz/status" \
-H "X-API-Key: your-api-key"
```
```json Response theme={null}
{
"id": "camp_abc123xyz",
"name": "Tech Startup Founders",
"status": "RUNNING",
"progress": {
"extracted": 75,
"enriched": 70,
"requested": 100,
"percent_complete": 75
},
"created_at": "2024-02-24T10:30:00.000Z"
}
```
# List Leads
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-leads
GET /v1/leads
Retrieve leads with optional filtering by campaign and status
## Request
Your API key
Campaign ID to filter leads by (required)
Filter by lead status. Options: `PENDING`, `PROCESSING`, `CONNECTION_SENT`, `CONNECTION_ACCEPTED`, `MESSAGE_SENT`, `REPLY_RECEIVED`, `FOLLOWUP_SENT`, `BLOCKED`, `PROFILE_UNREACHABLE`, `RATE_LIMITED`, `FAILED`, `SUCCESS`, `UNSUBSCRIBED`, `IRRELEVANT`, `SKIPPED`, `DONE`, `MEETING_BOOKED`, `OPPORTUNITY`, `LIKED_POST`
Return full lead data including all dynamic fields. Set to `true` for complete data.
Page number for pagination
Number of items per page (max: 100)
## Response
Array of lead objects
Unique lead identifier
LinkedIn profile URL of the lead
Lead's first name (if available)
Lead's last name (if available)
Lead's company (if available)
Lead's job title (if available)
Current lead status
Custom lead status for CRM categorization: `LEAD`, `INTERESTED`, `MEETING_BOOKED`, `MEETING_COMPLETE_NOT_CLOSED`, `CLOSED`, `WRONG_PERSON`, `NOT_INTERESTED`, `NO_RESPONSE`
The campaign this lead belongs to
ISO 8601 timestamp when the lead was added
Unique lead identifier
LinkedIn profile URL
First name
Last name
Company name
Job title
Email address
Location
Industry
Bio/about text
Website URL
Profile picture URL
Is premium LinkedIn account
Is open profile
Number of connections
Number of followers
Current lead status
Custom lead status
Campaign ID
All dynamic lead data including custom fields
When the lead was added
When the lead was last updated
Pagination metadata
Current page number
Items per page
Total number of leads
Total number of pages
```bash cURL theme={null}
curl "https://api.sendpilot.ai/v1/leads?campaignId=camp_xyz789&status=CONNECTION_ACCEPTED" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const response = await fetch(
'https://api.sendpilot.ai/v1/leads?campaignId=camp_xyz789&status=CONNECTION_ACCEPTED',
{
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.sendpilot.ai/v1/leads',
params={
'campaignId': 'camp_xyz789',
'status': 'CONNECTION_ACCEPTED'
},
headers={'X-API-Key': 'YOUR_API_KEY'}
)
data = response.json()
```
```json 200 theme={null}
{
"leads": [
{
"id": "lead_abc123",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"firstName": "John",
"lastName": "Doe",
"company": "TechCorp",
"title": "VP of Engineering",
"status": "CONNECTION_ACCEPTED",
"customLeadStatus": "LEAD",
"campaignId": "camp_xyz789",
"createdAt": "2024-02-20T10:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 45,
"totalPages": 1
}
}
```
Use `full=true` to get complete lead data including all enriched fields and custom data. This is useful when you need detailed information about leads.
# Get Conversation Messages
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-messages
GET /v1/inbox/conversations/{conversationId}/messages
Retrieve messages from a specific conversation
Returns messages for a specific conversation with pagination support using continuation tokens.
## Request
Your API key
The conversation/chat ID to fetch messages from
The LinkedIn sender account ID that owns this conversation
Number of messages to return (max: 100)
Token for fetching the next page of messages. Returned in the previous response.
## Response
The conversation ID
Array of message objects
Unique message identifier
Message content
Message sender information
Sender's LinkedIn identifier
Sender's full name
Sender's LinkedIn profile URL
Message recipient information
Recipient's LinkedIn identifier
Recipient's full name
Recipient's LinkedIn profile URL
Message direction: `sent` or `received`
ISO 8601 timestamp when the message was sent
Read status: `read`, `unread`, or `unknown`
Content type: `TEXT`, `IMAGE`, `FILE`, etc.
Array of attachments (if any)
Attachment type
Attachment URL
Attachment filename
Attachment size in bytes
Pagination metadata
Whether there are more messages to fetch
Token to use for fetching the next page of messages
```bash cURL theme={null}
curl "https://api.sendpilot.ai/v1/inbox/conversations/2-OVp-y-UNyFXBYvx0FqmQ/messages?accountId=sender_abc123&limit=50" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const conversationId = '2-OVp-y-UNyFXBYvx0FqmQ';
const response = await fetch(
`https://api.sendpilot.ai/v1/inbox/conversations/${conversationId}/messages?accountId=sender_abc123&limit=50`,
{
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
conversation_id = '2-OVp-y-UNyFXBYvx0FqmQ'
response = requests.get(
f'https://api.sendpilot.ai/v1/inbox/conversations/{conversation_id}/messages',
params={
'accountId': 'sender_abc123',
'limit': 50
},
headers={'X-API-Key': 'YOUR_API_KEY'}
)
data = response.json()
```
```json 200 theme={null}
{
"conversationId": "2-OVp-y-UNyFXBYvx0FqmQ",
"messages": [
{
"id": "msg_abc123",
"content": "Hi John! Thanks for connecting. I wanted to follow up on our conversation about your company's growth plans.",
"sender": {
"id": "jane-smith-12345",
"name": "Jane Smith",
"profileUrl": "https://www.linkedin.com/in/jane-smith"
},
"recipient": {
"id": "john-doe-67890",
"name": "John Doe",
"profileUrl": "https://www.linkedin.com/in/john-doe"
},
"direction": "sent",
"sentAt": "2024-02-24T14:00:00.000Z",
"readStatus": "read",
"contentType": "TEXT"
},
{
"id": "msg_def456",
"content": "Thanks for reaching out! I'd love to discuss this further. Are you available for a call next week?",
"sender": {
"id": "john-doe-67890",
"name": "John Doe",
"profileUrl": "https://www.linkedin.com/in/john-doe"
},
"recipient": {
"id": "jane-smith-12345",
"name": "Jane Smith",
"profileUrl": "https://www.linkedin.com/in/jane-smith"
},
"direction": "received",
"sentAt": "2024-02-24T15:30:00.000Z",
"readStatus": "unread",
"contentType": "TEXT"
}
],
"pagination": {
"hasMore": true,
"continuationToken": "eyJjb250aW51YXRpb24iOiIxNzA4Nzg0MjAwMDAwIn0="
}
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "SENDER_NOT_FOUND",
"message": "LinkedIn sender with ID 'sender_abc123' not found in this workspace"
}
```
The `accountId` parameter is required to verify that the conversation belongs to a LinkedIn account in your workspace.
Use the `continuationToken` from the response to fetch older messages. Pass it as a query parameter in subsequent requests.
# List LinkedIn Senders
Source: https://docs.sendpilot.ai/api-reference/endpoint/get-senders
GET /v1/inbox/senders
Retrieve all LinkedIn accounts connected to your workspace
Returns all LinkedIn accounts (senders) that are connected to your workspace and can send messages.
## Request
Your API key
## Response
Array of LinkedIn sender accounts
Unique sender identifier
Full name of the LinkedIn account
LinkedIn profile URL
Account status: `active`, `disconnected`, `rate_limited`
Maximum messages per day
Messages sent so far today
Messages remaining for today
Total number of senders
```bash cURL theme={null}
curl https://api.sendpilot.ai/v1/inbox/senders \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/inbox/senders', {
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY
}
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.sendpilot.ai/v1/inbox/senders',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
data = response.json()
```
```json 200 theme={null}
{
"senders": [
{
"id": "sender_abc123",
"name": "Jane Smith",
"linkedinUrl": "https://www.linkedin.com/in/jane-smith",
"status": "active",
"dailyMessageLimit": 100,
"messagesSentToday": 25,
"remainingMessages": 75
},
{
"id": "sender_def456",
"name": "John Doe",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"status": "active",
"dailyMessageLimit": 100,
"messagesSentToday": 0,
"remainingMessages": 100
}
],
"total": 2
}
```
Only active senders can be used to send messages. Check the `status` field before selecting a sender.
# Update Campaign
Source: https://docs.sendpilot.ai/api-reference/endpoint/patch-campaign
PATCH /v1/campaigns/{id}
Pause or resume a campaign
Update a campaign's state. Currently supports pausing and resuming campaigns.
## Request
Your API key
The campaign ID
The action to perform. Options: `pause`, `resume`
## Response
Whether the action was successful
Campaign identifier
The action that was performed
The new status of the campaign
Confirmation message
```bash Pause Campaign theme={null}
curl -X PATCH https://api.sendpilot.ai/v1/campaigns/camp_xyz789 \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "pause"
}'
```
```bash Resume Campaign theme={null}
curl -X PATCH https://api.sendpilot.ai/v1/campaigns/camp_xyz789 \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "resume"
}'
```
```javascript Node.js theme={null}
// Pause campaign
const response = await fetch('https://api.sendpilot.ai/v1/campaigns/camp_xyz789', {
method: 'PATCH',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'pause' // or 'resume'
})
});
const result = await response.json();
```
```python Python theme={null}
import requests
response = requests.patch(
'https://api.sendpilot.ai/v1/campaigns/camp_xyz789',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={'action': 'pause'} # or 'resume'
)
result = response.json()
```
```json Paused theme={null}
{
"success": true,
"campaignId": "camp_xyz789",
"action": "pause",
"newStatus": "paused",
"message": "Campaign paused successfully"
}
```
```json Resumed theme={null}
{
"success": true,
"campaignId": "camp_xyz789",
"action": "resume",
"newStatus": "active",
"message": "Campaign resumed successfully"
}
```
```json 400 Invalid State theme={null}
{
"statusCode": 400,
"error": "Bad Request",
"code": "INVALID_CAMPAIGN_STATE",
"message": "Cannot pause campaign. Current status is 'PAUSED', expected 'STARTED'"
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "CAMPAIGN_NOT_FOUND",
"message": "Campaign with ID 'camp_xyz789' not found in this workspace"
}
```
When a campaign is paused or resumed, the corresponding webhook event (`campaign.paused` or `campaign.resumed`) will be sent to your registered webhook endpoints.
# Update Lead Status
Source: https://docs.sendpilot.ai/api-reference/endpoint/patch-lead-status
PATCH /v1/leads/{id}/status
Update the status of a lead
Manually update a lead's status. This is useful for marking leads as opportunities or not interested based on external information. You can update either the campaign status, custom lead status, or both in a single request.
## Request
Your API key
The lead ID
The campaign status for the lead. Options:
* `MEETING_BOOKED` - Mark as meeting booked
* `OPPORTUNITY` - Mark as a sales opportunity
* `NOT_INTERESTED` - Mark as not interested
* `DONE` - Mark as completed
The custom lead status for CRM categorization. Options:
* `LEAD` - New lead (default)
* `INTERESTED` - Lead has shown interest
* `MEETING_BOOKED` - Meeting has been scheduled
* `MEETING_COMPLETE_NOT_CLOSED` - Meeting completed but deal not closed
* `CLOSED` - Deal closed/won
* `WRONG_PERSON` - Wrong contact/person
* `NOT_INTERESTED` - Lead is not interested
* `NO_RESPONSE` - No response received
Optional note to add to the lead's history
At least one of `status` or `customLeadStatus` must be provided. You can update both in a single request.
## Response
Whether the update was successful
Lead identifier
The new status
Confirmation message
```bash cURL theme={null}
curl -X PATCH https://api.sendpilot.ai/v1/leads/lead_abc123/status \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "OPPORTUNITY",
"note": "Interested in enterprise plan"
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/leads/lead_abc123/status', {
method: 'PATCH',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'OPPORTUNITY',
note: 'Interested in enterprise plan'
})
});
const result = await response.json();
```
```python Python theme={null}
import requests
response = requests.patch(
'https://api.sendpilot.ai/v1/leads/lead_abc123/status',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
'status': 'OPPORTUNITY',
'note': 'Interested in enterprise plan'
}
)
result = response.json()
```
```json 200 theme={null}
{
"success": true,
"leadId": "lead_abc123",
"status": "OPPORTUNITY",
"message": "Lead status updated to 'OPPORTUNITY'"
}
```
```json 400 theme={null}
{
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid status value"
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "LEAD_NOT_FOUND",
"message": "Lead with ID 'lead_abc123' not found in this workspace"
}
```
When a lead's status is updated, a `lead.updated` webhook event will be sent to your registered webhook endpoints.
# Add Leads
Source: https://docs.sendpilot.ai/api-reference/endpoint/post-add-leads
POST /v1/leads
Add one or more leads to an existing campaign
Add new leads to an existing campaign. The leads will be queued for processing according to the campaign's sequence.
## Request
Your API key
The campaign ID to add leads to
Array of lead objects to add (max 100 per request)
LinkedIn profile URL of the lead. This is the only required field.
Lead's first name (optional, will be scraped if not provided)
Lead's last name (optional, will be scraped if not provided)
Lead's email address
Lead's company name
Lead's job title
**Custom fields:** You can pass any additional custom fields as flat properties directly on each lead object. See the [Dynamic/Custom Fields](#dynamiccustom-fields) section below for details.
## Dynamic/Custom Fields
Leads support **dynamic custom fields** that can be passed as flat properties alongside the required `linkedinUrl`. This allows you to include any additional data for personalization in your campaign messages.
**How it works:** Simply add any custom key-value pairs directly to the lead object. There's no need for a nested `customFields` object—all properties are accepted at the top level.
**Common use cases for custom fields:**
* `industry` - Lead's industry for targeted messaging
* `region` - Geographic region for localized outreach
* `referredBy` - Referral source tracking
* `customScore` - Lead scoring from your CRM
* `eventName` - Conference or event where you met
* `icebreaker` - Pre-written personalized opener
## Response
Whether the operation completed successfully
Number of leads successfully added
Number of leads skipped (already exist in campaign)
Number of leads with invalid data
Array of error objects for leads that failed validation (only present if there are errors)
Index of the failed lead in the input array
The LinkedIn URL that failed
Reason for the failure
```bash cURL theme={null}
curl -X POST https://api.sendpilot.ai/v1/leads \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "camp_xyz789",
"leads": [
{
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"firstName": "John",
"lastName": "Doe",
"company": "TechCorp",
"title": "VP of Engineering",
"industry": "Technology",
"region": "North America"
},
{
"linkedinUrl": "https://www.linkedin.com/in/jane-smith",
"firstName": "Jane",
"lastName": "Smith",
"referredBy": "Conference 2024",
"customScore": 85,
"icebreaker": "Loved your talk on AI automation"
}
]
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/leads', {
method: 'POST',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
campaignId: 'camp_xyz789',
leads: [
{
linkedinUrl: 'https://www.linkedin.com/in/john-doe',
firstName: 'John',
lastName: 'Doe',
company: 'TechCorp',
// Custom fields passed directly
industry: 'SaaS',
leadSource: 'Website Demo Request'
}
]
})
});
const result = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.sendpilot.ai/v1/leads',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
'campaignId': 'camp_xyz789',
'leads': [
{
'linkedinUrl': 'https://www.linkedin.com/in/john-doe',
'firstName': 'John',
'lastName': 'Doe',
# Custom fields as flat properties
'eventName': 'SaaStr 2024',
'boothVisit': True,
'interestLevel': 'High'
}
]
}
)
result = response.json()
```
```json 201 theme={null}
{
"success": true,
"leadsAdded": 2,
"duplicatesSkipped": 0,
"invalidEntries": 0
}
```
```json 207 Multi-Status theme={null}
{
"success": true,
"leadsAdded": 1,
"duplicatesSkipped": 1,
"invalidEntries": 1,
"errors": [
{
"index": 2,
"linkedinUrl": "invalid-url",
"reason": "Invalid LinkedIn URL format"
}
]
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "CAMPAIGN_NOT_FOUND",
"message": "Campaign with ID 'camp_xyz789' not found in this workspace"
}
```
Duplicate detection is based on the LinkedIn URL. If a lead with the same URL already exists in the campaign, it will be skipped.
**Using custom fields in messages:** Reference your custom fields in campaign message templates using the `{{fieldName}}` syntax. For example, if you pass `industry: "Technology"`, use `{{industry}}` in your message template.
# Send Connection Request
Source: https://docs.sendpilot.ai/api-reference/endpoint/post-connect
POST /v1/inbox/connect
Send a LinkedIn connection request to a profile
Send a connection request to a LinkedIn profile. Optionally include a connection note (only works for premium LinkedIn accounts).
## Request
Your API key
The LinkedIn sender account ID to use. Get available senders from the [List Senders](/api-reference/endpoint/get-senders) endpoint.
The LinkedIn profile URL of the person to connect with (e.g., `https://www.linkedin.com/in/john-doe`)
Optional connection note (max 300 characters). Note: Connection notes only work for premium LinkedIn accounts.
## Response
Whether the connection request was sent successfully
Unique identifier for the connection request
The LinkedIn URL of the recipient
Request status: `sent` or `already_connected`
ISO 8601 timestamp when the request was sent
```bash cURL theme={null}
curl -X POST https://api.sendpilot.ai/v1/inbox/connect \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"senderId": "sender_abc123",
"recipientLinkedinUrl": "https://www.linkedin.com/in/john-doe",
"message": "Hi John, I came across your profile and would love to connect!"
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/inbox/connect', {
method: 'POST',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
senderId: 'sender_abc123',
recipientLinkedinUrl: 'https://www.linkedin.com/in/john-doe',
message: 'Hi John, I came across your profile and would love to connect!'
})
});
const result = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.sendpilot.ai/v1/inbox/connect',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
'senderId': 'sender_abc123',
'recipientLinkedinUrl': 'https://www.linkedin.com/in/john-doe',
'message': 'Hi John, I came across your profile and would love to connect!'
}
)
result = response.json()
```
```json 200 - Request Sent theme={null}
{
"success": true,
"requestId": "conn_xyz789",
"recipientLinkedinUrl": "https://www.linkedin.com/in/john-doe",
"status": "sent",
"timestamp": "2024-02-24T15:30:00.000Z"
}
```
```json 200 - Already Connected theme={null}
{
"success": true,
"requestId": "conn_xyz789",
"recipientLinkedinUrl": "https://www.linkedin.com/in/john-doe",
"status": "already_connected",
"timestamp": "2024-02-24T15:30:00.000Z"
}
```
```json 400 theme={null}
{
"statusCode": 400,
"error": "Bad Request",
"code": "SENDER_NOT_ACTIVE",
"message": "LinkedIn sender 'sender_abc123' is not active. Current status: disconnected"
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "SENDER_NOT_FOUND",
"message": "LinkedIn sender with ID 'sender_abc123' not found in this workspace"
}
```
```json 429 theme={null}
{
"statusCode": 429,
"error": "Too Many Requests",
"code": "DAILY_LIMIT_EXCEEDED",
"message": "LinkedIn sender 'sender_abc123' has reached its daily connection request limit. Resets at midnight UTC."
}
```
When a connection request is sent, a `connection.sent` webhook event will be triggered. When the connection is accepted, a `connection.accepted` webhook event will be triggered.
Connection notes are only supported for LinkedIn Premium accounts. For free accounts, the connection request will be sent without a note.
LinkedIn has daily limits on connection requests. Check the sender's remaining capacity using the [List Senders](/api-reference/endpoint/get-senders) endpoint.
# Create Lead Database Search
Source: https://docs.sendpilot.ai/api-reference/endpoint/post-lead-database-search
POST /v1/lead-database/searches
Create a new lead database search to find leads matching your criteria
Initiates a bulk search in the Lead Database to find leads matching your specified filters. The API supports 120+ filters across multiple categories for precise lead targeting.
## Request
Your API key
## Request Body
A name for this search (for your reference)
Maximum number of leads to find. There is no hard limit; results are capped by available credits.
Search filters to match leads. All filters are optional - combine as needed.
Download the complete list of supported filter values here: [lead-database-filter-values.json](https://sendpilotstorage.blob.core.windows.net/api-docs/lead-database-filter-values.json)
Optional HTTPS URL to receive a completion notification when the search completes. Must be a public host.
***
## Filter Categories
### Person Filters
Free-text search by person name
Job titles to include (e.g., `["CEO", "CTO", "Founder"]`)
Job titles to exclude
Professional skills to include (e.g., `["Python", "Machine Learning"]`)
Professional skills to exclude
Languages spoken (e.g., `["English", "Spanish"]`)
Languages to exclude
Language proficiency levels (e.g., `["english-5"]`)
Language proficiency levels to exclude
LinkedIn usernames or profile URLs to include
LinkedIn usernames or profile URLs to exclude
Locations/countries to include (e.g., `["United States", "Canada"]`)
Locations/countries to exclude
Keywords to search in profile summaries
Keywords to exclude from profile summaries
Seniority levels (e.g., `["C-Level", "Director", "VP", "Manager"]`)
Seniority levels to exclude
Standard departments (e.g., `["Engineering", "Sales", "Marketing"]`)
Departments to exclude
Experimental/emerging department classifications
Experimental departments to exclude
Professional certifications (e.g., `["PMP", "AWS Certified"]`)
Certifications to exclude
***
### Experience Filters
Minimum total professional experience in months
Maximum total professional experience in months
Minimum duration at current job in months
Maximum duration at current job in months
***
### Education Filters
Educational institutions (e.g., `["Stanford University", "MIT"]`)
Institutions to exclude
Fields of study/majors (e.g., `["Computer Science", "Business"]`)
Majors to exclude
Keywords to search in education history
Education keywords to exclude
***
### Company Filters
Company names to include (e.g., `["Google", "Microsoft"]`)
Company names to exclude
Company domains, comma-separated (e.g., `google.com, microsoft.com`)
Company domains to exclude, comma-separated
Company LinkedIn usernames/URLs
Company LinkedIn usernames to exclude
Company types (e.g., `["Public", "Private", "Non-Profit"]`)
Company types to exclude
Industries (e.g., `["Technology", "Healthcare", "Finance"]`)
Industries to exclude
Experimental/emerging industry classifications
Experimental industries to exclude
Company headquarters locations (e.g., `["San Francisco", "New York"]`)
HQ locations to exclude
Company size ranges (e.g., `["11-50", "51-200", "201-500"]`)
Company sizes to exclude using mapped integer values
Company status: `active` or `closed`
Status comments to filter by
Filter for publicly listed companies only
Filter for B2B companies only
***
### Industry Codes
SIC (Standard Industrial Classification) codes
SIC codes to exclude
NAICS (North American Industry Classification System) codes
NAICS codes to exclude
General keywords to search for
Keywords to exclude
***
### Funding Filters
Last funding date range: `30`, `60`, `90`, or `90+` days
Funding round names (e.g., `["Series A", "Series B", "Seed"]`)
Ownership status filter
Minimum funding amount
Maximum funding amount
IPO date range start (`dd/mm/yyyy`)
IPO date range end (`dd/mm/yyyy`)
Acquisition date range start (`dd/mm/yyyy`)
Acquisition date range end (`dd/mm/yyyy`)
Minimum annual revenue
Maximum annual revenue
***
### Website Analytics Filters
Minimum monthly website visits
Maximum monthly website visits
Minimum global website ranking
Maximum global website ranking
Minimum country-specific ranking
Maximum country-specific ranking
Minimum category ranking
Maximum category ranking
Minimum bounce rate percentage
Maximum bounce rate percentage
Minimum pages per visit
Maximum pages per visit
Minimum visit duration in seconds
Maximum visit duration in seconds
Website topics to include
Website topics to exclude
***
### Technology Filters
Technologies used (e.g., `["React", "AWS", "Salesforce"]`)
Technologies to exclude
Company has public pricing page
Company offers product demos
Company has public documentation
Company offers free trials
Company offers downloadable products
Company has mobile applications
Company has online reviews
***
### Job Posting Filters
Job titles being recruited (e.g., `["Software Engineer", "Sales Manager"]`)
Job posting titles to exclude
Job posting locations
Job posting locations to exclude
Job functions being recruited
Job functions to exclude
Job posting date range start (`dd/mm/yyyy`)
Job posting date range end (`dd/mm/yyyy`)
Employment type (e.g., `["Full-time", "Part-time", "Contract"]`)
Job posting seniority level
***
### Recommendation Filters
Keywords in LinkedIn recommendations
Recommendation keywords to exclude
LinkedIn usernames of recommenders
Recommender usernames to exclude
Minimum employee satisfaction score
Maximum employee satisfaction score
***
### Intent Filters
Bombora intent topics (e.g., `["Cloud Computing", "CRM Software"]`)
Intent score ranges to filter by
***
### News Filters
Keywords in recent news articles
News keywords to exclude
***
## Response
Unique identifier for the search
Name of the search
Current status: `pending`, `processing`, `completed`, `failed`
ISO 8601 timestamp when the search was created
```bash cURL theme={null}
curl -X POST "https://api.sendpilot.ai/v1/lead-database/searches" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "Tech Founders Q1 2024",
"limit": 100,
"filters": {
"job_titles": ["CEO", "CTO", "Founder"],
"locations": ["United States"],
"industries": ["Technology", "Software"],
"company_sizes": ["11-50", "51-200"],
"seniority_levels": ["C-Level", "VP"],
"min_total_experience_duration_months": 60,
"technologies_used": ["AWS", "React"]
}
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/lead-database/searches', {
method: 'POST',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Tech Founders Q1 2024',
limit: 100,
filters: {
job_titles: ['CEO', 'CTO', 'Founder'],
locations: ['United States'],
industries: ['Technology', 'Software'],
company_sizes: ['11-50', '51-200'],
seniority_levels: ['C-Level', 'VP'],
min_total_experience_duration_months: 60,
technologies_used: ['AWS', 'React']
}
})
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.sendpilot.ai/v1/lead-database/searches',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
'name': 'Tech Founders Q1 2024',
'limit': 100,
'filters': {
'job_titles': ['CEO', 'CTO', 'Founder'],
'locations': ['United States'],
'industries': ['Technology', 'Software'],
'company_sizes': ['11-50', '51-200'],
'seniority_levels': ['C-Level', 'VP'],
'min_total_experience_duration_months': 60,
'technologies_used': ['AWS', 'React']
}
}
)
data = response.json()
```
```json 200 theme={null}
{
"id": "search_abc123xyz",
"name": "Tech Founders Q1 2024",
"status": "pending",
"created_at": "2024-02-24T10:30:00.000Z"
}
```
All filters are optional and can be combined. Use excluded\_\* variants to explicitly remove matches. Array filters use OR logic within the same filter and AND logic across different filters.
# Create Lead Extractor Campaign
Source: https://docs.sendpilot.ai/api-reference/endpoint/post-lead-extractor-campaign
POST /v1/lead-extractor/campaigns
Create a new lead extraction campaign from LinkedIn search URLs
Initiates a lead extraction campaign to scrape leads from LinkedIn search results. Requires an active LinkedIn account connected to your workspace.
## Request
Your API key
A name for this extraction campaign
LinkedIn search URLs to extract leads from
Maximum number of leads to extract (1-10000)
Type of URLs provided: `linkedin_search` or `sales_navigator`
Extraction mode: `extraction_only` or `with_enrichment`
## Response
Unique identifier for the campaign
Name of the campaign
Current status: `pending`, `running`, `completed`, `failed`, `cancelled`
ISO 8601 timestamp when the campaign was created
Estimated credits that will be consumed
```bash cURL theme={null}
curl -X POST https://api.sendpilot.ai/v1/lead-extractor/campaigns \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Tech Startup Founders",
"urls": [
"https://www.linkedin.com/search/results/people/?keywords=CEO%20startup"
],
"limit": 100,
"url_type": "linkedin_search",
"mode": "with_enrichment"
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/lead-extractor/campaigns', {
method: 'POST',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Tech Startup Founders',
urls: ['https://www.linkedin.com/search/results/people/?keywords=CEO%20startup'],
limit: 100,
url_type: 'linkedin_search',
mode: 'with_enrichment'
})
});
const result = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.sendpilot.ai/v1/lead-extractor/campaigns',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
'name': 'Tech Startup Founders',
'urls': ['https://www.linkedin.com/search/results/people/?keywords=CEO%20startup'],
'limit': 100,
'url_type': 'linkedin_search',
'mode': 'with_enrichment'
}
)
result = response.json()
```
```json 201 theme={null}
{
"id": "camp_abc123xyz",
"name": "Tech Startup Founders",
"status": "pending",
"created_at": "2024-02-24T10:30:00.000Z",
"estimated_credits": 200
}
```
```json 400 theme={null}
{
"statusCode": 400,
"error": "Bad Request",
"code": "NO_LINKEDIN_ACCOUNT",
"message": "No active LinkedIn account found in workspace. Please connect a LinkedIn account first."
}
```
```json 403 theme={null}
{
"statusCode": 403,
"error": "Forbidden",
"code": "INSUFFICIENT_CREDITS",
"message": "Insufficient credits. Required: 200, Available: 50"
}
```
Credits are calculated as: 1 credit per lead for extraction, plus 1 additional credit per lead if enrichment is enabled.
# Send Message
Source: https://docs.sendpilot.ai/api-reference/endpoint/post-send-message
POST /v1/inbox/send
Send a direct message via LinkedIn to a recipient by their LinkedIn URL
Send a LinkedIn message directly to a recipient. The recipient must be a 1st-degree connection of the sender (connection must already be accepted).
## Request
Your API key
The LinkedIn sender account ID to use. Get available senders from the [List Senders](/api-reference/endpoint/get-senders) endpoint.
The LinkedIn profile URL of the recipient (e.g., `https://www.linkedin.com/in/john-doe`)
The message content to send (max 8000 characters)
## Response
Whether the message was sent successfully
Unique identifier for the sent message
The LinkedIn URL of the recipient
Message status: `sent`
ISO 8601 timestamp when the message was sent
```bash cURL theme={null}
curl -X POST https://api.sendpilot.ai/v1/inbox/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"senderId": "sender_abc123",
"recipientLinkedinUrl": "https://www.linkedin.com/in/john-doe",
"message": "Hi John! Thanks for connecting. I wanted to follow up on our conversation about..."
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api.sendpilot.ai/v1/inbox/send', {
method: 'POST',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
senderId: 'sender_abc123',
recipientLinkedinUrl: 'https://www.linkedin.com/in/john-doe',
message: 'Hi John! Thanks for connecting...'
})
});
const result = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.sendpilot.ai/v1/inbox/send',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
'senderId': 'sender_abc123',
'recipientLinkedinUrl': 'https://www.linkedin.com/in/john-doe',
'message': 'Hi John! Thanks for connecting...'
}
)
result = response.json()
```
```json 200 theme={null}
{
"success": true,
"messageId": "msg_xyz789",
"recipientLinkedinUrl": "https://www.linkedin.com/in/john-doe",
"status": "sent",
"timestamp": "2024-02-24T15:30:00.000Z"
}
```
```json 400 theme={null}
{
"statusCode": 400,
"error": "Bad Request",
"code": "SENDER_NOT_ACTIVE",
"message": "LinkedIn sender 'sender_abc123' is not active. Current status: disconnected"
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "SENDER_NOT_FOUND",
"message": "LinkedIn sender with ID 'sender_abc123' not found in this workspace"
}
```
```json 429 theme={null}
{
"statusCode": 429,
"error": "Too Many Requests",
"code": "DAILY_LIMIT_EXCEEDED",
"message": "LinkedIn sender 'sender_abc123' has reached its daily message limit. Resets at midnight UTC."
}
```
Messages can only be sent to recipients who are 1st-degree connections of the sender. Attempting to message a non-connected profile will result in an error.
When a message is sent, a `message.sent` webhook event will be triggered if you have webhooks configured.
To send a message to a lead by their ID (with automatic LinkedIn URL lookup), use the [Send Message to Lead](/api-reference/endpoint/post-send-to-lead) endpoint instead.
# Send Message to Lead
Source: https://docs.sendpilot.ai/api-reference/endpoint/post-send-to-lead
POST /v1/inbox/send/lead/{leadId}
Send a direct message to a lead by their ID
Send a LinkedIn message to a lead using their ID. The lead's LinkedIn URL is looked up automatically from the database. Supports template variables like `{{firstName}}` and `{{lastName}}`.
## Request
Your API key
The lead ID to send the message to
The LinkedIn sender account ID to use. Get available senders from the [List Senders](/api-reference/endpoint/get-senders) endpoint.
The message content to send (max 8000 characters). Supports template variables:
* `{{firstName}}` - Lead's first name
* `{{lastName}}` - Lead's last name
## Response
Whether the message was sent successfully
Unique identifier for the sent message
The LinkedIn URL of the lead
The lead ID the message was sent to
Message status: `sent`
ISO 8601 timestamp when the message was sent
```bash cURL theme={null}
curl -X POST https://api.sendpilot.ai/v1/inbox/send/lead/lead_abc123 \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"senderId": "sender_def456",
"message": "Hi {{firstName}}! Thanks for connecting. I wanted to follow up on our conversation..."
}'
```
```javascript Node.js theme={null}
const leadId = 'lead_abc123';
const response = await fetch(`https://api.sendpilot.ai/v1/inbox/send/lead/${leadId}`, {
method: 'POST',
headers: {
'X-API-Key': process.env.SENDPILOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
senderId: 'sender_def456',
message: 'Hi {{firstName}}! Thanks for connecting...'
})
});
const result = await response.json();
```
```python Python theme={null}
import requests
lead_id = 'lead_abc123'
response = requests.post(
f'https://api.sendpilot.ai/v1/inbox/send/lead/{lead_id}',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
'senderId': 'sender_def456',
'message': 'Hi {{firstName}}! Thanks for connecting...'
}
)
result = response.json()
```
```json 200 theme={null}
{
"success": true,
"messageId": "msg_xyz789",
"recipientLinkedinUrl": "https://www.linkedin.com/in/john-doe",
"leadId": "lead_abc123",
"status": "sent",
"timestamp": "2024-02-24T15:30:00.000Z"
}
```
```json 400 theme={null}
{
"statusCode": 400,
"error": "Bad Request",
"code": "MISSING_LINKEDIN_URL",
"message": "Lead 'lead_abc123' does not have a LinkedIn URL"
}
```
```json 404 theme={null}
{
"statusCode": 404,
"error": "Not Found",
"code": "LEAD_NOT_FOUND",
"message": "Lead with ID 'lead_abc123' not found in this workspace"
}
```
```json 429 theme={null}
{
"statusCode": 429,
"error": "Too Many Requests",
"code": "DAILY_LIMIT_EXCEEDED",
"message": "LinkedIn sender 'sender_def456' has reached its daily message limit."
}
```
The lead must be a 1st-degree connection of the sender. Attempting to message a non-connected lead will result in an error.
Use template variables like `{{firstName}}` to personalize your messages. The variables are automatically replaced with the lead's actual data.
# API Reference
Source: https://docs.sendpilot.ai/api-reference/introduction
Integrate with the SendPilot External API
The SendPilot API enables you to programmatically manage LinkedIn outreach campaigns, add leads, update lead statuses, send messages, and receive real-time webhook notifications.
## Base URL
```
https://api.sendpilot.ai/v1
```
## Authentication
All API requests require authentication using an API key header. Include the following header in every request:
```bash theme={null}
X-API-Key: YOUR_API_KEY
```
### Example Request
```bash theme={null}
curl -X GET https://api.sendpilot.ai/v1/campaigns \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"
```
Keep your API key secret. Never commit it to version control or expose it in client-side code.
## Error Handling
The API uses standard HTTP status codes and returns structured error responses:
```json theme={null}
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request"
}
```
### Common Status Codes
| Code | Description |
| ----- | ----------------------------------------- |
| `200` | Success |
| `201` | Created |
| `400` | Bad request (invalid payload) |
| `401` | Unauthorized (invalid or missing API key) |
| `403` | Forbidden (insufficient permissions) |
| `404` | Resource not found |
| `500` | Internal server error |
## Webhooks
SendPilot sends real-time notifications to your webhook endpoints for various events:
**Message Events:**
* `message.sent` - When a message is sent to a lead
* `message.received` - When a lead replies to your message
**Connection Events:**
* `connection.sent` - When a connection request is sent
* `connection.accepted` - When a connection request is accepted
**Campaign Events:**
* `campaign.started` - When a campaign starts
* `campaign.paused` - When a campaign is paused
* `campaign.resumed` - When a campaign is resumed
**Lead Events:**
* `lead.status.changed` - When a lead's status changes
Learn more in our [Webhooks documentation](/webhooks/overview).
## Getting Started
Navigate to Integrations → API Keys in the SendPilot dashboard and create a new API key.
Test your API key by listing your campaigns:
```bash theme={null}
curl https://api.sendpilot.ai/v1/campaigns \
-H "X-API-Key: YOUR_API_KEY"
```
Configure webhook subscriptions in the dashboard to receive real-time event notifications.
# SendPilot API
Source: https://docs.sendpilot.ai/index
Build powerful integrations with LinkedIn outreach automation
The SendPilot API gives you programmatic access to manage LinkedIn outreach campaigns, leads, and messaging. Build custom integrations, automate workflows, and sync data with your existing tools.
## Quick Start
Explore all available endpoints
Set up API key authentication
Receive real-time event notifications
Integration guides for Zapier, n8n, Make.com
## What You Can Build
Sync leads and campaign data with Salesforce, HubSpot, or your custom CRM
Automatically add leads from forms, events, or external sources
Build internal analytics and reporting tools
## Integrations
Connect SendPilot with your favorite tools:
Connect to 5,000+ apps with no code
Self-hosted workflow automation
Visual automation for complex workflows
## API Capabilities
| Feature | Description |
| ----------------------- | ------------------------------------------ |
| **Campaign Management** | List, pause, and resume campaigns |
| **Lead Management** | Add leads, update status, retrieve details |
| **Inbox Messaging** | Send messages via LinkedIn |
| **Webhooks** | Real-time notifications for all events |
## Example Request
```bash theme={null}
curl https://api.sendpilot.ai/v1/campaigns \
-H "X-API-Key: YOUR_API_KEY"
```
```json theme={null}
{
"campaigns": [
{
"id": "camp_xyz789",
"name": "Q1 Tech Founders Outreach",
"status": "started",
"totalLeads": 150
}
],
"pagination": {
"total": 1,
"page": 1,
"limit": 20
}
}
```
## Need Help?
Contact our support team
Browse the full documentation
# Make.com Integration
Source: https://docs.sendpilot.ai/use-cases/make
Visual automation platform for complex workflows
Make.com (formerly Integromat) is a powerful visual automation platform that allows you to build complex workflows with a drag-and-drop interface.
## Why Make.com?
* **Visual Builder**: Build complex workflows visually
* **1000+ Integrations**: Connect to major apps and services
* **Advanced Logic**: Routers, filters, iterators, and aggregators
* **Data Transformation**: Built-in functions for data manipulation
## Setting Up SendPilot in Make.com
### Step 1: Create a Webhook Module (Trigger)
1. Create a new Scenario in Make.com
2. Click **+** to add a module
3. Search for **Webhooks** → **Custom Webhook**
4. Click **Add** to create a new webhook
5. Copy the webhook URL
### Step 2: Register Webhook in SendPilot
1. Go to SendPilot **Integrations** → **Webhooks**
2. Click **Add Webhook**
3. Paste your Make.com webhook URL
4. Select events to subscribe to
5. Save
### Step 3: Add HTTP Module for API Calls
For calling SendPilot API:
1. Add **HTTP** → **Make a Request** module
2. Configure:
* URL: `https://api.sendpilot.ai/v1/leads`
* Method: `POST`
* Headers:
* `X-API-Key`: `YOUR_API_KEY`
* `Content-Type`: `application/json`
* Body: Your JSON payload
## Example Scenarios
### 1. Reply Handler → Slack + CRM
**Scenario Flow:**
```
[Webhook: SendPilot Events]
→ [Router]
→ Path 1 (Reply): [Slack] → [HubSpot]
→ Path 2 (Connection): [Slack]
```
**Webhook Module:**
* Name: "SendPilot Events"
* Data Structure: Select "Add" to define the payload structure
**Router Filters:**
* Path 1: `eventType` = `message.received`
* Path 2: `eventType` = `connection.accepted`
**Slack Module:**
* Channel: `#sales-leads`
* Text: `🎉 {{1.data.leadId}} replied: {{1.data.replyPreview}}`
### 2. Google Sheets → Add Leads
**Scenario Flow:**
```
[Schedule: Every Hour]
→ [Google Sheets: Search Rows (status = "new")]
→ [Iterator]
→ [HTTP: POST /v1/leads]
→ [Google Sheets: Update Row (status = "sent")]
```
**HTTP Module Configuration:**
```json theme={null}
{
"url": "https://api.sendpilot.ai/v1/leads",
"method": "POST",
"headers": [
{
"name": "X-API-Key",
"value": "YOUR_API_KEY"
},
{
"name": "Content-Type",
"value": "application/json"
}
],
"body": {
"campaignId": "your_campaign_id",
"leads": [
{
"linkedinUrl": "{{iterator.linkedinUrl}}",
"firstName": "{{iterator.firstName}}",
"lastName": "{{iterator.lastName}}",
"company": "{{iterator.company}}"
}
]
}
}
```
### 3. Connection Accepted → Full Enrichment Flow
**Scenario Flow:**
```
[Webhook: connection.accepted]
→ [HTTP: Clearbit Enrichment]
→ [Data Store: Save enrichment]
→ [Set Variables]
→ [Salesforce: Update Contact]
→ [Slack: Notify team]
```
### 4. Multi-Channel Campaign Management
**Scenario Flow:**
```
[Webhook: lead.status.changed]
→ [Router]
→ [newStatus = REPLY_RECEIVED]:
[HTTP: PATCH /v1/leads/:id/status]
[Intercom: Tag User]
→ [newStatus = DONE]:
[HTTP: PATCH /v1/campaigns/:id (pause)]
[Slack: Campaign goal reached!]
```
## Setting Up Data Structures
Define the webhook payload structure for better mapping:
1. Click on the webhook module
2. Click **Add** next to Data Structure
3. Name it: "SendPilot Webhook Event"
4. Add fields:
```json theme={null}
{
"eventId": "string",
"eventType": "string",
"timestamp": "date",
"workspaceId": "string",
"data": {
"leadId": "string",
"campaignId": "string",
"linkedinUrl": "string",
"replyPreview": "string"
}
}
```
## HTTP Module Templates
### Add Leads to Campaign
```
Module: HTTP - Make a Request
URL: https://api.sendpilot.ai/v1/leads
Method: POST
Headers:
X-API-Key: {{YOUR_API_KEY}}
Content-Type: application/json
Body Type: Raw
Content Type: JSON
Request Content:
{
"campaignId": "{{campaignId}}",
"leads": [
{
"linkedinUrl": "{{linkedinUrl}}",
"firstName": "{{firstName}}",
"lastName": "{{lastName}}",
"company": "{{company}}"
}
]
}
```
### Update Lead Status
```
Module: HTTP - Make a Request
URL: https://api.sendpilot.ai/v1/leads/{{leadId}}/status
Method: PATCH
Headers:
X-API-Key: {{YOUR_API_KEY}}
Content-Type: application/json
Body Type: Raw
Content Type: JSON
Request Content:
{
"status": "DONE",
"note": "Converted via automation"
}
```
### Pause Campaign
```
Module: HTTP - Make a Request
URL: https://api.sendpilot.ai/v1/campaigns/{{campaignId}}
Method: PATCH
Headers:
X-API-Key: {{YOUR_API_KEY}}
Content-Type: application/json
Request Content:
{
"action": "pause"
}
```
### Get Campaign Details
```
Module: HTTP - Make a Request
URL: https://api.sendpilot.ai/v1/campaigns/{{campaignId}}
Method: GET
Headers:
X-API-Key: {{YOUR_API_KEY}}
```
## Using Routers
Routers let you handle different event types in one scenario:
```
[Webhook]
→ [Router]
→ Filter: eventType = message.sent
→ [Slack: "Message sent to {{leadId}}"]
→ Filter: eventType = message.received
→ [HubSpot: Create Task]
→ [Slack: "Reply from {{leadId}}!"]
→ Filter: eventType = connection.accepted
→ [Salesforce: Update Lead Status]
→ Fallback (no filter)
→ [Logger: Store event for analysis]
```
## Error Handling
Make.com provides robust error handling:
1. **Error Handler Module**: Add after any module
2. **Break**: Stop scenario on error
3. **Resume**: Continue with fallback value
4. **Commit**: Save data and continue
5. **Rollback**: Undo all changes
**Example:**
```
[HTTP Request]
→ Success: [Continue]
→ Error: [Error Handler]
→ [Slack: Alert ops team]
→ [Break]
```
## Best Practices
Store your API key in scenario variables or Team variables for easy management.
Filter webhook events early to save operations.
Cache enrichment data in Make.com Data Stores to avoid redundant API calls.
For bulk operations, schedule during off-peak hours.
Use Make.com's history feature to replay and debug scenarios.
## Scenario Blueprint
Here's a complete blueprint you can import:
```json theme={null}
{
"name": "SendPilot Reply Handler",
"flow": [
{
"id": 1,
"module": "gateway:CustomWebHook",
"mapper": {
"name": "SendPilot Events"
}
},
{
"id": 2,
"module": "builtin:BasicRouter"
},
{
"id": 3,
"module": "slack:CreateMessage",
"mapper": {
"channel": "#sales-leads",
"text": "🎉 Reply from {{1.data.linkedinUrl}}"
},
"filter": {
"name": "Reply Events",
"conditions": [
{
"a": "{{1.eventType}}",
"o": "text:equal",
"b": "message.received"
}
]
}
}
]
}
```
## Pricing Considerations
Make.com uses operations-based pricing:
* Each module execution = 1 operation
* Webhooks trigger 1 operation per event
* Use filters to minimize unnecessary operations
Use the **Ignore** module to test webhook payloads without consuming operations during development.
# n8n Integration
Source: https://docs.sendpilot.ai/use-cases/n8n
Self-hosted workflow automation with full control
n8n is an open-source workflow automation tool that you can self-host for complete data privacy and control. It offers powerful nodes for HTTP requests and webhooks to integrate with SendPilot.
## Why n8n?
* **Self-hosted**: Keep your data on your own servers
* **Open source**: Full transparency and customization
* **No vendor lock-in**: Export and own your workflows
* **Powerful**: Complex branching, loops, and error handling
## Setting Up SendPilot in n8n
### Step 1: Create a Webhook Node (Trigger)
1. Create a new workflow in n8n
2. Add a **Webhook** node as the trigger
3. Set method to `POST`
4. Copy the webhook URL (e.g., `https://your-n8n.com/webhook/abc123`)
### Step 2: Register Webhook in SendPilot
1. Go to SendPilot **Integrations** → **Webhooks**
2. Click **Add Webhook**
3. Paste your n8n webhook URL
4. Select events: `message.received`, `connection.accepted`, etc.
5. Save
### Step 3: Configure HTTP Request Node (Actions)
For calling SendPilot API, use the **HTTP Request** node:
```
Node: HTTP Request
Method: POST
URL: https://api.sendpilot.ai/v1/leads
Authentication: None (use headers)
Headers:
X-API-Key: YOUR_API_KEY
Content-Type: application/json
Body:
{
"campaignId": "{{$json.campaignId}}",
"leads": [...]
}
```
## Example Workflows
### 1. Lead Reply → Slack + HubSpot
```
[Webhook Trigger]
→ [IF: eventType == "message.received"]
→ [Slack: Send Message to #sales]
→ [HubSpot: Create Task]
```
**Webhook Node Configuration:**
* Path: `/sendpilot-events`
* Method: POST
**IF Node Configuration:**
* Value 1: `{{$json["eventType"]}}`
* Operation: Equal
* Value 2: `message.received`
**Slack Node Configuration:**
* Channel: `#sales-leads`
* Message: `🎉 Reply from {{$json["data"]["linkedinUrl"]}}: {{$json["data"]["replyPreview"]}}`
### 2. Google Sheet → Add Leads to Campaign
```
[Schedule Trigger (every hour)]
→ [Google Sheets: Read Rows]
→ [HTTP Request: POST /v1/leads]
→ [Google Sheets: Update Row (mark as processed)]
```
**HTTP Request Node:**
```json theme={null}
{
"method": "POST",
"url": "https://api.sendpilot.ai/v1/leads",
"headers": {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
"body": {
"campaignId": "your_campaign_id",
"leads": [
{
"linkedinUrl": "={{$json[\"LinkedIn URL\"]}}",
"firstName": "={{$json[\"First Name\"]}}",
"lastName": "={{$json[\"Last Name\"]}}",
"company": "={{$json[\"Company\"]}}"
}
]
}
}
```
### 3. Connection Accepted → Enrich + CRM Sync
```
[Webhook: connection.accepted]
→ [HTTP Request: Clearbit Enrichment]
→ [Merge: Combine data]
→ [Salesforce: Update Lead]
→ [Slack: Notify team]
```
### 4. Daily Campaign Report
```
[Schedule: Daily at 9am]
→ [HTTP Request: GET /v1/campaigns]
→ [Code: Calculate metrics]
→ [Slack: Send report]
```
**HTTP Request Node (Get Campaigns):**
```json theme={null}
{
"method": "GET",
"url": "https://api.sendpilot.ai/v1/campaigns",
"headers": {
"X-API-Key": "YOUR_API_KEY"
}
}
```
## Reusable Credential Setup
Create a custom credential for SendPilot API:
1. Go to **Credentials** in n8n
2. Click **Add Credential**
3. Select **Header Auth**
4. Configure:
* Name: `SendPilot API`
* Header Name: `X-API-Key`
* Header Value: `YOUR_API_KEY`
Now you can select this credential in HTTP Request nodes.
## Error Handling
n8n provides excellent error handling:
```
[HTTP Request]
→ Success: [Continue workflow]
→ Error: [Slack: Send error alert]
→ [Log error to database]
```
Add an **Error Trigger** workflow to catch failures:
```
[Error Trigger]
→ [Slack: Alert #ops channel]
→ [Save to error log]
```
## Complete Workflow JSON
Here's a complete workflow you can import into n8n:
```json theme={null}
{
"name": "SendPilot Reply Handler",
"nodes": [
{
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [250, 300],
"parameters": {
"path": "sendpilot-events",
"httpMethod": "POST"
}
},
{
"name": "Filter Replies",
"type": "n8n-nodes-base.if",
"position": [450, 300],
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$json[\"eventType\"]}}",
"value2": "message.received"
}
]
}
}
},
{
"name": "Slack",
"type": "n8n-nodes-base.slack",
"position": [650, 250],
"parameters": {
"channel": "#sales-leads",
"text": "🎉 New reply from {{$json[\"data\"][\"linkedinUrl\"]}}!"
}
}
]
}
```
## Best Practices
Store your API key in n8n environment variables, not hardcoded in workflows.
Use the HTTP Request node's built-in retry options for resilience.
Add logging nodes to track workflow execution for debugging.
Use n8n's "Execute Workflow" feature to test with real webhook payloads.
## Deployment
### Self-Hosted
```bash theme={null}
docker run -it --rm \
-p 5678:5678 \
-e N8N_BASIC_AUTH_ACTIVE=true \
-e N8N_BASIC_AUTH_USER=admin \
-e N8N_BASIC_AUTH_PASSWORD=secure_password \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n
```
### n8n Cloud
Use [n8n.cloud](https://n8n.cloud) for a managed solution with the same features.
# Use Cases
Source: https://docs.sendpilot.ai/use-cases/overview
Integrate SendPilot with your favorite automation tools
SendPilot's API and webhooks enable powerful integrations with automation platforms. Build custom workflows that connect your LinkedIn outreach with your CRM, marketing tools, and business processes.
## Popular Integrations
Connect SendPilot to 5,000+ apps with no-code automation
Self-hosted workflow automation with full control
Visual automation platform for complex workflows
## Common Automation Workflows
### Lead Enrichment
When a new lead is added to SendPilot, automatically:
* Enrich lead data from Clearbit or Apollo
* Add the lead to your CRM (Salesforce, HubSpot, Pipedrive)
* Notify your sales team on Slack
### Reply Handling
When a lead replies to your message:
* Create a task in your CRM
* Send a Slack notification to the assigned rep
* Update lead status in your database
### Campaign Management
Based on external triggers:
* Pause campaigns when daily goals are met
* Add leads from form submissions
* Sync lead statuses with your CRM
### Analytics & Reporting
Track outreach performance:
* Log all events to Google Sheets or Airtable
* Send daily digest to Slack
* Update dashboards in real-time
## Building Your Integration
### Step 1: Get Your API Key
1. Log in to SendPilot dashboard
2. Navigate to **Integrations** → **API Keys**
3. Click **Create API Key**
4. Copy your API key securely
### Step 2: Set Up Webhooks
1. Go to **Integrations** → **Webhooks**
2. Add your automation platform's webhook URL
3. Select the events you want to receive
### Step 3: Build Your Workflow
Use the guides below to set up your integration:
* [Zapier Integration Guide](/use-cases/zapier)
* [n8n Integration Guide](/use-cases/n8n)
* [Make.com Integration Guide](/use-cases/make)
## API Endpoints for Automation
| Use Case | Endpoint | Method |
| ----------------------- | ---------------------------- | ------- |
| Add leads from form | `POST /v1/leads` | API |
| Sync lead status to CRM | `PATCH /v1/leads/:id/status` | API |
| Pause campaign on goal | `PATCH /v1/campaigns/:id` | API |
| Track replies | `message.received` | Webhook |
| Track connections | `connection.accepted` | Webhook |
## Need Help?
Complete API documentation
All available webhook events
# Zapier Integration
Source: https://docs.sendpilot.ai/use-cases/zapier
Connect SendPilot to 5,000+ apps with Zapier
Zapier allows you to connect SendPilot with thousands of apps including Salesforce, HubSpot, Slack, Google Sheets, and more — all without writing any code.
## Overview
With Zapier, you can:
* **Triggers**: React to SendPilot events (lead replies, connections, status changes)
* **Actions**: Control SendPilot from other apps (add leads, update status, send messages)
## Setting Up SendPilot in Zapier
### Step 1: Create a Webhook Trigger
1. Create a new Zap in Zapier
2. Select **Webhooks by Zapier** as the trigger app
3. Choose **Catch Hook** as the trigger event
4. Copy the webhook URL provided by Zapier
### Step 2: Register Webhook in SendPilot
1. Go to SendPilot **Integrations** → **Webhooks**
2. Click **Add Webhook**
3. Paste your Zapier webhook URL
4. Select the events you want to trigger on (e.g., `message.received`)
5. Save the webhook
### Step 3: Test the Connection
1. In Zapier, click **Test trigger**
2. Trigger an event in SendPilot (e.g., simulate a reply)
3. Zapier will capture the test data
## Common Zapier Workflows
### 1. New Reply → Slack Notification
When a lead replies, notify your sales team instantly.
```
Trigger: Webhooks by Zapier (Catch Hook)
↓
Filter: Event Type = "message.received"
↓
Action: Slack - Send Channel Message
- Channel: #sales-leads
- Message: "🎉 New reply from {{linkedinUrl}}!"
```
### 2. New Reply → HubSpot Task
Create a follow-up task when a lead engages.
```
Trigger: Webhooks by Zapier (Catch Hook)
↓
Filter: Event Type = "message.received"
↓
Action: HubSpot - Create Task
- Title: "Follow up with LinkedIn lead"
- Due Date: Tomorrow
- Notes: "Lead replied: {{replyPreview}}"
```
### 3. Form Submission → Add Lead to Campaign
Add leads from your website form to a SendPilot campaign.
```
Trigger: Typeform - New Entry
↓
Action: Webhooks by Zapier - POST
- URL: https://api.sendpilot.ai/v1/leads
- Headers: X-API-Key: YOUR_API_KEY
- Body: {
"campaignId": "your_campaign_id",
"leads": [{
"linkedinUrl": "{{linkedin_url_field}}",
"firstName": "{{first_name}}",
"email": "{{email}}"
}]
}
```
### 4. Connection Accepted → CRM Update
Update your CRM when a lead accepts your connection.
```
Trigger: Webhooks by Zapier (Catch Hook)
↓
Filter: Event Type = "connection.accepted"
↓
Action: Salesforce - Update Record
- Object: Lead
- Find by: LinkedIn URL
- Status: "Connected"
```
### 5. Lead Status Changed → Google Sheet Log
Log all lead activity to a spreadsheet.
```
Trigger: Webhooks by Zapier (Catch Hook)
↓
Filter: Event Type = "lead.status.changed"
↓
Action: Google Sheets - Create Row
- Lead ID: {{leadId}}
- LinkedIn URL: {{linkedinUrl}}
- Previous Status: {{previousStatus}}
- New Status: {{newStatus}}
- Timestamp: {{timestamp}}
```
## Using SendPilot API Actions
To call SendPilot API from Zapier, use **Webhooks by Zapier** (Custom Request):
### Add Leads Example
```
Action: Webhooks by Zapier - Custom Request
Method: POST
URL: https://api.sendpilot.ai/v1/leads
Headers:
X-API-Key: YOUR_API_KEY
Content-Type: application/json
Body (JSON):
{
"campaignId": "campaign_id_here",
"leads": [
{
"linkedinUrl": "{{LinkedIn URL}}",
"firstName": "{{First Name}}",
"lastName": "{{Last Name}}",
"company": "{{Company}}"
}
]
}
```
### Update Lead Status Example
```
Action: Webhooks by Zapier - Custom Request
Method: PATCH
URL: https://api.sendpilot.ai/v1/leads/{{lead_id}}/status
Headers:
X-API-Key: YOUR_API_KEY
Content-Type: application/json
Body (JSON):
{
"status": "REPLY_RECEIVED"
}
```
## Best Practices
Add a Filter step to process only specific events. Filter by `eventType` to handle different events differently.
Add error handling paths in your Zaps to catch API failures.
Zapier has its own rate limits. Use delays between actions if processing many events.
Always test with real data before going live.
## Webhook Payload Reference
When SendPilot sends a webhook to Zapier, the payload looks like:
```json theme={null}
{
"eventId": "evt_123456789",
"eventType": "message.received",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123",
"data": {
"leadId": "lead_xyz",
"campaignId": "camp_123",
"linkedinUrl": "https://linkedin.com/in/john-doe",
"replyPreview": "Thanks for reaching out..."
}
}
```
Use these fields in your Zapier actions with the `{{field_name}}` syntax.
# campaign.paused
Source: https://docs.sendpilot.ai/webhooks/events/campaign-paused
Triggered when a campaign is paused
This event is triggered when a campaign is paused, either manually through the dashboard/API or automatically due to system conditions.
## When This Event Fires
* Campaign is paused via the [Update Campaign API](/api-reference/endpoint/patch-campaign)
* Campaign is manually paused through the dashboard
* Campaign transitions to `PAUSED` status
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "campaign.paused",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"campaignId": "camp_xyz789",
"campaignName": "Q1 Tech Founders Outreach",
"pausedAt": "2024-02-24T10:30:00.000Z",
"pausedBy": "api"
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------- | ------ | ------------------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `campaign.paused` |
| `timestamp` | string | ISO 8601 timestamp when the event occurred |
| `workspaceId` | string | Your workspace ID |
| `data.campaignId` | string | Unique campaign identifier |
| `data.campaignName` | string | Human-readable campaign name |
| `data.pausedAt` | string | ISO 8601 timestamp when the campaign was paused |
| `data.pausedBy` | string | How the campaign was paused: `api`, `user`, or `system` |
## Use Cases
Track when campaigns stop running
Alert team when campaigns are paused unexpectedly
Track campaign uptime and pauses
Trigger follow-up actions when campaigns pause
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', async (req, res) => {
const event = req.body;
if (event.eventType === 'campaign.paused') {
const { campaignId, campaignName, pausedAt, pausedBy } = event.data;
// Log the pause event
await analytics.track('campaign_paused', {
campaignId,
campaignName,
pausedBy
});
// Alert if paused by system (might indicate an issue)
if (pausedBy === 'system') {
await slack.postMessage({
channel: '#alerts',
text: `⚠️ Campaign auto-paused by system!\n` +
`Name: ${campaignName}\n` +
`Time: ${pausedAt}\n` +
`Please check the campaign for issues.`
});
} else {
await slack.postMessage({
channel: '#campaigns',
text: `⏸️ Campaign paused\n` +
`Name: ${campaignName}\n` +
`Paused by: ${pausedBy}`
});
}
console.log(`Campaign ${campaignName} paused by ${pausedBy}`);
}
res.status(200).send('OK');
});
```
When a campaign is paused, no new actions will be taken for leads. In-progress actions may still complete. The campaign can be resumed via the API or dashboard.
# campaign.resumed
Source: https://docs.sendpilot.ai/webhooks/events/campaign-resumed
Triggered when a paused campaign is resumed
This event is triggered when a paused campaign is resumed and continues processing leads.
## When This Event Fires
* Campaign is resumed via the [Update Campaign API](/api-reference/endpoint/patch-campaign)
* Campaign is manually resumed through the dashboard
* Campaign transitions from `PAUSED` to `STARTED` status
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "campaign.resumed",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"campaignId": "camp_xyz789",
"campaignName": "Q1 Tech Founders Outreach",
"resumedAt": "2024-02-24T10:30:00.000Z",
"resumedBy": "api"
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------- | ------ | ------------------------------------------------ |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `campaign.resumed` |
| `timestamp` | string | ISO 8601 timestamp when the event occurred |
| `workspaceId` | string | Your workspace ID |
| `data.campaignId` | string | Unique campaign identifier |
| `data.campaignName` | string | Human-readable campaign name |
| `data.resumedAt` | string | ISO 8601 timestamp when the campaign was resumed |
| `data.resumedBy` | string | How the campaign was resumed: `api` or `user` |
## Use Cases
Track when campaigns resume operation
Alert team when campaigns are back online
Track campaign uptime and activity periods
Sync campaign status with external systems
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', async (req, res) => {
const event = req.body;
if (event.eventType === 'campaign.resumed') {
const { campaignId, campaignName, resumedAt, resumedBy } = event.data;
// Log the resume event
await analytics.track('campaign_resumed', {
campaignId,
campaignName,
resumedBy
});
// Notify team
await slack.postMessage({
channel: '#campaigns',
text: `▶️ Campaign resumed!\n` +
`Name: ${campaignName}\n` +
`Resumed by: ${resumedBy}\n` +
`Time: ${resumedAt}`
});
// Update external tracking systems
await externalCRM.updateCampaignStatus(campaignId, 'active');
console.log(`Campaign ${campaignName} resumed by ${resumedBy}`);
}
res.status(200).send('OK');
});
```
When a campaign is resumed, it continues processing leads from where it left off. The sequence position for each lead is preserved.
# campaign.started
Source: https://docs.sendpilot.ai/webhooks/events/campaign-started
Triggered when a campaign is started
This event is triggered when a campaign begins execution, either from initial launch or when resuming from a paused state.
## When This Event Fires
* A new campaign is launched
* A campaign begins processing leads
* Campaign transitions to `STARTED` status
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "campaign.started",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"campaignId": "camp_xyz789",
"campaignName": "Q1 Tech Founders Outreach",
"totalLeads": 150,
"startedAt": "2024-02-24T10:30:00.000Z"
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------- | ------ | -------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `campaign.started` |
| `timestamp` | string | ISO 8601 timestamp when the event occurred |
| `workspaceId` | string | Your workspace ID |
| `data.campaignId` | string | Unique campaign identifier |
| `data.campaignName` | string | Human-readable campaign name |
| `data.totalLeads` | number | Total number of leads in the campaign |
| `data.startedAt` | string | ISO 8601 timestamp when the campaign started |
## Use Cases
Track when campaigns go live
Alert team members that outreach has begun
Log campaign start times for analytics
Track active campaigns for capacity planning
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', async (req, res) => {
const event = req.body;
if (event.eventType === 'campaign.started') {
const { campaignId, campaignName, totalLeads, startedAt } = event.data;
// Notify team
await slack.postMessage({
channel: '#campaigns',
text: `🚀 Campaign started!\n` +
`Name: ${campaignName}\n` +
`Leads: ${totalLeads}\n` +
`Started at: ${startedAt}`
});
// Log for analytics
await analytics.track('campaign_started', {
campaignId,
campaignName,
totalLeads
});
console.log(`Campaign ${campaignName} started with ${totalLeads} leads`);
}
res.status(200).send('OK');
});
```
# connection_request.accepted
Source: https://docs.sendpilot.ai/webhooks/events/connection-request-accepted
Triggered when a lead accepts your connection request
This event is triggered when a lead accepts your LinkedIn connection request. This is a key milestone indicating the lead is now a 1st-degree connection and can receive direct messages.
## When This Event Fires
* A lead clicks "Accept" on your connection request
* The acceptance is detected by SendPilot
* The lead's status changes to `CONNECTION_ACCEPTED`
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "connection_request.accepted",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"leadId": "lead_abc123",
"campaignId": "camp_xyz789",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"senderId": "sender_def456",
"acceptedAt": "2024-02-24T10:30:00.000Z"
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------ | ------ | --------------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `connection_request.accepted` |
| `timestamp` | string | ISO 8601 timestamp when the event was detected |
| `workspaceId` | string | Your workspace ID |
| `data.leadId` | string | The lead who accepted the connection |
| `data.campaignId` | string | The campaign this lead belongs to |
| `data.linkedinUrl` | string | LinkedIn profile URL of the lead |
| `data.senderId` | string | LinkedIn sender account whose request was accepted |
| `data.acceptedAt` | string | ISO 8601 timestamp when the connection was accepted |
## Use Cases
Notify sales team of new warm connections
Update lead status in your CRM
Track connection acceptance rates
Trigger welcome message or nurture sequence
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', async (req, res) => {
const event = req.body;
if (event.eventType === 'connection_request.accepted') {
const { leadId, campaignId, linkedinUrl, acceptedAt } = event.data;
// Update CRM
await crm.updateLead(leadId, {
status: 'connected',
connectionAcceptedAt: acceptedAt
});
// Notify sales team
await slack.postMessage({
channel: '#new-connections',
text: `🤝 New connection accepted!\n` +
`Profile: ${linkedinUrl}\n` +
`Campaign: ${campaignId}`
});
// The lead is now a 1st-degree connection - follow-up messages
// will be sent automatically by the campaign sequence
console.log(`Connection accepted by ${linkedinUrl}`);
}
res.status(200).send('OK');
});
```
When a connection is accepted, the lead becomes a 1st-degree connection. The campaign will automatically proceed to send follow-up messages according to the sequence.
# connection_request.sent
Source: https://docs.sendpilot.ai/webhooks/events/connection-request-sent
Triggered when a connection request is sent to a lead
This event is triggered when a LinkedIn connection request is successfully sent to a lead through campaign automation.
## When This Event Fires
* Campaign automation sends a connection request
* The connection request is successfully delivered to LinkedIn
* The lead's status changes to `CONNECTION_SENT`
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "connection_request.sent",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"leadId": "lead_abc123",
"campaignId": "camp_xyz789",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"senderId": "sender_def456",
"note": "Hi John, I noticed we share an interest in SaaS growth..."
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------ | ------ | --------------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `connection_request.sent` |
| `timestamp` | string | ISO 8601 timestamp when the request was sent |
| `workspaceId` | string | Your workspace ID |
| `data.leadId` | string | The lead who received the connection request |
| `data.campaignId` | string | The campaign this lead belongs to |
| `data.linkedinUrl` | string | LinkedIn profile URL of the lead |
| `data.senderId` | string | LinkedIn sender account that sent the request |
| `data.note` | string | Connection note if included (LinkedIn Premium only) |
## Use Cases
Track daily connection request volume
Log outreach activity in your CRM
Create follow-up tasks if no response after X days
Build custom reports on outreach activity
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', async (req, res) => {
const event = req.body;
if (event.eventType === 'connection_request.sent') {
const { leadId, campaignId, linkedinUrl, note } = event.data;
// Log to CRM
await crm.logActivity({
leadId,
type: 'connection_request_sent',
message: note || 'Connection request sent (no note)',
campaignId
});
// Schedule a follow-up task if no response in 7 days
await taskQueue.schedule({
type: 'check_connection_status',
leadId,
executeAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
console.log(`Connection request sent to ${linkedinUrl}`);
}
res.status(200).send('OK');
});
```
The `note` field is only populated for LinkedIn Premium accounts that include a message with their connection requests. Free accounts will have this field empty or undefined.
# lead_database.search.completed
Source: https://docs.sendpilot.ai/webhooks/events/lead-database-search-completed
Triggered when a Lead Database search completes
This event is triggered when a Lead Database (A-Leads) bulk search finishes processing and all leads are stored in the database.
## When This Event Fires
* A Lead Database search initiated via the API completes
* All leads from the search have been processed and stored
* The search status changes to `completed`
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "lead_database.search.completed",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"search_id": "search_abc123",
"search_name": "Tech Founders Q1 2024",
"total_leads_found": 150,
"requested_limit": 200,
"started_at": "2024-02-24T10:25:00.000Z",
"completed_at": "2024-02-24T10:30:00.000Z",
"duration_ms": 300000,
"filters": {
"job_titles": ["CEO", "CTO", "Founder"],
"locations": ["United States", "Canada"],
"industries": ["Technology", "Software"],
"company_sizes": ["11-50", "51-200"]
},
"leads": [
{
"id": "lead_xyz789",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"email": "john.doe@example.com",
"phone": "+1-555-123-4567",
"linkedin_url": "https://www.linkedin.com/in/johndoe",
"job_title": "CEO",
"company": "TechCorp Inc",
"location": "San Francisco, CA",
"country": "United States",
"industry": "Technology",
"seniority": "C-Level",
"company_linkedin_url": "https://www.linkedin.com/company/techcorp",
"website": "https://techcorp.com",
"employees": "51-200"
}
],
"results_url": "/v1/lead-database/searches/search_abc123/results"
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------------ | ------ | -------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `lead_database.search.completed` |
| `timestamp` | string | ISO 8601 timestamp when the search completed |
| `workspaceId` | string | Your workspace ID |
| `data.search_id` | string | Unique identifier for the search |
| `data.search_name` | string | Name you provided for the search |
| `data.total_leads_found` | number | Total number of leads found |
| `data.requested_limit` | number | Maximum leads you requested |
| `data.started_at` | string | When the search started |
| `data.completed_at` | string | When the search completed |
| `data.duration_ms` | number | Processing time in milliseconds |
| `data.filters` | object | Filters used for the search |
| `data.leads` | array | Array of all found leads with full data |
| `data.results_url` | string | API endpoint to fetch results |
### Lead Object Fields
Each lead in the `leads` array contains:
| Field | Type | Description |
| ---------------------- | ------ | ---------------------- |
| `id` | string | Unique lead identifier |
| `first_name` | string | Lead's first name |
| `last_name` | string | Lead's last name |
| `full_name` | string | Lead's full name |
| `email` | string | Email address |
| `phone` | string | Phone number |
| `linkedin_url` | string | LinkedIn profile URL |
| `job_title` | string | Current job title |
| `company` | string | Current company name |
| `location` | string | Location |
| `country` | string | Country |
| `industry` | string | Industry |
| `seniority` | string | Seniority level |
| `company_linkedin_url` | string | Company LinkedIn URL |
| `website` | string | Company website |
| `employees` | string | Company size |
## Use Cases
Automatically import leads into your CRM when search completes
Trigger campaign creation with the found leads
Alert your team when lead lists are ready
Trigger additional enrichment workflows
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', (req, res) => {
const event = req.body;
if (event.eventType === 'lead_database.search.completed') {
const { search_id, search_name, total_leads_found, leads } = event.data;
// Import leads to CRM
for (const lead of leads) {
await crm.createLead({
firstName: lead.first_name,
lastName: lead.last_name,
email: lead.email,
company: lead.company,
title: lead.job_title,
linkedinUrl: lead.linkedin_url,
source: `SendPilot Search: ${search_name}`
});
}
// Send notification
await slack.notify({
channel: '#lead-generation',
text: `✅ Lead Database search "${search_name}" completed!\nFound ${total_leads_found} leads.`
});
console.log(`Search ${search_id} completed with ${total_leads_found} leads`);
}
res.status(200).send('OK');
});
```
The `leads` array contains all found leads with complete data. For large result sets, consider processing leads asynchronously.
# lead_extractor.job.completed
Source: https://docs.sendpilot.ai/webhooks/events/lead-extractor-job-completed
Triggered when a Lead Extractor campaign completes
This event is triggered when a Lead Extractor (LinkedIn scraper) campaign finishes extracting and optionally enriching leads.
## When This Event Fires
* A Lead Extractor campaign initiated via the API completes
* All leads have been extracted from the provided LinkedIn search URLs
* Enrichment is complete (if enabled)
* The campaign status changes to `FINISHED`
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "lead_extractor.job.completed",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"campaign_id": "camp_abc123",
"campaign_name": "Tech Startup Founders",
"campaign_type": "SEARCH",
"mode": "with_enrichment",
"search_urls": [
"https://www.linkedin.com/search/results/people/?keywords=CEO%20startup"
],
"total_leads_extracted": 100,
"total_leads_enriched": 95,
"requested_limit": 100,
"started_at": "2024-02-24T10:00:00.000Z",
"completed_at": "2024-02-24T10:30:00.000Z",
"duration_ms": 1800000,
"credits_used": {
"extraction": 100,
"enrichment": 100,
"total": 200
},
"leads": [
{
"id": "lead_xyz789",
"linkedin_identifier": "johndoe",
"linkedin_url": "https://www.linkedin.com/in/johndoe",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"headline": "CEO at TechCorp | Building the future of AI",
"summary": "Experienced technology executive with 15+ years...",
"location": "San Francisco Bay Area",
"city": "San Francisco",
"country": "United States",
"profile_picture_url": "https://media.licdn.com/...",
"company": "TechCorp Inc",
"job_position": "CEO",
"email": "john.doe@techcorp.com",
"phone": "+1-555-123-4567",
"connections": 500,
"followers": 12500,
"experience": [
{
"title": "CEO",
"company": "TechCorp Inc",
"duration": "2020 - Present"
}
],
"education": [
{
"school": "Stanford University",
"degree": "MBA"
}
],
"skills": ["Leadership", "Strategy", "AI/ML"]
}
],
"results_url": "/v1/lead-extractor/campaigns/camp_abc123/results"
}
}
```
## Payload Fields
| Field | Type | Description |
| ---------------------------- | ------ | ------------------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `lead_extractor.job.completed` |
| `timestamp` | string | ISO 8601 timestamp when the job completed |
| `workspaceId` | string | Your workspace ID |
| `data.campaign_id` | string | Unique identifier for the campaign |
| `data.campaign_name` | string | Name you provided for the campaign |
| `data.campaign_type` | string | Type: `SEARCH`, `SALES_NAVIGATOR`, or `POST_ENGAGEMENT` |
| `data.mode` | string | `extraction_only` or `with_enrichment` |
| `data.search_urls` | array | LinkedIn search URLs that were scraped |
| `data.total_leads_extracted` | number | Total leads extracted |
| `data.total_leads_enriched` | number | Leads with enrichment data (0 if extraction\_only) |
| `data.requested_limit` | number | Maximum leads you requested |
| `data.started_at` | string | When the campaign started |
| `data.completed_at` | string | When the campaign completed |
| `data.duration_ms` | number | Processing time in milliseconds |
| `data.credits_used` | object | Credits consumed breakdown |
| `data.leads` | array | Array of all extracted leads with full data |
| `data.results_url` | string | API endpoint to fetch results |
### Lead Object Fields
Each lead in the `leads` array contains extensive profile data:
| Field | Type | Description |
| --------------------- | ------ | ------------------------- |
| `id` | string | Unique lead identifier |
| `linkedin_identifier` | string | LinkedIn username |
| `linkedin_url` | string | Full LinkedIn profile URL |
| `first_name` | string | First name |
| `last_name` | string | Last name |
| `full_name` | string | Full name |
| `headline` | string | LinkedIn headline |
| `summary` | string | Profile summary/about |
| `location` | string | Location string |
| `city` | string | City |
| `country` | string | Country |
| `profile_picture_url` | string | Profile photo URL |
| `company` | string | Current company |
| `job_position` | string | Current job title |
| `email` | string | Email (if enriched) |
| `phone` | string | Phone (if enriched) |
| `connections` | number | Connection count |
| `followers` | number | Follower count |
| `experience` | array | Work experience history |
| `education` | array | Education history |
| `skills` | array | Listed skills |
## Use Cases
Automatically import enriched leads into your CRM
Trigger LinkedIn outreach campaigns with extracted leads
Score and prioritize leads based on profile data
Keep your lead database in sync with LinkedIn
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', (req, res) => {
const event = req.body;
if (event.eventType === 'lead_extractor.job.completed') {
const {
campaign_id,
campaign_name,
total_leads_extracted,
credits_used,
leads
} = event.data;
// Import leads to CRM with enriched data
for (const lead of leads) {
await crm.createOrUpdateLead({
firstName: lead.first_name,
lastName: lead.last_name,
email: lead.email,
phone: lead.phone,
company: lead.company,
title: lead.job_position,
linkedinUrl: lead.linkedin_url,
headline: lead.headline,
location: lead.location,
source: `SendPilot Extractor: ${campaign_name}`
});
}
// Send notification with credits summary
await slack.notify({
channel: '#lead-generation',
text: `✅ Lead Extractor "${campaign_name}" completed!\n` +
`Extracted: ${total_leads_extracted} leads\n` +
`Credits used: ${credits_used.total}`
});
console.log(`Campaign ${campaign_id} completed with ${total_leads_extracted} leads`);
}
res.status(200).send('OK');
});
```
The `leads` array contains all extracted leads with complete profile data. For campaigns with enrichment enabled, email and phone fields will be populated when available.
# lead.updated
Source: https://docs.sendpilot.ai/webhooks/events/lead-updated
Triggered when a lead's status changes
This event is triggered when a lead's status changes, whether through campaign automation or manual updates via the API.
## When This Event Fires
* Lead status is updated via the [Update Lead Status API](/api-reference/endpoint/patch-lead-status)
* Campaign automation changes lead status
* Lead progresses through the campaign sequence
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "lead.updated",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"leadId": "lead_abc123",
"campaignId": "camp_xyz789",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"previousStatus": "REPLY_RECEIVED",
"newStatus": "OPPORTUNITY"
}
}
```
## Payload Fields
| Field | Type | Description |
| --------------------- | ------ | ------------------------------------------ |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `lead.updated` |
| `timestamp` | string | ISO 8601 timestamp when the status changed |
| `workspaceId` | string | Your workspace ID |
| `data.leadId` | string | The lead whose status changed |
| `data.campaignId` | string | The campaign this lead belongs to |
| `data.linkedinUrl` | string | LinkedIn profile URL of the lead |
| `data.previousStatus` | string | Status before the change |
| `data.newStatus` | string | New status after the change |
## Campaign Status Values
| Status | Description |
| --------------------- | ---------------------------------- |
| `PENDING` | Lead added but no action taken yet |
| `PROCESSING` | Lead is being processed |
| `CONNECTION_SENT` | Connection request sent |
| `CONNECTION_ACCEPTED` | Connection accepted by lead |
| `MESSAGE_SENT` | Follow-up message sent |
| `REPLY_RECEIVED` | Lead has replied |
| `FOLLOWUP_SENT` | Follow-up message sent |
| `BLOCKED` | Lead blocked the sender |
| `PROFILE_UNREACHABLE` | Profile is unreachable |
| `RATE_LIMITED` | Rate limited by LinkedIn |
| `FAILED` | Action failed |
| `SUCCESS` | Campaign completed successfully |
| `UNSUBSCRIBED` | Lead unsubscribed |
| `IRRELEVANT` | Lead marked as irrelevant |
| `SKIPPED` | Lead was skipped |
| `DONE` | Lead journey completed |
| `MEETING_BOOKED` | Meeting has been booked |
| `OPPORTUNITY` | Marked as sales opportunity |
| `LIKED_POST` | Lead liked a post |
## Custom Lead Status Values
| Status | Description |
| ----------------------------- | ------------------------------------- |
| `LEAD` | New lead (default) |
| `INTERESTED` | Lead has shown interest |
| `MEETING_BOOKED` | Meeting has been scheduled |
| `MEETING_COMPLETE_NOT_CLOSED` | Meeting completed but deal not closed |
| `CLOSED` | Deal closed/won |
| `WRONG_PERSON` | Wrong contact/person |
| `NOT_INTERESTED` | Lead is not interested |
| `NO_RESPONSE` | No response received |
## Use Cases
Keep your CRM in sync with lead status changes
Track leads through your sales pipeline
Alert team when leads become opportunities
Trigger external workflows based on status
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', async (req, res) => {
const event = req.body;
if (event.eventType === 'lead.updated') {
const { leadId, previousStatus, newStatus, linkedinUrl } = event.data;
// Sync status to CRM
await crm.updateLead(leadId, {
status: mapToCustomStatus(newStatus),
lastStatusChange: event.timestamp
});
// Alert on high-value transitions
if (newStatus === 'OPPORTUNITY') {
await slack.postMessage({
channel: '#sales-opportunities',
text: `🎯 New opportunity!\n` +
`Lead: ${linkedinUrl}\n` +
`Previous status: ${previousStatus}`
});
}
// Handle negative outcomes
if (newStatus === 'NOT_INTERESTED') {
await analytics.track('lead_lost', {
leadId,
previousStatus
});
}
console.log(`Lead ${leadId}: ${previousStatus} → ${newStatus}`);
}
res.status(200).send('OK');
});
function mapToCustomStatus(sendpilotStatus) {
const mapping = {
'PENDING': 'new',
'CONNECTION_SENT': 'contacted',
'CONNECTION_ACCEPTED': 'connected',
'MESSAGE_SENT': 'in_conversation',
'REPLY_RECEIVED': 'engaged',
'OPPORTUNITY': 'qualified',
'NOT_INTERESTED': 'closed_lost',
'DONE': 'closed_won'
};
return mapping[sendpilotStatus] || 'unknown';
}
```
Status changes can be frequent as leads progress through campaign sequences. Consider batching or debouncing if you're making external API calls on each status change.
# message.sent
Source: https://docs.sendpilot.ai/webhooks/events/message-sent
Triggered when a message is sent to a lead
This event is triggered when a LinkedIn message is successfully sent to a lead, either through campaign automation or via the API.
## When This Event Fires
* Campaign automation sends a follow-up message
* Message is sent via the [Send Message API](/api-reference/endpoint/post-send-message)
* A message is successfully delivered to the lead's LinkedIn inbox
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "message.sent",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"leadId": "lead_abc123",
"campaignId": "camp_xyz789",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"senderId": "sender_def456",
"message": "Hi John! Thank you for connecting. I wanted to reach out about our new product that could help your team increase productivity by 40%. Would you be open to a quick 15-minute call this week?",
"sequenceStep": 2
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------- | ------ | --------------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `message.sent` |
| `timestamp` | string | ISO 8601 timestamp when the message was sent |
| `workspaceId` | string | Your workspace ID |
| `data.leadId` | string | The lead who received the message |
| `data.campaignId` | string | The campaign this lead belongs to |
| `data.linkedinUrl` | string | LinkedIn profile URL of the lead |
| `data.senderId` | string | LinkedIn sender account that sent the message |
| `data.message` | string | Full message content that was sent |
| `data.sequenceStep` | number | Which step in the campaign sequence (if applicable) |
## Use Cases
Log message activity in your CRM to maintain conversation history
Track message delivery and sequence progression
Alert team members when outreach is sent
Build an audit log of all outreach activity
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', (req, res) => {
const event = req.body;
if (event.eventType === 'message.sent') {
const { leadId, campaignId, message, sequenceStep } = event.data;
// Log to your CRM
await crm.logActivity({
leadId,
type: 'linkedin_message_sent',
content: message,
metadata: { campaignId, sequenceStep }
});
console.log(`Message sent to lead ${leadId} (step ${sequenceStep})`);
}
res.status(200).send('OK');
});
```
The `message` field contains the full message content that was sent to the lead.
# reply.received
Source: https://docs.sendpilot.ai/webhooks/events/reply-received
Triggered when a reply is received from a lead
This event is triggered when a lead replies to your LinkedIn message.
## When This Event Fires
* A lead responds to a campaign message
* A lead sends a new message in an existing conversation
* Any inbound message is received from a lead
## Payload
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "reply.received",
"timestamp": "2024-02-24T14:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
"leadId": "lead_abc123",
"campaignId": "camp_xyz789",
"linkedinUrl": "https://www.linkedin.com/in/john-doe",
"senderId": "sender_def456",
"reply": "Hi! Thanks for reaching out. I'd be happy to learn more about your product. How about we schedule a call for next Tuesday at 2pm?"
}
}
```
## Payload Fields
| Field | Type | Description |
| ------------------ | ------ | ----------------------------------------------- |
| `eventId` | string | Unique event identifier for idempotency |
| `eventType` | string | Always `reply.received` |
| `timestamp` | string | ISO 8601 timestamp when the reply was received |
| `workspaceId` | string | Your workspace ID |
| `data.leadId` | string | The lead who sent the reply |
| `data.campaignId` | string | The campaign this lead belongs to |
| `data.linkedinUrl` | string | LinkedIn profile URL of the lead |
| `data.senderId` | string | LinkedIn sender account that received the reply |
| `data.reply` | string | Full reply content from the lead |
## Use Cases
Log replies in your CRM and update lead status
Alert sales team immediately when leads respond
Analyze reply sentiment and intent automatically
Trigger automated follow-up workflows
## Example Handler
```javascript theme={null}
app.post('/webhooks/sendpilot', (req, res) => {
const event = req.body;
if (event.eventType === 'reply.received') {
const { leadId, campaignId, reply } = event.data;
// Update lead status in CRM
await crm.updateLead(leadId, {
status: 'replied',
lastReply: reply,
repliedAt: event.timestamp
});
// Send Slack notification
await slack.notify({
channel: '#sales-replies',
text: `🎉 New reply from lead ${leadId}:\n"${reply}"`
});
console.log(`Reply received from lead ${leadId}`);
}
res.status(200).send('OK');
});
```
The `reply` field contains the full message content from the lead.
# Webhooks Overview
Source: https://docs.sendpilot.ai/webhooks/overview
Receive real-time notifications when events occur in SendPilot
Webhooks allow you to receive real-time HTTP notifications when events occur in your SendPilot workspace. Instead of polling the API, webhooks push data to your server as events happen.
## How Webhooks Work
```mermaid theme={null}
sequenceDiagram
participant SP as SendPilot
participant WH as Your Webhook Endpoint
participant App as Your Application
SP->>WH: POST event payload
WH->>App: Process event
App->>WH: Return 200 OK
WH->>SP: Acknowledge receipt
```
1. An event occurs in SendPilot (e.g., a lead replies to a message)
2. SendPilot sends an HTTP POST request to your webhook URL
3. Your server processes the event and returns a 2xx response
4. SendPilot marks the delivery as successful
## Available Events
* `message.sent` - Message sent to a lead
* `reply.received` - Reply received from a lead
* `connection_request.sent` - Connection request sent
* `connection_request.accepted` - Connection accepted
* `campaign.started` - Campaign started
* `campaign.paused` - Campaign paused
* `campaign.resumed` - Campaign resumed
* `campaign.finished` - Campaign finished
* `lead.tag.updated` - Lead tags updated
* `lead.updated` - Lead status updated
* `lead_database.search.completed` - Search completed with all leads
* `lead_extractor.job.completed` - Extraction job completed with all leads
## Webhook Payload Structure
All webhook payloads follow a consistent structure:
```json theme={null}
{
"eventId": "evt_1708456789123_abc123def",
"eventType": "reply.received",
"timestamp": "2024-02-24T10:30:00.000Z",
"workspaceId": "ws_abc123xyz",
"data": {
// Event-specific data
}
}
```
| Field | Type | Description |
| ------------- | ------ | ------------------------------------------------------ |
| `eventId` | string | Unique identifier for this event (use for idempotency) |
| `eventType` | string | The type of event that occurred |
| `timestamp` | string | ISO 8601 timestamp when the event occurred |
| `workspaceId` | string | Your workspace ID |
| `data` | object | Event-specific payload data |
## Delivery & Retries
SendPilot uses reliable webhook delivery with automatic retries:
* **Timeout**: Your endpoint must respond within 30 seconds
* **Success**: Any 2xx response is considered successful
* **Retries**: Failed deliveries are retried with exponential backoff:
* 1st retry: 5 seconds
* 2nd retry: 30 seconds
* 3rd retry: 2 minutes
* 4th retry: 15 minutes
* 5th retry: 1 hour
* **Max retries**: 5 attempts total
If all retries fail, the event is marked as failed. You can view failed deliveries in the SendPilot dashboard.
## Security
### Signature Verification
All webhook requests include a signature header for verification:
```
Webhook-Signature: v1,t=1708456789,s=abc123...
```
Verify the signature by computing HMAC-SHA256 of the request body using your webhook secret:
```javascript theme={null}
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const parts = signature.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
const providedSignature = parts.find(p => p.startsWith('s=')).slice(2);
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(providedSignature),
Buffer.from(expectedSignature)
);
}
```
### IP Allowlisting
For additional security, you can allowlist SendPilot's webhook IPs. Contact support for the current IP ranges.
## Best Practices
Return a 2xx response as fast as possible. Process events asynchronously to avoid timeouts.
Use the `eventId` field for idempotency. The same event may be delivered multiple times in rare cases.
Always verify the webhook signature to ensure the request came from SendPilot.
Log incoming webhooks for debugging. This helps troubleshoot integration issues.
Always use HTTPS endpoints to protect sensitive data in transit.
Lead Database and Lead Extractor webhooks include full lead data. Process these asynchronously for large result sets.
## Next Steps
Learn how to configure webhook subscriptions
# Setting Up Webhooks
Source: https://docs.sendpilot.ai/webhooks/setup
Configure webhook subscriptions in SendPilot
This guide walks you through setting up webhook subscriptions to receive real-time event notifications.
## Creating a Webhook Subscription
### Via Dashboard
1. Log in to your SendPilot account
2. Navigate to **Integrations** → **Webhooks**
3. Click **Add Webhook**
4. Configure your webhook:
* **Name**: A descriptive name for this webhook
* **URL**: Your HTTPS endpoint that will receive events
* **Events**: Select which events to subscribe to
5. Click **Create**
6. Copy your webhook secret for signature verification
### Via API
You can also manage webhooks programmatically through the SendPilot dashboard API.
## Endpoint Requirements
Your webhook endpoint must:
* Accept **HTTP POST** requests
* Use **HTTPS** (HTTP is not supported for security)
* Respond with a **2xx status code** within 30 seconds
* Accept **JSON** content type
### Example Endpoint (Node.js/Express)
```javascript theme={null}
const express = require('express');
const crypto = require('crypto');
const app = express();
// Parse raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks/sendpilot', (req, res) => {
// Verify signature
const signature = req.headers['webhook-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifySignature(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}
// Parse the event
const event = JSON.parse(req.body);
// Handle the event asynchronously
handleEvent(event).catch(console.error);
// Respond immediately
res.status(200).send('OK');
});
function verifySignature(payload, signature, secret) {
const parts = signature.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
const providedSig = parts.find(p => p.startsWith('s=')).slice(2);
const signedPayload = `${timestamp}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(providedSig),
Buffer.from(expectedSig)
);
}
async function handleEvent(event) {
console.log(`Received ${event.eventType}:`, event.data);
switch (event.eventType) {
case 'message.received':
// Handle reply from lead
await notifySalesTeam(event.data);
break;
case 'connection.accepted':
// Handle new connection
await syncToCRM(event.data);
break;
// ... handle other events
}
}
app.listen(3000);
```
### Example Endpoint (Python/Flask)
```python theme={null}
from flask import Flask, request, abort
import hmac
import hashlib
import json
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
@app.route('/webhooks/sendpilot', methods=['POST'])
def handle_webhook():
# Verify signature
signature = request.headers.get('Webhook-Signature')
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
abort(401)
# Parse event
event = request.get_json()
# Handle asynchronously (use Celery, RQ, etc. in production)
handle_event(event)
return 'OK', 200
def verify_signature(payload, signature, secret):
parts = dict(p.split('=') for p in signature.split(','))
timestamp = parts['t']
provided_sig = parts['s']
signed_payload = f"{timestamp}.{payload.decode()}"
expected_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(provided_sig, expected_sig)
def handle_event(event):
event_type = event['eventType']
data = event['data']
if event_type == 'message.received':
notify_sales_team(data)
elif event_type == 'connection.accepted':
sync_to_crm(data)
# ... handle other events
if __name__ == '__main__':
app.run(port=3000)
```
## Testing Webhooks
### Using the Test Feature
1. Go to **Integrations** → **Webhooks** in the dashboard
2. Find your webhook subscription
3. Click **Send Test Event**
4. Select an event type
5. Check your endpoint for the test payload
### Local Development
For local development, use a tunnel service to expose your local server:
```bash theme={null}
# Using ngrok
ngrok http 3000
# Your webhook URL will be:
# https://abc123.ngrok.io/webhooks/sendpilot
```
Remember to update your webhook URL to your production endpoint before going live.
## Managing Subscriptions
### Update Events
To change which events trigger your webhook:
1. Go to **Integrations** → **Webhooks**
2. Click on the webhook to edit
3. Select or deselect event types
4. Save changes
### Rotate Secrets
To rotate your webhook secret:
1. Go to **Integrations** → **Webhooks**
2. Click on the webhook to edit
3. Click **Rotate Secret**
4. Update your server with the new secret
5. Confirm the rotation
After rotation, both the old and new secrets will work for 24 hours to allow for seamless updates.
### Disable/Delete
To temporarily disable or permanently delete a webhook:
1. Go to **Integrations** → **Webhooks**
2. Click on the webhook
3. Toggle **Enabled** to disable, or click **Delete** to remove
## Monitoring
### Delivery Logs
View webhook delivery history:
1. Go to **Integrations** → **Webhooks**
2. Click on a webhook
3. View the **Delivery History** tab
Each delivery shows:
* Event type and ID
* HTTP response code
* Response time
* Retry count (if any)
### Failed Deliveries
For failed deliveries, you can:
* View the error message
* See retry attempts
* Manually retry the delivery
## Troubleshooting
1. Verify your endpoint URL is correct and uses HTTPS
2. Check that your server is publicly accessible
3. Ensure the webhook is enabled
4. Check server logs for incoming requests
1. Verify you're using the correct webhook secret
2. Ensure you're verifying against the raw request body
3. Check that timestamps haven't expired (5 minute tolerance)
1. Ensure your endpoint responds within 30 seconds
2. Process events asynchronously
3. Return 200 immediately, handle logic in background
1. Use the `eventId` field for idempotency
2. Store processed event IDs in your database
3. Skip events that have already been processed