# 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