--- name: needle description: | Needle is an outbound engine for AI agents: leads, campaigns, LinkedIn, Instagram, places, suppressions, and compose. One API key unlocks the REST API and the MCP server. Use when an agent needs to source leads, run outreach, integrate Needle into an app, or obtain an API key. --- # Needle Needle helps agents search people and places, build lead lists, run multi-channel campaigns on LinkedIn and Instagram, keep suppressions, preview messages, and send from connected accounts. The same outbound engine is available as a REST API and as MCP tools. One API key unlocks both. Listing MCP tools does not require a key; calling tools and REST endpoints does. ## Get started No install. Get an API key, then pick a path. - **Dashboard / API key**: https://needle.app/api-keys - **Sign up**: https://needle.app/login - **MCP**: `undefined/mcp/v1` - **REST**: `undefined/api/v1` - **OpenAPI**: https://needle.app/api/v1/openapi - **Docs**: https://needle.app/docs/introduction - **Full index**: https://needle.app/llms-full.txt This skill is the routing layer. After you know the path, use the API Reference or MCP catalog for schemas. ## Get credentials Needle users create an API key in the dashboard: - Open https://needle.app/api-keys - Create a key. It looks like `sk-ndl-…`. - Each key is bound to one user and one organization. If they do not have an account yet, send them to Path D. Sending, following, and inviting require a connected LinkedIn or Instagram account. Connecting an account is dashboard-only at `/accounts`. The API and MCP can list, update, and delete connected accounts; they cannot start the OAuth connect flow. **Already have `NEEDLE_API_KEY`?** Skip credential setup. Pick a path below. ## Choose your path All paths use the same API key. The difference is what you do next. - **Need outbound during this session** → Path A (MCP tools) - **Need to add Needle to app code** → Path B (REST in the product) - **Need a finished outbound workflow** → Path C (source, campaign, or one-off send) - **Need more than one of the above** → do them in sequence; one key covers everything - **Need an account or API key** → Path D - **Do not want to connect MCP** → Path E (REST directly) | Path | Question it answers | Where the work runs | | --- | --- | --- | | A | Which Needle tool should I call right now? | In the agent session, over MCP | | B | How do I add a Needle API call to this codebase? | Inside the user's product code | | C | What is the finished outbound workflow? | In the agent session, producing a result | | D | How does the human get a key? | Dashboard | | E | How do I call Needle over HTTP? | REST at `/api/v1` | ## Path A: Live MCP tools Use this when you need outbound during your work: searching people, building leads, running campaigns, sending LinkedIn or Instagram messages, or checking suppressions. Connect at `undefined/mcp/v1` with `Authorization: Bearer sk-ndl-…`. Listing tools at `https://needle.app/mcp/v1/tools` does not require a key; calling them does. ```json { "mcpServers": { "needle": { "url": "undefined/mcp/v1", "headers": { "Authorization": "Bearer sk-ndl-…" } } } } ``` Works with Claude Code, Cowork, Codex, Cursor, Hermes, or any MCP client. Route by job: - **Who am I bound to?** → `needleGetMe` - **What can I send from?** → `needleListConnectedAccounts` - **Find people** → `needleSearchLinkedinPeople`, then `needleGetLinkedinProfile` - **Find companies / locations** → `needleSearchPlaces`, then `needleGetPlace` - **Build a list** → `needleCreateLead` / `needleListLeads` - **Do not contact** → `needleCheckSuppressions` / `needleCreateSuppressions` - **Preview copy** → `needleComposePreview` before any send - **One-off LinkedIn** → `needleSendLinkedinInvitation` or `needleSendLinkedinMessage` - **One-off Instagram** → `needleFollowInstagramUser` or `needleSendInstagramMessage` - **Campaign** → `needleCreateCampaign`, `needleEnrollCampaignLeads`, `needleListPendingApprovals`, `needleApprovePendingTask` Default flow for live outbound: 1. `needleGetMe` to confirm the user and organization. 2. `needleListConnectedAccounts` before any send. If none are connected, send the human to `/accounts`. 3. Search people or places, then create leads. 4. `needleCheckSuppressions` before outreach. 5. `needleComposePreview` when the message is generated. 6. Enroll in a campaign, or send a one-off invitation/message. 7. Watch `needleGetCampaignStats` / `needleSearchEvents` after a campaign is live. If the task becomes "wire Needle into product code," switch to Path B. ### MCP tools #### Me - [`needleGetMe`](/docs/mcp/needle-get-me): Get the authenticated user #### Connected accounts - [`needleListConnectedAccounts`](/docs/mcp/needle-list-connected-accounts): List connected accounts - [`needleGetConnectedAccount`](/docs/mcp/needle-get-connected-account): Get a connected account - [`needleUpdateConnectedAccount`](/docs/mcp/needle-update-connected-account): Update a connected account - [`needleDeleteConnectedAccount`](/docs/mcp/needle-delete-connected-account): Delete a connected account - [`needleListConnectedAccountEvents`](/docs/mcp/needle-list-connected-account-events): List connected account events #### Leads - [`needleListLeads`](/docs/mcp/needle-list-leads): List leads - [`needleCreateLead`](/docs/mcp/needle-create-lead): Create a lead - [`needleGetLead`](/docs/mcp/needle-get-lead): Get a lead - [`needleUpdateLead`](/docs/mcp/needle-update-lead): Update a lead - [`needleDeleteLead`](/docs/mcp/needle-delete-lead): Delete a lead - [`needleListLeadEvents`](/docs/mcp/needle-list-lead-events): List lead events #### Campaigns - [`needleListCampaigns`](/docs/mcp/needle-list-campaigns): List campaigns - [`needleCreateCampaign`](/docs/mcp/needle-create-campaign): Create a campaign - [`needleGetCampaign`](/docs/mcp/needle-get-campaign): Get a campaign - [`needleUpdateCampaign`](/docs/mcp/needle-update-campaign): Update a campaign - [`needleArchiveCampaign`](/docs/mcp/needle-archive-campaign): Archive a campaign - [`needleGetCampaignStats`](/docs/mcp/needle-get-campaign-stats): Get campaign stats - [`needleListCampaignEvents`](/docs/mcp/needle-list-campaign-events): List campaign events - [`needleListCampaignLeads`](/docs/mcp/needle-list-campaign-leads): List campaign leads - [`needleEnrollCampaignLeads`](/docs/mcp/needle-enroll-campaign-leads): Enroll leads - [`needleRemoveCampaignLead`](/docs/mcp/needle-remove-campaign-lead): Stop a lead in a campaign - [`needleListPendingApprovals`](/docs/mcp/needle-list-pending-approvals): List pending approvals - [`needleApprovePendingTask`](/docs/mcp/needle-approve-pending-task): Approve a pending send #### Events - [`needleSearchEvents`](/docs/mcp/needle-search-events): Search events #### LinkedIn - [`needleGetLinkedinProfile`](/docs/mcp/needle-get-linkedin-profile): Get a LinkedIn profile - [`needleSearchLinkedinPeople`](/docs/mcp/needle-search-linkedin-people): Search LinkedIn people - [`needleSendLinkedinMessage`](/docs/mcp/needle-send-linkedin-message): Send a LinkedIn message - [`needleListLinkedinConversationMessages`](/docs/mcp/needle-list-linkedin-conversation-messages): List messages in a conversation - [`needleListLinkedinInvitations`](/docs/mcp/needle-list-linkedin-invitations): List pending invitations - [`needleSendLinkedinInvitation`](/docs/mcp/needle-send-linkedin-invitation): Send a connection invitation - [`needleWithdrawLinkedinInvitation`](/docs/mcp/needle-withdraw-linkedin-invitation): Withdraw an invitation - [`needleListLinkedinRelations`](/docs/mcp/needle-list-linkedin-relations): List first-degree connections #### Instagram - [`needleGetInstagramProfile`](/docs/mcp/needle-get-instagram-profile): Get an Instagram profile - [`needleSendInstagramMessage`](/docs/mcp/needle-send-instagram-message): Send an Instagram message - [`needleListInstagramConversationMessages`](/docs/mcp/needle-list-instagram-conversation-messages): List messages in a conversation - [`needleFollowInstagramUser`](/docs/mcp/needle-follow-instagram-user): Follow an Instagram user - [`needleUnfollowInstagramUser`](/docs/mcp/needle-unfollow-instagram-user): Unfollow an Instagram user #### Places - [`needleSearchPlaces`](/docs/mcp/needle-search-places): Search places - [`needleGetPlace`](/docs/mcp/needle-get-place): Get a place #### Compose - [`needleComposePreview`](/docs/mcp/needle-compose-preview): Preview an AI-composed message #### Suppressions - [`needleListSuppressions`](/docs/mcp/needle-list-suppressions): List suppressions - [`needleCreateSuppressions`](/docs/mcp/needle-create-suppressions): Create suppressions - [`needleCheckSuppressions`](/docs/mcp/needle-check-suppressions): Check people and companies against the suppression list - [`needleDeleteSuppression`](/docs/mcp/needle-delete-suppression): Delete a suppression ## Path B: Integrate Needle into an app Use this when you are building an application, agent, or workflow that calls Needle **from code**, meaning the integration will run inside the user's product rather than from this session's MCP client. This is the key difference from Path A: Path A calls MCP tools during the current session. Path B writes code that keeps running after the agent stops, using `NEEDLE_API_KEY` from the project's `.env` or runtime config. ```dotenv NEEDLE_API_KEY=sk-ndl-… ``` **Base URL:** `undefined/api/v1` **Auth header:** `Authorization: Bearer sk-ndl-…` - **OpenAPI:** https://needle.app/api/v1/openapi - **API Reference:** https://needle.app/docs/api-reference/introduction Smoke-test with `GET /api/v1/me` before writing feature code: ```bash curl undefined/api/v1/me \ -H "Authorization: Bearer sk-ndl-…" \ -H "Content-Type: application/json" ``` Route the feature the same way as Path A, then call the matching REST endpoint from Path E. Sends still need a connected account in the organization that owns the key. If you do not have a key yet, do Path D first. ## Path C: Repeatable outbound workflows Use this when the goal is a finished outbound result (a lead list, a live campaign, a one-off send, or a suppression pass), not raw tool output and not product-code integration. ### Source a list 1. `needleSearchLinkedinPeople` (keywords or a LinkedIn / Sales Navigator `searchUrl`) or `needleSearchPlaces`. 2. `needleGetLinkedinProfile` / `needleGetPlace` when you need details. 3. `needleCreateLead` for each person you will contact. A lead can carry LinkedIn and/or Instagram identities. 4. `needleCheckSuppressions` and drop anyone you should not contact. ### Run a campaign 1. Confirm at least one connected account (`needleListConnectedAccounts`). 2. `needleCreateCampaign` with the sequence graph, schedule, limits, and the connected accounts to send from. 3. `needleEnrollCampaignLeads`. 4. `needleListPendingApprovals` and `needleApprovePendingTask` when a send is waiting. 5. `needleGetCampaignStats` and `needleSearchEvents` to see what happened. ### One-off outreach 1. Get the profile. 2. `needleComposePreview` for the message. 3. LinkedIn: `needleSendLinkedinInvitation` or `needleSendLinkedinMessage`. 4. Instagram: `needleFollowInstagramUser` or `needleSendInstagramMessage`. ### Keep people off the list `needleCreateSuppressions` for people or companies that should not be contacted. Campaigns honor the suppression policy you set on create/update. If the request shifts to "wire Needle into product code," switch to Path B. If you only need a single tool call, stay on Path A. ## Path D: Account or API key Use this when the human still needs to sign up, sign in, or create a key. 1. If they have no account, open https://needle.app/login and sign up. 2. Create a key at https://needle.app/api-keys. Save it as `NEEDLE_API_KEY`. 3. To send, connect LinkedIn and/or Instagram at https://needle.app/accounts. 4. Continue with Path A, B, C, or E. There is no keyless tier. Listing MCP tools at `/mcp/v1/tools` is public; every other call needs a key. ## Path E: REST API without MCP Use this when you do not want to connect an MCP client. This works for both live work in this session and app integrations (Path B). **Base URL:** `undefined/api/v1` **Auth header:** `Authorization: Bearer sk-ndl-…` ```bash curl undefined/api/v1/me \ -H "Authorization: Bearer sk-ndl-…" \ -H "Content-Type: application/json" ``` Successful requests return 2xx. `401` means a missing or invalid key. `422` means an invalid payload. ### REST endpoints #### Me - `GET /api/v1/me`: [Get the authenticated user](/docs/api-reference/get-me) #### Connected accounts - `GET /api/v1/connected-accounts`: [List connected accounts](/docs/api-reference/list-connected-accounts) - `GET /api/v1/connected-accounts/{connectedAccountId}`: [Get a connected account](/docs/api-reference/get-connected-account) - `PATCH /api/v1/connected-accounts/{connectedAccountId}`: [Update a connected account](/docs/api-reference/update-connected-account) - `DELETE /api/v1/connected-accounts/{connectedAccountId}`: [Delete a connected account](/docs/api-reference/delete-connected-account) - `GET /api/v1/connected-accounts/{connectedAccountId}/events`: [List connected account events](/docs/api-reference/list-connected-account-events) #### Leads - `GET /api/v1/leads`: [List leads](/docs/api-reference/list-leads) - `POST /api/v1/leads`: [Create a lead](/docs/api-reference/create-lead) - `GET /api/v1/leads/{leadId}`: [Get a lead](/docs/api-reference/get-lead) - `PATCH /api/v1/leads/{leadId}`: [Update a lead](/docs/api-reference/update-lead) - `DELETE /api/v1/leads/{leadId}`: [Delete a lead](/docs/api-reference/delete-lead) - `GET /api/v1/leads/{leadId}/events`: [List lead events](/docs/api-reference/list-lead-events) #### Campaigns - `GET /api/v1/campaigns`: [List campaigns](/docs/api-reference/list-campaigns) - `POST /api/v1/campaigns`: [Create a campaign](/docs/api-reference/create-campaign) - `GET /api/v1/campaigns/{campaignId}`: [Get a campaign](/docs/api-reference/get-campaign) - `PATCH /api/v1/campaigns/{campaignId}`: [Update a campaign](/docs/api-reference/update-campaign) - `DELETE /api/v1/campaigns/{campaignId}`: [Archive a campaign](/docs/api-reference/archive-campaign) - `GET /api/v1/campaigns/{campaignId}/stats`: [Get campaign stats](/docs/api-reference/get-campaign-stats) - `GET /api/v1/campaigns/{campaignId}/events`: [List campaign events](/docs/api-reference/list-campaign-events) - `GET /api/v1/campaigns/{campaignId}/leads`: [List campaign leads](/docs/api-reference/list-campaign-leads) - `POST /api/v1/campaigns/{campaignId}/leads`: [Enroll leads](/docs/api-reference/enroll-campaign-leads) - `DELETE /api/v1/campaigns/{campaignId}/leads/{leadId}`: [Stop a lead in a campaign](/docs/api-reference/remove-campaign-lead) - `GET /api/v1/campaigns/{campaignId}/pending`: [List pending approvals](/docs/api-reference/list-pending-approvals) - `PATCH /api/v1/campaigns/{campaignId}/pending/{taskId}`: [Approve a pending send](/docs/api-reference/approve-pending-task) #### Events - `POST /api/v1/events/search`: [Search events](/docs/api-reference/search-events) #### LinkedIn - `GET /api/v1/linkedin/profiles/{identity}`: [Get a LinkedIn profile](/docs/api-reference/get-linkedin-profile) - `POST /api/v1/linkedin/search`: [Search LinkedIn people](/docs/api-reference/search-linkedin-people) - `POST /api/v1/linkedin/messages`: [Send a LinkedIn message](/docs/api-reference/send-linkedin-message) - `GET /api/v1/linkedin/conversations/{conversationId}/messages`: [List messages in a conversation](/docs/api-reference/list-linkedin-conversation-messages) - `GET /api/v1/linkedin/invitations`: [List pending invitations](/docs/api-reference/list-linkedin-invitations) - `POST /api/v1/linkedin/invitations`: [Send a connection invitation](/docs/api-reference/send-linkedin-invitation) - `DELETE /api/v1/linkedin/invitations/{invitationId}`: [Withdraw an invitation](/docs/api-reference/withdraw-linkedin-invitation) - `GET /api/v1/linkedin/relations`: [List first-degree connections](/docs/api-reference/list-linkedin-relations) #### Instagram - `GET /api/v1/instagram/profiles/{identity}`: [Get an Instagram profile](/docs/api-reference/get-instagram-profile) - `POST /api/v1/instagram/messages`: [Send an Instagram message](/docs/api-reference/send-instagram-message) - `GET /api/v1/instagram/conversations/{conversationId}/messages`: [List messages in a conversation](/docs/api-reference/list-instagram-conversation-messages) - `POST /api/v1/instagram/follows`: [Follow an Instagram user](/docs/api-reference/follow-instagram-user) - `DELETE /api/v1/instagram/follows/{identity}`: [Unfollow an Instagram user](/docs/api-reference/unfollow-instagram-user) #### Places - `POST /api/v1/places/search`: [Search places](/docs/api-reference/search-places) - `GET /api/v1/places/{placeId}`: [Get a place](/docs/api-reference/get-place) #### Compose - `POST /api/v1/compose`: [Preview an AI-composed message](/docs/api-reference/compose-preview) #### Suppressions - `GET /api/v1/suppressions`: [List suppressions](/docs/api-reference/list-suppressions) - `POST /api/v1/suppressions`: [Create suppressions](/docs/api-reference/create-suppressions) - `POST /api/v1/suppressions/check`: [Check people and companies against the suppression list](/docs/api-reference/check-suppressions) - `DELETE /api/v1/suppressions/{suppressionId}`: [Delete a suppression](/docs/api-reference/delete-suppression) ## References - **This skill:** https://needle.app/agent-onboarding/SKILL.md - **Documentation:** https://needle.app/docs/introduction - **API Reference:** https://needle.app/docs/api-reference/introduction - **MCP catalog:** https://needle.app/docs/mcp/introduction - **OpenAPI:** https://needle.app/api/v1/openapi - **Full index:** https://needle.app/llms-full.txt --- # Introduction Needle is an outbound engine for revenue teams. Keep a list of leads, add connected accounts for LinkedIn and Instagram, and enroll people in campaigns. The same work is available in the dashboard, over the REST API, and as MCP tools. See [Authentication](/docs/authentication) for API keys. ## Core concepts - [Organizations](/docs/organizations): The workspace that owns leads, campaigns, and connected accounts. - [Leads](/docs/leads): People you can enroll in a campaign. - [Campaigns](/docs/campaigns): Sequences this organization is running. - [Enrollments](/docs/enrollments): A lead moving through a campaign sequence. - [Connected Accounts](/docs/connected-accounts): LinkedIn and Instagram senders this organization reaches out from. - [Suppressions](/docs/suppressions): People and companies on the do-not-contact list. - [Events](/docs/events): Activity across campaigns, leads, and connected accounts. - [Places](/docs/places): Companies and locations you can turn into lead lists. - [Compose](/docs/compose): Preview an AI-written message before you send it. - [Agents](/docs/agents): Personal agents that work in Slack and Teams. ## REST API The public API lives under `/api/v1`. See the [API Reference](/docs/api-reference/introduction) for endpoints, parameters, and schemas. ## MCP Connect an agent at `/mcp/v1` with the same API key. Listing tools does not require a key; calling them does. See the [MCP catalog](/docs/mcp/introduction) for every tool. ## OpenAPI The machine-readable spec is at `/api/v1/openapi`. Use it to generate clients or import the API into your own tooling. ## For agents Start with the [onboarding skill](/docs/agent-onboarding). It routes to MCP, the REST API, credentials, and common outbound workflows. --- # Authentication API keys for the REST API and MCP server. Every REST and MCP request uses the same header. Missing or invalid keys return `401`. `Authorization: Bearer sk-ndl-…` ## Create a key 1. Open [API keys](/api-keys) in the dashboard. 2. Create a key. It looks like `sk-ndl-…`. 3. Copy it once. The full key is only shown then. ## Scope Each key is bound to one user and one organization. The dashboard, API, and MCP all work in the organization the key is bound to. - **User**: The key acts as the user who created it. - **Organization**: Requests run in the organization the key is bound to. ## REST API Send the header on every request to `/api/v1`. See the [API Reference](/docs/api-reference/introduction). ```bash curl undefined/api/v1/me \ -H "Authorization: Bearer sk-ndl-…" \ -H "Content-Type: application/json" ``` ## MCP Call tools with the same key. Listing tools at `/mcp/v1/tools` does not require a key. See the [MCP catalog](/docs/mcp/introduction). ```json { "mcpServers": { "needle": { "url": "undefined/mcp/v1", "headers": { "Authorization": "Bearer sk-ndl-…" } } } } ``` --- # Organizations The workspace that owns leads, campaigns, and connected accounts. ## What an organization is An organization is the workspace this data belongs to. Leads, campaigns, connected accounts, suppressions, events, and agents all live on one organization. ## Members People join with a role: owner, admin, or member. Owners can invite people, change roles, and remove members. The owner role can only be changed by Needle support. Invite from settings. Creating an organization can include invites. ## Switching You can belong to more than one organization. Switch from the user menu, or create another organization there. ## API keys Each API key is bound to one user and one organization. The dashboard, API, and MCP all work in the organization the current session or key is bound to. --- # Leads People you can enroll in a campaign. ## What a lead is A lead is a person you can enroll in a campaign. Add them in the dashboard or through the API. ## Contact You can store a first name, last name, title, company, domain, email, timezone, and extra properties. ## Identities At least one LinkedIn or Instagram identity is required. LinkedIn uses a public identifier; Instagram uses a username. ## In campaigns The lead page shows every campaign they have been enrolled in, and whether they are live in one now. A lead can only be live in one campaign at a time. --- # Campaigns Sequences this organization is running. ## What a campaign is A campaign is a sequence this organization is running. Create one as a draft (in the dashboard that starts with an empty sequence), then add steps. You can set it active or paused, and archive it when you are done. ## Sequence A sequence can start and end, wait for a duration, take an action on LinkedIn or Instagram, branch on a condition (whether they are already a LinkedIn connection, or whether a lead attribute is set), or wait until a reply or an accepted connection request, with a timeout. Message steps can be a template or an AI prompt. An AI step can wait for someone to approve the text before it sends. ## Sending Sending follows the campaign schedule: the days and hours you pick, local to each lead. You also set a gap between sends, and an optional cap on how many leads may start each day. New campaigns start in dry run: the sequence runs, but nothing is sent. ## Suppression policy A campaign can skip people contacted recently, skip people who never replied after a number of touches, and bypass some soft suppression reasons. --- # Enrollments A lead moving through a campaign sequence. ## What an enrollment is Enrolling a lead starts them on a campaign sequence. Enroll through the API, up to 500 people at a time. The campaign page lists every enrollment. ## Status An enrollment is active, waiting (for a reply, an accepted invite, or a timeout), completed, stopped, or failed. ## How it ends You can stop a lead in a campaign. Enrollment also ends when they finish the sequence, reply, time out, get suppressed, or you archive the campaign. A lead already live in another campaign is skipped. --- # Connected Accounts LinkedIn and Instagram senders this organization reaches out from. ## What a connected account is Connected accounts are the LinkedIn and Instagram senders this organization reaches out from. A connected account is active, paused, needs reconnect, or disconnected. ## Sending Campaigns send from the organization's connected accounts on the channels the sequence uses. Each connected account has usage limits per action. You can pause a connected account or change those limits. ## Connecting Connect one in the dashboard. The API can list, update, and delete connected accounts. Connecting a new one is dashboard-only. ## LinkedIn Through a connected account, without a campaign, you can look up a profile, send a message, or read a conversation. You can also search people, send or withdraw a connection request, and list connections. In a campaign sequence the LinkedIn steps are connection request, withdraw request, message, InMail, and profile view. A sequence can wait until a reply or an accepted connection request, and can branch on whether they are already a connection. ## Instagram Through a connected account, without a campaign, you can look up a profile, send a message, or read a conversation. You can also follow and unfollow. In a campaign sequence the Instagram steps are message, profile view, follow, and unfollow. A sequence can wait until a reply. --- # Suppressions People and companies on the do-not-contact list. ## What a suppression is A suppression is a do-not-contact rule for a person or a company. Add a lead or a company domain, pick a reason, and optionally a note and an expiry. ## How campaigns use them Campaigns skip people and companies on this list. Some reasons are hard and cannot be overridden. A campaign can be allowed to bypass soft reasons, and can also skip people contacted recently or who never replied, using its suppression policy. ## Checking You can check identities against the list before you contact them. --- # Events Activity across campaigns, leads, and connected accounts. ## What an event is Events are the activity log. You will see enrollments, sends, replies, skips, suppressions, and status changes. ## Where to browse them Filter by type, channel, campaign, lead, connected account, and date on the events page. A campaign page also has its own event list. --- # Places Companies and locations you can turn into lead lists. ## What a place is A place is a company or location: name, address, website, phone, hours, and rating. It is not someone you message. ## Search Search by query or map area, then open a place. Use what you find to add people as leads. --- # Compose Preview an AI-written message before you send it. ## What compose does Compose previews an AI-written message before you send it. You choose the channel, the connected account, the contact, and a prompt. ## Context You can ground the draft in their profile, posts, conversation, or lead properties. --- # Agents Personal agents that work in Slack and Teams. ## What an agent is An agent works in Slack and Teams with the tools you connect it to. Create one in the dashboard. --- # Needle API Public REST API for Needle Agents. Authenticate every request with `Authorization: Bearer sk-ndl-…`. Each API key is bound to one user and one organization. Version: 1.0.0 Authenticate every request with `Authorization: Bearer sk-ndl-…`. ## Contents - Me - Get the authenticated user - Connected accounts - List connected accounts - Get a connected account - Update a connected account - Delete a connected account - List connected account events - Leads - List leads - Create a lead - Get a lead - Update a lead - Delete a lead - List lead events - Campaigns - List campaigns - Create a campaign - Get a campaign - Update a campaign - Archive a campaign - Get campaign stats - List campaign events - List campaign leads - Enroll leads - Stop a lead in a campaign - List pending approvals - Approve a pending send - Events - Search events - LinkedIn - Get a LinkedIn profile - Search LinkedIn people - Send a LinkedIn message - List messages in a conversation - List pending invitations - Send a connection invitation - Withdraw an invitation - List first-degree connections - Instagram - Get an Instagram profile - Send an Instagram message - List messages in a conversation - Follow an Instagram user - Unfollow an Instagram user - Places - Search places - Get a place - Compose - Preview an AI-composed message - Suppressions - List suppressions - Create suppressions - Check people and companies against the suppression list - Delete a suppression ## Me ### Get the authenticated user `GET /api/v1/me` Operation ID: `getMe` Returns the user and organization this API key is bound to. **200**: The authenticated user. - `user` (MeUser, required) - `id` (string · uuid, required) - `email` (string, required) - `name` (string | null, required) - `imageUrl` (string | null, required) - `organization` (object, required) - `id` (string · uuid, required) - `name` (string, required) - `domain` (string | null, required) - `imageUrl` (string | null, required) - `role` ("member" | "admin" | "owner", required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Connected accounts ### List connected accounts `GET /api/v1/connected-accounts` Operation ID: `listConnectedAccounts` **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of connected accounts, newest first. Pass nextCursor as cursor for the next page. - `connectedAccounts` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Get a connected account `GET /api/v1/connected-accounts/{connectedAccountId}` Operation ID: `getConnectedAccount` **Path parameters** - `connectedAccountId` (string · uuid, required): Connected account id **200**: Account and usage. - `connectedAccount` (object, required) - `id` (string · uuid, required) - `ownerUserId` (string · uuid, required) - `ownerOrgId` (string · uuid, required) - `channel` ("linkedin" | "instagram", required) - `status` ("active" | "paused" | "auth_required" | "disconnected", required) - `properties` (object, required) - `displayName` (string | null, required) - `channelIdentity` (string | null, required) - `imageUrl` (string | null, required) - `quotaOverrides` (object | null, required) - `nextActionAt` (string · date-time | null, required) - `pausedUntil` (string · date-time | null, required) - `consecutiveFailures` (integer, required) - `lastErrorKind` (string | null, required) - `createdAt` (string · date-time, required) - `updatedAt` (string · date-time, required) - `updatedBy` (string · uuid | null, required) - `deletedAt` (string · date-time | null, required) - `deletedBy` (string · uuid | null, required) - `usage` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Update a connected account `PATCH /api/v1/connected-accounts/{connectedAccountId}` Operation ID: `updateConnectedAccount` **Path parameters** - `connectedAccountId` (string · uuid, required): Connected account id **Request body** (required) - `paused` (boolean, optional) - `quotaOverrides` (object | null, optional) **200**: Updated account. - `connectedAccount` (object, required) - `id` (string · uuid, required) - `ownerUserId` (string · uuid, required) - `ownerOrgId` (string · uuid, required) - `channel` ("linkedin" | "instagram", required) - `status` ("active" | "paused" | "auth_required" | "disconnected", required) - `properties` (object, required) - `displayName` (string | null, required) - `channelIdentity` (string | null, required) - `imageUrl` (string | null, required) - `quotaOverrides` (object | null, required) - `nextActionAt` (string · date-time | null, required) - `pausedUntil` (string · date-time | null, required) - `consecutiveFailures` (integer, required) - `lastErrorKind` (string | null, required) - `createdAt` (string · date-time, required) - `updatedAt` (string · date-time, required) - `updatedBy` (string · uuid | null, required) - `deletedAt` (string · date-time | null, required) - `deletedBy` (string · uuid | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Delete a connected account `DELETE /api/v1/connected-accounts/{connectedAccountId}` Operation ID: `deleteConnectedAccount` **Path parameters** - `connectedAccountId` (string · uuid, required): Connected account id **200**: Account deleted. - `deleted` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List connected account events `GET /api/v1/connected-accounts/{connectedAccountId}/events` Operation ID: `listConnectedAccountEvents` **Path parameters** - `connectedAccountId` (string · uuid, required): Connected account id **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of events for this connected account, newest first. Pass nextCursor as cursor for the next page. - `events` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Leads ### List leads `GET /api/v1/leads` Operation ID: `listLeads` **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of leads in this organization, newest first. Pass nextCursor as cursor for the next page. - `leads` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Create a lead `POST /api/v1/leads` Operation ID: `createLead` **Request body** (required) - `identities` (object, required) - `linkedin` (string, optional) - `instagram` (string, optional) - `timezone` (string, required) - `firstName` (string, optional) - `lastName` (string, optional) - `companyName` (string, optional) - `companyDomain` (string, optional) - `jobTitle` (string, optional) - `email` (string, optional) - `imageUrl` (string, optional) - `properties` (object, optional) **201**: Created lead. - `lead` (object, required) - `id` (string · uuid, required) - `firstName` (string | null, required) - `lastName` (string | null, required) - `companyName` (string | null, required) - `companyDomain` (string | null, required) - `jobTitle` (string | null, required) - `email` (string | null, required) - `imageUrl` (string | null, required) - `timezone` (string, required) - `identities` (object[], required) - `liveEnrollment` (object | null, required) - `campaignId` (string · uuid, required) - `campaignName` (string, required) - `status` ("active" | "waiting" | "completed" | "stopped" | "failed", required) - `createdAt` (string · date-time, required) - `updatedAt` (string · date-time, required) - `properties` (object, required) - `enrollments` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Get a lead `GET /api/v1/leads/{leadId}` Operation ID: `getLead` **Path parameters** - `leadId` (string · uuid, required): Lead id **200**: Lead, identities, and enrollments. - `lead` (object, required) - `id` (string · uuid, required) - `firstName` (string | null, required) - `lastName` (string | null, required) - `companyName` (string | null, required) - `companyDomain` (string | null, required) - `jobTitle` (string | null, required) - `email` (string | null, required) - `imageUrl` (string | null, required) - `timezone` (string, required) - `identities` (object[], required) - `liveEnrollment` (object | null, required) - `campaignId` (string · uuid, required) - `campaignName` (string, required) - `status` ("active" | "waiting" | "completed" | "stopped" | "failed", required) - `createdAt` (string · date-time, required) - `updatedAt` (string · date-time, required) - `properties` (object, required) - `enrollments` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Update a lead `PATCH /api/v1/leads/{leadId}` Operation ID: `updateLead` **Path parameters** - `leadId` (string · uuid, required): Lead id **Request body** (required) - `identities` (object, optional) - `linkedin` (string, optional) - `instagram` (string, optional) - `timezone` (string, optional) - `firstName` (string, optional) - `lastName` (string, optional) - `companyName` (string, optional) - `companyDomain` (string, optional) - `jobTitle` (string, optional) - `email` (string, optional) - `imageUrl` (string, optional) - `properties` (object, optional) **200**: Updated lead. - `lead` (object, required) - `id` (string · uuid, required) - `firstName` (string | null, required) - `lastName` (string | null, required) - `companyName` (string | null, required) - `companyDomain` (string | null, required) - `jobTitle` (string | null, required) - `email` (string | null, required) - `imageUrl` (string | null, required) - `timezone` (string, required) - `identities` (object[], required) - `liveEnrollment` (object | null, required) - `campaignId` (string · uuid, required) - `campaignName` (string, required) - `status` ("active" | "waiting" | "completed" | "stopped" | "failed", required) - `createdAt` (string · date-time, required) - `updatedAt` (string · date-time, required) - `properties` (object, required) - `enrollments` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Delete a lead `DELETE /api/v1/leads/{leadId}` Operation ID: `deleteLead` **Path parameters** - `leadId` (string · uuid, required): Lead id **200**: Lead deleted. - `deleted` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List lead events `GET /api/v1/leads/{leadId}/events` Operation ID: `listLeadEvents` **Path parameters** - `leadId` (string · uuid, required): Lead id **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of events for this lead, newest first. Pass nextCursor as cursor for the next page. - `events` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Campaigns ### List campaigns `GET /api/v1/campaigns` Operation ID: `listCampaigns` **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) - `status` ("draft" | "active" | "paused" | "completed" | "archived", optional) **200**: A page of campaigns, newest first. Pass nextCursor as cursor for the next page. - `campaigns` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Create a campaign `POST /api/v1/campaigns` Operation ID: `createCampaign` **Request body** (required) - `name` (string, required) - `graph` (object, required) - `nodes` (object[], required) - `edges` (object[], required) - `schedule` (object, optional) - `workingDays` (integer[], optional) - `startTime` (string, optional) - `endTime` (string, optional) - `limits` (object, optional) - `minGapSeconds` (integer, optional) - `maxGapSeconds` (integer, optional) - `maxNewLeadsPerDay` (integer | null, optional) - `suppressionPolicy` (object, optional) - `bypassSuppressionReasons` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive"[], optional) - `cooldownDays` (integer, optional) - `unresponsiveAfterTouches` (integer, optional) - `autoSuppressKeywords` (string[], optional) **201**: Created campaign. - `campaign` (object, required) - `id` (string · uuid, required) - `ownerOrgId` (string · uuid, required) - `name` (string, required) - `status` ("draft" | "active" | "paused" | "completed" | "archived", required) - `graph` (object, required) - `nodes` (object[], required) - `edges` (object[], required) - `schedule` (object, required) - `workingDays` (integer[], required) - `startTime` (string, required) - `endTime` (string, required) - `limits` (object, required) - `minGapSeconds` (integer, required) - `maxGapSeconds` (integer, required) - `maxNewLeadsPerDay` (integer | null, optional) - `suppressionPolicy` (object, required) - `bypassSuppressionReasons` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive"[], required) - `cooldownDays` (integer, required) - `unresponsiveAfterTouches` (integer, required) - `autoSuppressKeywords` (string[], required) - `dryRun` (boolean, required) - `startedAt` (string · date-time | null, required) - `startedBy` (string · uuid | null, required) - `pausedAt` (string · date-time | null, required) - `pausedBy` (string · uuid | null, required) - `completedAt` (string · date-time | null, required) - `archivedAt` (string · date-time | null, required) - `archivedBy` (string · uuid | null, required) - `createdAt` (string · date-time, required) - `createdBy` (string · uuid | null, required) - `updatedAt` (string · date-time, required) - `updatedBy` (string · uuid | null, required) - `connectedAccounts` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Get a campaign `GET /api/v1/campaigns/{campaignId}` Operation ID: `getCampaign` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **200**: The campaign. - `campaign` (object, required) - `id` (string · uuid, required) - `ownerOrgId` (string · uuid, required) - `name` (string, required) - `status` ("draft" | "active" | "paused" | "completed" | "archived", required) - `graph` (object, required) - `nodes` (object[], required) - `edges` (object[], required) - `schedule` (object, required) - `workingDays` (integer[], required) - `startTime` (string, required) - `endTime` (string, required) - `limits` (object, required) - `minGapSeconds` (integer, required) - `maxGapSeconds` (integer, required) - `maxNewLeadsPerDay` (integer | null, optional) - `suppressionPolicy` (object, required) - `bypassSuppressionReasons` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive"[], required) - `cooldownDays` (integer, required) - `unresponsiveAfterTouches` (integer, required) - `autoSuppressKeywords` (string[], required) - `dryRun` (boolean, required) - `startedAt` (string · date-time | null, required) - `startedBy` (string · uuid | null, required) - `pausedAt` (string · date-time | null, required) - `pausedBy` (string · uuid | null, required) - `completedAt` (string · date-time | null, required) - `archivedAt` (string · date-time | null, required) - `archivedBy` (string · uuid | null, required) - `createdAt` (string · date-time, required) - `createdBy` (string · uuid | null, required) - `updatedAt` (string · date-time, required) - `updatedBy` (string · uuid | null, required) - `connectedAccounts` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Update a campaign `PATCH /api/v1/campaigns/{campaignId}` Operation ID: `updateCampaign` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **Request body** (required) - `name` (string, optional) - `status` ("active" | "paused", optional) - `graph` (object, optional) - `nodes` (object[], required) - `edges` (object[], required) - `schedule` (object, optional) - `workingDays` (integer[], optional) - `startTime` (string, optional) - `endTime` (string, optional) - `limits` (object, optional) - `minGapSeconds` (integer, optional) - `maxGapSeconds` (integer, optional) - `maxNewLeadsPerDay` (integer | null, optional) - `suppressionPolicy` (object, optional) - `bypassSuppressionReasons` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive"[], optional) - `cooldownDays` (integer, optional) - `unresponsiveAfterTouches` (integer, optional) - `autoSuppressKeywords` (string[], optional) - `dryRun` (boolean, optional) **200**: Updated campaign. - `campaign` (object, required) - `id` (string · uuid, required) - `ownerOrgId` (string · uuid, required) - `name` (string, required) - `status` ("draft" | "active" | "paused" | "completed" | "archived", required) - `graph` (object, required) - `nodes` (object[], required) - `edges` (object[], required) - `schedule` (object, required) - `workingDays` (integer[], required) - `startTime` (string, required) - `endTime` (string, required) - `limits` (object, required) - `minGapSeconds` (integer, required) - `maxGapSeconds` (integer, required) - `maxNewLeadsPerDay` (integer | null, optional) - `suppressionPolicy` (object, required) - `bypassSuppressionReasons` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive"[], required) - `cooldownDays` (integer, required) - `unresponsiveAfterTouches` (integer, required) - `autoSuppressKeywords` (string[], required) - `dryRun` (boolean, required) - `startedAt` (string · date-time | null, required) - `startedBy` (string · uuid | null, required) - `pausedAt` (string · date-time | null, required) - `pausedBy` (string · uuid | null, required) - `completedAt` (string · date-time | null, required) - `archivedAt` (string · date-time | null, required) - `archivedBy` (string · uuid | null, required) - `createdAt` (string · date-time, required) - `createdBy` (string · uuid | null, required) - `updatedAt` (string · date-time, required) - `updatedBy` (string · uuid | null, required) - `connectedAccounts` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Archive a campaign `DELETE /api/v1/campaigns/{campaignId}` Operation ID: `archiveCampaign` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **200**: Campaign archived. - `archived` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Get campaign stats `GET /api/v1/campaigns/{campaignId}/stats` Operation ID: `getCampaignStats` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **200**: Funnel, enrollments, steps, and account health. - `stats` (object, required) - `funnel` (object, required) - `submitted` (number, required) - `enrolled` (number, required) - `skippedByReason` (object, required) - `invitesSent` (number, required) - `invitesAccepted` (number, required) - `acceptanceRate` (number | null, required) - `messagesSent` (number, required) - `repliedLeads` (number, required) - `replyRate` (number | null, required) - `enrollments` (object, required) - `active` (number, required) - `waiting` (number, required) - `completed` (number, required) - `stopped` (number, required) - `failed` (number, required) - `byExitReason` (object, required) - `steps` (object[], required) - `channels` (object[], required) - `timeSeries` (object[], required) - `connectedAccounts` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List campaign events `GET /api/v1/campaigns/{campaignId}/events` Operation ID: `listCampaignEvents` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of campaign events, newest first. Pass nextCursor as cursor for the next page. - `events` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List campaign leads `GET /api/v1/campaigns/{campaignId}/leads` Operation ID: `listCampaignLeads` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of leads in the campaign, newest first. Pass nextCursor as cursor for the next page. - `leads` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Enroll leads `POST /api/v1/campaigns/{campaignId}/leads` Operation ID: `enrollCampaignLeads` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **Request body** (required) - `leads` (object[], required) **201**: Enrollment results per lead. - `results` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Stop a lead in a campaign `DELETE /api/v1/campaigns/{campaignId}/leads/{leadId}` Operation ID: `removeCampaignLead` **Path parameters** - `campaignId` (string · uuid, required): Campaign id - `leadId` (string · uuid, required): Lead id **200**: Lead enrollment stopped. - `stopped` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List pending approvals `GET /api/v1/campaigns/{campaignId}/pending` Operation ID: `listPendingApprovals` **Path parameters** - `campaignId` (string · uuid, required): Campaign id **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of tasks waiting for human approval, newest first. Pass nextCursor as cursor for the next page. - `pending` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Approve a pending send `PATCH /api/v1/campaigns/{campaignId}/pending/{taskId}` Operation ID: `approvePendingTask` **Path parameters** - `campaignId` (string · uuid, required): Campaign id - `taskId` (string · uuid, required): Pending task id **Request body** (required) - `text` (string, optional) **200**: Task approved. - `approved` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Events ### Search events `POST /api/v1/events/search` Operation ID: `searchEvents` **Request body** (required) - `types` ("lead_enrolled" | "lead_skipped" | "message_generated" | "task_skipped" | "task_failed" | "enrollment_completed" | "enrollment_stopped" | "lead_suppressed" | "account_paused" | "campaign_status_changed" | "linkedin_profile_visited" | "linkedin_invite_sent" | "linkedin_invite_withdrawn" | "linkedin_message_sent" | "linkedin_follow_sent" | "linkedin_unfollowed" | "linkedin_invite_accepted" | "linkedin_message_received" | "linkedin_account_status_changed" | "instagram_message_sent" | "instagram_profile_visited" | "instagram_follow_sent" | "instagram_unfollowed" | "instagram_message_received" | "instagram_account_status_changed"[], optional) - `channel` ("linkedin" | "instagram", optional) - `campaignId` (string · uuid, optional) - `leadId` (string · uuid, optional) - `accountId` (string · uuid, optional) - `from` (string · date, optional) - `to` (string · date, optional) - `cursor` (string, optional) - `limit` (integer, optional) **200**: A page of events, newest first. Filter by campaign, lead, account, type, channel, or date. Pass nextCursor as cursor for the next page. - `events` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## LinkedIn ### Get a LinkedIn profile `GET /api/v1/linkedin/profiles/{identity}` Operation ID: `getLinkedinProfile` **Path parameters** - `identity` (string, required): Public identifier, provider id, or profile URL **Query parameters** - `connectedAccountId` (string · uuid, required): Connected account to act as **200**: LinkedIn profile. - `profile` (object, required) - `publicIdentifier` (string | null, required) - `profileUrl` (string | null, required) - `firstName` (string | null, required) - `lastName` (string | null, required) - `fullName` (string | null, required) - `headline` (string | null, required) - `summary` (string | null, required) - `location` (string | null, required) - `companyName` (string | null, required) - `jobTitle` (string | null, required) - `imageUrl` (string | null, required) - `imageUrlLarge` (string | null, required) - `backgroundImageUrl` (string | null, required) - `followerCount` (number | null, required) - `degree` ("self" | "first" | "second" | "third_plus" | "out_of_network" | "unknown", required) - `invitationPending` (boolean, required): An invitation from this account is awaiting an answer. - `isConnected` (boolean, required): A first-degree connection, so a plain message will be delivered. - `remainingToday` (integer, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Search LinkedIn people `POST /api/v1/linkedin/search` Operation ID: `searchLinkedinPeople` Provide either `keywords` or a LinkedIn/Sales Navigator `searchUrl`, not both. **Request body** (required) - `connectedAccountId` (string · uuid, required): Connected account to act as - `keywords` (string, optional) - `searchUrl` (string · uri, optional) - `cursor` (string, optional) - `limit` (integer, optional) **200**: Search hits and an optional cursor. - `hits` (object[], required) - `cursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Send a LinkedIn message `POST /api/v1/linkedin/messages` Operation ID: `sendLinkedinMessage` **Request body** (required) - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Public identifier, provider id, or profile URL - `text` (string, required) - `dryRun` (boolean, optional) **201**: Send result. - `result` (object | null, required) - `providerMessageId` (string | null, required) - `conversationId` (string | null, required) - `sentAt` (string · date-time, required) - `remainingToday` (integer, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List messages in a conversation `GET /api/v1/linkedin/conversations/{conversationId}/messages` Operation ID: `listLinkedinConversationMessages` **Path parameters** - `conversationId` (string, required): Conversation id **Query parameters** - `connectedAccountId` (string · uuid, required): Connected account to act as - `limit` (integer, optional) **200**: Conversation messages. - `messages` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List pending invitations `GET /api/v1/linkedin/invitations` Operation ID: `listLinkedinInvitations` **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) - `connectedAccountId` (string · uuid, required): Connected account to act as **200**: A page of pending invitations. Pass nextCursor as cursor for the next page. - `invitations` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Send a connection invitation `POST /api/v1/linkedin/invitations` Operation ID: `sendLinkedinInvitation` **Request body** (required) - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Public identifier, provider id, or profile URL - `note` (string, optional) - `dryRun` (boolean, optional) **201**: Invitation result. - `result` (object | null, required) - `providerInvitationId` (string | null, required) - `sentAt` (string · date-time, required) - `remainingToday` (integer, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Withdraw an invitation `DELETE /api/v1/linkedin/invitations/{invitationId}` Operation ID: `withdrawLinkedinInvitation` **Path parameters** - `invitationId` (string, required): Provider invitation id **Query parameters** - `connectedAccountId` (string · uuid, required): Connected account to act as **200**: Invitation withdrawn. - `withdrawn` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List first-degree connections `GET /api/v1/linkedin/relations` Operation ID: `listLinkedinRelations` **Query parameters** - `connectedAccountId` (string · uuid, required): Connected account to act as - `cursor` (string, optional) **200**: Relations and an optional cursor. - `relations` (object[], required) - `cursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Instagram ### Get an Instagram profile `GET /api/v1/instagram/profiles/{identity}` Operation ID: `getInstagramProfile` **Path parameters** - `identity` (string, required): Username, provider id, or profile URL **Query parameters** - `connectedAccountId` (string · uuid, required): Connected account to act as **200**: Instagram profile. - `profile` (object, required) - `username` (string | null, required) - `profileUrl` (string | null, required) - `firstName` (string | null, required) - `lastName` (string | null, required) - `fullName` (string | null, required) - `biography` (string | null, required) - `location` (string | null, required) - `imageUrl` (string | null, required) - `imageUrlLarge` (string | null, required) - `backgroundImageUrl` (string | null, required) - `followerCount` (number | null, required) - `followRequestPending` (boolean, required): A follow request awaiting an answer; only private accounts can be in this state. - `isFollowing` (boolean, required): This account already follows them. - `remainingToday` (integer, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Send an Instagram message `POST /api/v1/instagram/messages` Operation ID: `sendInstagramMessage` **Request body** (required) - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Username, provider id, or profile URL - `text` (string, required) - `dryRun` (boolean, optional) **201**: Send result. - `result` (object | null, required) - `providerMessageId` (string | null, required) - `conversationId` (string | null, required) - `sentAt` (string · date-time, required) - `remainingToday` (integer, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### List messages in a conversation `GET /api/v1/instagram/conversations/{conversationId}/messages` Operation ID: `listInstagramConversationMessages` **Path parameters** - `conversationId` (string, required): Conversation id **Query parameters** - `connectedAccountId` (string · uuid, required): Connected account to act as - `limit` (integer, optional) **200**: Conversation messages. - `messages` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Follow an Instagram user `POST /api/v1/instagram/follows` Operation ID: `followInstagramUser` **Request body** (required) - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Username, provider id, or profile URL - `dryRun` (boolean, optional) **201**: Follow result. - `result` (object | null, required) - `providerInvitationId` (string | null, required) - `sentAt` (string · date-time, required) - `remainingToday` (integer, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Unfollow an Instagram user `DELETE /api/v1/instagram/follows/{identity}` Operation ID: `unfollowInstagramUser` **Path parameters** - `identity` (string, required): Username, provider id, or profile URL **Query parameters** - `connectedAccountId` (string · uuid, required): Connected account to act as **200**: Unfollowed. - `unfollowed` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Places ### Search places `POST /api/v1/places/search` Operation ID: `searchPlaces` Build a list of businesses and locations. Pass a query for text search, or a location (and optional type) for nearby search. Page text-search results with the returned cursor. **Request body** (required) - `query` (string, optional): Free-text query, e.g. "dentists in Munich". Required unless `location` is set. - `location` (object, optional): Center point. Required unless `query` is set. Without a query this is a nearby search inside the radius. - `latitude` (number, required) - `longitude` (number, required) - `radiusMeters` (integer, optional): Radius when `location` is set. Defaults to 50 km. Nearby search stays inside this circle; text search is biased toward it. - `type` (string, optional): A Google Places type such as `restaurant` or `dentist`. - `language` (string, optional): BCP-47 language code for names and addresses. - `region` (string, optional): CLDR region code that biases results, e.g. `DE`. - `cursor` (string, optional): Text-search pagination cursor. Nearby search does not page. - `limit` (integer, optional) **200**: Place hits and an optional cursor. - `hits` (object[], required) - `cursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Get a place `GET /api/v1/places/{placeId}` Operation ID: `getPlace` **Path parameters** - `placeId` (string, required): Google Place id **200**: Place details. - `place` (object, required) - `id` (string, required) - `name` (string | null, required) - `formattedAddress` (string | null, required) - `city` (string | null, required) - `region` (string | null, required) - `country` (string | null, required) - `postalCode` (string | null, required) - `location` (object | null, required) - `latitude` (number, required) - `longitude` (number, required) - `googleMapsUrl` (string | null, required) - `websiteUrl` (string | null, required) - `websiteDomain` (string | null, required) - `phone` (string | null, required) - `types` (string[], required) - `primaryType` (string | null, required) - `businessStatus` ("operational" | "temporarily_closed" | "permanently_closed" | "unknown", required) - `rating` (number | null, required) - `ratingCount` (integer | null, required) - `weekdayHours` (string[] | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Compose ### Preview an AI-composed message `POST /api/v1/compose` Operation ID: `composePreview` **Request body** (required) - `channel` ("linkedin" | "instagram", required) - `connectedAccountId` (string · uuid, required) - `identity` (string, required) - `prompt` (string, required) - `context` ("profile" | "posts" | "conversation" | "lead_properties"[], optional) - `model` (string, optional) - `maxChars` (integer, optional) - `leadProperties` (object, optional) **200**: Generated text and compose metadata. - `text` (string, required) - `meta` (object, required) - `model` (string, required) - `contextUsed` (object[], required) - `usedFallback` (boolean, required) - `generatedAt` (string · date-time, required) - `providerCalls` (integer, required) - `remainingBudget` (object, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ## Suppressions ### List suppressions `GET /api/v1/suppressions` Operation ID: `listSuppressions` **Query parameters** - `cursor` (string, optional) - `limit` (integer, optional) - `reason` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive", optional) **200**: A page of suppression entries, newest first. Pass nextCursor as cursor for the next page. - `suppressions` (object[], required) - `nextCursor` (string | null, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Create suppressions `POST /api/v1/suppressions` Operation ID: `createSuppressions` **Request body** (required) - `entries` (object[], required) **201**: Created entries. - `created` (integer, required) - `alreadyListed` (integer, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Check people and companies against the suppression list `POST /api/v1/suppressions/check` Operation ID: `checkSuppressions` **Request body** (required) - `identities` (object[], required) **200**: Match results. - `results` (object[], required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload ### Delete a suppression `DELETE /api/v1/suppressions/{suppressionId}` Operation ID: `deleteSuppression` **Path parameters** - `suppressionId` (string · uuid, required): Suppression id **200**: Suppression deleted. - `deleted` (boolean, required) **Errors** - `400`: Bad request - `401`: Unauthorized: missing or invalid API key - `403`: Forbidden - `404`: Not found - `422`: Invalid request payload --- # Needle MCP server Connect at `/mcp/v1` with `Authorization: Bearer sk-ndl-…`. Listing tools does not require a key; calling them does. ## Contents - Me - [Get the authenticated user](/docs/mcp/needle-get-me) - Connected accounts - [List connected accounts](/docs/mcp/needle-list-connected-accounts) - [Get a connected account](/docs/mcp/needle-get-connected-account) - [Update a connected account](/docs/mcp/needle-update-connected-account) - [Delete a connected account](/docs/mcp/needle-delete-connected-account) - [List connected account events](/docs/mcp/needle-list-connected-account-events) - Leads - [List leads](/docs/mcp/needle-list-leads) - [Create a lead](/docs/mcp/needle-create-lead) - [Get a lead](/docs/mcp/needle-get-lead) - [Update a lead](/docs/mcp/needle-update-lead) - [Delete a lead](/docs/mcp/needle-delete-lead) - [List lead events](/docs/mcp/needle-list-lead-events) - Campaigns - [List campaigns](/docs/mcp/needle-list-campaigns) - [Create a campaign](/docs/mcp/needle-create-campaign) - [Get a campaign](/docs/mcp/needle-get-campaign) - [Update a campaign](/docs/mcp/needle-update-campaign) - [Archive a campaign](/docs/mcp/needle-archive-campaign) - [Get campaign stats](/docs/mcp/needle-get-campaign-stats) - [List campaign events](/docs/mcp/needle-list-campaign-events) - [List campaign leads](/docs/mcp/needle-list-campaign-leads) - [Enroll leads](/docs/mcp/needle-enroll-campaign-leads) - [Stop a lead in a campaign](/docs/mcp/needle-remove-campaign-lead) - [List pending approvals](/docs/mcp/needle-list-pending-approvals) - [Approve a pending send](/docs/mcp/needle-approve-pending-task) - Events - [Search events](/docs/mcp/needle-search-events) - LinkedIn - [Get a LinkedIn profile](/docs/mcp/needle-get-linkedin-profile) - [Search LinkedIn people](/docs/mcp/needle-search-linkedin-people) - [Send a LinkedIn message](/docs/mcp/needle-send-linkedin-message) - [List messages in a conversation](/docs/mcp/needle-list-linkedin-conversation-messages) - [List pending invitations](/docs/mcp/needle-list-linkedin-invitations) - [Send a connection invitation](/docs/mcp/needle-send-linkedin-invitation) - [Withdraw an invitation](/docs/mcp/needle-withdraw-linkedin-invitation) - [List first-degree connections](/docs/mcp/needle-list-linkedin-relations) - Instagram - [Get an Instagram profile](/docs/mcp/needle-get-instagram-profile) - [Send an Instagram message](/docs/mcp/needle-send-instagram-message) - [List messages in a conversation](/docs/mcp/needle-list-instagram-conversation-messages) - [Follow an Instagram user](/docs/mcp/needle-follow-instagram-user) - [Unfollow an Instagram user](/docs/mcp/needle-unfollow-instagram-user) - Places - [Search places](/docs/mcp/needle-search-places) - [Get a place](/docs/mcp/needle-get-place) - Compose - [Preview an AI-composed message](/docs/mcp/needle-compose-preview) - Suppressions - [List suppressions](/docs/mcp/needle-list-suppressions) - [Create suppressions](/docs/mcp/needle-create-suppressions) - [Check people and companies against the suppression list](/docs/mcp/needle-check-suppressions) - [Delete a suppression](/docs/mcp/needle-delete-suppression) ## Me ### needleGetMe **Get the authenticated user** Returns the user and organization this caller is bound to. **Input** object ## Connected accounts ### needleListConnectedAccounts **List connected accounts** List a page of connected LinkedIn and Instagram accounts. Use nextCursor as cursor for the next page. **Input** - `cursor` (string, optional) - `limit` (integer, optional) ### needleGetConnectedAccount **Get a connected account** Get a connected account and its usage. **Input** - `connectedAccountId` (string · uuid, required): Connected account id ### needleUpdateConnectedAccount **Update a connected account** Update a connected account. **Input** - `connectedAccountId` (string · uuid, required): Connected account id - `paused` (boolean, optional) - `quotaOverrides` (object | null, optional) ### needleDeleteConnectedAccount **Delete a connected account** Delete a connected account. **Input** - `connectedAccountId` (string · uuid, required): Connected account id ### needleListConnectedAccountEvents **List connected account events** List a page of events for a connected account. Use nextCursor as cursor for the next page. **Input** - `connectedAccountId` (string · uuid, required): Connected account id - `cursor` (string, optional) - `limit` (integer, optional) ## Leads ### needleListLeads **List leads** List a page of leads in this organization. Use nextCursor as cursor for the next page. **Input** - `cursor` (string, optional) - `limit` (integer, optional) ### needleCreateLead **Create a lead** Create a lead with LinkedIn and/or Instagram identities. **Input** - `identities` (object, required) - `linkedin` (string, optional) - `instagram` (string, optional) - `timezone` (string, required) - `firstName` (string, optional) - `lastName` (string, optional) - `companyName` (string, optional) - `companyDomain` (string, optional) - `jobTitle` (string, optional) - `email` (string, optional) - `imageUrl` (string, optional) - `properties` (object, optional) ### needleGetLead **Get a lead** Get a lead, its identities, and enrollments. **Input** - `leadId` (string · uuid, required): Lead id ### needleUpdateLead **Update a lead** Update a lead profile or identities. **Input** - `leadId` (string · uuid, required): Lead id - `identities` (object, optional) - `linkedin` (string, optional) - `instagram` (string, optional) - `timezone` (string, optional) - `firstName` (string, optional) - `lastName` (string, optional) - `companyName` (string, optional) - `companyDomain` (string, optional) - `jobTitle` (string, optional) - `email` (string, optional) - `imageUrl` (string, optional) - `properties` (object, optional) ### needleDeleteLead **Delete a lead** Delete a lead. **Input** - `leadId` (string · uuid, required): Lead id ### needleListLeadEvents **List lead events** List a page of events for a lead. Use nextCursor as cursor for the next page. **Input** - `leadId` (string · uuid, required): Lead id - `cursor` (string, optional) - `limit` (integer, optional) ## Campaigns ### needleListCampaigns **List campaigns** List a page of campaigns. Filter by status. Use nextCursor as cursor for the next page. **Input** - `cursor` (string, optional) - `limit` (integer, optional) - `status` ("draft" | "active" | "paused" | "completed" | "archived", optional) ### needleCreateCampaign **Create a campaign** Create a campaign. **Input** - `name` (string, required) - `graph` (object, required) - `nodes` (object[], required) - `edges` (object[], required) - `schedule` (object, optional) - `workingDays` (integer[], optional) - `startTime` (string, optional) - `endTime` (string, optional) - `limits` (object, optional) - `minGapSeconds` (integer, optional) - `maxGapSeconds` (integer, optional) - `maxNewLeadsPerDay` (integer | null, optional) - `suppressionPolicy` (object, optional) - `bypassSuppressionReasons` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive"[], optional) - `cooldownDays` (integer, optional) - `unresponsiveAfterTouches` (integer, optional) - `autoSuppressKeywords` (string[], optional) ### needleGetCampaign **Get a campaign** Get a campaign by id. **Input** - `campaignId` (string · uuid, required): Campaign id ### needleUpdateCampaign **Update a campaign** Update a campaign. **Input** - `campaignId` (string · uuid, required): Campaign id - `name` (string, optional) - `status` ("active" | "paused", optional) - `graph` (object, optional) - `nodes` (object[], required) - `edges` (object[], required) - `schedule` (object, optional) - `workingDays` (integer[], optional) - `startTime` (string, optional) - `endTime` (string, optional) - `limits` (object, optional) - `minGapSeconds` (integer, optional) - `maxGapSeconds` (integer, optional) - `maxNewLeadsPerDay` (integer | null, optional) - `suppressionPolicy` (object, optional) - `bypassSuppressionReasons` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive"[], optional) - `cooldownDays` (integer, optional) - `unresponsiveAfterTouches` (integer, optional) - `autoSuppressKeywords` (string[], optional) - `dryRun` (boolean, optional) ### needleArchiveCampaign **Archive a campaign** Archive a campaign. **Input** - `campaignId` (string · uuid, required): Campaign id ### needleGetCampaignStats **Get campaign stats** Funnel, enrollments, steps, and account health for a campaign. **Input** - `campaignId` (string · uuid, required): Campaign id ### needleListCampaignEvents **List campaign events** List a page of campaign events. Use nextCursor as cursor for the next page. **Input** - `campaignId` (string · uuid, required): Campaign id - `cursor` (string, optional) - `limit` (integer, optional) ### needleListCampaignLeads **List campaign leads** List a page of leads enrolled in a campaign. Use nextCursor as cursor for the next page. **Input** - `campaignId` (string · uuid, required): Campaign id - `cursor` (string, optional) - `limit` (integer, optional) ### needleEnrollCampaignLeads **Enroll leads** Enroll leads in a campaign. Returns accepted or skipped per lead. **Input** - `campaignId` (string · uuid, required): Campaign id - `leads` (object[], required) ### needleRemoveCampaignLead **Stop a lead in a campaign** Stop a live lead enrollment in a campaign. **Input** - `campaignId` (string · uuid, required): Campaign id - `leadId` (string · uuid, required): Lead id ### needleListPendingApprovals **List pending approvals** List a page of campaign send tasks waiting for human approval. Use nextCursor as cursor for the next page. **Input** - `campaignId` (string · uuid, required): Campaign id - `cursor` (string, optional) - `limit` (integer, optional) ### needleApprovePendingTask **Approve a pending send** Approve a pending send. Optionally replace the generated text first. **Input** - `campaignId` (string · uuid, required): Campaign id - `taskId` (string · uuid, required): Pending task id - `text` (string, optional) ## Events ### needleSearchEvents **Search events** Search a page of events. Filter by campaign, lead, account, type, channel, or date. Use nextCursor as cursor for the next page. **Input** - `types` ("lead_enrolled" | "lead_skipped" | "message_generated" | "task_skipped" | "task_failed" | "enrollment_completed" | "enrollment_stopped" | "lead_suppressed" | "account_paused" | "campaign_status_changed" | "linkedin_profile_visited" | "linkedin_invite_sent" | "linkedin_invite_withdrawn" | "linkedin_message_sent" | "linkedin_follow_sent" | "linkedin_unfollowed" | "linkedin_invite_accepted" | "linkedin_message_received" | "linkedin_account_status_changed" | "instagram_message_sent" | "instagram_profile_visited" | "instagram_follow_sent" | "instagram_unfollowed" | "instagram_message_received" | "instagram_account_status_changed"[], optional) - `channel` ("linkedin" | "instagram", optional) - `campaignId` (string · uuid, optional) - `leadId` (string · uuid, optional) - `accountId` (string · uuid, optional) - `from` (string · date, optional) - `to` (string · date, optional) - `cursor` (string, optional) - `limit` (integer, optional) ## LinkedIn ### needleGetLinkedinProfile **Get a LinkedIn profile** Get a LinkedIn profile by public identifier, provider id, or URL. **Input** - `identity` (string, required): Public identifier, provider id, or profile URL - `connectedAccountId` (string · uuid, required): Connected account to act as ### needleSearchLinkedinPeople **Search LinkedIn people** Search LinkedIn people. Provide either keywords or a LinkedIn/Sales Navigator searchUrl, not both. **Input** - `connectedAccountId` (string · uuid, required): Connected account to act as - `keywords` (string, optional) - `searchUrl` (string · uri, optional) - `cursor` (string, optional) - `limit` (integer, optional) ### needleSendLinkedinMessage **Send a LinkedIn message** Send a LinkedIn message as a connected account. **Input** - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Public identifier, provider id, or profile URL - `text` (string, required) - `dryRun` (boolean, optional) ### needleListLinkedinConversationMessages **List messages in a conversation** List messages in a LinkedIn conversation. **Input** - `conversationId` (string, required): Conversation id - `connectedAccountId` (string · uuid, required): Connected account to act as - `limit` (integer, optional) ### needleListLinkedinInvitations **List pending invitations** List a page of pending LinkedIn connection invitations from this account. Use nextCursor as cursor for the next page. **Input** - `cursor` (string, optional) - `limit` (integer, optional) - `connectedAccountId` (string · uuid, required): Connected account to act as ### needleSendLinkedinInvitation **Send a connection invitation** Send a LinkedIn connection invitation. **Input** - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Public identifier, provider id, or profile URL - `note` (string, optional) - `dryRun` (boolean, optional) ### needleWithdrawLinkedinInvitation **Withdraw an invitation** Withdraw a pending LinkedIn invitation. **Input** - `invitationId` (string, required): Provider invitation id - `connectedAccountId` (string · uuid, required): Connected account to act as ### needleListLinkedinRelations **List first-degree connections** List first-degree LinkedIn connections. Use the returned cursor for the next page. **Input** - `connectedAccountId` (string · uuid, required): Connected account to act as - `cursor` (string, optional) ## Instagram ### needleGetInstagramProfile **Get an Instagram profile** Get an Instagram profile by username, provider id, or URL. **Input** - `identity` (string, required): Username, provider id, or profile URL - `connectedAccountId` (string · uuid, required): Connected account to act as ### needleSendInstagramMessage **Send an Instagram message** Send an Instagram direct message as a connected account. **Input** - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Username, provider id, or profile URL - `text` (string, required) - `dryRun` (boolean, optional) ### needleListInstagramConversationMessages **List messages in a conversation** List messages in an Instagram conversation. **Input** - `conversationId` (string, required): Conversation id - `connectedAccountId` (string · uuid, required): Connected account to act as - `limit` (integer, optional) ### needleFollowInstagramUser **Follow an Instagram user** Follow an Instagram user as a connected account. **Input** - `connectedAccountId` (string · uuid, required): Connected account to act as - `identity` (string, required): Username, provider id, or profile URL - `dryRun` (boolean, optional) ### needleUnfollowInstagramUser **Unfollow an Instagram user** Unfollow an Instagram user as a connected account. **Input** - `identity` (string, required): Username, provider id, or profile URL - `connectedAccountId` (string · uuid, required): Connected account to act as ## Places ### needleSearchPlaces **Search places** Build a list of businesses and locations. Pass a query for text search, or a location (and optional type) for nearby search. Page text-search results with the returned cursor. **Input** - `query` (string, optional): Free-text query, e.g. "dentists in Munich". Required unless `location` is set. - `location` (object, optional): Center point. Required unless `query` is set. Without a query this is a nearby search inside the radius. - `latitude` (number, required) - `longitude` (number, required) - `radiusMeters` (integer, optional): Radius when `location` is set. Defaults to 50 km. Nearby search stays inside this circle; text search is biased toward it. - `type` (string, optional): A Google Places type such as `restaurant` or `dentist`. - `language` (string, optional): BCP-47 language code for names and addresses. - `region` (string, optional): CLDR region code that biases results, e.g. `DE`. - `cursor` (string, optional): Text-search pagination cursor. Nearby search does not page. - `limit` (integer, optional) ### needleGetPlace **Get a place** Get place details by Google Place id. **Input** - `placeId` (string, required): Google Place id ## Compose ### needleComposePreview **Preview an AI-composed message** Preview an AI-composed outreach message without sending it. **Input** - `channel` ("linkedin" | "instagram", required) - `connectedAccountId` (string · uuid, required) - `identity` (string, required) - `prompt` (string, required) - `context` ("profile" | "posts" | "conversation" | "lead_properties"[], optional) - `model` (string, optional) - `maxChars` (integer, optional) - `leadProperties` (object, optional) ## Suppressions ### needleListSuppressions **List suppressions** List a page of suppression entries. Optionally filter by reason. Use nextCursor as cursor for the next page. **Input** - `cursor` (string, optional) - `limit` (integer, optional) - `reason` ("manual" | "unsubscribed" | "not_interested" | "existing_customer" | "open_opportunity" | "competitor" | "employee" | "invalid_recipient" | "complained" | "recently_contacted" | "unresponsive", optional) ### needleCreateSuppressions **Create suppressions** Create suppression entries for people or companies. **Input** - `entries` (object[], required) ### needleCheckSuppressions **Check people and companies against the suppression list** Check whether identities are contactable or suppressed. **Input** - `identities` (object[], required) ### needleDeleteSuppression **Delete a suppression** Delete a suppression entry. **Input** - `suppressionId` (string · uuid, required): Suppression id --- # Changelog Notable changes to the Needle Agents REST API and MCP server. ## Agent-native Outbound Engine v1.0.0 · Aug 30, 2026 Introducing the Needle outbound engine for agents to run multi-channel campaigns. ![Agent-native Outbound Engine](/images/blog/introducing-agents-cover.png) Agents should be able to run outbound without wiring five tools together. This release is that engine: source people, run LinkedIn and Instagram campaigns, and call the same work from any agent over API or MCP. Sends stay on the rails these channels need: per-account cadence, suppressions for people and companies you should not contact, and API keys bound to one user and one organization. ### Leads - Build lead lists from LinkedIn and the Places API. ### Campaigns - Run multi-channel, multi-account outreach on LinkedIn and Instagram. ### API - REST API at `/api/v1`. - MCP server at `/mcp/v1`. - Works with Claude Code, Cowork, Codex, Cursor, Hermes, or any other agent.