Needle
BUSINESS · CRM & SUPPORT
Outreach campaigns, connected social accounts, leads, files, and suppressions in the account they connected.
Acts as the person, not as itself
Each user connects their own account. Every call carries both identities — the agent and the person it is acting for — so the agent can never reach past what that individual can already do.
Credentials never touch the agent
Tokens live in the vault and attach server-side at call time. The agent holds a session, not a secret, and revoking access does not mean rotating a key.
Every call on the record
Who asked, which agent acted, which action ran, and the verdict that let it through — one audit trail across every integration, not one per vendor.
What an agent can do
Each action is granted on its own. An agent allowed to read is not thereby allowed to write, and the scope beside each row is what the acting user must have connected for it to run at all.
needle_delete_campaigns_by_campaignidWRITEArchive a campaign so it stops and leaves the active list, via DELETE /api/v1/campaigns/{campaignId}. ONE-WAY THROUGH THIS API. It is spelled DELETE and named 'archive' because the campaign and its history survive -- but 'Update a campaign' accepts only `active` and `paused` for `status`, so nothing here can bring an archived campaign back. If the intent is to stop sending for now, set `status: "paused"` instead, which is reversible from this API; archive when the campaign is finished for good.
needle_delete_campaigns_by_campaignid_leads_by_leadidWRITEEnd one lead's enrollment and drop its queued steps, via DELETE /api/v1/campaigns/{campaignId}/leads/{leadId}. THE EMERGENCY STOP FOR ONE PERSON. Everything this campaign still had queued for the lead is dropped, and dropped queue entries do not come back -- re-enrolling restarts the sequence from the beginning rather than resuming it. Messages already sent are on LinkedIn or Instagram and are unaffected. To stop contacting the person everywhere rather than in this one campaign, add them with 'Create suppressions'.
needle_delete_connected_accounts_by_connectedaccountidWRITEDisconnect a LinkedIn or Instagram account from Needle, via DELETE /api/v1/connected-accounts/{connectedAccountId}. PERMANENT FROM THIS API'S POINT OF VIEW, AND THAT IS THE SHARP EDGE HERE: reconnecting is dashboard-only. Needle's documentation states that "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" -- so this call can undo something no tool in this integration can redo, and every campaign sending through the account stops. If the intent is a temporary stop, use 'Pause or resume a connected account' instead, which is reversible from here.
needle_delete_connected_accounts_by_connectedaccountid_limits_by_actionWRITERemove one action's override so it runs on the channel defaults again, via DELETE /api/v1/connected- accounts/{connectedAccountId}/limits/{action}. Not destructive, and that is deliberate rather than an oversight: it deletes an OVERRIDE, not data, and the effect is reinstating Needle's own default cap. Anything it undoes can be set again with 'Override a limit for one action'. Use it to back out a raised cap after a campaign finishes, which is the safer resting state for the account.
needle_delete_filesWRITEDelete one stored file, via DELETE /api/v1/files. PERMANENT, AND IT BREAKS WHAT STILL POINTS AT IT. Needle's own words: "Deletes a private file stored with POST /api/v1/files. Pass the url returned by that call or by GET /api/v1/files. Messages and campaign steps that still reference it will fail when sending." A CAREFULLY SHAPED CALL: the file is named in the BODY (`url`), not in the path, so this is one of the few DELETEs on the platform that carries a JSON body -- check campaign steps with 'Get a campaign' before removing a file an active sequence attaches.
needle_delete_instagram_follows_by_identityWRITEUnfollow an account, or withdraw a pending follow request, via DELETE /api/v1/instagram/follows/{identity}. Needle's own words: "Unfollows an account, or withdraws a follow request still waiting for an answer." Marked destructive for the same reason as the follow: it changes a public relationship under a real person's handle, and follow/unfollow churn is exactly the pattern Instagram restricts accounts for. Metered as `instagram_unfollow` separately from the follow.
needle_delete_leads_by_leadidWRITERemove one person from the lead database, via DELETE /api/v1/leads/{leadId}. PERMANENT, and it reaches further than the record: the lead's campaign enrollments and its queued work go with it. Deleting a lead is NOT how to stop contacting someone -- a deleted lead can be re-imported by the next bulk upload and contacted again. To stop contacting a person for good, add them with 'Create suppressions', which survives re-import; delete the lead only when the record itself should not exist.
needle_delete_linkedin_invitations_by_invitationidWRITEWithdraw one pending sent invitation, via DELETE /api/v1/linkedin/invitations/{invitationId}. Needle's own words: "Withdraws a pending sent invitation, using an invitationId from GET /api/v1/linkedin/invitations." ONE-WAY: the invitation cannot be un-withdrawn, and LinkedIn does not let the same person be invited again immediately afterwards -- withdrawing to 'retry' is how an account ends up unable to invite that person for weeks. Withdraw to free up outstanding-invitation capacity, not to resend. `linkedin_cancel_invitation` is separately metered.
needle_delete_suppressions_by_suppressionidWRITERemove one do-not-contact entry, via DELETE /api/v1/suppressions/{suppressionId}. MAKES A SUPPRESSED PERSON CONTACTABLE AGAIN, which is why this read-like removal is the most consequential delete on the surface: if the entry's reason was `unsubscribed` or `complained`, the person asked not to be contacted and removing the record is how they get contacted anyway. Check the reason with 'List suppressions' first, and treat `unsubscribed`, `complained` and `not_interested` as entries not to remove. Entries Needle wrote itself (`recently_contacted`, `unresponsive`) are the ones it is usually safe to clear.
needle_get_campaignsREADList this organization's campaigns, via GET /api/v1/campaigns. A campaign is the sequence that does the outreach: a graph of steps, a schedule and a set of limits. `status` narrows to `draft`, `active`, `paused`, `completed` or `archived`. Paged with `cursor`/`limit`.
needle_get_campaigns_by_campaignidREADRead one campaign in full, via GET /api/v1/campaigns/{campaignId}. The whole definition: the step graph, the schedule, the pacing limits and the suppression policy. This is the safe way to get a valid `graph` -- read a campaign that works, change the part you mean to change, and send it back through 'Update a campaign' -- rather than authoring a graph against a schema this tool deliberately does not restate.
needle_get_campaigns_by_campaignid_eventsREADRead what a campaign has actually done, via GET /api/v1/campaigns/{campaignId}/events. The campaign-scoped slice of the event stream: each send, reply, failure and state change in order. Where 'Get campaign stats' gives the totals, this gives the rows behind them, which is what a discrepancy investigation needs. Paged with `cursor`/`limit`.
needle_get_campaigns_by_campaignid_leadsREADList the leads enrolled in one campaign, via GET /api/v1/campaigns/{campaignId}/leads. Who is in the sequence and where each of them has got to. Paged with `cursor`/`limit`. To add people use 'Enroll leads'; to take one out use 'Stop a lead in a campaign'.
needle_get_campaigns_by_campaignid_pendingREADList the campaign steps held for a human decision, via GET /api/v1/campaigns/{campaignId}/pending. A campaign can be configured to hold its sends for approval instead of sending them; those are the `pending_approval` tasks, and this is the queue of them for one campaign. Each row carries the task id 'Approve a pending send' takes and the text that would go out. Paged with `cursor`/`limit`.
needle_get_campaigns_by_campaignid_statsREADRead one campaign's aggregate results, via GET /api/v1/campaigns/{campaignId}/stats. The rolled-up counts for the campaign -- how many leads are enrolled and how the outreach has gone. Cheap and summary-level; for the individual events behind a number, use 'List campaign events' or 'Search events', and for what has NOT happened yet, 'List campaign tasks'.
needle_get_campaigns_by_campaignid_tasksREADRead the work a campaign still owes its leads, via GET /api/v1/campaigns/{campaignId}/tasks. Needle's own words: "The work this campaign owes its leads, and when each step is due." Due times here are the campaign's intent; the RATE they actually go out at is per connected account, so cross-read with 'List a connected account's queued work' when timing matters. `status` narrows to `pending` or `pending_approval`.
needle_get_connected_accountsREADList the channel accounts this organization has connected to Needle, via GET /api/v1/connected-accounts. THE STARTING POINT FOR EVERY CHANNEL TOOL: almost every LinkedIn and Instagram operation here takes a `connectedAccountId`, and this is the only place those ids come from. CONNECTING AN ACCOUNT IS DASHBOARD-ONLY. Needle's own documentation says so in as many words -- connecting happens at https://needle.app/accounts, and "The API and MCP can list, update, and delete connected accounts; they cannot start the OAuth connect flow." If this list comes back empty, no send, follow, invite or profile read can do anything until a human connects an account in Needle. That is a Needle-to-LinkedIn/Instagram connection and has nothing to do with the API key this Agentic Fabriq connection holds. Paged: pass `nextCursor` back as `cursor` and stop when it is null.
needle_get_connected_accounts_by_connectedaccountidREADRead one connected channel account, via GET /api/v1/connected- accounts/{connectedAccountId}. The single-id form of the listing: which channel it is, whose account it is, and whether it is currently paused. Read this before a bulk send to confirm the account is live -- a paused account still accepts queued work and sends none of it. An id from another organization answers 404 rather than 403, because a Needle key can only see inside the one organization it is bound to.
needle_get_connected_accounts_by_connectedaccountid_eventsREADRead the activity history of one connected account, via GET /api/v1/connected-accounts/{connectedAccountId}/events. What this account actually did and what happened back: sends, invitations, replies, failures. This is the per-account slice of the same event stream 'Search events' filters globally, and it is the first place to look when a campaign reports fewer sends than expected -- a limit that bit shows up here as the account's own history rather than as a campaign statistic. Paged with `cursor`/`limit`.
needle_get_connected_accounts_by_connectedaccountid_limitsREADRead every metered action on one account with its caps and usage so far, via GET /api/v1/connected-accounts/{connectedAccountId}/limits. Needle's own words: "Every metered action on this account with its channel defaults, the caps currently enforced, the override behind them (if any), and usage so far. Limit errors (429) point at one of these actions." READ THIS BEFORE PLANNING A BATCH. The caps are per account and per action -- invitations, messages, InMails, profile views, searches, comments, reactions, follows -- and they are the reason a send fails while the credential is perfectly good. Note that the OpenAPI document declares no 429 response on ANY operation while three of its own descriptions say 429 is what a hit limit returns; trust the prose and this tool, not the declared status list.
needle_get_connected_accounts_by_connectedaccountid_tasksREADRead the outbound queue for one connected account, via GET /api/v1/connected-accounts/{connectedAccountId}/tasks. Needle's own words: "The queue for one account. Because limits and pacing are per account, this is the order and rate its work will actually go out at." So this, not the campaign's own task list, is what tells you when something will really be sent. `status` narrows to `pending` (queued and will go) or `pending_approval` (queued and waiting for a human to release it with 'Approve a pending send').
needle_get_filesREADList the private files stored in this Needle organization, via GET /api/v1/files. Needle's own words: "Lists private files in this organization. Pass a returned url on a LinkedIn send or campaign step. Pass nextCursor as cursor for the next page." THE `url` IS THE ONE TO PASS ON, not the `downloadUrl` -- Needle says the download link is a preview and must not be given to LinkedIn. Paged with `cursor`/`limit`.
needle_get_instagram_conversationsREADList the connected account's Instagram inbox, via GET /api/v1/instagram/conversations. Needle's own words: "Returns the conversations of the connected account. Take conversationId from here to read the messages or the participants." The reply-detection surface for Instagram, and the source of every `conversationId` on this channel. `limit` defaults to 20 and caps at 100, and Needle warns that "a page can hold fewer, so follow cursor until it comes back null" -- a short page is not the end of the list.
needle_get_instagram_conversations_by_conversationid_messagesREADRead one Instagram conversation's messages, via GET /api/v1/instagram/conversations/{conversationId}/messages. Needle's own words: "Returns the messages of one conversation, most recent first, each with the reactions on it." Each row carries the `messageId` that 'React to an Instagram message' takes. Like its LinkedIn twin this route takes `limit` (default 20) but NO `cursor`, so it is not cursor-paged -- raise the limit rather than looking for a next page.
needle_get_instagram_conversations_by_conversationid_participantsREADRead who is in one Instagram conversation, via GET /api/v1/instagram/conversations/{conversationId}/participants. Needle's own words: "Returns the accounts in a conversation. Take identity from here to read a participant's profile, posts or followers." That is the practical use: it turns an opaque conversation into usernames the profile, post and follower tools accept.
needle_get_instagram_followersREADList the followers of an account, via GET /api/v1/instagram/followers. Needle's own words: "Returns the followers of an account, or of the connected account when identity is omitted." NOTE THE SMALLER PAGE: `limit` caps at 25 here, not the 100 most of this surface allows, so a large follower list is a long walk -- follow `nextCursor` until it is null and expect many pages.
needle_get_instagram_followingREADList what an account follows, via GET /api/v1/instagram/following. Needle's own words: "Returns the accounts one account follows, or the ones the connected account follows when identity is omitted." This one WORKS, unlike its LinkedIn namesake, which Needle documents as not supported yet. `limit` caps at 25 as on the followers listing.
needle_get_instagram_meREADRead the connected account's own Instagram profile, via GET /api/v1/instagram/me. Needle's own words: "Returns the profile of the connected account itself, including its follower, following and post counts." The Instagram-side identity check, next to 'Get the authenticated Needle user' which answers the Needle-side one. Useful before publishing: it confirms which account a post would appear on.
needle_get_instagram_postsREADList the posts of one Instagram account, via GET /api/v1/instagram/posts. Needle's own words: "Returns the posts of one account, most recent first. A username that does not exist comes back with no posts rather than an error." THAT IS WORTH REPEATING: an empty result does not mean the account has no posts, it may mean the username was wrong, so confirm with 'Get an Instagram profile' before concluding anything from zero rows. `identity` is REQUIRED and is a username without @. Paged with `cursor`/`limit`.
needle_get_instagram_posts_by_postidREADRead one Instagram post, via GET /api/v1/instagram/posts/{postId}. Needle's own words: "Returns one post, by postId or by the short code from its URL." Unlike LinkedIn, where an id taken from a post URL usually does not resolve, Instagram's URL short code IS accepted here -- so both the id from a listing and the code from a browser address bar work.
needle_get_instagram_posts_by_postid_commentsREADRead a post's comments, or the replies to one comment, via GET /api/v1/instagram/posts/{postId}/comments. Needle's own words: "Returns the comments on a post, or the replies to one comment. Take commentId from a row to read that reply thread." The `commentId` on each row is also what 'Comment on an Instagram post' and 'Like an Instagram post' take to reply to or like a comment rather than the post. Paged with `cursor`/`limit`; a short page is not the end.
needle_get_instagram_posts_by_postid_reactionsREADRead who liked a post or one of its comments, via GET /api/v1/instagram/posts/{postId}/reactions. Needle's own words: "Returns the accounts that liked a post, or one of its comments." Despite the `reactions` path, Instagram has exactly one reaction on a post and it is a like -- the plural is the route's spelling, not a range of choices. Pass `commentId` to read a comment's likes instead. Paged with `cursor`/`limit`.
needle_get_instagram_profiles_by_identityREADRead one Instagram profile and the connected account's relationship to it, via GET /api/v1/instagram/profiles/{identity}. Needle's own words: "Returns a profile by username, with the connected account's relationship to it. Counts against the daily profile view allowance. Returns 429 when the account is at its limit." A METERED READ -- `instagram_profile_view` is capped on 'List a connected account's limits and usage'. `identity` is a username WITHOUT the leading @. The relationship fields are what 'Follow an Instagram user' and 'Unfollow an Instagram user' should be decided from, including `followRequestPending` for a private account.
needle_get_leadsREADList the people in this organization's lead database, via GET /api/v1/leads. A lead is Needle's record of a person, keyed by the channel handles it carries. Campaigns enroll leads BY ID, so this listing (or 'Create a lead') is where the ids for 'Enroll leads' come from. Paged with `cursor`/`limit`; follow `nextCursor` until null.
needle_get_leads_by_leadidREADRead one lead in full, via GET /api/v1/leads/{leadId}. Everything the record carries: its channel identities, the profile fields, the timezone the schedule is evaluated in, and the free-form `properties` a campaign's message templates personalize from. Read this before enrolling when a campaign template references a property -- an empty field a message uses is an enrollment skip (`missing_personalization`), and the skip names the field.
needle_get_leads_by_leadid_eventsREADRead one lead's activity history, via GET /api/v1/leads/{leadId}/events. Everything that has happened to and from this person across every campaign: what was sent, when, on which channel, and what came back. This is the record to read before contacting someone again, and the one that answers 'have we already talked to them'. Paged with `cursor`/`limit`.
needle_get_leads_by_leadid_tasksREADRead every action campaigns have queued for one lead, via GET /api/v1/leads/{leadId}/tasks. Needle's own words: "Every action campaigns have queued for this lead, across its enrollments." Use it to see what is about to happen to a person before it does -- and 'Stop a lead in a campaign' to cancel it. `status` narrows to `pending` or `pending_approval`.
needle_get_linkedin_companies_by_identifierREADRead one company page, via GET /api/v1/linkedin/companies/{identifier}. Needle's own words: "Returns a company by public identifier, numeric id, or company URL." THE URL FORM IS WHY THIS TOOL'S PATH ARGUMENT IS PERCENT-ENCODED RATHER THAN REFUSED FOR CONTAINING SLASHES -- Needle documents `https://www.linkedin.com/company/heravideo` as a legal value for `identifier`, and it has to survive being put in a path segment. The numeric id in the reply is what 'Search LinkedIn companies' returns and what the company endpoints accept.
needle_get_linkedin_companies_by_identifier_jobsREADList the jobs a company currently has posted, via GET /api/v1/linkedin/companies/{identifier}/jobs. Needle's own words: "Jobs currently posted by a company. Use cursor for the next page." The same `identifier` forms as 'Get a LinkedIn company' -- public identifier, numeric id or a company URL. For jobs across many companies at once, 'Search LinkedIn jobs' is the better tool; this one is the per-company view.
needle_get_linkedin_conversationsREADList the connected account's LinkedIn inbox, via GET /api/v1/linkedin/conversations. Needle's own words: "Conversations in the account inbox. Use a conversationId from here to read its messages." This is the reply- detection surface: a campaign that wants to stop messaging someone who answered reads the inbox here, not the campaign statistics. Paged with `cursor`/`limit`.
needle_get_linkedin_conversations_by_conversationid_messagesREADRead the messages of one LinkedIn conversation, via GET /api/v1/linkedin/conversations/{conversationId}/messages. Needle's own words: "Returns the messages in one conversation. Take conversationId from the conversations listing." A conversation id is opaque and comes from 'List LinkedIn conversations'; it is not something to construct. `limit` defaults to 20. Note the shape of this one: it takes `limit` but NO `cursor`, so it is not cursor-paged the way the rest of this surface is -- raise `limit` (up to 100) rather than looking for a next page.
needle_get_linkedin_followersREADList the followers of the connected account, a person or a company, via GET /api/v1/linkedin/followers. Needle's own words: "Followers of the connected account, or of the person or company given in identity. A company page is readable only by its admins." So a company `identity` the connected account does not administer is a permission failure rather than an empty list. Omit `identity` for the connected account's own followers. Paged with `cursor`/`limit`.
needle_get_linkedin_followingREADList what the connected account follows, via GET /api/v1/linkedin/following. NOT IMPLEMENTED BY NEEDLE TODAY, AND NEEDLE SAYS SO ITSELF: its OpenAPI description for this operation reads "Accounts the connected account follows. Not supported yet -- requests return an error." It is shipped rather than withheld because it is a declared route that the vendor intends to implement, and withholding it would mean re-adding it later; but do not choose it expecting data. The Instagram equivalent, 'List accounts the Instagram account follows', carries no such warning and does work. If this ever starts answering, it pages with `cursor`/`limit` like its neighbours.
needle_get_linkedin_inmails_balanceREADRead the InMail credits left on the connected account, via GET /api/v1/linkedin/inmails/balance. Needle's own words: "InMail credits left on the connected account. Credits come with a Sales Navigator seat and renew monthly, separately from the daily action allowances." Two different budgets, and this is the one that costs money: the daily caps on 'List a connected account's limits and usage' reset every day, these credits renew monthly and run out for the rest of the month. Read it before a batch of InMails.
needle_get_linkedin_invitationsREADList connection invitations this account has sent and that are still pending, via GET /api/v1/linkedin/invitations. Needle's own words: "Connection invitations sent from this account that are still pending. For incoming ones, use GET /api/v1/linkedin/invitations/received." This is where the `invitationId` for 'Withdraw a LinkedIn invitation' comes from. Worth reading before a batch of invitations: LinkedIn caps how many invitations may be outstanding at once, so a large pending list is itself a reason the next invitation fails. Paged with `cursor`/`limit`.
needle_get_linkedin_invitations_receivedREADList pending connection invitations sent TO this account, via GET /api/v1/linkedin/invitations/received. Needle's own words: "Connection invitations received by this account that are still pending. Accept or decline them with POST /api/v1/linkedin/invitations/received/{invitationId}." Mind the direction: the `invitationId` here belongs to the RECEIVED queue and is not interchangeable with the one 'List sent LinkedIn invitations' returns. Paged with `cursor`/`limit`.
needle_get_linkedin_postsREADList the posts authored by one profile, via GET /api/v1/linkedin/posts. Needle's own words: "Posts authored by a person. A company identifier also works, but GET /api/v1/linkedin/companies/{identifier}/posts resolves it in one call instead of two." THAT SUGGESTED ROUTE DOES NOT EXIST IN NEEDLE'S OWN OPENAPI DOCUMENT (read 2026-09-25: the company paths it declares are `/companies/{identifier}` and `/companies/{identifier}/jobs`, and there is no `/posts` under either), so this tool is the way to read a company's posts and no tool for that suggestion has been invented here. `identity` is REQUIRED -- there is no 'my own posts' default. Paged with `cursor`/`limit`.
needle_get_linkedin_posts_by_postidREADRead one post, via GET /api/v1/linkedin/posts/{postId}. Needle's own words: "Returns one post by postId, as returned by a post listing or search. An id copied from a post URL often does not resolve." Take the id from 'Search LinkedIn posts' or 'List LinkedIn posts by a person or company' rather than from a browser address bar -- that is the single most common 404 on this part of the surface.
needle_get_linkedin_posts_by_postid_commentsREADRead a post's comments, or the replies to one comment, via GET /api/v1/linkedin/posts/{postId}/comments. Needle's own words: "Top-level comments on a post, or replies to one comment when commentId is given." `sortBy` takes `MOST_RECENT` or `MOST_RELEVANT` -- LinkedIn's own spellings, in capitals. Each row carries the `commentId` that 'Comment on a LinkedIn post' and 'React to a LinkedIn post' take when replying to or reacting to a comment rather than the post. Paged with `cursor`/`limit`.
needle_get_linkedin_profiles_by_identityREADRead one LinkedIn profile as the connected account sees it, via GET /api/v1/linkedin/profiles/{identity}. Needle's own words: "Returns a profile by public identifier or user id. Counts against the daily profile view allowance. Returns 429 when the account is at its limit." SO THIS IS A METERED READ, not a free one -- `linkedin_profile_view` is one of the capped actions on 'List a connected account's limits and usage', and a loop over a lead list will exhaust it. Read it before messaging: `isConnected` says whether a plain message can reach the person at all, and `isOpenProfile` says whether an InMail reaches them without spending a credit.
needle_get_linkedin_relationsREADList the connected account's own first-degree connections, via GET /api/v1/linkedin/relations. Needle's own words: "First-degree connections of the connected account. These are the recipients a plain message can reach." So this is the audience for 'Send a LinkedIn message' without spending an InMail credit. Cursor-paged, and note that this route takes `cursor` but NO `limit` -- follow `nextCursor` until it is null and expect the page size Needle chooses.
needle_get_linkedin_search_parametersREADTurn filter text into the LinkedIn ids the search tools require, via GET /api/v1/linkedin/search/parameters. THE LOOKUP EVERY STRUCTURED SEARCH DEPENDS ON. Needle's own words: "Resolves filter ids (location, industry, job title, company, ...) from keywords. Pass the returned id to search filters that take an id instead of text." `type` must be one the chosen service actually has: standard LinkedIn takes LOCATION, INDUSTRY, COMPANY, SCHOOL, SERVICE, JOB_TITLE, JOB_FUNCTION, CONNECTIONS, PEOPLE and LANGUAGE, while Sales Navigator takes REGION, SALES_INDUSTRY, COMPANY, SCHOOL, JOB_TITLE, DEPARTMENT, GROUPS, PERSONA, ACCOUNT_LISTS, LEAD_LISTS, CONNECTIONS and LANGUAGE. LANGUAGE is the exception that returns ISO 639-1 codes rather than LinkedIn ids. Leave `keywords` out to list saved groups, personas and lists whole. `service` defaults to `classic`.
needle_get_meREADRead the user and organization this API key is bound to, via GET /api/v1/me. THE CONNECTION'S IDENTITY CHECK, and Needle's own documented smoke test -- it is the probe this provider declares. The reply names the user the key acts as, that user's role in the organization (member, admin or owner), and the organization every other call in this integration runs inside. A Needle key is bound to ONE user and ONE organization and cannot be pointed at another, so this is how to confirm WHICH workspace a connection reaches before writing anything into it. Costs no channel allowance and needs no connected account, which is why it is safe as a health check.
needle_get_places_by_placeidREADRead one place by its Google Place id, via GET /api/v1/places/{placeId}. The detail view behind a 'Search places' hit. `placeId` is a Google Place id, not a Needle id -- Needle is passing this identifier through, so an id obtained from Google's own tooling works here too. Needs no connected account and costs no channel allowance.
needle_get_suppressionsREADList the organization's do-not-contact entries, via GET /api/v1/suppressions. The compliance record: who must not be contacted and why. `reason` narrows to one of Needle's own categories -- `manual`, `unsubscribed`, `not_interested`, `existing_customer`, `open_opportunity`, `competitor`, `employee`, `invalid_recipient`, `complained`, `recently_contacted` or `unresponsive`. Some of those are written by Needle itself as campaigns run, so this list grows without anyone calling 'Create suppressions'. Paged with `cursor`/`limit`.
needle_patch_campaigns_by_campaignidWRITEChange a campaign's steps, schedule, limits or run state, via PATCH /api/v1/campaigns/{campaignId}. THIS IS ALSO THE START AND STOP BUTTON: `status` takes `active` or `paused` and nothing else -- there is no `draft` and no `archived` here, so a campaign cannot be returned to draft or un-archived through this API. `graph` REPLACES the step graph rather than merging into it, so send the whole thing (read it first with 'Get a campaign'); the same goes for `schedule`, `limits` and `suppressionPolicy`. `dryRun: true` validates the change and reports what it would do without applying it, which is the right first call for any graph edit on a live campaign.
needle_patch_campaigns_by_campaignid_pending_by_taskidWRITERelease one held campaign step, optionally rewriting its text first, via PATCH /api/v1/campaigns/{campaignId}/pending/{taskId}. THIS IS THE AUTHORIZATION TO SEND, which is why it is marked destructive even though it writes nothing itself: the step goes out to a real person on LinkedIn or Instagram, and neither channel lets this integration unsend it. `text` REPLACES the message body before it goes -- omit it to send what the campaign composed. Read the queue with 'List sends waiting for approval' and look at the text before releasing it, because the composed draft is exactly what the recipient will see.
needle_patch_connected_accounts_by_connectedaccountidWRITEChange a connected account's paused state or its limit overrides, via PATCH /api/v1/connected-accounts/{connectedAccountId}. `paused: true` is the emergency stop for one channel account: campaigns stop sending through it while queued work stays queued, so resuming with `paused: false` restarts rather than replays. `limitOverrides` sets several action caps in one call, the same values 'Override a limit' sets one action at a time. Fields left out are unchanged. This is a Needle- side setting -- it does not touch LinkedIn or Instagram and nobody on the channel can see it.
needle_patch_connected_accounts_by_connectedaccountid_limits_by_actionWRITESet hourly, daily or weekly caps for one metered action on one account, via PATCH /api/v1/connected- accounts/{connectedAccountId}/limits/{action}. Needle's own words: "Fields you leave out keep the channel default. A cap can be tightened or loosened, never removed. Raising a cap above the default is allowed but answered with a warning: it increases the risk of the channel restricting the account." That warning is the point -- LinkedIn and Instagram restrict accounts that behave unusually, and the account at risk belongs to a real person. `action` is one of Needle's metered action names (linkedin_invitation, linkedin_message, linkedin_inmail, linkedin_profile_view, instagram_message, instagram_follow and the rest); read the exact set from 'List a connected account's limits and usage'.
needle_patch_leads_by_leadidWRITEChange a lead's identities, profile fields or properties, via PATCH /api/v1/leads/{leadId}. Send only what changes; omitted fields are left alone. The identity formats are the same as on create -- Needle: "a LinkedIn public identifier, /in/ path, profile URL, or user id; or an Instagram username, @handle, or profile URL." This is the repair tool for an enrollment that was skipped: add the missing handle, or fill the property the message template names, and enroll again.
needle_post_campaignsWRITECreate an outreach sequence, via POST /api/v1/campaigns. `name` and `graph` are REQUIRED, and `graph` is the whole campaign: `nodes` are the steps (the sends, waits and conditions) and `edges` are what follows what. THE GRAPH SCHEMA IS DEEP -- Needle's own document spends about 7.5 KB on it -- so this tool takes it as an object rather than restating a shape that would drift from the vendor's. Read a working campaign with 'Get a campaign' and modify that graph rather than composing one from scratch. `schedule` (`workingDays`, `startTime`, `endTime`) is evaluated in each LEAD's timezone, not yours. `limits` (`minGapSeconds`, `maxGapSeconds`, `maxNewLeadsPerDay`) paces the campaign on top of the per-account caps, and the tighter of the two wins. `suppressionPolicy` is the compliance surface: cooldown days, how many unanswered touches count as unresponsive, and the keywords that auto-suppress a lead who asks to be left alone. A campaign is created as a DRAFT and sends nothing until 'Update a campaign' sets its status to active.
needle_post_campaigns_by_campaignid_leadsWRITEAdd existing leads to a campaign by id, via POST /api/v1/campaigns/{campaignId}/leads. ENROLLS LEADS THAT ALREADY EXIST -- create them first with 'Create a lead' or 'Create leads in bulk'. PARTIAL SUCCESS IS THE NORMAL OUTCOME and the skip reasons are Needle's own: `missing_identity` (no handle for the channel the campaign sends on), `missing_personalization` (a lead field the message uses is empty or still contains a {{...}} placeholder -- the detail names the field), `enrolled_elsewhere` (already live in another campaign), `suppressed` (on the do-not-contact list), `recently_contacted`, `unresponsive`, and `lead_not_found` (the id does not name a lead in this organization). Results come back in request order, so read them positionally and do not treat a 201 as 'everyone was enrolled'. This queues work rather than sending anything, which is why it is not marked destructive -- but it is the call that starts real outreach to real people.
needle_post_composeREADDraft a personalized message without sending it, via POST /api/v1/compose. A PREVIEW, NOT A SEND -- nothing reaches the recipient and no channel allowance is spent, which is why it is grouped with the reads. Needle's own words: "identity accepts the same forms as leads: a LinkedIn public identifier, /in/ path, profile URL, or user id; or an Instagram username, @handle, or profile URL." `channel`, `connectedAccountId`, `identity` and `prompt` are all required; `context` adds supporting strings, `leadProperties` supplies the personalization values a real lead would carry, and `maxChars` bounds the draft. Use it to check what a campaign step would write before enrolling anybody, and pass the result to 'Send a LinkedIn message' or 'Send an Instagram message' only after reading it.
needle_post_events_searchREADQuery the organization's whole event stream with filters, via POST /api/v1/events/search. THE CROSS-CUTTING HISTORY TOOL. Where the per-campaign, per-lead and per-account event listings each answer one question, this one filters the whole stream by any combination of `types`, `channel` (`linkedin` or `instagram`), `campaignId`, `leadId`, `accountId` and a `from`/`to` window, and pages with `cursor`/`limit`. A POST that READS -- the filter set is too large for a query string, so the verb is about the request shape, not about a change. Nothing is written and nothing is metered on the channel.
needle_post_filesWRITEStore a private file in Needle, or mint a signed URL to upload one, via POST /api/v1/files. TWO MODES, AND NEITHER SENDS BYTES THROUGH THIS TOOL. Needle's own words: "Stores a private object in Needle. Pass filename and contentType to mint a short-lived signed PUT URL, then PUT the bytes to uploadUrl. Or pass sourceUrl to copy a public https file into Needle. Then pass the returned url when sending a LinkedIn message or InMail, or on a campaign send step. downloadUrl is a preview link; do not pass it to LinkedIn." So for the first mode the caller must PUT the bytes to `uploadUrl` itself, outside this integration, before the signed URL expires; the second mode (`sourceUrl`) is the one that completes in a single call. Keep the returned `url` -- it is what the send tools and 'Delete a file' take, and the only identifier a file has here.
needle_post_files_accessWRITEMint a browser-openable preview link for a stored file, via POST /api/v1/files/access. Needle's own words: "Mints a preview URL that redirects to a short-lived signed GET so the browser can open a file stored with POST /api/v1/files. The preview URL stays valid until expiresAt. Do not pass it to LinkedIn." That last sentence is the point of the distinction: this URL is for a human to look at, and the `url` from 'Create a file upload' is the one attachments take. It expires -- read `expiresAt` rather than storing the link.
needle_post_instagram_followsWRITEFollow an account from the connected Instagram account, via POST /api/v1/instagram/follows. A PUBLIC ACTION UNDER A REAL PERSON'S HANDLE: the target is notified and the follow is visible on the connected account's profile. Needle's own words: "Follows an account. A private account gets a follow request instead, reported as followRequestPending on its profile." Metered as `instagram_follow`, and follow volume is one of the signals Instagram restricts accounts over -- read the caps on 'List a connected account's limits and usage' before looping.
needle_post_instagram_messagesWRITESend a direct message from the connected Instagram account, via POST /api/v1/instagram/messages. IRREVERSIBLE AND UNDER A REAL PERSON'S NAME. Needle's own words: "Sends a direct message, opening the conversation when there is none. Attachments are read from the URLs given." Note the difference from LinkedIn: Instagram attachments are read from whatever URL is supplied, so they need not be Needle-hosted -- but a URL that is not publicly reachable simply fails to attach. Metered as `instagram_message`.
needle_post_instagram_messages_by_messageid_reactionsWRITEAdd an emoji reaction to one message, via POST /api/v1/instagram/messages/{messageId}/reactions. Needle's own words: "Adds an emoji reaction to a message. Read the reactions already on it from the messages of the conversation." Visible to the other party immediately and not removable through this API, which is why it is marked destructive despite being a single emoji. `messageId` comes from 'List messages in an Instagram conversation'; `reaction` is the emoji itself.
needle_post_instagram_postsWRITEPublish a feed post or a story from the connected account, via POST /api/v1/instagram/posts. PUBLISHES TO A REAL PERSON'S PUBLIC PROFILE and cannot be deleted through this API. Needle's own words: "Publishes a feed post or a story from the connected account. At least one image or video is required." `media` is required and non-empty; `postType` chooses `feed` (permanent until the owner deletes it by hand) or `story` (expires on Instagram's own schedule). The broadest-audience write on this provider: everything else here reaches one person or one thread, this reaches every follower.
needle_post_instagram_posts_by_postid_commentsWRITEPost a public comment on a post, or a reply to a comment, via POST /api/v1/instagram/posts/{postId}/comments. IRREVERSIBLE AND PUBLIC. Needle's own words: "Comments on a post, or replies to a comment. A comment cannot be edited or removed through this API." It appears under the connected account's real handle on somebody else's post, where their whole audience sees it, and only the account owner can delete it by hand in Instagram.
needle_post_instagram_posts_by_postid_reactionsWRITELike a post or one of its comments, via POST /api/v1/instagram/posts/{postId}/reactions. Needle's own words: "Likes a post, or one of its comments. A like is the only reaction a post takes." It is attributed to the connected account and visible to the author, and nothing here removes it -- which is why a one-tap action is marked destructive on this surface. Pass `commentId` to like a comment rather than the post.
needle_post_leadsWRITEAdd one person to the lead database, via POST /api/v1/leads. `identities` and `timezone` are both REQUIRED, and `identities` is where the flexibility lives: Needle states that "identities.linkedin accepts a public identifier (john-doe), /in/john-doe, in/john-doe, a profile URL (https://www.linkedin.com/in/john-doe), or a user id (ACoAAA...). identities.instagram accepts a username (john-doe), @john-doe, or a profile URL (https://www.instagram.com/john-doe)." A lead with no handle for the channel a campaign sends on is skipped at enrollment with `missing_identity`, so give it the identity for the channel you intend to use. `timezone` is what the campaign schedule is evaluated in -- a wrong one sends at the wrong local hour, which is the most common quiet failure here. `properties` is a free-form object for personalization values, and an empty one that a message template references is its own enrollment skip (`missing_personalization`). For more than a handful of people, use 'Create leads in bulk', which upserts instead of duplicating.
needle_post_leads_bulkWRITEUpsert up to 500 leads in one call, via POST /api/v1/leads/bulk. THE IDEMPOTENT ONE, and the reason to prefer it over a loop of single creates. Needle's own words: "Upserts up to 500 leads by identity, so re-sending a list settles onto the leads it created the first time rather than duplicating them. A row whose identity already belongs to a lead returns that lead as 'matched', with the fields the row carried patched onto it. A row with no readable handle is skipped rather than failing the batch, since it could never be messaged. Same identity formats as POST /leads. Results come back in request order." So read the results positionally and check each row's outcome: a partial success is the normal case, not an error.
needle_post_linkedin_companies_searchREADSearch LinkedIn for company pages, via POST /api/v1/linkedin/companies/search. Needle's own words: "Company search by keywords or a company search URL, one or the other. Hits carry the numeric id, which the company endpoints accept as identifier." One or the other is literal -- send `keywords` or `searchUrl`, not both. The numeric id in each hit is what 'Get a LinkedIn company' and 'List jobs posted by a LinkedIn company' take. Paged with `cursor`/`limit`.
needle_post_linkedin_inmailsWRITESend an InMail that does not need a connection, via POST /api/v1/linkedin/inmails. COSTS REAL MONEY AND CANNOT BE UNSENT. Needle's own words: "Sends an InMail. Does not require a connection. Reaches in-network recipients and out-of-network Open Profiles. Costs one credit unless LinkedIn treats the recipient as Open Profile, and requires a Sales Navigator seat on the connected account." Check the balance with 'Get remaining InMail credits' and check `isOpenProfile` with 'Get a LinkedIn profile' before spending one. `subject` is required here and has no equivalent on a plain message. Attachments are Needle-hosted files from 'Create a file upload' -- pass the `url`, never the `downloadUrl`. Metered as `linkedin_inmail` on top of the credit cost.
needle_post_linkedin_invitationsWRITESend a connection request, with an optional note, via POST /api/v1/linkedin/invitations. IRREVERSIBLE AND VISIBLE: the invitation appears in someone's LinkedIn from the connected account's own profile. Needle's own constraints: "Sends a connection invitation with an optional note of up to 300 characters. Fails if the recipient is already connected or has an invitation pending." Invitations are the most tightly policed action LinkedIn has -- `linkedin_invitation` is capped per hour, day and week on 'List a connected account's limits and usage', and exceeding what LinkedIn considers normal is what gets a real person's account restricted. Withdrawing later does not undo the notification the recipient already saw.
needle_post_linkedin_invitations_received_by_invitationidWRITEAccept or decline one pending received invitation, via POST /api/v1/linkedin/invitations/received/{invitationId}. Needle's own words: "Accepts or declines a pending received invitation. Neither action can be reversed through this API." Accepting makes the sender a first-degree connection of a real person's account -- which changes who can message them and what they can see -- and declining cannot be taken back. `action` is `accept` or `decline`, and the `invitationId` must come from 'List received LinkedIn invitations'.
needle_post_linkedin_jobs_searchREADSearch LinkedIn's job listings, via POST /api/v1/linkedin/jobs/search. Needle's own words: "Job search by keywords, companies, or filters; a jobs search URL replaces them all. location, industries, jobTitles and jobFunctions take ids from GET /api/v1/linkedin/search/parameters." So resolve those four with 'Resolve LinkedIn search filter ids' before using them -- passing text where an id is expected silently narrows to nothing rather than erroring. `postedWithinDays` and `sortBy` (`relevance` or `date`) are the freshness controls. A POST that reads; paged with `cursor`/`limit`.
needle_post_linkedin_messagesWRITESend a direct message from the connected account, via POST /api/v1/linkedin/messages. IRREVERSIBLE AND PUBLIC UNDER A REAL PERSON'S NAME: the message arrives in someone else's LinkedIn inbox from the connected account's own profile, and nothing in this integration can unsend it. Needle's own constraint: "Only first-degree connections can receive one, so check isConnected on the profile first and send an InMail otherwise. Open Profile (isOpenProfile) can receive InMail without a connection." So the correct sequence is 'Get a LinkedIn profile' -> `isConnected` -> this tool, or 'Send a Sales Navigator InMail' when it is false. Attachments are Needle-hosted files: upload with 'Create a file upload' first and pass the `url` it returns -- NOT the `downloadUrl`, which Needle explicitly says must not be given to LinkedIn. Metered as `linkedin_message`.
needle_post_linkedin_people_searchREADSearch standard LinkedIn for people by keywords, filters or a search URL, via POST /api/v1/linkedin/people/search. Needle's own words: "Search standard LinkedIn with keywords, focused structured filters, or a Classic people search URL." `searchUrl` is the shortcut: paste a search you already built in LinkedIn's own UI and the structured filters are not needed. The id-valued filters (`locations`, `industries`, `companies`, `schools`, ...) take LinkedIn's own ids rather than text -- resolve them first with 'Resolve LinkedIn search filter ids', passing `service: "classic"`. A POST that READS: nothing is written, but `linkedin_search` IS a metered action, so a search burns the account's daily search allowance. Page with `cursor`/`limit`. For Sales Navigator's far larger filter set, use the Sales Navigator tool instead.
needle_post_linkedin_posts_by_postid_commentsWRITEPost a public comment on a post, or a reply to a comment, via POST /api/v1/linkedin/posts/{postId}/comments. IRREVERSIBLE AND PUBLIC. Needle's own words: "Adds a comment to a post, or a reply when commentId is given. Comments are public and cannot be edited or removed through this API." It is published under the connected account's real name to everyone who can see the post, and neither this integration nor Needle can take it down -- only the account owner can, by hand, in LinkedIn. NOTE ON THE VENDOR'S SCHEMA: Needle's OpenAPI declares no `postId` path parameter for this operation even though its path contains `{postId}` and the sibling GET declares it. This build supplies `postId` as a required argument anyway, because a request without it would put the literal `{postId}` on the wire. Metered as `linkedin_comment`.
needle_post_linkedin_posts_by_postid_reactionsWRITEAdd a reaction to a post or to one of its comments, via POST /api/v1/linkedin/posts/{postId}/reactions. PUBLIC AND ATTRIBUTED: a reaction shows the connected account's name and photo to the author and, through LinkedIn's feed, to that account's own network -- it is a small action with a wide audience. Needle's own words: "Adds a reaction to a post, or to a comment when commentId is given. Defaults to like." `reaction` takes `like`, `celebrate`, `support`, `love`, `insightful` or `funny`. Nothing here removes a reaction. SAME SCHEMA GAP AS THE COMMENT TOOL: Needle's OpenAPI declares no `postId` path parameter for this operation; this build supplies it because the path requires it. Metered as `linkedin_reaction`.
needle_post_linkedin_posts_searchREADSearch LinkedIn for posts, via POST /api/v1/linkedin/posts/search. Needle's own words: "Post search by keywords or a post search URL, one or the other. Each hit carries the postId used by the post, comment and reaction endpoints." THAT LAST SENTENCE MATTERS: the `postId` this returns is the only reliable source of one -- Needle warns elsewhere that "an id copied from a post URL often does not resolve". Paged with `cursor`/`limit`.
needle_post_linkedin_sales_navigator_people_searchREADSearch Sales Navigator leads by keywords, filters or a search URL, via POST /api/v1/linkedin/sales-navigator/people/search. REQUIRES A SALES NAVIGATOR SEAT on the connected account; without one this is the wrong tool and 'Search LinkedIn people (standard search)' is the right one. The filter set is the widest on this provider -- roughly forty fields, most of them paired with an `excluded*` twin (`industries`/`excludedIndustries`, `jobTitles`/`excludedJobTitles`, and so on), plus seniorities, tenure bands, spotlights, saved personas, account lists and lead lists. The id-valued ones take LinkedIn ids from 'Resolve LinkedIn search filter ids' with `service: "sales_navigator"`, whose type vocabulary differs from classic (REGION and SALES_INDUSTRY rather than LOCATION and INDUSTRY). `searchUrl` replaces the whole filter set with a search built in Sales Navigator's own UI. Metered as `linkedin_search`; page with `cursor`/`limit`.
needle_post_places_searchREADFind businesses and locations by text or near a point, via POST /api/v1/places/search. Needle's own words: "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." Two modes in one call: `query` for a text search, or `location` (with `radiusMeters` and an optional `type`) for a nearby one. Only the text mode pages. Nothing about this touches LinkedIn or Instagram and it needs no connected account -- it is a lead-sourcing tool that feeds 'Create leads in bulk'. The ids it returns are Google Place ids, which is what 'Get a place' takes.
needle_post_suppressionsWRITEAdd people or companies to the do-not-contact list, via POST /api/v1/suppressions. THE RIGHT WAY TO STOP CONTACTING SOMEONE, and the reason it is not marked destructive: it only ever prevents outreach. A suppression outlives the lead record, so it survives a re-import that would otherwise make a deleted person contactable again -- which is precisely what deleting a lead does NOT give you. `entries` is a list, so an unsubscribe batch is one call. Enrollment skips anyone suppressed with the reason `suppressed`.
needle_post_suppressions_checkREADAsk whether specific people or companies are suppressed, via POST /api/v1/suppressions/check. THE PRE-FLIGHT FOR ANY SEND THIS INTEGRATION MAKES DIRECTLY. Campaign enrollment checks suppressions by itself, but 'Send a LinkedIn message', 'Send a Sales Navigator InMail' and 'Send an Instagram message' do not -- they send to whatever identity they are given. Call this first. Needle's own words: "identity accepts the same forms as leads: a LinkedIn public identifier, /in/ path, profile URL, or user id; or an Instagram username, @handle, or profile URL." A POST that reads; each entry may also carry a `companyDomain` and a `campaignId` to scope the question.
Often connected alongside
Put Needle behind one governed endpoint.
Same permissions, same audit trail, whatever else you connect next.