# Linked API — Complete Documentation > Secure automation API for LinkedIn – teams and AI agents. Automate messaging, networking, data extraction, and outreach on LinkedIn. Use via API, SDK, CLI, AI agents, or no-code platforms. --- # API Reference ## Core concepts This page covers the fundamental concepts and design principles behind Linked API, providing the foundation you need to build effective integrations. ## Workflows as basis Linked API is built around the **concept of workflows**. This means that any automation you want to perform (such as sending a connection request, commenting on a post, retrieving company data, etc.) must be executed as a workflow. Workflows are constructed using building blocks called **actions**. Each action represents a specific operation within your workflow. To learn more about this concept, see the [actions overview](/docs/actions-overview) and [building workflows](/docs/building-workflows) pages. This workflow-based approach, rather than REST-style endpoints, allows you to build any automation sequence imaginable. It provides complete flexibility, which is why we chose workflows as our foundation. ## Request timing Since Linked API [fully emulates](/safety) actions of a real LinkedIn user, requests are not executed instantly. For example, simple workflows like visiting a person's page and liking their latest post might take around 20 seconds, while more complex workflows, such as retrieving detailed company information including a list of its employees, can take several minutes. ## Sequential execution Linked API executes workflows sequentially. While you can send multiple workflow requests in quick succession, they **will not be executed in parallel**. Each workflow request is **accepted immediately**, but its execution will not start until the previous workflow is completed. For example, if you request a workflow to [check a connection status](/docs/checking-connection-status) and immediately request another workflow to [send a message](/docs/sending-message), the second workflow will begin **only after the first workflow is completed**. This approach ensures alignment with realistic user behavior on LinkedIn, as it mirrors how a real user would interact with the platform. ## Limits management LinkedIn limits vary depending on your account's age, social selling index, subscription (e.g., Sales Navigator), and other factors. > It is your responsibility to understand and follow the limits appropriate for your account. To help you stay within safe boundaries, Linked API allows you to configure action limits for each connected account directly from the platform dashboard. When aworkflow contains an action that would exceed a configured limit, that action will return a `limitExceeded` error instead of executing. You can also [manage limits programmatically](/docs/admin-limits) via the Admin API, [monitor your activity](/docs/checking-api-usage-statistics), [check SSI](/docs/action-st-retrieve-ssi), and review our [guide on LinkedIn limits](/guides/understanding-linkedin-limits). ## URL normalization Linked API normalizes all LinkedIn URLs in responses, regardless of the format provided in action parameters. Normalized URLs consistently follow these rules: - Use `https` protocol. - Include `www` subdomain. - Exclude trailing slash (`/`). For example, if action parameters include a URL like `http://linkedin.com/in/person1/`, the response will return it as `https://www.linkedin.com/in/person1`. Consider this behavior when implementing URL comparisons or storage in your integration. ## Timezone normalization All date and time values returned by Linked API are in the **UTC** **timezone**. To display these values correctly in your integration, you should apply the appropriate offset for your user's local timezone. ## Handling missing values When certain data is unavailable on LinkedIn, some fields in action results may return as `null` or empty array (`[]`). Missing fields are always **explicitly included** in the response rather than being omitted. ## Making requests This page explains how to make requests to Linked API, including available endpoints, authorization process, response structure, and common errors. ## Endpoints `https://api.linkedapi.io/workflows` – for [executing workflows](/docs/executing-workflows). `https://api.linkedapi.io/conversations/poll` – for [polling conversations](/docs/working-with-conversations). `https://api.linkedapi.io/stats/actions` – for checking [API usage statistics](/docs/checking-api-usage-statistics). `https://api.linkedapi.io/admin/*` – for [managing your account](/docs/admin-overview). ## Authorization Requests to all endpoints must include **2 authorization headers**: - `linked-api-token` – your main token that enables overall Linked API access. - `identification-token` – unique token specific to each managed LinkedIn account. You can obtain these tokens through [our platform](https://app.linkedapi.io/), as demonstrated below: ![](/images/docs/tokens.webp) ## Response structure All responses in Linked API have the following structure: 1. **For successful requests:** ```json { "success": true, "result": { /* Endpoint specific result */ } } ``` 2. **For requests with errors:** ```json { "success": false, "error": { /* Common or endpoint specific error */ } } ``` ## Common errors Below is a list of common errors relevant to all endpoints. Additionally, some endpoints have their own specific errors, so check the related documentation pages. ### linkedApiTokenRequired ```json { "success": false, "error": { "type": "linkedApiTokenRequired", "message": "'linked-api-token' is missing in request headers." } } ``` ### invalidLinkedApiToken ```json { "success": false, "error": { "type": "invalidLinkedApiToken", "message": "The provided 'linked-api-token' is invalid." } } ``` ### identificationTokenRequired ```json { "success": false, "error": { "type": "identificationTokenRequired", "message": "'identification-token' is missing in request headers." } } ``` ### invalidIdentificationToken ```json { "success": false, "error": { "type": "invalidIdentificationToken", "message": "The provided 'identification-token' is invalid." } } ``` ### subscriptionRequired ```json { "success": false, "error": { "type": "subscriptionRequired", "message": "No purchased subscription seats available for this LinkedIn account." } } ``` > 💡 Add additional seats, renew the subscription for existing seats, or check for any payment issues on the [platform](https://app.linkedapi.io/). ### tooManyRequests ```json { "success": false, "error": { "type": "tooManyRequests", "message": "Too many requests. Please try again later." } } ``` ## Executing workflows Linked API is built around the **concept of workflows**. This means that any automation you want to perform (such as sending a connection request, commenting on a post, retrieving company data, etc.) must be executed as a workflow. > For details on how to build workflows, visit the [building workflows](/docs/building-workflows) page. Executing a workflow consists of 2 key steps: 1. Starting a workflow and receiving its `workflowId`, current `workflowStatus`, and human-readable `message`. 2. Periodically checking for the result using the `workflowId`. ## Starting workflows To start executing a workflow, make a `POST` request to the following endpoint: ```text POST https://api.linkedapi.io/workflows ``` In the request body, include a JSON that describes your workflow, for example: ```json { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person1" } ``` Upon successful request, you will receive the following response: ```json { "success": true, "result": { "workflowId": "wf-64835e7c-a0gc-40ae-8338-108f02sSe643", "workflowStatus": "pending", "pendingReason": "queued", "message": "This workflow is scheduled and will start after 2 other workflows already scheduled ahead of it." } } ``` - `workflowId` – unique workflow identifier for checking the result. - `workflowStatus` – current workflow status. Possible values at start are `pending` and `running`. - `pendingReason` – why the workflow has not started yet. `queued` means it is waiting its turn behind other work on the same account; `outsideWorkingHours` means it is parked until the account [working hours](/docs/working-hours) reopen. It is `null` once the workflow is no longer pending. - `message` – human-readable execution message, such as queue position or estimated duration. This value can change while polling. In case of an unsuccessful request, you will receive the following response: ```json { "success": false, "error": { "type": "linkedApiTokenRequired", "message": "'linked-api-token' is missing in request headers." } } ``` - `error` – either a [common error](/docs/making-requests) or one of the following specific errors: - `invalidWorkflow` – workflow configuration is not valid due to violated [action constraints](/docs/actions-overview) or invalid [action parameters](/docs/actions-overview): `{validation_details}`. - `outsideWorkingHours` – the account is outside its configured [working hours](/docs/working-hours) and its off-hours policy is set to reject. Nothing is queued. ## Checking for result > Prefer to be notified instead of polling? Register a [webhook](/docs/webhooks) and Linked API will `POST` an event to your endpoint when the workflow completes. After starting your workflow and receiving the `workflowId`, you need to periodically check the workflow result. To do this, send a `GET` request to the following endpoint: ```text GET https://api.linkedapi.io/workflows/{workflowId} ``` Depending on the workflow execution status, you'll receive one of these results: 1. **Workflow is pending (queued for execution):** ```json { "success": true, "result": { "workflowStatus": "pending", "pendingReason": "queued", "message": "This workflow is scheduled and will start after 2 other workflows already scheduled ahead of it." } } ``` Workflows are executed sequentially per LinkedIn account. If you submit multiple workflows, the first one starts executing immediately while the others are queued. The `pending` status means the workflow is waiting in the queue and will start running once the previous workflows complete. A workflow can also be pending because the account is outside its [working hours](/docs/working-hours). In that case `pendingReason` is `outsideWorkingHours` and the workflow will not move until the window reopens, which can be the next working day: ```json { "success": true, "result": { "workflowStatus": "pending", "pendingReason": "outsideWorkingHours", "message": "This workflow is scheduled and will start when the account working hours open at Mon, Aug 10, 09:00 (Europe/Berlin)." } } ``` Back off your polling in that case rather than retrying every few seconds – the `message` names the time the window opens. 2. **Workflow is running:** ```json { "success": true, "result": { "workflowStatus": "running", "pendingReason": null, "message": "This workflow usually takes about 30-60 seconds." } } ``` While a workflow is `pending` or `running`, the `message` field may update between polling requests. For example, a queued workflow can first return a queue-position message and later return an estimated-duration message after it starts running. 3. **Workflow completed successfully:** ```json { "success": true, "result": { "workflowStatus": "completed", "completion": { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "pending" } } } } ``` - `completion` – results of all actions included in this workflow. 4. **Workflow completed with action error:** ```json { "success": true, "result": { "workflowStatus": "completed", "completion": { "actionType": "st.checkConnectionStatus", "success": false, "error": { "type": "limitExceeded", "message": "The configured limit for this action category has been exceeded." } } } } ``` 5. **Workflow failed:** ```json { "success": true, "result": { "workflowStatus": "failed", "failure": { "reason": "linkedinAccountSignedOut", "message": "This LinkedIn account has been signed out in our cloud browser." } } } ``` - `failure` – object describing the workflow failure reason: - `linkedinAccountSignedOut` – your LinkedIn account has been signed out in our cloud browser. This occasionally happens as LinkedIn may sign out accounts after an extended period. You'll need to visit [our platform](https://app.linkedapi.io/) and reconnect your account. - `languageNotSupported` – your LinkedIn account uses a language other than English, which is currently the only supported option. If you encounter this issue, please contact our support. We prioritize adding new languages based on user requests, so your feedback is important. > In case of an unsuccessful request, you'll receive one of the [common errors](/docs/making-requests). ## Cancelling workflows Some workflows in Linked API can take a long time to complete. If you decide you no longer need a workflow to finish, you have the option to cancel it. You can cancel a running or pending workflow directly on [our platform](https://app.linkedapi.io/stats). Simply click the three dots menu next to the workflow and select "Cancel". Alternatively, you can cancel a workflow via API. To do this, you need to make a `DELETE` request to the following endpoint: ```text DELETE https://api.linkedapi.io/workflows/{workflowId} ``` Upon successful cancellation, you will receive the following response: ```json { "success": true, "result": { "cancelled": true } } ``` > In case of an unsuccessful request, you'll receive one of the [common errors](/docs/making-requests). **Important notes:** - **Partial execution.** The workflow is cancelled at its current execution point. Any actions that have already been completed cannot be undone. For example, if the workflow has already sent messages or connection requests from your LinkedIn account, these actions will remain executed. - **No intermediate data.** Once a workflow is cancelled, no data is preserved or returned. You will not receive any intermediate results or partial data that may have been collected before cancellation. ## Working hours This page explains how per-account working hours affect your API requests, what the off-hours policies do, and how to handle a workflow that is parked until the window reopens. ## What working hours are Each connected LinkedIn account can carry a **weekly activity window** – the hours during which Linked API is allowed to act on it. A LinkedIn account that is active around the clock looks like a user who never sleeps, which is one of the strongest automation signals there is. Giving an account a human-shaped schedule removes that signal without costing you any throughput inside the window. Working hours are configured per account in [our platform](https://app.linkedapi.io/), next to the account limits. There is no API for changing them – they are an account-owner decision, not a per-request one. ![](/images/docs/working-hours-settings.webp) The window is stored with an explicit timezone, so it follows the account's local wall clock across daylight-saving changes rather than drifting by an hour twice a year. | Setting | Meaning | | --- | --- | | Days | Which weekdays the account is active on. | | Time range | Start and end of the daily window, in the account's timezone. | | Timezone | The zone the window is expressed in. Defaults to the timezone of the device you configure it from. | | Outside working hours | What happens to requests that arrive outside the window – see below. | An account with **no** window configured is never gated. If you have never touched this setting, nothing about your integration changes. ## Off-hours policies The policy decides what happens to a request that arrives outside the window. It is the **Outside working hours** selector in the screenshot above. ### Queue until the window opens The workflow is accepted and parked. It starts automatically when the window reopens – you do not need to resubmit it. This is the default for newly connected accounts. ### Reject with an error The request fails immediately with the `outsideWorkingHours` error and nothing is queued. ```json { "success": false, "error": { "type": "outsideWorkingHours", "message": "This LinkedIn account is outside its configured working hours." } } ``` ### Run anyway Your API requests run at any time – the window is not enforced for them. Note that this applies to **your** requests only. Work Linked API schedules for itself – inbox and network synchronisation – stays inside the window under every policy, and the cloud browser behind the account is released outside the window under every policy too. That is where the account-safety and infrastructure benefits of the schedule come from. ## Telling a parked workflow from a queued one Every response describing a workflow that has not started yet carries a `pendingReason`. It appears both in the response to `POST /workflows` and in the [workflow result](/docs/executing-workflows) response. ```json { "success": true, "result": { "workflowId": "wf-64835e7c-a0gc-40ae-8338-108f02sSe643", "workflowStatus": "pending", "pendingReason": "outsideWorkingHours", "message": "This workflow is scheduled and will start when the account working hours open at Mon, Aug 10, 09:00 (Europe/Berlin)." } } ``` | Value | Meaning | | --- | --- | | `queued` | Waiting its turn behind other work on the same account. Minutes, typically. | | `outsideWorkingHours` | Parked until the account working hours reopen. Can be the next working day. | | `null` | The workflow is not pending. | This distinction matters for polling. A `queued` workflow is worth polling every few seconds. An `outsideWorkingHours` one will not move until the window reopens, so polling it in a tight loop achieves nothing – back off until the time named in `message`, or surface that time to your user. ## When a parked workflow expires A parked workflow does not wait forever. If it is still pending 7 days after it was first parked, and the window has opened at least once in that time without the workflow being started, it fails with a dedicated error: ```json { "success": true, "result": { "workflowStatus": "failed", "failure": { "reason": "workingHoursWaitExpired", "message": "This workflow remained pending for 7 days after it was first parked by the account working hours, and was not started." } } } ``` A workflow that expires this way never entered the `running` state, so it emits the `workflowCreated` and `workflowCompleted` [webhook events](/docs/webhooks) but no `workflowStarted`. This is worth watching if you combine a narrow window with a high request volume: an account is only able to run one workflow at a time, so a window of a couple of hours per week puts a hard ceiling on how much work can drain through it. ## Seeing the current state The account list in the platform shows each account's schedule underneath its connection status, and marks the ones that are outside their window right now – so you can tell at a glance why nothing is starting for an account. ![](/images/docs/working-hours-account-list.webp) An account with no schedule line carries no window at all and is never gated. ## Building workflows Workflows are constructed using building blocks called actions, each having its own purpose (you may learn more about actions on the [actions overview](/docs/actions-overview) page). A workflow, after being successfully executed, produces a `completion`, that contains the results of all actions. See the [executing workflows](/docs/executing-workflows) page for details. Here you'll learn how to build various workflows, from single-action to advanced. > In most cases, you don't need to build workflows from scratch. The left menu contains pages with pre-built workflows for popular scenarios you can simply copy and use. ## Single-action workflows Single-action workflows contain exactly one action. You can only use actions that allow [root start in their constraints](/docs/actions-overview). ### Example 1 Let's say you want to build a workflow to check the connection status between your LinkedIn account and another person. You only need to use [st.checkConnectionStatus](/docs/action-st-check-connection-status) action to accomplish this. **Workflow:** ```json { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person1" } ``` **Completion:** ```json { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "pending" } } ``` ### Example 2 Let's say you want to build a workflow to retrieve basic information about a company. You only need to use [st.openCompanyPage](/docs/action-st-open-company-page) action to accomplish this. **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/company1", "basicInfo": true } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/company1", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "Cupertino, California", "headquarters": "US", "industry": "Information Technology", "specialties": "Cloud Computing, AI, Software Development", "website": "https://techcorp.com", "employeeCount": 500, "yearFounded": 2019, "ventureFinancing": true, "jobsCount": 12 } } ``` ## Array workflows Array workflows contain multiple actions executed sequentially. You can only use actions that allow [root start in their constraints](/docs/actions-overview). > When you need to perform multiple identical operations, use one array workflow with multiple actions instead of separate single-action workflows. This approach is significantly faster due to API optimizations. ### Example 1 Let's say you want to build a workflow to check the connection status between your LinkedIn account and 5 different people. You need to use [st.checkConnectionStatus](/docs/action-st-check-connection-status) action multiple times to accomplish this. **Workflow:** ```json [ { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person1" }, { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person2" }, { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person3" }, { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person4" }, { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person5" } ] ``` **Completion:** ```json [ { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "connected" } }, { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "notConnected" } }, { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "pending" } }, { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "connected" } }, { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "connected" } } ] ``` ### Example 2 Let's say you want to build a workflow to retrieve basic information about several people. You need to use [st.openPersonPage](/docs/action-st-open-person-page) action multiple times to accomplish this. **Workflow:** ```json [ { "actionType": "st.openPersonPage", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1", "basicInfo": true }, { "actionType": "st.openPersonPage", "label": "person2", "personUrl": "https://www.linkedin.com/in/person2", "basicInfo": true } ] ``` **Completion:** ```json [ { "actionType": "st.openPersonPage", "label": "person1", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/person1", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8OeJoj0mrkxx7Jiuy0", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "countryCode": "US", "position": "Software Engineer", "companyName": "TechCorp", "companyUrl": "https://www.linkedin.com/company/12345678" } }, { "actionType": "st.openPersonPage", "label": "person2", "success": true, "data": { "name": "Jane Doe", "publicUrl": "https://www.linkedin.com/in/person2", "headline": "Product Manager at Cognito Inc.", "location": "Lisbon, Portugal", "countryCode": "PT", "position": "Product Manager", "companyName": "Cognito Inc.", "companyUrl": "https://www.linkedin.com/company/87654321" } } ] ``` ## Advanced workflows With Linked API, you can build **any workflow you can imagine** by combining arrays and parent-child action relationships through `then` parameter. Only two rules apply: - You must follow the [constraints of all actions](/docs/actions-overview) used. - The final workflow cannot exceed **5 levels of nesting**. ### Example 1 Let's say you want to build a workflow that searches for companies using specific filters and keywords, then retrieves basic information about all companies from the search results. You need to use [st.searchCompanies](/docs/action-st-search-companies), [st.doForCompanies](/docs/action-st-do-for-companies) and [st.openCompanyPage](/docs/action-st-open-company-page) actions to accomplish this. **Workflow:** ```json { "actionType": "st.searchCompanies", "term": "Tech Inc", "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Robotics Engineering"], "annualRevenue": { "min": "0", "max": "2.5" } }, "then": { "actionType": "st.doForCompanies", "then": { "actionType": "st.openCompanyPage", "basicInfo": true } } } ``` **Completion:** ```json { "actionType": "st.searchCompanies", "success": true, "data": [ { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "industry": "Information Technology", "location": "California", "then": { "actionType": "st.openCompanyPage", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "California", "headquarters": "US", "industry": "Information Technology", "specialties": "Cloud Computing, AI, Software Development", "website": "https://techcorp.com", "employeeCount": 500, "yearFounded": 2019, "ventureFinancing": true, "jobsCount": 12 } } }, { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "industry": "Software Development", "location": "Mountain View", "then": { "actionType": "st.openCompanyPage", "success": true, "data": { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "description": "TechLife is a worldwide leader in software development for sustainability.", "location": "Mountain View", "headquarters": "US", "industry": "Software Development", "specialties": "Software Development, AI", "website": "https://techlife.com", "employeeCount": 163, "yearFounded": 2016, "ventureFinancing": false, "jobsCount": 4 } } } ] } ``` ### Example 2 Let's say you want to build a workflow that retrieves basic company information, finds all managers, gets their basic information and education details, then sends them connection requests. You need to use [st.openCompanyPage](/docs/action-st-open-company-page), [st.retrieveCompanyEmployees](/docs/action-st-retrieve-company-employees), [st.doForPeople](/docs/action-st-do-for-people), [st.openPersonPage](/docs/action-st-open-person-page), [st.retrievePersonEducation](/docs/action-st-retrieve-person-education) and [st.sendConnectionRequest](/docs/action-st-send-connection-request) actions to accomplish this. **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/techcorp", "basicInfo": true, "then": { "actionType": "st.retrieveCompanyEmployees", "filter": { "position": "Manager" }, "then": { "actionType": "st.doForPeople", "then": { "actionType": "st.openPersonPage", "basicInfo": true, "then": [ { "actionType": "st.retrievePersonEducation" }, { "actionType": "st.sendConnectionRequest" } ] } } } } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "California", "headquarters": "US", "industry": "Information Technology", "specialties": "Cloud Computing, AI, Software Development", "website": "https://techcorp.com", "employeeCount": 500, "yearFounded": 2019, "ventureFinancing": true, "jobsCount": 12, "then": { "actionType": "st.retrieveCompanyEmployees", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Product Manager at TechCorp", "location": "New York, USA", "then": { "actionType": "st.openPersonPage", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "headline": "Product Manager at TechCorp", "location": "New York, USA", "countryCode": "US", "position": "Product Manager", "companyName": "TechCorp", "hashedCompanyUrl": "https://www.linkedin.com/company/87654321", "then": [ { "actionType": "st.retrievePersonEducation", "success": true, "data": [ { "schoolName": "Harvard University", "schoolHashedUrl": "https://www.linkedin.com/company/12345678", "details": "Master of Science in Computer Science, Artificial Intelligence" }, { "schoolName": "MIT", "schoolHashedUrl": "https://www.linkedin.com/company/87654321", "details": "Bachelor of Science in Electrical Engineering and Computer Science" } ] }, { "actionType": "st.sendConnectionRequest", "success": true } ] } } } ... ] } } } ``` > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Actions overview Each action represents a specific operation within a [workflow](/docs/building-workflows). On this page, you'll find details about action definition and an overview of all available actions. ## Action definition Each action is defined by 3 characteristics: **constraints**, **parameters**, and **result options**: ### Constraints Constraints specify the valid parent-child relationships between actions in a workflow (defined using the `then` parameter in action JSON structure). On documentation pages, constraints are described with three elements: > ⏺️ **Root Start:** indicates whether an action can be placed at the beginning of a workflow (in the root position). > ⬆️ **Parent Actions:** specifies which actions can be parent actions to this action. > ⬇️ **Child Actions:** specifies which actions can be child actions of this action. If constraints are violated, you'll receive an `invalidWorkflow` error. ### Parameters Parameters configure the action's behavior and are unique to each action type. However, there are 3 common parameters: ```json { "actionType": "st.checkConnectionStatus", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1", "then": { ... } } ``` - `actionType` – required value that specifies which action is being executed. - `label` (optional) – custom label for tracking this action in workflow completion. When included, this same `label` appears in the result JSON, making it easy to locate the action. - `then` (optional) – object or array of child actions to be executed within this action (see [constraints](/docs/actions-overview) for more details). ### Result options Actions can have 2 possible result options: successful and unsuccessful execution, indicated by the `success` field being either `true` or `false`. The result format is unique for each action type. Here are examples: 1. **Successful execution:** ```json { "actionType": "st.checkConnectionStatus", "label": "person1", // if included in parameters "success": true, "data": { "connectionStatus": "pending" } } ``` - `data` – object containing action-specific results and child action results. 2. **Unsuccessful execution:** ```json { "actionType": "st.checkConnectionStatus", "label": "person1", // if included in parameters "success": false, "error": { "type": "personNotFound", "message": "The provided URL is not an existing LinkedIn person." } } ``` - `error` – object containing error details, including `type` and `message`. ### Common errors Some errors can be returned by any action, regardless of its type: - `limitExceeded` – configured limit for this action category has been exceeded. - `unexpectedError` – unexpected error occurred during action execution. In addition to these, each action may define its own specific errors (such as `personNotFound`, `messagingNotAllowed`, etc.). Refer to the individual action pages for their specific error types. ## Available actions Linked API provides 2 types of actions, each with its own namespace: - **Standard actions [st.]**: actions that relate to the standard LinkedIn interface. - **Sales Navigator actions [nv.]**: actions that relate to the Sales Navigator interface. ### Standard actions [st.] - [st.sendMessage](/docs/action-st-send-message) – allows you to send a message to a person. - [st.syncConversation](/docs/action-st-sync-conversation) – allows you to sync a conversation so you can [start polling](/docs/working-with-conversations) it. - [st.syncInbox](/docs/action-st-sync-inbox) – allows you to enable [whole-inbox monitoring](/docs/monitoring-inbox) so you can poll every incoming conversation. - [st.manageConversation](/docs/action-st-manage-conversation) – allows you to manage a conversation thread by archiving, starring, or muting it. - [st.checkConnectionStatus](/docs/action-st-check-connection-status) – allows you to check the connection status between your account and another person. - [st.sendConnectionRequest](/docs/action-st-send-connection-request) – allows you to send a connection request to a person. - [st.withdrawConnectionRequest](/docs/action-st-withdraw-connection-request) – allows you to withdraw the connection request sent to a person. - [st.acceptInvitation](/docs/action-st-accept-invitation) – allows you to accept an incoming connection, company-follow, or newsletter-subscription invitation. - [st.ignoreInvitation](/docs/action-st-ignore-invitation) – allows you to ignore an incoming connection, company-follow, or newsletter-subscription invitation. - [st.retrievePendingRequests](/docs/action-st-retrieve-pending-requests) – allows you to retrieve pending connection requests sent from your account. - [st.retrieveInvitations](/docs/action-st-retrieve-invitations) – allows you to retrieve incoming connection, company-follow, and newsletter-subscription invitations. - [st.retrieveConnections](/docs/action-st-retrieve-connections) – allows you to retrieve your connections and perform additional person-related actions if needed. - [st.removeConnection](/docs/action-st-remove-connection) – allows you to remove a person from your connections. - [st.syncNetwork](/docs/action-st-sync-network) – allows you to enable [network monitoring](/docs/monitoring-network) so you can poll connection events as they happen. - [st.searchCompanies](/docs/action-st-search-companies) – allows you to search for companies applying various filtering criteria. - [st.searchPeople](/docs/action-st-search-people) – allows you to search for people applying various filtering criteria. - [st.searchJobs](/docs/action-st-search-jobs) – allows you to search for jobs applying various filtering criteria. - [st.openCompanyPage](/docs/action-st-open-company-page) – allows you to open a company page to retrieve its basic information and perform additional company-related actions if needed. - [st.retrieveCompanyEmployees](/docs/action-st-retrieve-company-employees) – allows you to retrieve company employees and perform additional person-related actions if needed. - [st.retrieveCompanyDMs](/docs/action-st-retrieve-company-dms) – allows you to retrieve company decision makers and perform additional person-related actions if needed. - [st.retrieveCompanyPosts](/docs/action-st-retrieve-company-posts) – allows you to retrieve posts published by a company and perform additional post-related actions if needed. - [st.openPersonPage](/docs/action-st-open-person-page) – allows you to open a person page to retrieve their basic information and perform additional person-related actions if needed. - [st.retrievePersonExperience](/docs/action-st-retrieve-person-experience) – allows you to retrieve information about a person's experience. - [st.retrievePersonEducation](/docs/action-st-retrieve-person-education) – allows you to retrieve information about a person's education. - [st.retrievePersonSkills](/docs/action-st-retrieve-person-skills) – allows you to retrieve information about a person's skills. - [st.retrievePersonLanguages](/docs/action-st-retrieve-person-languages) – allows you to retrieve information about a person's languages. - [st.retrievePersonPosts](/docs/action-st-retrieve-person-posts) – allows you to retrieve posts published by a person and perform additional post-related actions if needed. - [st.retrievePersonComments](/docs/action-st-retrieve-person-comments) – allows you to retrieve comments left by a person. - [st.retrievePersonReactions](/docs/action-st-retrieve-person-reactions) – allows you to retrieve reactions made by a person. - [st.openPost](/docs/action-st-open-post) – allows you to open a post to retrieve its data and perform additional post-related actions if needed. - [st.reactToPost](/docs/action-st-react-to-post) – allows you to react to a post using any available reaction type. - [st.commentOnPost](/docs/action-st-comment-on-post) – allows you to leave a comment on a post. - [st.retrievePostComments](/docs/action-st-retrieve-post-comments) – allows you to retrieve comments for a post and perform additional comment-related actions if needed. - [st.openComment](/docs/action-st-open-comment) – allows you to open a comment to react to it or reply to it. - [st.reactToComment](/docs/action-st-react-to-comment) – allows you to react to a comment using any available reaction type. - [st.replyToComment](/docs/action-st-reply-to-comment) – allows you to reply to a comment. - [st.openJob](/docs/action-st-open-job) – allows you to open a LinkedIn job and optionally retrieve its details. - [st.doForCompanies](/docs/action-st-do-for-companies) – allows you to apply the actions specified in its `then` parameter to each company provided by the parent action. - [st.doForPeople](/docs/action-st-do-for-people) – allows you to apply the actions specified in its `then` parameter to each person provided by the parent action. - [st.doForPosts](/docs/action-st-do-for-posts) – allows you to apply the actions specified in its `then` parameter to each post provided by the parent action. - [st.doForComments](/docs/action-st-do-for-comments) – allows you to apply the actions specified in its `then` parameter to each comment provided by the parent action. - [st.doForJobs](/docs/action-st-do-for-jobs) – allows you to apply the actions specified in its `then` parameter to each job provided by the parent action. - [st.retrieveSSI](/docs/action-st-retrieve-ssi) – allows you to retrieve your current [SSI (Social Selling Index)](/guides/linkedin-social-selling-index). - [st.retrievePerformance](/docs/action-st-retrieve-performance) – allows you to retrieve performance analytics from your [LinkedIn dashboard](https://www.linkedin.com/dashboard/). - [st.retrieveFeed](/docs/action-st-retrieve-feed) – allows you to retrieve posts from your own home feed. ### Sales Navigator actions [nv.] - [nv.sendMessage](/docs/action-nv-send-message) – allows you to send a message to a person in Sales Navigator. - [nv.syncConversation](/docs/action-nv-sync-conversation) – allows you to sync a conversation in Sales Navigator so you can [start polling](/docs/working-with-conversations) it. - [nv.syncInbox](/docs/action-nv-sync-inbox) – allows you to enable [whole-inbox monitoring](/docs/monitoring-inbox) in Sales Navigator so you can poll every incoming conversation. - [nv.manageConversation](/docs/action-nv-manage-conversation) – allows you to manage a conversation thread in Sales Navigator by archiving or unarchiving it. - [nv.searchCompanies](/docs/action-nv-search-companies) – allows you to search for companies in Sales Navigator applying various filtering criteria. - [nv.searchPeople](/docs/action-nv-search-people) – allows you to search for people in Sales Navigator applying various filtering criteria. - [nv.openCompanyPage](/docs/action-nv-open-company-page) – allows you to open a company page in Sales Navigator to retrieve its basic information and perform additional company-related actions if needed. - [nv.retrieveCompanyEmployees](/docs/action-nv-retrieve-company-employees) – allows you to retrieve company employees from Sales Navigator and perform additional person-related actions if needed. - [nv.retrieveCompanyDMs](/docs/action-nv-retrieve-company-dms) – allows you to retrieve company decision makers from Sales Navigator and perform additional person-related actions if needed. - [nv.openPersonPage](/docs/action-nv-open-person-page) – allows you to open a person page in Sales Navigator to retrieve their basic information and perform additional person-related actions if needed. - [nv.doForCompanies](/docs/action-nv-do-for-companies) – allows you to apply the actions specified in its `then` parameter to each company provided by the parent action in Sales Navigator. - [nv.doForPeople](/docs/action-nv-do-for-people) – allows you to apply the actions specified in its `then` parameter to each person provided by the parent action in Sales Navigator. ## Webhook Events Receive workflow, account, and LinkedIn activity events in real time. Instead of [polling for a workflow result](/docs/executing-workflows), register a webhook once and Linked API delivers an event to your endpoint whenever a workflow changes state, a LinkedIn account changes status, a message is observed in a monitored inbox, or a connection changes in a monitored network. ## How it works - You register a single endpoint URL that receives events. A client may hold one active webhook at a time. - Every event is delivered as an HTTP `POST` with a JSON body (the event envelope below). - Respond with any `2xx` status to acknowledge. A non-`2xx` response, or a timeout, is retried with exponential backoff for up to 8 attempts before the delivery is marked failed. Register and manage your webhook through the [Admin API](/docs/admin-webhooks). ## Event envelope Every delivery has the same shape: ```json { "id": "workflow.completed:wf-64835e7c-...", "type": "workflow.completed", "createdAt": "2026-06-25T12:00:00.000Z", "data": { "workflowId": "wf-64835e7c-...", "accountId": "f9b4346a-...", "status": "completed", "result": { } } } ``` - `id` – stable, unique event identifier. Use it to deduplicate: a retried delivery reuses the same `id`. - `type` – event type, one of the values listed below. - `createdAt` – ISO 8601 timestamp of when the event was produced. - `data` – event-specific payload, described per event type below. > Delivery order is best-effort and not guaranteed. Correlate events by `type`, `data.status`, and `createdAt` rather than by arrival order. ## Workflow events `data` carries `workflowId`, `accountId`, and `status`. All events for one run share the same `workflowId`, so you can correlate them. - `workflow.created` – your request was accepted and queued. `status` is `pending`. - `workflow.started` – the workflow actually started running. `status` is `running`. - `workflow.completed` – the workflow reached a terminal state. `status` is `completed` or `failed`. `data.result` is included only on `workflow.completed`, and only in `fat` payload mode. Cancelled workflows do not emit a webhook. ## Account events `data` carries `accountId` and `status`, emitted when a connected LinkedIn account changes status. - `account.reconnectionRequired` – the account needs the user to reconnect. `status` is `reconnection_required`. - `account.active` – the account (re)connected and is operational. `status` is `active`. - `account.frozen` – the account was frozen, for example due to an unpaid subscription. `status` is `frozen`. - `account.deleted` – the account was deleted. `status` is `deleted`. ## Inbox events The `inbox.*` namespace covers messages observed in a connected account's inbox, as opposed to the state of your own Linked API entities. These events fire for accounts that have [inbox monitoring](/docs/monitoring-inbox) enabled via `st.syncInbox` / `nv.syncInbox`, one event per inbox message. - `inbox.messageReceived` – an incoming message was observed in the inbox. - `inbox.messageSent` – an outgoing message from the account was observed. This covers messages sent through the LinkedIn UI **and** messages sent through the Linked API, so an API-sent message emits `inbox.messageSent` in addition to its `workflow.completed`. `data` carries the message with the following fields: - `accountId` – the connected LinkedIn account the message belongs to. - `type` – inbox type the message belongs to (`st` or `nv`). - `threadId` – identifier of the conversation thread. Pass it to [`st.sendMessage`](/docs/action-st-send-message) / [`nv.sendMessage`](/docs/action-nv-send-message) to reply. - `personUrl` – LinkedIn URL of the other participant. - `messageId` – unique identifier of the message, matching the `id` returned by [inbox polling](/docs/monitoring-inbox). - `sender` – `us` for `inbox.messageSent`, `them` for `inbox.messageReceived`. - `text` – message text. - `time` – ISO 8601 timestamp of the message. > Because `inbox.messageSent` also fires for outbound messages your own automations send, filter by `data.sender` and `data.type` rather than assuming every event is a fresh inbound reply. ## Network events The `network.*` namespace covers changes to a connected account's connection graph. These events fire for accounts that have [network monitoring](/docs/monitoring-network) enabled via `st.syncNetwork`, one event per connection change. - `network.connectionRequestReceived` – someone sent the account a connection request. A new incoming pending invitation was observed. - `network.connectionAccepted` – a new connection that matches a request the account sent. The other person accepted an outgoing invitation. - `network.connectionAdded` – a new connection that is not attributable to a request the account sent, for example an incoming request the account accepted, or a connection formed outside the API. `data` carries the connection event with the following fields: - `accountId` – the connected LinkedIn account the event belongs to. - `personUrl` – LinkedIn URL of the other person. - `detectedAt` – ISO 8601 timestamp of when Linked API observed the event, matching the `detectedAt` returned by [network polling](/docs/monitoring-network). ## Payload modes The payload mode controls how much data `workflow.completed` carries: - `fat` (default) – the full workflow result is inlined in `data.result`. - `thin` – `data.result` is omitted; fetch the result via the [workflow API](/docs/executing-workflows) using `data.workflowId`. You can switch the mode at any time when [managing the webhook](/docs/admin-webhooks). ## Receiving events Your endpoint should: 1. Read the JSON body and branch on `type`. 2. Respond `2xx` quickly to acknowledge. Do heavy work asynchronously – a slow endpoint will time out and be retried. 3. Deduplicate on the envelope `id`. Deliveries are at-least-once, so the same event can arrive more than once. ## Sending message To send a message to a person in LinkedIn, you need to include [st.sendMessage](/docs/action-st-send-message) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.sendMessage", "personUrl": "https://www.linkedin.com/in/person1", "text": "Hi! I'd love to connect and discuss some ideas." } ``` **Completion:** ```json { "actionType": "st.sendMessage", "success": true } ``` > 💡 If you want to send a message in **Sales Navigator** (spend your Sales Navigator in-mail credits), use [`nv.sendMessage`](/docs/action-nv-send-message) action. > 💡 To reply into a known conversation without resolving the person's profile URL, pass a `threadId` (as returned by [inbox](/docs/monitoring-inbox) or [conversation](/docs/working-with-conversations) polling) instead of `personUrl`. See [`st.sendMessage`](/docs/action-st-send-message). > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Working with conversations Working with LinkedIn conversations involves 2 key steps: 1. **Syncing**. Each conversation needs to be synced before you can work with it. This is a time-consuming process that retrieves conversation history from LinkedIn and prepares it for future updates. Syncing lasts for a limited period — 30 days by default, up to 90. 2. **Polling**. While the syncing period lasts, you can continuously poll the conversation to get existing messages and receive new updates **without needing to sync again**. Once it ends, polling keeps returning everything collected so far, but new messages stop arriving until you sync the conversation again. > 💡 This page covers watching **specific people**, including their full history. To monitor your **entire** inbox and react to any new incoming message without syncing people one by one, see [Monitoring the inbox](/docs/monitoring-inbox). ## Syncing Depending on the number of conversations you want to sync, [execute a workflow](/docs/executing-workflows) with one or multiple [`st.syncConversation`](/docs/action-st-sync-conversation) actions. Here's an example: **Workflow:** ```json [ { "actionType": "st.syncConversation", "personUrl": "https://www.linkedin.com/in/person1" }, { "actionType": "st.syncConversation", "personUrl": "https://www.linkedin.com/in/person2", "days": 14 } ] ``` **Completion:** ```json [ { "actionType": "st.syncConversation", "success": true, "data": { "syncUntil": "2023-01-31T00:00:00Z" } }, { "actionType": "st.syncConversation", "success": true, "data": { "syncUntil": "2023-01-15T00:00:00Z" } } ] ``` Pass `days` (1 to 90, 30 by default) to control how long the conversation stays synchronized. The period is counted from the moment the action starts running, and `syncUntil` in the completion tells you when it ends. Running the action again for the same person starts a new period and keeps the history already collected. > 💡 For syncing conversations in **Sales Navigator**, use [`nv.syncConversation`](/docs/action-nv-sync-conversation) action. ## Polling Unlike syncing, you don't need to execute workflows to poll conversations. Instead, send a `POST` request to the following endpoint: ```text POST https://api.linkedapi.io/conversations/poll ``` In the request body, include an array of conversations you want to poll: ```json [ { "personUrl": "https://www.linkedin.com/in/person1", "since": "2023-01-01T00:00:00Z", "type": "st" }, { "personUrl": "https://www.linkedin.com/in/person2", "since": "2023-01-04T00:00:00Z", "type": "nv" } ] ``` - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person whose conversation you want to poll. - `since` (optional) – timestamp indicating the starting point for retrieving messages. If not provided, the entire conversation history will be returned. - `type` – enum indicating the conversation type: - `st` – for standard conversations. - `nv` – for Sales Navigator conversations. If your request is correct, you will receive the following response: ```json { "success": true, "result": [ { "personUrl": "https://www.linkedin.com/in/person1", "since": "2023-01-01T00:00:00Z", "type": "st", "syncUntil": "2023-01-31T00:00:00Z", "messages": [ { "id": "f4d92946-8821-6da6-ff4c-c36f0671292a", "sender": "us", "text": "Hello, how are you?", "time": "2023-01-02T10:30:00Z" }, { "id": "2a1b7f25-daba-5e20-70e9-c6427bb4f660", "sender": "them", "text": "I'm doing well, thanks for asking!", "time": "2023-01-02T10:35:00Z" } ] }, { "personUrl": "https://www.linkedin.com/in/person2", "since": "2023-01-04T00:00:00Z", "type": "nv", "syncUntil": "2023-02-03T00:00:00Z", "messages": [ { "id": "737e3d1c-15a8-6683-060f-5147eeb6ac26", "sender": "us", "text": "Hey! Are you available for a quick chat?", "time": "2023-01-03T09:00:00Z" }, { "id": "a002c77f-e318-619a-b52f-6b629501e522", "sender": "them", "text": "Sure! Let me know when works for you.", "time": "2023-01-03T09:05:00Z" }, { "id": "2b55d567-29e0-0632-2b21-ad2caf240bef", "sender": "us", "text": "How about this afternoon?", "time": "2023-01-03T09:10:00Z" } ] } ] } ``` - `syncUntil` – moment when synchronization of this conversation stops. Once it is in the past, the messages below are still returned but no longer updated — sync the conversation again to resume updates. - `messages` – array of messages. - `id` – unique identifier for the message. - `threadId` – identifier of the conversation thread. Pass it to [`st.sendMessage`](/docs/action-st-send-message) / [`nv.sendMessage`](/docs/action-nv-send-message) to reply directly into the thread. May be `null` for messages captured before a thread identifier was known. - `sender` – enum indicating who sent the message. Possible values: - `us` – message was sent by you. - `them` – message was sent by the person. - `text` – message text. - `time` – timestamp when the message was sent or received. In case of an unsuccessful request, you will receive the following response: ```json { "success": false, "error": { "type": "conversationsNotSynced", "message": "The conversations must be synced before polling: {conversations_list}." } } ``` - `error` – either a [common error](/docs/making-requests) or the `conversationsNotSynced` error. **Recommended flow for integrations:** If you integrate this endpoint into a frontend application, we recommend the following approach: 1. Leave `since` empty to retrieve the full conversation history. 2. For subsequent requests, pass the **timestamp of the last message** you received as `since` to retrieve only new messages. 3. Continue using `since` to fetch new messages periodically. If new messages are returned, update the `since` timestamp to the latest message time. This flow helps keep conversations up to date and allows your app to handle new message events (e.g., display notifications, play sounds, and so on). > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Monitoring the inbox Inbox monitoring lets you track **every** incoming conversation on an account, instead of syncing people one by one. It involves 2 key steps: 1. **Enabling**. Enable inbox sync once per account. From that point on, Linked API watches your whole inbox in the background and captures new messages across all threads. 2. **Polling**. After inbox sync is enabled, poll a single endpoint to retrieve captured messages and receive new ones — **without syncing each conversation**. > 💡 **Inbox monitoring vs. [conversation syncing](/docs/working-with-conversations).** [`st.syncConversation`](/docs/action-st-sync-conversation) watches one specific person and retrieves that conversation's full history. `st.syncInbox` watches your **entire** inbox but only captures messages that arrive **after** it is enabled. Use conversation syncing when you need the history of specific people; use inbox monitoring when you need to react to any new incoming message. ## Enabling [Execute a workflow](/docs/executing-workflows) with the [`st.syncInbox`](/docs/action-st-sync-inbox) action (or [`nv.syncInbox`](/docs/action-nv-sync-inbox) for Sales Navigator). You only need to do this once per account. Standard and Sales Navigator inboxes can both be enabled on the same account; polled messages are tagged with a `type` so you can tell them apart. **Workflow:** ```json { "actionType": "st.syncInbox" } ``` **Completion:** ```json { "actionType": "st.syncInbox", "success": true } ``` ## Polling Unlike enabling, you don't execute workflows to poll the inbox. Instead, send a `POST` request to the following endpoint: ```text POST https://api.linkedapi.io/inbox/poll ``` The request body is a single object. All fields are optional: ```json { "since": "2023-01-01T00:00:00Z", "type": "st", "threadId": "2-abc123..." } ``` - `since` (optional) – timestamp indicating the starting point for retrieving messages. If not provided, all captured messages are returned. - `type` (optional) – enum to filter by inbox type. If omitted, both standard and Sales Navigator messages are returned: - `st` – standard inbox messages only. - `nv` – Sales Navigator inbox messages only. - `threadId` (optional) – restrict the result to a single conversation thread. If your request is correct, you will receive the following response: ```json { "success": true, "result": { "messages": [ { "id": "f4d92946-8821-6da6-ff4c-c36f0671292a", "type": "st", "threadId": "2-abc123...", "personUrl": "https://www.linkedin.com/in/person1", "sender": "them", "text": "Hi! Are you available for a quick chat?", "time": "2023-01-02T10:35:00Z" }, { "id": "2a1b7f25-daba-5e20-70e9-c6427bb4f660", "type": "st", "threadId": "2-abc123...", "personUrl": "https://www.linkedin.com/in/person1", "sender": "us", "text": "Sure, let's talk this afternoon.", "time": "2023-01-02T10:38:00Z" } ] } } ``` - `messages` – flat array of messages across all threads, ordered newest first. - `id` – unique identifier for the message. - `type` – inbox type the message belongs to (`st` or `nv`). - `threadId` – identifier of the conversation thread. Pass it to [`st.sendMessage`](/docs/action-st-send-message) / [`nv.sendMessage`](/docs/action-nv-send-message) to reply directly into the thread. - `personUrl` – LinkedIn URL of the other participant. - `sender` – enum indicating who sent the message. Possible values: - `us` – message was sent by you (through the LinkedIn UI or the Linked API). - `them` – message was sent by the other person. - `text` – message text. - `time` – timestamp when the message was sent or received. **Recommended flow for integrations:** 1. Leave `since` empty on the first request to retrieve everything captured so far. 2. For subsequent requests, pass the **timestamp of the most recent message** you received as `since` to fetch only newer messages. 3. Continue polling with an advancing `since`. Whenever new messages arrive, update `since` to the latest message time. > 💡 For push-based delivery instead of polling, subscribe to the inbox [Webhook Events](/docs/webhooks). They fire for accounts with inbox monitoring enabled and carry the same message fields. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Checking connection status This page describes how to create workflows for checking connection statuses between your LinkedIn account and other people. ## Single person check To check the connection status between your account and another person, you need to include [`st.checkConnectionStatus`](/docs/action-st-check-connection-status) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.checkConnectionStatus", "personUrl": "https://www.linkedin.com/in/person1" } ``` **Completion:** ```json { "actionType": "st.checkConnectionStatus", "success": true, "data": { "connectionStatus": "pending" } } ``` ## Multiple people check To check the connection statuses between your account and several people, you need to use [`st.checkConnectionStatus`](/docs/action-st-check-connection-status) multiple times. Here's an example: **Workflow:** ```json [ { "actionType": "st.checkConnectionStatus", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1" }, { "actionType": "st.checkConnectionStatus", "label": "person2", "personUrl": "https://www.linkedin.com/in/person2" }, { "actionType": "st.checkConnectionStatus", "label": "person3", "personUrl": "https://www.linkedin.com/in/person3" } ] ``` **Completion:** ```json [ { "actionType": "st.checkConnectionStatus", "label": "person1", "success": true, "data": { "connectionStatus": "connected" } }, { "actionType": "st.checkConnectionStatus", "label": "person2", "success": true, "data": { "connectionStatus": "pending" } }, { "actionType": "st.checkConnectionStatus", "label": "person3", "success": true, "data": { "connectionStatus": "notConnected" } } ] ``` > 💡 Use this approach for multiple checks as it's significantly faster than creating separate workflows for each person. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Working with invitations This page describes how to create workflows for sending and withdrawing connection requests, and retrieving, accepting, and ignoring invitations on LinkedIn. ## Sending connection request To send a connection request from your account to another person, you need to include [`st.sendConnectionRequest`](/docs/action-st-send-connection-request) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.sendConnectionRequest", "personUrl": "https://www.linkedin.com/in/person1", "note": "Hi, I’d like to connect with you to discuss potential collaboration.", "email": "example1@gmail.com" } ``` **Completion:** ```json { "actionType": "st.sendConnectionRequest", "success": true } ``` ## Withdrawing connection request To withdraw the connection request sent from your account, you need to include [`st.withdrawConnectionRequest`](/docs/action-st-withdraw-connection-request) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.withdrawConnectionRequest", "personUrl": "https://www.linkedin.com/in/person1", "unfollow": true } ``` **Completion:** ```json { "actionType": "st.withdrawConnectionRequest", "success": true } ``` ## Retrieving pending connection requests To retrieve a list of pending connection requests sent from your account, you need to include [`st.retrievePendingRequests`](/docs/action-st-retrieve-pending-requests) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.retrievePendingRequests" } ``` **Completion:** ```json { "actionType": "st.retrievePendingRequests", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CTO at Tech Solutions" }, { "name": "Carlos Santos", "publicUrl": "https://www.linkedin.com/in/carlossantos", "headline": "Product Manager at Startup Hub" } ] } ``` ## Retrieving invitations Use [`st.retrieveInvitations`](/docs/action-st-retrieve-invitations) to read all incoming connection, company-follow, and newsletter-subscription invitations from the invitation manager. The action is root-only and does not support `then`. > 💡 To react to new connection requests as they arrive instead of polling this action on a schedule, enable [network monitoring](/docs/monitoring-network) with [`st.syncNetwork`](/docs/action-st-sync-network) and consume `connectionRequestReceived` events. **Workflow:** ```json { "actionType": "st.retrieveInvitations" } ``` **Completion:** ```json { "actionType": "st.retrieveInvitations", "success": true, "data": [ { "invitationType": "connect", "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "note": "I would like to connect." }, { "invitationType": "companyFollow", "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "companyUrl": "https://www.linkedin.com/company/example-company", "companyName": "Example Company" }, { "invitationType": "newsletterSubscribe", "name": "Carlos Santos", "publicUrl": "https://www.linkedin.com/in/carlossantos", "newsletterUrl": "https://www.linkedin.com/newsletters/example-1234567890", "newsletterName": "Example Newsletter" } ] } ``` Use the type-specific URL from each item when accepting or ignoring it: | `invitationType` | Required target | | --------------------- | ----------------------------------------------- | | `connect` | `personUrl`, using the item's `publicUrl` value | | `companyFollow` | `companyUrl` | | `newsletterSubscribe` | `newsletterUrl` | ## Accepting an invitation Use [`st.acceptInvitation`](/docs/action-st-accept-invitation) with the invitation type and exactly one matching target URL. ```json { "actionType": "st.acceptInvitation", "invitationType": "connect", "personUrl": "https://www.linkedin.com/in/johndoe" } ``` For a company-follow invitation: ```json { "actionType": "st.acceptInvitation", "invitationType": "companyFollow", "companyUrl": "https://www.linkedin.com/company/example-company" } ``` For a newsletter-subscription invitation: ```json { "actionType": "st.acceptInvitation", "invitationType": "newsletterSubscribe", "newsletterUrl": "https://www.linkedin.com/newsletters/example-1234567890" } ``` A successful completion contains `"success": true` and no data. ## Ignoring an invitation [`st.ignoreInvitation`](/docs/action-st-ignore-invitation) uses the same target contract: ```json { "actionType": "st.ignoreInvitation", "invitationType": "connect", "personUrl": "https://www.linkedin.com/in/johndoe" } ``` Change `invitationType` and the URL field together for company-follow or newsletter-subscription invitations. A successful completion contains `"success": true` and no data. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed constraints, parameters, and results, refer to the corresponding [action documentation pages](/docs/actions-overview). ## Managing existing connections This page describes how to create workflows for retrieving and removing people from your existing LinkedIn connections. ## Retrieving connections To retrieve your connections, you need to include [`st.retrieveConnections`](/docs/action-st-retrieve-connections) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.retrieveConnections", "filter": { "firstName": "John", "lastName": "Doe", "position": "CEO", "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "currentCompanies": ["Tech Solutions", "Innovatech"], "previousCompanies": ["FutureCorp"], "schools": ["Harvard University", "MIT"] } } ``` **Completion:** ```json { "actionType": "st.retrieveConnections", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "location": "Lisbon, Portugal" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CEO at Tech Solutions", "location": "San Francisco Bay Area" }, { "name": "Carlos Santos", "publicUrl": "https://www.linkedin.com/in/carlossantos", "headline": "Project Manager at Startup Hub", "location": "Madrid" } ] } ``` > 💡 If you want to perform actions on the retrieved connections, use [`st.doForPeople`](/docs/action-st-do-for-people) action. ## Removing connection To remove a person from your connections, use [`st.removeConnection`](/docs/action-st-remove-connection) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.removeConnection", "personUrl": "https://www.linkedin.com/in/person1" } ``` **Completion:** ```json { "actionType": "st.removeConnection", "success": true } ``` > 💡 If you need to remove several people from your connections, use this action multiple times within an [array workflow](/docs/building-workflows). This approach is significantly faster than creating separate workflows for each person due to API optimizations. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Monitoring the network Network monitoring lets you track changes to an account's connection graph in the background, instead of repeatedly retrieving connections and pending requests and diffing them yourself. It involves 2 key steps: 1. **Enabling**. Enable network sync once per account. From that point on, Linked API watches the account's network in the background and captures connection events. 2. **Polling**. After network sync is enabled, poll a single endpoint to retrieve captured events and receive new ones — **without retrieving the whole connection list each time**. > 💡 **Monitoring does not import history.** Only changes that happen **after** you enable it are captured — your existing connections and pending invitations are used as a silent baseline and never emitted. When you need the state as it is right now (a full list, a one-off audit, backfilling before monitoring was on), reach for [`st.retrieveConnections`](/docs/action-st-retrieve-connections) and [`st.retrieveInvitations`](/docs/action-st-retrieve-invitations) instead. ## Enabling [Execute a workflow](/docs/executing-workflows) with the [`st.syncNetwork`](/docs/action-st-sync-network) action. You only need to do this once per account. **Workflow:** ```json { "actionType": "st.syncNetwork" } ``` **Completion:** ```json { "actionType": "st.syncNetwork", "success": true } ``` ## Event types Network monitoring emits three kinds of events: - `connectionRequestReceived` – someone sent **you** a connection request. A new incoming pending invitation was observed. - `connectionAccepted` – a new connection that matches a request **you** sent. The other person accepted your outgoing invitation. - `connectionAdded` – a new connection that is **not** attributable to a request you sent. For example, you accepted someone else's incoming request, or the connection was formed outside the API. > 💡 Together, `connectionAccepted` and `connectionAdded` cover every new first-degree connection: `connectionAccepted` is the subset you initiated, `connectionAdded` is everything else. If you only care that the network grew, handle both. ## Polling Unlike enabling, you don't execute workflows to poll the network. Instead, send a `POST` request to the following endpoint: ```text POST https://api.linkedapi.io/network/poll ``` The request body is a single object. All fields are optional: ```json { "since": "2023-01-01T00:00:00Z", "type": "connectionAccepted" } ``` - `since` (optional) – timestamp indicating the starting point for retrieving events. If not provided, all captured events are returned. - `type` (optional) – filter by a single event type. If omitted, all event types are returned: - `connectionRequestReceived` - `connectionAccepted` - `connectionAdded` If your request is correct, you will receive the following response: ```json { "success": true, "result": { "events": [ { "id": "f4d92946-8821-6da6-ff4c-c36f0671292a", "type": "connectionAccepted", "personUrl": "https://www.linkedin.com/in/person1", "detectedAt": "2023-01-02T10:35:00Z" }, { "id": "2a1b7f25-daba-5e20-70e9-c6427bb4f660", "type": "connectionRequestReceived", "personUrl": "https://www.linkedin.com/in/person2", "detectedAt": "2023-01-02T09:12:00Z" } ] } } ``` - `events` – array of connection events, ordered newest first. - `id` – unique identifier for the event. - `type` – one of `connectionRequestReceived`, `connectionAccepted`, or `connectionAdded`. - `personUrl` – LinkedIn URL of the other person. - `detectedAt` – timestamp when Linked API observed the event. **Recommended flow for integrations:** 1. Leave `since` empty on the first request to retrieve everything captured so far. 2. For subsequent requests, pass the **timestamp of the most recent event** you received as `since` to fetch only newer events. 3. Continue polling with an advancing `since`. Whenever new events arrive, update `since` to the latest `detectedAt`. > Events are retained for **90 days**. Poll (or receive webhooks) often enough to consume them within that window — older events are pruned and will no longer be returned. > 💡 For push-based delivery instead of polling, subscribe to the network [Webhook Events](/docs/webhooks). They fire for accounts with network monitoring enabled and carry the same event fields. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Reacting and commenting This page describes how to create workflows for reacting and commenting on LinkedIn posts. ## Reacting to post To react to a post, you need to include [`st.reactToPost`](/docs/action-st-react-to-post) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.reactToPost", "postUrl": "https://www.linkedin.com/posts/post1", "type": "like" } ``` **Completion:** ```json { "actionType": "st.reactToPost", "success": true } ``` ## Commenting on post To comment on a post, you need to include [`st.commentOnPost`](/docs/action-st-comment-on-post) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.commentOnPost", "postUrl": "https://www.linkedin.com/posts/post1", "text": "I completely agree with your point." } ``` **Completion:** ```json { "actionType": "st.commentOnPost", "success": true } ``` ## Reacting + commenting To react and comment on the same post, you need to include [`st.reactToPost`](/docs/action-st-react-to-post) and [`st.commentOnPost`](/docs/action-st-comment-on-post) as child actions of [`st.openPost`](/docs/action-st-open-post) action. Here's an example: **Workflow:** ```json { "actionType": "st.openPost", "postUrl": "https://www.linkedin.com/posts/post1", "basicInfo": false, "then": [ { "actionType": "st.reactToPost", "type": "like" }, { "actionType": "st.commentOnPost", "text": "I completely agree with your point." } ] } ``` **Completion:** ```json { "actionType": "st.openPost", "success": true, "data": { "then": [ { "actionType": "st.reactToPost", "success": true }, { "actionType": "st.commentOnPost", "success": true } ] } } ``` ## Reacting to a comment To react to a comment, you need to include [`st.reactToComment`](/docs/action-st-react-to-comment) action in your workflow. It accepts a `commentUrl`, which you can obtain from the `commentUrl` field returned by [`st.commentOnPost`](/docs/action-st-comment-on-post), [`st.replyToComment`](/docs/action-st-reply-to-comment), or [`st.retrievePostComments`](/docs/action-st-retrieve-post-comments). Here's an example: **Workflow:** ```json { "actionType": "st.reactToComment", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "type": "like" } ``` **Completion:** ```json { "actionType": "st.reactToComment", "success": true } ``` ## Replying to a comment To reply to a comment, you need to include [`st.replyToComment`](/docs/action-st-reply-to-comment) action in your workflow. It returns the `commentUrn` and `commentUrl` of the created reply. Here's an example: **Workflow:** ```json { "actionType": "st.replyToComment", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "text": "Thanks for sharing your thoughts!" } ``` **Completion:** ```json { "actionType": "st.replyToComment", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,1122334455)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(1122334455%2Curn%3Ali%3Aactivity%3A1234567890123456789)" } } ``` ## Engaging with all comments on a post To react to or reply to every comment on a post without knowing their URLs upfront, retrieve the comments with [`st.retrievePostComments`](/docs/action-st-retrieve-post-comments) and iterate over them with [`st.doForComments`](/docs/action-st-do-for-comments). Each iterated comment can either react/reply directly, or open first with [`st.openComment`](/docs/action-st-open-comment) to run several comment-scoped actions. Here's an example that likes and replies to each of the 3 most relevant comments: **Workflow:** ```json { "actionType": "st.retrievePostComments", "postUrl": "https://www.linkedin.com/posts/post1", "limit": 3, "sort": "mostRelevant", "then": { "actionType": "st.doForComments", "then": { "actionType": "st.openComment", "then": [ { "actionType": "st.reactToComment", "type": "celebrate" }, { "actionType": "st.replyToComment", "text": "Great point, thanks for sharing!" } ] } } } ``` **Completion:** ```json { "actionType": "st.retrievePostComments", "success": true, "data": [ { ... "then": { "actionType": "st.openComment", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,9876543210)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "then": [ { "actionType": "st.reactToComment", "success": true }, { "actionType": "st.replyToComment", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,1122334455)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(1122334455%2Curn%3Ali%3Aactivity%3A1234567890123456789)" } } ] } } } ] } ``` > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Visiting person page To visit a person's LinkedIn page so that they see your visit in their profile views and notifications, include [`st.openPersonPage`](/docs/action-st-open-person-page) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/person1", "basicInfo": false } ``` **Completion:** ```json { "actionType": "st.openPersonPage", "success": true } ``` > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Endorsing skills (soon) This workflow will be available soon. Stay tuned for updates. ```json { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/person1", "then": { "actionType": "st.endorseSkills", ... } } ``` ## Searching for companies To search for companies in LinkedIn, you need to include [`st.searchCompanies`](/docs/action-st-search-companies) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.searchCompanies", "term": "Tech Inc", "limit": 2, "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Robotics Engineering"] } } ``` **Completion:** ```json { "actionType": "st.searchCompanies", "success": true, "data": [ { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "industry": "Information Technology", "location": "California" }, { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "industry": "Software Development", "location": "Mountain View" } ] } ``` > 💡 If you want to perform actions on companies from search results, use [`st.doForCompanies`](/docs/action-st-do-for-companies) action. > 💡 If you want to search for companies in **Sales Navigator**, use [`nv.searchCompanies`](/docs/action-nv-search-companies) action. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Searching for people To search for people in LinkedIn, you need to include [`st.searchPeople`](/docs/action-st-search-people) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.searchPeople", "term": "John Doe", "limit": 2, "filter": { "firstName": "John", "lastName": "Doe", "position": "CEO", "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "currentCompanies": ["Tech Solutions", "Innovatech"], "previousCompanies": ["FutureCorp"], "schools": ["Harvard University", "MIT"] } } ``` **Completion:** ```json { "actionType": "st.searchPeople", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "location": "London" }, { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johnyd", "headline": "Product Manager at Semsoft", "location": "New York" } ] } ``` > 💡 If you want to perform actions on people from search results, use [`st.doForPeople`](/docs/action-st-do-for-people) action. > 💡 If you want to search for people in **Sales Navigator**, use [`nv.searchPeople`](/docs/action-nv-search-people) action. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Searching for jobs To search for jobs in LinkedIn, include the [`st.searchJobs`](/docs/action-st-search-jobs) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.searchJobs", "term": "product manager", "limit": 2, "filter": { "location": "San Francisco, California, United States", "datePosted": "pastWeek", "experienceLevels": ["midSeniorLevel", "director"], "employmentTypes": ["fullTime"], "workplaceTypes": ["remote", "hybrid"], "easyApply": true } } ``` **Completion:** ```json { "actionType": "st.searchJobs", "success": true, "data": [ { "jobId": "4416248954", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "title": "Senior Product Manager", "companyName": "Example Company", "location": "San Francisco, CA", "workplaceType": "remote", "salary": { "currency": "usd", "minAmount": 140000, "maxAmount": 180000, "period": "yearly" }, "easyApply": true, "isPromoted": false }, { "jobId": "4427334841", "jobUrl": "https://www.linkedin.com/jobs/view/4427334841/", "title": "Director of Product", "companyName": "Another Company", "location": "New York, NY", "workplaceType": "hybrid", "salary": null, "easyApply": false, "isPromoted": true } ] } ``` > 💡 If you want to retrieve full details for each job from the search results, use [`st.doForJobs`](/docs/action-st-do-for-jobs) with [`st.openJob`](/docs/action-st-open-job). > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Retrieving company data This page describes how to create workflows for retrieving various types of LinkedIn company data, including basic information, posts, employees, and decision-makers. ## Retrieving basic info To retrieve basic information about a company, you need to include [`st.openСompanyPage`](/docs/action-st-open-company-page) action in your workflow, with `basicInfo` parameter set to `true`. Here's an example: **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/company1", "basicInfo": true } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/company1", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "Cupertino, California", "headquarters": "US", "industry": "Information Technology", "specialties": "Cloud Computing, AI, Software Development", "website": "https://techcorp.com", "employeesCount": 500, "yearFounded": 2019, "ventureFinancing": true, "jobsCount": 12 } } ``` > 💡 If you want to retrieve basic information about a company from **Sales Navigator**, use [`nv.openCompanyPage`](/docs/action-nv-open-company-page) action. ## Retrieving employees To retrieve company employees, you need to include [`st.retrieveCompanyEmployees`](/docs/action-st-retrieve-company-employees) action as a child of [`st.openCompanyPage`](/docs/action-st-open-company-page). Here's an example: **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/company1", "basicInfo": false, "then": [ { "actionType": "st.retrieveCompanyEmployees", "limit": 100, "filter": { "position": "Manager", "locations": ["New York", "San Francisco", "London"], "schools": ["Harvard University", "MIT"] } } ] } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { "then": [ { "actionType": "st.retrieveCompanyEmployees", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Manager at TechCorp", "location": "New York, USA" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA" } ] } ] } } ``` > 💡 If you want to perform actions on the retrieved employees, use [`st.doForPeople`](/docs/action-st-do-for-people) action. > 💡 If you want to retrieve company employees from **Sales Navigator**, use the following actions: [`nv.openCompanyPage`](/docs/action-nv-open-company-page), [`nv.retrieveCompanyEmployees`](/docs/action-nv-retrieve-company-employees). ## Retrieving decision makers To retrieve company employees, you need to include [`st.retrieveCompanyDMs`](/docs/action-st-retrieve-company-dms) action as a child of [`st.openCompanyPage`](/docs/action-st-open-company-page). Here's an example: **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/company1", "basicInfo": false, "then": [ { "actionType": "st.retrieveCompanyEmployees", "limit": 100, "filter": { "position": "Manager", "locations": ["New York", "San Francisco", "London"], "schools": ["Harvard University", "MIT"] } }, { "actionType": "st.retrieveCompanyDMs", "limit": 3 } ] } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { "then": [ { "actionType": "st.retrieveCompanyEmployees", "success": true, "data": [ ... ] }, { "actionType": "st.retrieveCompanyDMs", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder of TechCorp", "location": "New York, USA", "countryCode": "US" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CEO at TechCorp", "location": "San Francisco, USA", "countryCode": "US" } ] } ] } } ``` > 💡 If you want to perform actions on the retrieved decision makers, use [`st.doForPeople`](/docs/action-st-do-for-people) action. > 💡 If you want to retrieve decision makers from **Sales Navigator**, use the following actions: [`nv.openCompanyPage`](/docs/action-nv-open-company-page), [`nv.retrieveCompanyDMs`](/docs/action-nv-retrieve-company-dms). ## Retrieving posts To retrieve posts published by a company, you need to include [`st.retrieveCompanyPosts`](/docs/action-st-retrieve-company-posts) action as a child of [`st.openCompanyPage`](/docs/action-st-open-company-page). Here's an example: **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/company1", "basicInfo": false, "then": [ { "actionType": "st.retrieveCompanyEmployees", "limit": 100, "filter": { "position": "Manager", "locations": ["New York", "San Francisco", "London"], "schools": ["Harvard University", "MIT"] } }, { "actionType": "st.retrieveCompanyDMs", "limit": 3 }, { "actionType": "st.retrieveCompanyPosts", "limit": 5, "since": "2025-01-01T00:00:00Z" } ] } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { "then": [ { "actionType": "st.retrieveCompanyEmployees", "success": true, "data": [ ... ] }, { "actionType": "st.retrieveCompanyDMs", "success": true, "data": [ ... ] }, { "actionType": "st.retrieveCompanyPosts", "success": true, "data": [ { "url": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789", "activityUrn": "urn:li:activity:1234567890123456789", "time": "2023-01-02T12:30:00Z", "type": "original", "author": { "type": "company", "name": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company" }, "reposter": null, "repostText": null, "text": "Check out our latest product launch!", "hashtags": ["product", "launch"], "mentions": [], "externalLinks": ["https://example.com/launch"], "images": [ "https://static.linkedin.com/image1.jpg", "https://static.linkedin.com/image2.jpg" ], "documentSlides": [], "hasVideo": false, "videoThumbnail": null, "hasPoll": false, "reactionsCount": 27, "commentsCount": 8, "repostsCount": 2 }, { "url": "https://www.linkedin.com/feed/update/urn:li:activity:2345678901234567890", "activityUrn": "urn:li:activity:2345678901234567890", "time": "2023-01-01T09:15:00Z", "type": "repost", "author": { "type": "person", "name": "Example Author", "profileUrl": "https://www.linkedin.com/in/example-author", "headline": null }, "reposter": { "type": "company", "name": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company" }, "repostText": "Thank you to everyone who joined our webinar!", "text": "Original post content about the webinar", "hashtags": [], "mentions": [], "externalLinks": [], "images": [], "documentSlides": [], "hasVideo": true, "videoThumbnail": "https://media.licdn.com/dms/image/video-cover.jpg", "hasPoll": false, "reactionsCount": 6, "commentsCount": 0, "repostsCount": 1 } ] } ] } } ``` > 💡 If you want to perform actions on the retrieved posts, use [`st.doForPosts`](/docs/action-st-do-for-posts) action. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Retrieving person data This page describes how to create workflows for retrieving various types of LinkedIn person data, including basic information, experience, education, and more. ## Retrieving basic info To retrieve basic information about a person, you need to include [`st.openPersonPage`](/docs/action-st-open-person-page) action in your workflow, with `basicInfo` parameter set to `true`. Here's an example: **Workflow:** ```json { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/person1", "basicInfo": true } ``` **Completion:** ```json { "actionType": "st.openPersonPage", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/person1", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "countryCode": "US", "position": "Software Engineer", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678", "followersCount": 2322 } } ``` > 💡 If you want to retrieve basic information about a person from **Sales Navigator**, use [`nv.openPersonPage`](/docs/action-nv-open-person-page) action. ## Retrieving experience To retrieve information about a person's experience, you need to include [`st.retrievePersonExperience`](/docs/action-st-retrieve-person-experience) action as a child of [`st.openPersonPage`](/docs/action-st-open-person-page). Here's an example: **Workflow:** ```json { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/person1", "basicInfo": false, "then": [ { "actionType": "st.retrievePersonExperience" } ] } ``` **Completion:** ```json { "actionType": "st.openPersonPage", "success": true, "data": { "then": [ { "actionType": "st.retrievePersonExperience", "success": true, "data": [ { "position": "Software Engineer", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678", "employmentType": "fullTime", "locationType": "onSite", "description": "Developing innovative software solutions.", "duration": 24, "startTime": "2021-01-01T00:00:00Z", "endTime": null, "location": "San Francisco, USA" }, { "position": "Junior Developer", "companyName": "CodeBase Inc.", "companyHashedUrl": "https://www.linkedin.com/company/87654321", "employmentType": "internship", "locationType": "remote", "description": "Worked on front-end development tasks.", "duration": 12, "startTime": "2020-01-01T00:00:00Z", "endTime": "2020-12-31T00:00:00Z", "location": null } ] } ] } } ``` ## Retrieving education To retrieve information about a person's education, you need to include [`st.retrievePersonEducation`](/docs/action-st-retrieve-person-education) action as a child of [`st.openPersonPage`](/docs/action-st-open-person-page). Here's an example: **Workflow:** ```json { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/person1", "basicInfo": false, "then": [ { "actionType": "st.retrievePersonExperience" }, { "actionType": "st.retrievePersonEducation" } ] } ``` **Completion:** ```json { "actionType": "st.openPersonPage", "success": true, "data": { "then": [ { "actionType": "st.retrievePersonExperience", "success": true, "data": [ ... ] }, { "actionType": "st.retrievePersonEducation", "success": true, "data": [ { "schoolName": "Harvard University", "schoolHashedUrl": "https://www.linkedin.com/company/12345678", "details": "Master of Science in Computer Science, Artificial Intelligence" }, { "schoolName": "MIT", "schoolHashedUrl": "https://www.linkedin.com/company/87654321", "details": "Bachelor of Science in Electrical Engineering and Computer Science" } ] } ] } } ``` ## Retrieving other data To retrieve other types of person data, you can use the following actions as children of [`st.openPersonPage`](/docs/action-st-open-person-page) (just as in the examples above): - [`st.retrievePersonSkills`](/docs/action-st-retrieve-person-skills) – allows you to retrieve a person's skills. - [`st.retrievePersonLanguages`](/docs/action-st-retrieve-person-languages) – allows you to retrieve a person's languages. - [`st.retrievePersonPosts`](/docs/action-st-retrieve-person-posts) – allows you to retrieve posts published by a person and perform additional post-related actions if needed. - [`st.retrievePersonComments`](/docs/action-st-retrieve-person-comments) – allows you to retrieve comments left by a person. - [`st.retrievePersonReactions`](/docs/action-st-retrieve-person-reactions) – allows you to retrieve reactions made by a person. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Retrieving post data To retrieve LinkedIn post data, include the [`st.openPost`](/docs/action-st-open-post) action in your workflow and set `basicInfo` to `true`. Here's an example: **Workflow:** ```json { "actionType": "st.openPost", "postUrl": "https://www.linkedin.com/posts/post1", "basicInfo": true } ``` **Completion:** ```json { "actionType": "st.openPost", "success": true, "data": { "url": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789", "activityUrn": "urn:li:activity:1234567890123456789", "time": "2023-01-02T12:30:00Z", "type": "original", "author": { "type": "person", "name": "Example Person", "profileUrl": "https://www.linkedin.com/in/example-person", "headline": "Product Marketing Lead" }, "reposter": null, "text": "Check out our latest product launch!", "repostText": null, "hashtags": ["product", "launch"], "mentions": ["https://www.linkedin.com/company/example-company"], "externalLinks": ["https://example.com/launch"], "images": [ "https://static.linkedin.com/image1.jpg", "https://static.linkedin.com/image2.jpg" ], "documentSlides": [], "hasVideo": false, "videoThumbnail": null, "hasPoll": false, "reactionsCount": 27, "commentsCount": 8, "repostsCount": 12 } } ``` > 💡 If you want to react or comment on the opened post, use the following actions as children: [st.reactToPost](/docs/action-st-react-to-post), [st.commentOnPost](/docs/action-st-comment-on-post). > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Retrieving job data To retrieve LinkedIn job data, include the [`st.openJob`](/docs/action-st-open-job) action in your workflow and set `basicInfo` to `true`. Here's an example: **Workflow:** ```json { "actionType": "st.openJob", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "basicInfo": true } ``` **Completion:** ```json { "actionType": "st.openJob", "success": true, "data": { "jobId": "4416248954", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "title": "Senior Product Manager", "companyName": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company", "location": "San Francisco, CA", "postedDate": "1w", "applicantsCount": 84, "workplaceType": "remote", "employmentType": "Full-time", "salary": { "currency": "usd", "minAmount": 140000, "maxAmount": 180000, "period": "yearly" }, "description": "Example job description text.", "applyUrl": "https://www.linkedin.com/jobs/view/4416248954/apply/", "easyApply": true } } ``` > 💡 If you want to find jobs to retrieve, use [`st.searchJobs`](/docs/action-st-search-jobs) action. > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Retrieving SSI, performance This page describes how to create workflows for retrieving your current [SSI](/guides/linkedin-social-selling-index) and performance analytics from your [LinkedIn dashboard](https://www.linkedin.com/dashboard/). ## Retrieving current SSI To retrieve your current SSI, you need to include [`st.retrieveSSI`](/docs/action-st-retrieve-ssi) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.retrieveSSI" } ``` **Completion:** ```json { "actionType": "st.retrieveSSI", "success": true, "data": { "ssi": 78, "industryTop": 15, "networkTop": 10 } } ``` ## Retrieving performance To retrieve performance analytics from your [LinkedIn dashboard](https://www.linkedin.com/dashboard/), you need to include [`st.retrievePerformance`](/docs/action-st-retrieve-performance) action in your workflow. Here's an example: **Workflow:** ```json { "actionType": "st.retrievePerformance" } ``` **Completion:** ```json { "actionType": "st.retrievePerformance", "success": true, "data": { "followersCount": 1250, "postViewsLast7Days": 523, "profileViewsLast90Days": 342, "searchAppearancesPreviousWeek": 89 } } ``` > This page provides examples of [workflows](/docs/building-workflows) and their [completions](/docs/executing-workflows). For detailed documentation on constraints, parameters, and possible results of specific actions, always refer to the corresponding [action documentation pages](/docs/actions-overview). ## Checking API usage statistics This page explains how to check Linked API usage statistics. You can use this information to stay within LinkedIn limits for your account. > In addition to monitoring usage, you can configure action limits for each account on the [platform](https://app.linkedapi.io/). When a limit is reached, actions will automatically return a `limitExceeded` error instead of executing. We provide statistics as an array of all actions executed during a specific period. To access it, make a `GET` request to the following endpoint with 2 parameters: ```text GET https://api.linkedapi.io/stats/actions?start={}&end={} ``` - `start` – timestamp from which the statistics will be retrieved. - `end` – timestamp up to which the statistics will be retrieved. > The difference between `start` and `end` must not exceed 30 days. In response, you receive an array of actions executed during the specified period: ```json { "success": true, "result": [ { "actionType": "st.openCompanyPage", "success": true, "time": "2023-01-02T12:30:00Z" }, { "actionType": "st.sendMessage", "success": false, "time": "2023-01-03T12:30:00Z" }, { "actionType": "nv.retrieveCompanyEmployees", "success": true, "time": "2023-01-04T12:30:00Z" }, { "actionType": "st.sendConnectionRequest", "success": true, "time": "2023-01-07T12:30:00Z" } ... ] } ``` > In case of an unsuccessful request, you'll receive one of the [common errors](/docs/making-requests). ## Overview Admin endpoints let you programmatically manage your Linked API subscription status, seats, connected LinkedIn accounts, and rate limits. While the main API endpoints ([workflows](/docs/executing-workflows), [conversations](/docs/working-with-conversations), [statistics](/docs/checking-api-usage-statistics)) operate on behalf of a specific LinkedIn account, admin endpoints manage your Linked API account itself. They are synchronous (direct request-response, no workflows) and require only the `linked-api-token` header – no `identification-token` is needed. ## Authentication Admin endpoints require the `linked-api-token` header – the same token used for all other API requests. Account-specific operations accept `accountId` in the request body instead of the `identification-token` header. You can find your token in the [platform dashboard](https://app.linkedapi.io). ## Request format All endpoints use **POST** method with JSON body. The URL pattern is: ```text POST https://api.linkedapi.io/admin/. ``` | Header | Required | Description | |--------|----------|-------------| | `linked-api-token` | Yes | Your Linked API token | | `Content-Type` | Yes | `application/json` | ## Response format ### Success ```json { "success": true, "result": { ... } } ``` ### Error ```json { "success": false, "error": { "type": "errorType", "message": "Human-readable error description" } } ``` ### Common errors | Type | HTTP Status | Description | |------|-------------|-------------| | `linkedApiTokenRequired` | 401 | Missing `linked-api-token` header | | `invalidLinkedApiToken` | 401 | Token is invalid or expired | | `accountIdRequired` | 401 | Missing or invalid `accountId` | | `accountNotFound` | 401 | Account does not exist or does not belong to you | | `sessionNotFound` | 404 | Connection session not found | | `tooManyRequests` | 429 | Rate limit exceeded (100 requests per 60 seconds) | ## Next steps - [Subscription](/docs/admin-subscription) – manage subscription status and seats - [Accounts](/docs/admin-accounts) – list, refresh, disconnect, and monitor LinkedIn accounts - [Connection Sessions](/docs/admin-connection-sessions) – connect and reconnect accounts, and customize the completion screen - [Limits](/docs/admin-limits) – configure and monitor rate limits - [Webhooks](/docs/admin-webhooks) – register an endpoint to receive workflow and account events ## Subscription Manage your Linked API subscription programmatically. For general Admin API information, see the [Admin overview](/docs/admin-overview). ## subscription.getStatus Get current subscription status. ```bash curl -X POST https://api.linkedapi.io/admin/subscription.getStatus \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` **Response:** ```json { "success": true, "result": { "status": "active", "eligibleForTrial": false, "cancelAtPeriodEnd": false } } ``` | Field | Type | Description | |-------|------|-------------| | `status` | `string` or `undefined` | `active`, `trialing`, `past_due`, `canceled`, or `undefined` if no subscription | | `eligibleForTrial` | `boolean` | Whether a 7-day free trial is available | | `cancelAtPeriodEnd` | `boolean` | Whether the subscription is scheduled to cancel at the end of the current billing period | ## subscription.getSeats Get active subscription seats. ```bash curl -X POST https://api.linkedapi.io/admin/subscription.getSeats \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` **Response:** ```json { "success": true, "result": { "seats": [ { "seatType": "plus", "quantity": 10, "billingPeriod": "year" } ] } } ``` | Field | Type | Description | |-------|------|-------------| | `seatType` | `"core"` \| `"plus"` | Seat tier. `plus` unlocks Sales Navigator actions (`nv.*`) | | `quantity` | `number` | Number of seats. Each seat allows one connected LinkedIn account | | `billingPeriod` | `"month"` \| `"year"` | Billing cycle | ## subscription.setSeats Set the number of subscription seats. If you already have an active subscription, the quantity is updated immediately. If not, a Stripe checkout link is returned. New users can start with a 7-day free trial when they do not have any previous subscription history. The trial applies only when the initial checkout is eligible; otherwise checkout starts a regular paid subscription. ```bash curl -X POST https://api.linkedapi.io/admin/subscription.setSeats \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "quantity": 5, "billingPeriod": "year", "seatType": "plus" }' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `quantity` | `number` | Yes | Number of seats (1–1000) | | `billingPeriod` | `"month"` \| `"year"` | Yes | Billing cycle | | `seatType` | `"core"` \| `"plus"` | Yes | Seat tier | **Response (subscription exists):** ```json { "success": true, "result": { "status": "complete" } } ``` **Response (no subscription, checkout required):** ```json { "success": true, "result": { "status": "processing", "paymentLink": "https://checkout.stripe.com/..." } } ``` > **Note:** When reducing seats below the number of connected accounts, excess accounts will be automatically frozen. Changes are applied in Stripe immediately but may take a few seconds to reflect in the API due to webhook processing. ## Accounts Manage your connected LinkedIn accounts programmatically. For general Admin API information, see the [Admin overview](/docs/admin-overview). To connect a new account or reconnect an existing one, see [Connection Sessions](/docs/admin-connection-sessions). ## accounts.getAll Get all connected LinkedIn accounts and pending connection sessions. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.getAll \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` **Response:** ```json { "success": true, "result": { "accounts": [ { "id": "f9b4346a-...", "name": "John Doe", "url": "https://www.linkedin.com/in/johndoe/", "avatarUrl": "https://media.licdn.com/dms/image/...", "headline": "Founder at Example", "countryCode": "US", "identificationToken": "id_...", "status": "active", "connectedAt": "2026-02-20T11:11:51.732Z" }, { "id": "a91c20f8-...", "name": "Jane Doe", "url": "https://www.linkedin.com/in/janedoe/", "avatarUrl": null, "headline": null, "countryCode": "US", "identificationToken": "id_...", "status": "reconnection_required", "connectedAt": "2026-01-12T09:30:00.000Z", "reconnectionSessionId": "d90ac1f6-...", "reconnectionLink": "https://app.linkedapi.io/connection/d90ac1f6-..." } ], "pendingConnectionSessions": [ { "sessionId": "990eef7a-...", "status": "pending" } ] } } ``` **Account fields:** | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Account UUID | | `name` | `string` | LinkedIn account name | | `url` | `string` | Public LinkedIn profile URL for the connected account | | `avatarUrl` | `string \| null` | LinkedIn profile image URL, or `null` when it has not been parsed yet | | `headline` | `string \| null` | LinkedIn headline shown below the account name, or `null` when it has not been parsed yet | | `countryCode` | `string` | Country code selected during connection | | `identificationToken` | `string` | Token used in the `identification-token` header for Account API calls | | `status` | `string` | `active`, `frozen`, or `reconnection_required` | | `connectedAt` | `string` | ISO 8601 timestamp | | `reconnectionSessionId` | `string` | Present only when `status` is `reconnection_required`; session UUID for reconnecting the account | | `reconnectionLink` | `string` | Present only when `status` is `reconnection_required`; URL to open in a browser to reconnect the account | When an account is `reconnection_required`, `accounts.getAll` returns an active reconnection session. If no active reconnection session exists, one is created automatically before the response is returned. **Account statuses:** | Status | Description | |--------|-------------| | `active` | Account is connected and operational | | `frozen` | Account is frozen (subscription downgraded or expired) | | `reconnection_required` | LinkedIn session expired, account needs to be reconnected | ## accounts.reparseAccountInfo Refresh the stored profile information for a connected LinkedIn account. This starts a background workflow that opens the account's own LinkedIn session and reparses the account name, public profile URL, avatar URL, and headline. The refreshed values are returned by `accounts.getAll` after the workflow completes. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.reparseAccountInfo \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"accountId": "f9b4346a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | UUID of the account to refresh | **Response:** ```json { "success": true, "result": { "workflowId": "reparseAccountInfoWorkflow-..." } } ``` **Notes:** - The response confirms that the reparse workflow was started; it does not include the refreshed account object. - Call `accounts.getAll` after the workflow completes to read the updated `url`, `avatarUrl`, and `headline` fields. - `avatarUrl` and `headline` can remain `null` if LinkedIn does not render those values or the account session needs reconnection. ## accounts.disconnect Disconnect a LinkedIn account. This permanently removes the account, its browser profile, and proxy. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.disconnect \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"accountId": "f9b4346a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | UUID of the account to disconnect | **Response:** ```json { "success": true, "result": {} } ``` > **Warning:** This action is irreversible. The account must be reconnected from scratch. ## accounts.regenerateIdentificationToken Generate a new `identification-token` for an account. The old token becomes invalid immediately. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.regenerateIdentificationToken \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"accountId": "f9b4346a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | UUID of the account | **Response:** ```json { "success": true, "result": { "token": "id_new_token_here" } } ``` > **Important:** Update the `identification-token` header in all your Account API integrations immediately after regeneration. ## Connection Sessions A connection session is a short-lived link that opens the Linked API connection page, where the end user logs into LinkedIn. Use it to connect a new account or to reconnect one whose LinkedIn session has expired. For the accounts themselves, see [Accounts](/docs/admin-accounts). ## accounts.createConnectionSession Create a new connection session to connect a LinkedIn account. Returns a link that opens the connection page where the user logs into LinkedIn. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.createConnectionSession \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` **Body:** The body is optional. Send no body at all to get the default behavior, or pass `completion` to control what the user sees after a successful connection – see [Customizing the completion screen](#customizing-the-completion-screen). | Field | Type | Required | Description | |-------|------|----------|-------------| | `completion` | `object` | No | Post-connection behavior of the connection page | **Response:** ```json { "success": true, "result": { "sessionId": "990eef7a-...", "connectionLink": "https://app.linkedapi.io/connection/990eef7a-..." } } ``` | Field | Type | Description | |-------|------|-------------| | `sessionId` | `string` | Session UUID for tracking | | `connectionLink` | `string` | URL to open in a browser to complete the LinkedIn login | **Errors:** | Type | Description | |------|-------------| | `noAvailableSeats` | No available seats – all seats are occupied by active accounts or pending connection sessions | | `dailyConnectionAttemptsExceeded` | Too many connection attempts in the last 24 hours | | `invalidRequestPayload` | The `completion` object is malformed – the message names the offending field | > **Flow:** Create session → open `connectionLink` → user logs into LinkedIn → poll `accounts.getConnectionSession` until status is `success` → account appears in `accounts.getAll`. ## accounts.createReconnectionSession Create a new reconnection session for an account whose status is `reconnection_required`. If the account already has an active reconnection session, it is cancelled and replaced with a new one. If an in-progress reconnection session is applying a pending proxy change, Linked API returns that existing session instead so the proxy change state is preserved. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.createReconnectionSession \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"accountId": "a91c20f8-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | UUID of the account to reconnect | | `completion` | `object` | No | Post-connection behavior of the connection page – see [Customizing the completion screen](#customizing-the-completion-screen) below | **Response:** ```json { "success": true, "result": { "reconnectionSessionId": "d90ac1f6-...", "reconnectionLink": "https://app.linkedapi.io/connection/d90ac1f6-..." } } ``` | Field | Type | Description | |-------|------|-------------| | `reconnectionSessionId` | `string` | Session UUID for tracking | | `reconnectionLink` | `string` | URL to open in a browser to complete LinkedIn reconnection | **Errors:** | Type | Description | |------|-------------| | `invalidRequestPayload` | The account is not in `reconnection_required` status or cannot be reconnected | > **Flow:** Create reconnection session → open `reconnectionLink` → user logs into LinkedIn → poll `accounts.getConnectionSession` with `reconnectionSessionId` until status is `success`. ## accounts.getConnectionSession Check the status of a connection session. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.getConnectionSession \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"sessionId": "990eef7a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `sessionId` | `string` | Yes | Session UUID | **Response:** ```json { "success": true, "result": { "session": { "sessionId": "990eef7a-...", "status": "pending", "type": "initial" } } } ``` **Session statuses:** | Status | Description | |--------|-------------| | `pending` | Session created, waiting for user to open the link | | `preparing` | Browser is being provisioned | | `serving` | Browser is ready, waiting for user to connect | | `streaming` | User is connected and logging in | | `success` | Login completed, account is being created | | `expired` | Session timed out | | `error` | An error occurred | | `cancelled` | Session was cancelled | ## accounts.cancelConnectionSession Cancel an active connection session. ```bash curl -X POST https://api.linkedapi.io/admin/accounts.cancelConnectionSession \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"sessionId": "990eef7a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `sessionId` | `string` | Yes | Session UUID | **Response:** ```json { "success": true, "result": {} } ``` ## Customizing the completion screen After the user finishes logging into LinkedIn, the connection page shows a completion screen. The optional `completion` object accepted by `accounts.createConnectionSession` and `accounts.createReconnectionSession` controls what happens there – which is what you need when the connection page is embedded into your own product. Every field is optional, and omitting `completion` entirely keeps the default behavior. ```json { "completion": { "limits": { "mode": "ask" }, "button": { "text": "Back to Acme", "url": "https://app.acme.com/accounts" }, "successUrl": "https://app.acme.com/connected?session={SESSION_ID}", "failureUrl": "https://app.acme.com/failed?session={SESSION_ID}" } } ``` | Field | Type | Description | |-------|------|-------------| | `limits.mode` | `string` | How account limits are handled: `ask` (default), `defaults`, or `skip`. Ignored on reconnection, which never changes an account's limits | | `button.text` | `string` | Label of a custom button shown on the completion screen (1–40 characters) | | `button.url` | `string` | Where that button leads. `https` only, no placeholders | | `successUrl` | `string` | Redirect the user here after a successful connection, without any click | | `failureUrl` | `string` | Redirect the user here when the session ends in `error` or `expired` | **Limits modes:** | Mode | Behavior | |------|----------| | `ask` | Default. The user is shown the limits editor and chooses the values themselves | | `defaults` | Recommended limits are applied automatically, the editor is not shown | | `skip` | No limits are configured and the editor is not shown. The account runs without limits until you set them via [`limits.set`](/docs/admin-limits) | **Default button:** sessions created through this API do not show the "Continue to platform" button, because it leads to the Linked API dashboard, which your users have no access to. Instead, the completion screen tells the user the account is connected and the tab can be closed. To send the user somewhere, pass either `button` (they leave by clicking) or `successUrl` (they leave immediately). Passing both is rejected: with an instant redirect the button would never be visible. **Placeholders:** `successUrl` and `failureUrl` may contain placeholders that are substituted before the redirect. Values are URL-encoded. | Placeholder | Available in | Value | |-------------|--------------|-------| | `{SESSION_ID}` | `successUrl`, `failureUrl` | UUID of the connection session | | `{ACCOUNT_ID}` | `successUrl` | UUID of the account that was just connected | | `{ERROR_TYPE}` | `failureUrl` | Error type of the failed session, empty for `expired` | Any other placeholder, an unbalanced brace, or a non-`https` URL is rejected with `invalidRequestPayload` when the session is created – not later, when the user is already on the page. **When the redirect happens:** - `successUrl` fires after the limits step. With `mode: "ask"` that means after the user saves or skips the limits editor; with `defaults` or `skip` it fires as soon as the connection succeeds. - `failureUrl` covers the `error` and `expired` statuses. Sessions you cancel yourself through `accounts.cancelConnectionSession` do not redirect, and neither does an invalid or unknown session link, since there is no configuration to read for it. > **Do not rely on the redirect for fulfillment.** The user may close the tab or lose connectivity before it happens. Treat arriving on `successUrl` as a hint and confirm the real outcome with `accounts.getConnectionSession`. **Examples:** Fully embedded – no questions asked, the user is returned to your product in both outcomes: ```bash curl -X POST https://api.linkedapi.io/admin/accounts.createConnectionSession \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "completion": { "limits": { "mode": "defaults" }, "successUrl": "https://app.acme.com/connected?session={SESSION_ID}&account={ACCOUNT_ID}", "failureUrl": "https://app.acme.com/failed?session={SESSION_ID}&error={ERROR_TYPE}" } }' ``` Keep the limits editor, but finish with your own button instead of an automatic redirect: ```bash curl -X POST https://api.linkedapi.io/admin/accounts.createConnectionSession \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "completion": { "limits": { "mode": "ask" }, "button": { "text": "Back to Acme", "url": "https://app.acme.com/accounts" } } }' ``` ## Limits Rate limits control how many actions each LinkedIn account can perform per time period. For general Admin API information, see the [Admin overview](/docs/admin-overview). ## Limit categories | Category | Description | |----------|-------------| | `stPersonProfileViews` | Standard LinkedIn profile views | | `stCompanyPageViews` | Standard LinkedIn company page views | | `stConnectionRequests` | Connection requests sent | | `stMessages` | Messages sent | | `stSearchQueries` | LinkedIn search queries | | `stReactions` | Post reactions | | `stComments` | Post comments | | `stPosts` | Posts created | | `nvPersonProfileViews` | Sales Navigator profile views | | `nvCompanyPageViews` | Sales Navigator company page views | | `nvMessages` | Sales Navigator messages | **Periods:** `daily`, `weekly`, `monthly` ## limits.getDefaults Get the system default limits. These are applied when an account has no custom limits or after resetting to defaults. ```bash curl -X POST https://api.linkedapi.io/admin/limits.getDefaults \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` **Response:** ```json { "success": true, "result": { "limits": [ { "category": "stMessages", "period": "daily", "maxValue": 50, "isEnabled": true }, { "category": "stConnectionRequests", "period": "daily", "maxValue": 10, "isEnabled": true } ] } } ``` ## limits.get Get the current limits for a specific account. ```bash curl -X POST https://api.linkedapi.io/admin/limits.get \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"accountId": "f9b4346a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | Account UUID | **Response:** ```json { "success": true, "result": { "limits": [ { "category": "stMessages", "period": "daily", "maxValue": 25, "isEnabled": true } ] } } ``` ## limits.getUsage Get the current usage against limits for a specific account. ```bash curl -X POST https://api.linkedapi.io/admin/limits.getUsage \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"accountId": "f9b4346a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | Account UUID | **Response:** ```json { "success": true, "result": { "usage": [ { "category": "stMessages", "period": "daily", "maxValue": 50, "currentValue": 12, "isEnabled": true } ] } } ``` | Field | Type | Description | |-------|------|-------------| | `maxValue` | `number` | Maximum allowed actions | | `currentValue` | `number` | Actions performed in the current period | | `isEnabled` | `boolean` | Whether this limit is enforced | ## limits.set Set or update limits for an account. Only the specified limits are created or updated; other limits remain unchanged. ```bash curl -X POST https://api.linkedapi.io/admin/limits.set \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "accountId": "f9b4346a-...", "limits": [ { "category": "stMessages", "period": "daily", "maxValue": 25, "isEnabled": true }, { "category": "stConnectionRequests", "period": "weekly", "maxValue": 30 } ] }' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | Account UUID | | `limits` | `array` | Yes | Array of limit configurations | | `limits[].category` | `string` | Yes | Limit category (see table above) | | `limits[].period` | `string` | Yes | `daily`, `weekly`, or `monthly` | | `limits[].maxValue` | `number` | Yes | Maximum allowed actions (>= 0) | | `limits[].isEnabled` | `boolean` | No | Whether this limit is enforced (default: `true`) | **Response:** ```json { "success": true, "result": {} } ``` ## limits.delete Remove specific limits from an account. The account will fall back to default behavior for removed limits. ```bash curl -X POST https://api.linkedapi.io/admin/limits.delete \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "accountId": "f9b4346a-...", "limits": [ {"category": "stMessages", "period": "daily"}, {"category": "stConnectionRequests", "period": "weekly"} ] }' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | Account UUID | | `limits` | `array` | Yes | Array of limits to remove | | `limits[].category` | `string` | Yes | Limit category | | `limits[].period` | `string` | Yes | Limit period | **Response:** ```json { "success": true, "result": {} } ``` ## limits.resetToDefaults Reset all limits for an account to the system defaults. ```bash curl -X POST https://api.linkedapi.io/admin/limits.resetToDefaults \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{"accountId": "f9b4346a-..."}' ``` **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `accountId` | `string` | Yes | Account UUID | **Response:** ```json { "success": true, "result": {} } ``` ## Webhooks Register and manage the webhook that receives [workflow and account events](/docs/webhooks). For general Admin API information, see the [Admin overview](/docs/admin-overview). A client may hold one active webhook at a time. ## webhook.set Register the webhook. Enforces a maximum of one active webhook per client. There is no update endpoint – to change the destination, delete the webhook and set a new one. ```bash curl -X POST https://api.linkedapi.io/admin/webhook.set \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "url": "https://example.com/hooks/linkedapi", "payloadMode": "fat" }' ``` **Body:** - `url` – HTTPS endpoint that will receive deliveries. Required. - `payloadMode` – `fat` (default) or `thin`. See [payload modes](/docs/webhooks#payload-modes). Optional. **Response:** ```json { "success": true, "result": { "webhook": { "id": "whs-...", "url": "https://example.com/hooks/linkedapi", "payloadMode": "fat", "isActive": true, "createdAt": "2026-06-25T12:00:00.000Z" } } } ``` - `id` – webhook identifier, used by the other endpoints. - `url` – the registered destination. - `payloadMode` – current payload mode. - `isActive` – whether the webhook is active. - `createdAt` – ISO 8601 timestamp. > A `url` that is a bare IP address in a private or internal range is rejected. Deliveries are only ever sent to public addresses. ## webhook.get List the active webhook for this client. Returns an array with at most one entry. ```bash curl -X POST https://api.linkedapi.io/admin/webhook.get \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` **Response:** ```json { "success": true, "result": { "webhooks": [ { "id": "whs-...", "url": "https://example.com/hooks/linkedapi", "payloadMode": "fat", "isActive": true, "createdAt": "2026-06-25T12:00:00.000Z" } ] } } ``` ## webhook.setPayloadMode Switch the [payload mode](/docs/webhooks#payload-modes) between `fat` and `thin`. ```bash curl -X POST https://api.linkedapi.io/admin/webhook.setPayloadMode \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "id": "whs-...", "payloadMode": "thin" }' ``` **Body:** - `id` – webhook identifier. Required. - `payloadMode` – `fat` or `thin`. Required. ## webhook.delete Delete the webhook. This is a soft delete: the delivery history is preserved and any still-pending deliveries are dropped. Because only active webhooks count toward the one-per-client limit, you can register a fresh webhook afterwards. ```bash curl -X POST https://api.linkedapi.io/admin/webhook.delete \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "id": "whs-..." }' ``` **Body:** - `id` – webhook identifier. Required. ## webhook.deliveries Return the most recent deliveries (newest first) as a debug feed – useful for confirming an endpoint is receiving and acknowledging events. ```bash curl -X POST https://api.linkedapi.io/admin/webhook.deliveries \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` **Response:** ```json { "success": true, "result": { "deliveries": [ { "id": "whd-...", "eventType": "workflow.completed", "eventId": "workflow.completed:wf-...", "status": "success", "attempts": 1, "responseStatusCode": 200, "lastError": null, "createdAt": "2026-06-25T12:00:00.000Z", "updatedAt": "2026-06-25T12:00:01.000Z" } ] } } ``` - `id` – delivery identifier, passed to `webhook.replayDelivery`. - `eventType` – the event `type` that was delivered. - `eventId` – the envelope `id` of the delivered event. - `status` – `pending`, `delivering`, `success`, or `failed`. - `attempts` – delivery attempts made so far. - `responseStatusCode` – HTTP status your endpoint returned on the last attempt, or `null`. - `lastError` – error text from the last failed attempt, or `null`. - `createdAt` / `updatedAt` – ISO 8601 timestamps. ## webhook.replayDelivery Re-arm an already-settled (`success` or `failed`) delivery for redelivery. The same `eventId` is reused, so consumer-side deduplication still applies – this is a true redelivery, not a new event. ```bash curl -X POST https://api.linkedapi.io/admin/webhook.replayDelivery \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" \ -d '{ "deliveryId": "whd-..." }' ``` **Body:** - `deliveryId` – delivery identifier from `webhook.deliveries`. Required. ## webhook.sendTest Emit a synthetic `webhook.test` event to the active webhook through the normal delivery path. Its `data` carries a single `message` field. Use it to verify a freshly registered endpoint without waiting for a real event. ```bash curl -X POST https://api.linkedapi.io/admin/webhook.sendTest \ -H "Content-Type: application/json" \ -H "linked-api-token: linked_your_token_here" ``` ## st.sendMessage This action allows you to send a message to a person. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` or `threadId` parameter is provided. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** [st.manageConversation](/docs/action-st-manage-conversation). ## Parameters ```json { "actionType": "st.sendMessage", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1", "text": "Hi! I'd love to connect and discuss some ideas.", "then": { "actionType": "st.manageConversation", "operation": "archive" } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start unless `threadId` is provided, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to send a message to. - `threadId` (optional) – identifier of an existing conversation thread to reply into, as returned by [inbox polling](/docs/monitoring-inbox) or [conversation polling](/docs/working-with-conversations), or read from the address bar of an open conversation — the `` in `linkedin.com/messaging/thread/`. Provide either `personUrl` or `threadId`; if both are given, `threadId` takes precedence. - `text` – message text, must be up to **1900** characters. - `then` (optional) – object or array of child actions to be executed within this action. Only [st.manageConversation](/docs/action-st-manage-conversation) is allowed as a child, and it acts on the conversation this message was sent into, so its `threadId` must be omitted — only `operation` is provided. > 💡 Replying by `threadId` sends the message directly into a known conversation, which is convenient when reacting to an inbox event without resolving the person's profile URL first. ## Result options 1. **Successful message sending:** ```json { "actionType": "st.sendMessage", "label": "person1", "success": true, "data": { "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data.then` – included only when child actions are provided; contains the results of their execution. 2. **Failed message sending:** ```json { "actionType": "st.sendMessage", "label": "person1", "success": false, "error": { "type": "personNotFound", "message": "The provided URL is not an existing LinkedIn person." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. - `messagingNotAllowed` – sending a message to the person is not allowed. This could happen for several reasons: - You are not connected to the person. - LinkedIn has restricted your ability to send messages to the person, for example, due to reaching message limits or the person’s privacy settings. ## st.syncConversation This action allows you to sync a conversation so you can [start polling](/docs/working-with-conversations) it. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.syncConversation", "label": "sync1", "personUrl": "https://www.linkedin.com/in/person1", "days": 14 } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person whose conversation you want to synchronize. - `days` (optional) – how many days the conversation stays synchronized, from 1 to 90. Defaults to 30. ## Syncing period Synchronization is not permanent. The conversation is updated until the `syncUntil` moment returned by this action, counted from the moment the action starts running. After that moment the conversation stops being updated, but nothing is deleted: [polling](/docs/working-with-conversations) keeps returning the messages collected so far, together with the past `syncUntil` value. To resume updates, run this action again for the same person — the accumulated history is preserved, and a new period starts. A reply from the other person does not extend the period. ## Result options 1. **Successful syncing:** ```json { "actionType": "st.syncConversation", "label": "sync1", "success": true, "data": { "syncUntil": "2026-09-09T09:55:31.582Z" } } ``` - `label` – included only if specified in the action parameters. - `data.syncUntil` – moment when synchronization of this conversation stops. 2. **Failed syncing:** ```json { "actionType": "st.syncConversation", "label": "sync1", "success": false, "error": { "type": "personNotFound", "message": "The provided URL is not an existing LinkedIn person." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. ## st.syncInbox This action enables **whole-inbox monitoring** for your account so you can [poll the inbox](/docs/monitoring-inbox) for messages across every conversation. Unlike [`st.syncConversation`](/docs/action-st-sync-conversation), which watches a single person's conversation, `st.syncInbox` starts tracking **all** incoming threads in your inbox. Run it once per account. After it succeeds, new messages become available through the [inbox polling endpoint](/docs/monitoring-inbox) — no need to sync each conversation individually. > 💡 Only messages that arrive **after** inbox sync is enabled are captured. History predating the moment you enable it is not imported. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.syncInbox", "label": "enableInbox" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. This action takes no other parameters. ## Result options **Successful enabling:** ```json { "actionType": "st.syncInbox", "label": "enableInbox", "success": true } ``` - `label` – included only if specified in the action parameters. ## st.manageConversation This action allows you to manage a conversation thread — archive, star, or mute it — by its `threadId`. ## Constraints > ⏺️ **Root Start:** allowed, when `threadId` parameter is provided. > ⬆️ **Parent Actions:** [st.sendMessage](/docs/action-st-send-message). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.manageConversation", "label": "thread1", "threadId": "2-Zjhm...", "operation": "archive" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `threadId` (required for root start, forbidden for child start) – identifier of the conversation thread to manage, as returned by [inbox polling](/docs/monitoring-inbox) or [conversation polling](/docs/working-with-conversations). You can also read it from the address bar of an open conversation — it is the `` in `linkedin.com/messaging/thread/`. When run as a child of [st.sendMessage](/docs/action-st-send-message), the thread is inherited from the parent send and this parameter must be omitted. - `operation` – operation to apply to the thread. One of: - `archive` / `unarchive` – archive or unarchive the conversation. - `star` / `unstar` – star or unstar the conversation. - `mute` / `unmute` – mute or unmute the conversation. ## Result options 1. **Successful operation:** ```json { "actionType": "st.manageConversation", "label": "thread1", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **Failed operation:** ```json { "actionType": "st.manageConversation", "label": "thread1", "success": false, "error": { "type": "threadNotFound", "message": "The provided thread identifier does not match an existing conversation." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `threadNotFound` – provided `threadId` does not match an existing conversation. ## st.checkConnectionStatus This action allows you to check the connection status between your account and another person. > If you need to perform several connection status checks, use this action multiple times within an [array workflow](/docs/building-workflows). This approach is significantly faster than creating separate workflows for each check due to API optimizations. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.checkConnectionStatus", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1" } ``` - `personUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to check a connection status with. - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options 1. **Successful connection check:** ```json { "actionType": "st.checkConnectionStatus", "label": "person1", "success": true, "data": { "connectionStatus": "pending" } } ``` - `label` – included only if specified in the action parameters. - `data.connectionStatus` – enum with the following possible values: - `connected` – your account is connected with the person. - `notConnected` – your account is not connected with the person. - `pending` – your account has a pending connection request to the person. - `incoming` – the person has sent your account a connection request that is awaiting your response. 2. **Failed connection check:** ```json { "actionType": "st.checkConnectionStatus", "label": "person1", "success": false, "error": { "type": "personNotFound", "message": "The provided URL is not an existing LinkedIn person." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. ## st.sendConnectionRequest This action allows you to send a connection request to a person. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.sendConnectionRequest", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1", "note": "Hi, I’d like to connect with you to discuss potential collaboration.", "email": "example1@gmail.com" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to send a connection request to. - `note` (optional) – note to include with the connection request, must be up to **300** characters (LinkedIn allows **200** for free accounts and **300** for Premium). LinkedIn may also limit how many personalized invitation notes a free account can send. - `email` (optional) – email address required by some people for sending connection requests to them. If it is required and not provided, the connection request will fail. ## Result options 1. **Successful connection request sending:** ```json { "actionType": "st.sendConnectionRequest", "label": "person1", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **Failed connection request sending:** ```json { "actionType": "st.sendConnectionRequest", "label": "person1", "success": false, "error": { "type": "emailRequired", "message": "The person requires an email address to send a connection request." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. - `alreadyPending` – connection request to this person has already been sent and is still pending. - `alreadyConnected` – your LinkedIn account is already connected with this person. - `emailRequired` – person requires an email address to send a connection request. - `noteTooLong` – the note exceeds the character limit allowed by LinkedIn. Free accounts are limited to 200 characters, Premium accounts to 300. - `noteLimitExceeded` – your LinkedIn account has reached the limit for personalized invitation notes. Send the connection request without a note, or buy LinkedIn Premium to include one. - `requestNotAllowed` – LinkedIn has restricted sending a connection request to this person. This can happen for the following reasons: - The person has disabled connection requests in their privacy settings. - You recently sent and withdrew a connection request to this person. - You have reached LinkedIn's daily, weekly, or monthly limits for sending connection requests. ## st.withdrawConnectionRequest This action allows you to withdraw the connection request sent to a person. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.withdrawConnectionRequest", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1", "unfollow": true } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to withdraw the connection request from. - `unfollow` (optional) – boolean indicating whether you want to unfollow the person when withdrawing the request. The default value is `true`. ## Result options 1. **Successful connection request withdrawal:** ```json { "actionType": "st.withdrawConnectionRequest", "success": true, "label": "person1" } ``` - `label` – included only if specified in the action parameters. 2. **Failed connection request withdrawal:** ```json { "actionType": "st.withdrawConnectionRequest", "label": "person1", "success": false, "error": { "type": "notPending", "message": "There is no pending connection request to this person." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. - `notPending` – there is no pending connection request to this person. ## st.acceptInvitation This action accepts an incoming invitation from the LinkedIn invitation manager. It supports connection requests, company-follow invitations, and newsletter-subscription invitations. ## Constraints > ⏺️ **Root Start:** allowed when `invitationType` and its matching target URL are provided. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.acceptInvitation", "label": "invitation1", "invitationType": "connect", "personUrl": "https://www.linkedin.com/in/person1" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `invitationType` (required) – one of: - `connect` – requires `personUrl`. - `companyFollow` – requires `companyUrl`. - `newsletterSubscribe` – requires `newsletterUrl`. - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn person URL. Allowed only for `connect`. - `companyUrl` – LinkedIn company page URL. Allowed only for `companyFollow`. - `newsletterUrl` – LinkedIn newsletter URL. Allowed only for `newsletterSubscribe`. Provide exactly one target URL: the one required by `invitationType`. ## Result options 1. **Successful invitation acceptance:** ```json { "actionType": "st.acceptInvitation", "label": "invitation1", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **No matching invitation:** ```json { "actionType": "st.acceptInvitation", "label": "invitation1", "success": false, "error": { "type": "noPendingRequest", "message": "There is no matching pending incoming invitation to accept." } } ``` - `error.type` is `noPendingRequest` when no invitation matches the supplied type and target URL. ## st.ignoreInvitation This action ignores an incoming invitation from the LinkedIn invitation manager. It supports connection requests, company-follow invitations, and newsletter-subscription invitations. ## Constraints > ⏺️ **Root Start:** allowed when `invitationType` and its matching target URL are provided. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.ignoreInvitation", "label": "invitation1", "invitationType": "connect", "personUrl": "https://www.linkedin.com/in/person1" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `invitationType` (required) – one of: - `connect` – requires `personUrl`. - `companyFollow` – requires `companyUrl`. - `newsletterSubscribe` – requires `newsletterUrl`. - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn person URL. Allowed only for `connect`. - `companyUrl` – LinkedIn company page URL. Allowed only for `companyFollow`. - `newsletterUrl` – LinkedIn newsletter URL. Allowed only for `newsletterSubscribe`. Provide exactly one target URL: the one required by `invitationType`. ## Result options 1. **Successful invitation dismissal:** ```json { "actionType": "st.ignoreInvitation", "label": "invitation1", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **No matching invitation:** ```json { "actionType": "st.ignoreInvitation", "label": "invitation1", "success": false, "error": { "type": "noPendingRequest", "message": "There is no matching pending incoming invitation to ignore." } } ``` - `error.type` is `noPendingRequest` when no invitation matches the supplied type and target URL. ## st.retrievePendingRequests This action allows you to retrieve pending connection requests sent from your account. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePendingRequests", "label": "listPending1" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options This action always completes successfully. Even if you have no pending connection requests, it returns an empty array. ```json { "actionType": "st.retrievePendingRequests", "label": "listPending1", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "sentTime": "2 weeks ago" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CTO at Tech Solutions", "sentTime": "1 month ago" }, { "name": "Carlos Santos", "publicUrl": "https://www.linkedin.com/in/carlossantos", "headline": "Product Manager at Startup Hub", "sentTime": "3 months ago" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of pending connection requests. - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `sentTime` – time since the connection request was sent (e.g., "1 month ago"). ## st.retrieveInvitations This action retrieves all incoming invitations currently shown in the LinkedIn invitation manager. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrieveInvitations", "label": "invitations1" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `then` is not supported. ## Result options This action always completes successfully. If there are no incoming invitations, it returns an empty array. ```json { "actionType": "st.retrieveInvitations", "label": "invitations1", "success": true, "data": [ { "invitationType": "connect", "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "note": "I would like to connect." }, { "invitationType": "companyFollow", "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "companyUrl": "https://www.linkedin.com/company/example-company", "companyName": "Example Company" }, { "invitationType": "newsletterSubscribe", "name": "Carlos Santos", "publicUrl": "https://www.linkedin.com/in/carlossantos", "newsletterUrl": "https://www.linkedin.com/newsletters/example-1234567890", "newsletterName": "Example Newsletter" } ] } ``` Every item contains: - `invitationType` – `connect`, `companyFollow`, or `newsletterSubscribe`. - `name` – name of the person who sent the invitation. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of that person. Type-specific fields: - `connect`: `headline` and `note` (each can be `null`). - `companyFollow`: `companyUrl` and `companyName` (`companyName` can be `null`). - `newsletterSubscribe`: `newsletterUrl` and `newsletterName` (`newsletterName` can be `null`). ## st.retrieveConnections This action allows you to retrieve your connections and perform additional person-related actions if needed. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrieveConnections", "label": "listConnections1", "limit": 100, "since": "2022-01-01", "filter": { "firstName": "John", "lastName": "Doe", "position": "CEO", "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "currentCompanies": ["Tech Solutions", "Innovatech"], "previousCompanies": ["FutureCorp"], "schools": ["Harvard University", "MIT"] } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` (optional) – number of connections to return. Returns all connections if not provided. - `since` (optional) – ISO date string that filters connections to only include those made on or after the specified date. Only works when `filter` is not provided. - `filter` (optional) – object that specifies filtering criteria for people. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of person. - `lastName` (optional) – last name of person. - `position` (optional) – job position of person. - `locations` (optional) – array of free-form strings representing locations. Matches if person is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if person works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `currentCompanies` (optional) – array of company names. Matches if person currently works at any of the listed companies. - `previousCompanies` (optional) – array of company names. Matches if person previously worked at any of the listed companies. - `schools` (optional) – array of institution names. Matches if person currently attends or previously attended any of the listed institutions. ## Result options 1. **Successful retrieval:** ```json { "actionType": "st.retrieveConnections", "label": "listConnections1", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "connectedAt": "2025-01-02T00:00:00Z" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CEO at Tech Solutions", "connectedAt": "2024-11-22T00:00:00Z" }, { "name": "Carlos Santos", "publicUrl": "https://www.linkedin.com/in/carlossantos", "headline": "Project Manager at Startup Hub", "connectedAt": "2022-07-06T00:00:00Z" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of your connections with results of child actions execution. - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `connectedAt` – date when connection was established. Returned only when `filter` is not provided. 2. **Successful retrieval (with filter):** ```json { "actionType": "st.retrieveConnections", "label": "listConnections1", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "location": "Lisbon, Portugal" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CEO at Tech Solutions", "location": "San Francisco Bay Area" }, { "name": "Carlos Santos", "publicUrl": "https://www.linkedin.com/in/carlossantos", "headline": "Project Manager at Startup Hub", "location": "Madrid" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of your connections with results of child actions execution. - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. Returned only when `filter` is provided. 3. **Failed retrieval:** ```json { "actionType": "st.retrieveConnections", "label": "listConnections1", "success": false, "error": { "type": "retrievingNotAllowed", "message": "LinkedIn has blocked performing the retrieval." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## st.removeConnection This action allows you to remove a person from your connections. > If you need to remove several people from your connections, use this action multiple times within an [array workflow](/docs/building-workflows). This approach is significantly faster than creating separate workflows for each person due to API optimizations. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.removeConnection", "label": "remove1", "personUrl": "https://www.linkedin.com/in/person1" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to remove from your connections. ## Result options 1. **Successful connection removal:** ```json { "actionType": "st.removeConnection", "label": "remove1", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **Failed connection removal:** ```json { "actionType": "st.removeConnection", "label": "remove1", "success": false, "error": { "type": "connectionNotFound", "message": "This person is not in your connections." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `connectionNotFound` – person is not in your connections. ## st.syncNetwork This action enables **network monitoring** for your account so you can [poll the network](/docs/monitoring-network) for connection events as they happen. Instead of repeatedly retrieving connections and pending requests and comparing the results, `st.syncNetwork` starts watching the account's network in the background. Run it once per account. After it succeeds, connection events become available through the [network polling endpoint](/docs/monitoring-network) and the network [webhook events](/docs/webhooks). > 💡 Only changes that happen **after** network sync is enabled are captured. The current state predating the moment you enable it is used as a baseline and does not produce events. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.syncNetwork", "label": "enableNetwork" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. This action takes no other parameters. ## Result options **Successful enabling:** ```json { "actionType": "st.syncNetwork", "label": "enableNetwork", "success": true } ``` - `label` – included only if specified in the action parameters. ## st.searchCompanies This action allows you to search for companies applying various filtering criteria. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** [st.doForCompanies](/docs/action-st-do-for-companies). ## Parameters ```json { "actionType": "st.searchCompanies", "label": "techIncSearch1", "term": "Tech Inc", "limit": 2, "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Robotics Engineering"] }, "customSearchUrl": "https://www.linkedin.com/search/results/companies/?companySize=%5B%22B%22%5D&keywords=Linked%20API", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `term` (optional) – keyword or phrase to search. Either `term` or `customSearchUrl` must be provided; a request with neither (for example, with `filter` only) is rejected. - `limit` (optional) – number of search results to return. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for companies. When multiple filter fields are specified, they are combined using `AND` logic. - `sizes` (optional) – array of enums representing employee count ranges. Matches if company’s size falls within any of the listed ranges. Options: - `1-10`. - `11-50`. - `51-200`. - `201-500`. - `501-1000`. - `1001-5000`. - `5001-10000`. - `10001+`. - `locations` (optional) – array of free-form strings representing locations. Matches if company is headquartered in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if company operates in any of the listed industries. Takes specific values available in the LinkedIn interface. - `customSearchUrl` (optional) – URL copied from LinkedIn search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in the LinkedIn interface. Either `term` or `customSearchUrl` must be provided. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful search:** ```json { "actionType": "st.searchCompanies", "label": "techIncSearch1", "success": true, "data": [ { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "industry": "Information Technology", "location": "California", "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQE/company-logo_100_100/0/1700000000000", "then": { ... } }, { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "industry": "Software Development", "location": "Mountain View", "logoUrl": null, "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of search outputs with results of child actions execution. - `name` – name of the company. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `location` – free-form string representing the company headquarters location. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. - `then` – results of child actions execution. 2. **Failed search:** ```json { "actionType": "st.searchCompanies", "label": "techIncSearch1", "success": false, "error": { "type": "searchingNotAllowed", "message": "LinkedIn has blocked performing the search." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## st.searchPeople This action allows you to search for people applying various filtering criteria. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** [st.doForPeople](/docs/action-st-do-for-people). ## Parameters ```json { "actionType": "st.searchPeople", "label": "johnDoeSearch1", "term": "John Doe", "limit": 2, "filter": { "firstName": "John", "lastName": "Doe", "position": "CEO", "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "currentCompanies": ["Tech Solutions", "Innovatech"], "previousCompanies": ["FutureCorp"], "schools": ["Harvard University", "MIT"] }, "customSearchUrl": "https://www.linkedin.com/search/results/people/?geoUrn=%5B%22103644278%22%5D&keywords=Bill%20Gates", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `term` (optional) – keyword or phrase to search. Either `term` or `customSearchUrl` must be provided; a request with neither (for example, with `filter` only) is rejected. - `limit` (optional) – number of search results to return. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for people. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of person. - `lastName` (optional) – last name of person. - `position` (optional) – job position of person. - `locations` (optional) – array of free-form strings representing locations. Matches if person is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if person works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `currentCompanies` (optional) – array of company names. Matches if person currently works at any of the listed companies. - `previousCompanies` (optional) – array of company names. Matches if person previously worked at any of the listed companies. - `schools` (optional) – array of institution names. Matches if person currently attends or previously attended any of the listed institutions. - `customSearchUrl` (optional) – URL copied from LinkedIn search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in the LinkedIn interface. Either `term` or `customSearchUrl` must be provided. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful search:** ```json { "actionType": "st.searchPeople", "label": "johnDoeSearch1", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder & CEO at Example Company", "location": "London", "avatarUrl": "https://media.licdn.com/dms/image/v2/D5603AQE/profile-displayphoto-shrink_100_100/0/1700000000000", "then": { ... } }, { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johnyd", "headline": "Product Manager at Semsoft", "location": "New York", "avatarUrl": null, "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of search outputs with results of child actions execution. - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. - `then` – results of child actions execution. 2. **Failed search:** ```json { "actionType": "st.searchPeople", "label": "johnDoeSearch1", "success": false, "error": { "type": "searchingNotAllowed", "message": "LinkedIn has blocked performing the search." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## st.searchJobs This action allows you to search for jobs applying various filtering criteria. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** [st.doForJobs](/docs/action-st-do-for-jobs). ## Parameters ```json { "actionType": "st.searchJobs", "label": "productManagerSearch1", "term": "product manager", "limit": 2, "filter": { "location": "San Francisco, California, United States", "datePosted": "pastWeek", "experienceLevels": ["midSeniorLevel", "director"], "employmentTypes": ["fullTime"], "workplaceTypes": ["remote", "hybrid"], "companies": ["Example Company"], "industries": ["Software Development"], "jobFunctions": ["Product Management"], "easyApply": true, "hasVerifications": true, "under10Applicants": false, "inYourNetwork": false, "fairChanceEmployer": false }, "customSearchUrl": "https://www.linkedin.com/jobs/search/?keywords=product%20manager", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `term` (optional) – keyword or phrase to search. If omitted, Linked API starts from a broad jobs search and applies the provided filters. - `limit` (optional) – number of search results to return. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for jobs. When multiple filter fields are specified, they are combined using `AND` logic. - `location` (optional) – free-form location string selected through LinkedIn jobs location search. - `datePosted` (optional) – enum with possible values: - `anyTime`. - `past24Hours`. - `pastWeek`. - `pastMonth`. - `experienceLevels` (optional) – array of enums with possible values: - `internship`. - `entryLevel`. - `associate`. - `midSeniorLevel`. - `director`. - `executive`. - `employmentTypes` (optional) – array of enums with possible values: - `fullTime`. - `partTime`. - `contract`. - `temporary`. - `volunteer`. - `internship`. - `other`. - `workplaceTypes` (optional) – array of enums with possible values: - `onSite`. - `remote`. - `hybrid`. - `companies` (optional) – array of company names selected through LinkedIn jobs company search. - `industries` (optional) – array of industry names selected through LinkedIn jobs industry search. - `jobFunctions` (optional) – array of job function names selected through LinkedIn jobs job-function search. - `easyApply` (optional) – when `true`, filters to jobs with Easy Apply. - `hasVerifications` (optional) – when `true`, filters to jobs with verification signals. - `under10Applicants` (optional) – when `true`, filters to jobs with fewer than 10 applicants. - `inYourNetwork` (optional) – when `true`, filters to jobs from your network. - `fairChanceEmployer` (optional) – when `true`, filters to fair chance employer jobs. - `customSearchUrl` (optional) – URL copied from a LinkedIn jobs search page after configuring desired filters. When specified, it overrides `term` and `filter`. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful search:** ```json { "actionType": "st.searchJobs", "label": "productManagerSearch1", "success": true, "data": [ { "jobId": "4416248954", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "title": "Senior Product Manager", "companyName": "Example Company", "location": "San Francisco, CA", "workplaceType": "remote", "salary": { "currency": "usd", "minAmount": 140000, "maxAmount": 180000, "period": "yearly" }, "easyApply": true, "isPromoted": false, "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of search outputs with results of child actions execution. - `jobId` – LinkedIn job identifier, when it can be extracted. - `jobUrl` – LinkedIn job URL, when `jobId` can be extracted. - `title` – job title. - `companyName` – company name, if available. - `location` – free-form job location, if available. - `workplaceType` – workplace type label as shown by LinkedIn (such as `remote`, `hybrid`, or `on-site`), if available. Not one of the `workplaceTypes` filter values. - `salary` – parsed salary range, if LinkedIn shows one. - `currency` – lowercase currency code, such as `usd`, `eur`, or `gbp`. - `minAmount` – minimum amount in the parsed range. - `maxAmount` – maximum amount in the parsed range. - `period` – salary period. Possible values are `yearly`, `monthly`, and `hourly`. - `easyApply` – boolean indicating whether the search card mentions Easy Apply. - `isPromoted` – boolean indicating whether the search card is promoted. - `then` – results of child actions execution. 2. **Failed search:** ```json { "actionType": "st.searchJobs", "label": "productManagerSearch1", "success": false, "error": { "type": "searchingNotAllowed", "message": "LinkedIn has blocked performing the search." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## st.openCompanyPage This action allows you to open a company page to retrieve its basic information and perform additional company-related actions if needed. ## Constraints > ⏺️ **Root Start:** allowed, when `companyUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.doForCompanies](/docs/action-st-do-for-companies), [nv.openCompanyPage](/docs/action-nv-open-company-page). > ⬇️ **Child Actions:** [st.retrieveCompanyEmployees](/docs/action-st-retrieve-company-employees), [st.retrieveCompanyDMs](/docs/action-st-retrieve-company-dms), [st.retrieveCompanyPosts](/docs/action-st-retrieve-company-posts), [nv.openCompanyPage](/docs/action-nv-open-company-page). ## Parameters ```json { "actionType": "st.openCompanyPage", "label": "company1", "companyUrl": "https://www.linkedin.com/company/company1", "basicInfo": true, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `companyUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `basicInfo` (optional, default: `false`) – when set to `true`, the action includes basic company information in the results. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful page opening with `basicInfo` set to `true`:** ```json { "actionType": "st.openCompanyPage", "label": "company1", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/company1", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "Cupertino, California", "headquarters": "US", "industry": "Information Technology", "specialties": "Cloud Computing, AI, Software Development", "website": "https://techcorp.com", "employeesCount": 500, "yearFounded": 2019, "ventureFinancing": true, "jobsCount": 12, "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQE/company-logo_200_200/0/1700000000000", "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data` – basic information about the company and results of child actions execution. - `name` – name of the company. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `description` – description of the company. - `location` – free-form string representing the company headquarters location. - `headquarters` – two-character country code (e.g., "US", "UK") representing headquarters location. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `specialties` – comma-separated list of company's specialties. - `website` – company's official website URL. - `employeesCount` – total number of employees associated with the company. - `yearFounded` – year the company was established, if available. - `ventureFinancing` – boolean indicating whether the company has received venture financing. - `jobsCount` – number of current job vacancies posted by the company. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. - `then` – results of child actions execution. 2. **Successful page opening with `basicInfo` set to `false`:** ```json { "actionType": "st.openCompanyPage", "label": "company1", "success": true, "data": { "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data.then` – results of child actions execution. 3. **Failed page opening:** ```json { "actionType": "st.openCompanyPage", "label": "company1", "success": false, "error": { "type": "companyNotFound", "message": "The provided URL is not an existing LinkedIn company." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `companyNotFound` – provided URL is not an existing LinkedIn company. ## st.retrieveCompanyEmployees This action allows you to retrieve company employees and perform additional person-related actions if needed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openCompanyPage](/docs/action-st-open-company-page). > ⬇️ **Child Actions:** [st.doForPeople](/docs/action-st-do-for-people). ## Parameters ```json { "actionType": "st.retrieveCompanyEmployees", "label": "company1Employees", "limit": 300, "filter": { "firstName": "John", "lastName": "Doe", "position": "Manager", "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "schools": ["Harvard University", "MIT"] }, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` – (optional) maximum number of employees to retrieve. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for employees. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of employee. - `lastName` (optional) – last name of employee. - `position` (optional) – job position of employee. - `locations` (optional) – array of free-form strings representing locations. Matches if employee is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if employee works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `schools` (optional) – array of institution names. Matches if employee currently attends or previously attended any of the listed institutions. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful retrieval:** ```json { "actionType": "st.retrieveCompanyEmployees", "label": "company1Employees", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Manager at TechCorp", "location": "New York, USA", "then": { ... } }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of company employees with results of child actions execution. - `name` – full name of the employee. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the employee. - `headline` – headline of the employee. - `location` – free-form string indicating the employee's location. 2. **Failed retrieval:** ```json { "actionType": "st.retrieveCompanyEmployees", "label": "company1Employees", "success": false, "error": { "type": "retrievingNotAllowed", "message": "LinkedIn has blocked performing the retrieval." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## st.retrieveCompanyDMs This action allows you to retrieve company decision makers and perform additional person-related actions if needed. > Decision makers are ranked by seniority, starting with the highest-level positions. For example, founders and C-level executives are returned first, followed by VPs, directors, and so on. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openCompanyPage](/docs/action-st-open-company-page). > ⬇️ **Child Actions:** [st.doForPeople](/docs/action-st-do-for-people). ## Parameters ```json { "actionType": "st.retrieveCompanyDMs", "label": "company1DMs", "limit": 3, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` (optional) – number of decision makers to retrieve. Defaults to **20**, with a maximum value of **20**. If a company has fewer decision makers than specified, only the available ones will be returned. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful retrieval:** ```json { "actionType": "st.retrieveCompanyDMs", "label": "company1DMs", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder of TechCorp", "location": "New York, USA", "countryCode": "US", "then": { ... } }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CEO at TechCorp", "location": "San Francisco, USA", "countryCode": "US", "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of decision makers with results of child actions execution. - `name` – full name of the decision-maker. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the decision-maker. - `headline` – headline of the decision-maker. - `location` – free-form string indicating the decision-maker's location. - `countryCode` – two-character code of the decision-maker's country. - `then` – results of child actions execution. 2. **Failed retrieval:** ```json { "actionType": "st.retrieveCompanyDMs", "label": "company1DMs", "success": false, "error": { "type": "retrievingNotAllowed", "message": "LinkedIn has blocked performing the retrieval." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## st.retrieveCompanyPosts This action allows you to retrieve posts published by a company and perform additional post-related actions if needed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openCompanyPage](/docs/action-st-open-company-page). > ⬇️ **Child Actions:** [st.doForPosts](/docs/action-st-do-for-posts). ## Parameters ```json { "actionType": "st.retrieveCompanyPosts", "label": "company1Posts", "limit": 10, "since": "2023-01-01T00:00:00Z", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` (optional) – number of posts to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter posts published after the specified time. - `then` (optional) – object or array of child actions to be executed within this action. **Note**: both `limit` and `since` conditions are applied simultaneously. For example: - If `limit` is 10, but there are more than 10 posts since the specified timestamp, only 10 posts will be returned. - If `limit` is 15, but only 5 posts exist after the specified timestamp, only those 5 posts will be returned. ## Result options This action always completes successfully. Even if the company has no posts, it returns an empty array. ```json { "actionType": "st.retrieveCompanyPosts", "label": "company1Posts", "success": true, "data": [ { "url": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789", "activityUrn": "urn:li:activity:1234567890123456789", "time": "2023-01-02T12:30:00Z", "type": "original", "author": { "type": "company", "name": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company" }, "reposter": null, "text": "Check out our latest product launch!", "repostText": null, "hashtags": ["product", "launch"], "mentions": [], "externalLinks": ["https://example.com/launch"], "images": [ "https://static.linkedin.com/image1.jpg", "https://static.linkedin.com/image2.jpg" ], "documentSlides": [], "hasVideo": false, "videoThumbnail": null, "hasPoll": false, "reactionsCount": 27, "commentsCount": 8, "repostsCount": 2, "then": { ... } }, { "url": "https://www.linkedin.com/feed/update/urn:li:activity:2345678901234567890", "activityUrn": "urn:li:activity:2345678901234567890", "time": "2023-01-01T09:15:00Z", "type": "repost", "author": { "type": "person", "name": "Example Author", "profileUrl": "https://www.linkedin.com/in/example-author", "headline": null }, "reposter": { "type": "company", "name": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company" }, "text": "Original post content about the webinar.", "repostText": "Thank you to everyone who joined our webinar.", "hashtags": [], "mentions": [], "externalLinks": [], "images": [], "documentSlides": [], "hasVideo": true, "videoThumbnail": "https://media.licdn.com/dms/image/video-cover.jpg", "hasPoll": false, "reactionsCount": 6, "commentsCount": 0, "repostsCount": 1, "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of company posts with results of child actions execution. - `url` – URL of the post. - `activityUrn` – LinkedIn activity or UGC URN of the post, if available. - `time` – timestamp when the post was published. - `type` – type of the post. Enum with possible values: - `original` – for original posts. - `repost` – for reposts. - `author` – original content creator. Can be `null` if the actor cannot be parsed. - For person authors: `type`, `name`, `profileUrl`, `headline`. - For company authors: `type`, `name`, `companyUrl`. - `reposter` – person or company that reshared the post. Non-null only when `type` is `repost`. - For person reposters: `type`, `name`, `profileUrl`, `headline`. - For company reposters: `type`, `name`, `companyUrl`. - `text` – original author's post text, if available. - `repostText` – text added by the reposter on a repost with comment, if available. - `hashtags` – array of hashtags found in the post text, without leading `#`. - `mentions` – array of person and company profile URLs found in the post text. - `externalLinks` – array of outbound URLs found in the post text. - `images` – array of up to 3 preview image URLs, if available. - `documentSlides` – array of carousel or document slide image URLs, if available. - `hasVideo` – boolean indicating if the post contains a video. - `videoThumbnail` – URL of the video thumbnail, if available. - `hasPoll` – boolean indicating if the post contains a poll. - `reactionsCount` – number of reactions on the post. - `commentsCount` – number of comments on the post. - `repostsCount` – number of reposts on the post. - `then` – results of child actions execution. ## st.openPersonPage This action allows you to open a person page to retrieve their basic information and perform additional person-related actions if needed. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.doForPeople](/docs/action-st-do-for-people), [nv.openPersonPage](/docs/action-nv-open-person-page). > ⬇️ **Child Actions:** [st.sendMessage](/docs/action-st-send-message), [st.syncConversation](/docs/action-st-sync-conversation), [nv.syncConversation](/docs/action-nv-sync-conversation), [st.checkConnectionStatus](/docs/action-st-check-connection-status), [st.sendConnectionRequest](/docs/action-st-send-connection-request), [st.withdrawConnectionRequest](/docs/action-st-withdraw-connection-request), [st.removeConnection](/docs/action-st-remove-connection), [st.retrievePersonExperience](/docs/action-st-retrieve-person-experience), [st.retrievePersonEducation](/docs/action-st-retrieve-person-education), [st.retrievePersonSkills](/docs/action-st-retrieve-person-skills), [st.retrievePersonLanguages](/docs/action-st-retrieve-person-languages), [st.retrievePersonPosts](/docs/action-st-retrieve-person-posts), [st.retrievePersonComments](/docs/action-st-retrieve-person-comments), [st.retrievePersonReactions](/docs/action-st-retrieve-person-reactions), [nv.openPersonPage](/docs/action-nv-open-person-page). ## Parameters ```json { "actionType": "st.openPersonPage", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1", "basicInfo": true, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `basicInfo` (optional, default: `false`) – when set to `true`, the action includes basic person information in the results. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful page opening with `basicInfo` set to `true`:** ```json { "actionType": "st.openPersonPage", "label": "person1", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/person1", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "countryCode": "US", "position": "Software Engineer", "about": "Software Engineer with 8+ years of experience building scalable web applications.", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678", "followersCount": 37, "avatarUrl": "https://media.licdn.com/dms/image/v2/D5603AQE/profile-displayphoto-shrink_100_100/0/1700000000000", "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data` – basic information about the person and results of child actions execution. - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. - `countryCode` – two-character code of the person's country. - `position` – current job position of the person. - `about` – "About" section text from the person's profile. - `companyName` – name of the person's current company. - `companyHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person's current company. - `followersCount` – number of followers the person has. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. - `then` – results of child actions execution. 2. **Successful page opening with `basicInfo` set to `false`:** ```json { "actionType": "st.openPersonPage", "label": "person1", "success": true, "data": { "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data.then` – results of child actions execution. 3. **Failed page opening:** ```json { "actionType": "st.openPersonPage", "label": "person1", "success": false, "error": { "type": "personNotFound", "message": "The provided URL is not an existing LinkedIn person." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. ## st.retrievePersonExperience This action allows you to retrieve information about a person's experience. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePersonExperience", "label": "person1Experience" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options This action always completes successfully. Even if the person has no experience records, it returns an empty array. ```json { "actionType": "st.retrievePersonExperience", "label": "person1Experience", "success": true, "data": [ { "position": "Software Engineer", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678", "employmentType": "fullTime", "locationType": "onSite", "description": "Developing innovative software solutions.", "duration": 24, "startTime": "2021-01-01T00:00:00Z", "endTime": null, "location": "San Francisco, USA" }, { "position": "Junior Developer", "companyName": "CodeBase Inc.", "companyHashedUrl": "https://www.linkedin.com/company/87654321", "employmentType": "internship", "locationType": "remote", "description": "Worked on front-end development tasks.", "duration": 12, "startTime": "2020-01-01T00:00:00Z", "endTime": "2020-12-31T00:00:00Z", "location": null } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of person experience records. - `position` – job position held by the person. - `companyName` – name of the company where the person worked. - `companyHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company where the person worked. - `employmentType` – type of employment. Enum with the following values: - `fullTime` – full-time employment. - `partTime` – part-time employment. - `selfEmployed` – self-employed work. - `freelance` – freelance work. - `contract` – contract-based employment. - `internship` – internship position. - `apprenticeship` – apprenticeship program. - `seasonal` – seasonal employment. - `locationType` – type of location. Enum with the following values: - `remote` – position is fully remote. - `onSite` – position requires on-site work. - `hybrid` – position is a mix of remote and on-site work. - `description` – description of the job or responsibilities. - `duration` – number of months the person worked in the position. - `startTime` – timestamp of **the first day of the month** when the person started the position. - `endTime` – timestamp **of the last day of the month** when the person ended the position. Returns `null` if the person is still working in this position. - `location` – free-form string indicating the location of the position. ## st.retrievePersonEducation This action allows you to retrieve information about a person's education. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePersonEducation", "label": "person1Education" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options This action always completes successfully. Even if the person has no education records, it returns an empty array. ```json { "actionType": "st.retrievePersonEducation", "label": "person1Education", "success": true, "data": [ { "schoolName": "Harvard University", "schoolHashedUrl": "https://www.linkedin.com/company/12345678", "details": "Master of Science in Computer Science, Artificial Intelligence" }, { "schoolName": "MIT", "schoolHashedUrl": "https://www.linkedin.com/company/87654321", "details": "Bachelor of Science in Electrical Engineering and Computer Science" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of person education records. - `schoolName` – name of the institution. - `schoolHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the institution. - `details` – information about the person's education, such as the degree, major, field of study, and other related details. ## st.retrievePersonSkills This action allows you to retrieve information about a person's skills. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePersonSkills", "label": "person1Skills" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options This action always completes successfully. Even if the person has no mentioned skills, it returns an empty array. ```json { "actionType": "st.retrievePersonSkills", "label": "person1Skills", "success": true, "data": [ { "name": "Computer Science" }, { "name": "Java" }, { "name": "Project Management" }, { "name": "Marketing" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of person skills. - `name` – name of the skill. ## st.retrievePersonLanguages This action allows you to retrieve information about a person's languages. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePersonLanguages", "label": "person1Languages" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options This action always completes successfully. Even if the person has no mentioned languages, it returns an empty array. ```json { "actionType": "st.retrievePersonLanguages", "label": "person1Languages", "success": true, "data": [ { "name": "English", "proficiency": "nativeOrBilingual" }, { "name": "Spanish", "proficiency": "professionalWorking" }, { "name": "French", "proficiency": "limitedWorking" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of person languages. - `name` – name of the language. - `proficiency` – proficiency level in the language. Enum with the following possible values: - `elementary` – basic understanding. - `limitedWorking` – limited ability for routine tasks. - `professionalWorking` – effective in professional settings. - `fullProfessional` – near-native proficiency. - `nativeOrBilingual` – fluent, like a native speaker. ## st.retrievePersonPosts This action allows you to retrieve posts published by a person and perform additional post-related actions if needed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** [st.doForPosts](/docs/action-st-do-for-posts). ## Parameters ```json { "actionType": "st.retrievePersonPosts", "label": "person1Posts", "limit": 10, "since": "2023-01-01T00:00:00Z", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` (optional) – number of posts to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter posts published after the specified time. - `then` (optional) – object or array of child actions to be executed within this action. **Note**: both `limit` and `since` conditions are applied simultaneously. For example: - If `limit` is 10, but there are more than 10 posts since the specified timestamp, only 10 posts will be returned. - If `limit` is 15, but only 5 posts exist after the specified timestamp, only those 5 posts will be returned. ## Result options This action always completes successfully. Even if the person has no posts, it returns an empty array. ```json { "actionType": "st.retrievePersonPosts", "label": "person1Posts", "success": true, "data": [ { "url": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789", "activityUrn": "urn:li:activity:1234567890123456789", "time": "2023-01-02T12:30:00Z", "type": "original", "author": { "type": "person", "name": "Example Person", "profileUrl": "https://www.linkedin.com/in/example-person", "headline": "Product Marketing Lead" }, "reposter": null, "text": "Check out our latest product launch!", "repostText": null, "hashtags": ["product", "launch"], "mentions": ["https://www.linkedin.com/company/example-company"], "externalLinks": ["https://example.com/launch"], "images": [ "https://static.linkedin.com/image1.jpg", "https://static.linkedin.com/image2.jpg" ], "documentSlides": [], "hasVideo": false, "videoThumbnail": null, "hasPoll": false, "reactionsCount": 27, "commentsCount": 8, "repostsCount": 12, "then": { ... } }, { "url": "https://www.linkedin.com/feed/update/urn:li:activity:2345678901234567890", "activityUrn": "urn:li:activity:2345678901234567890", "time": "2023-01-01T09:15:00Z", "type": "repost", "author": { "type": "company", "name": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company" }, "reposter": { "type": "person", "name": "Example Reposter", "profileUrl": "https://www.linkedin.com/in/example-reposter", "headline": null }, "text": "Original post content about the webinar.", "repostText": "A useful summary for anyone planning a launch.", "hashtags": [], "mentions": [], "externalLinks": [], "images": [], "documentSlides": [], "hasVideo": true, "videoThumbnail": "https://media.licdn.com/dms/image/video-cover.jpg", "hasPoll": false, "reactionsCount": 6, "commentsCount": 0, "repostsCount": 1, "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of person posts with results of child actions execution. - `url` – URL of the post. - `activityUrn` – LinkedIn activity or UGC URN of the post, if available. - `time` – timestamp when the post was published. - `type` – type of the post. Enum with possible values: - `original` – for original posts. - `repost` – for reposts. - `author` – original content creator. Can be `null` if the actor cannot be parsed. - For person authors: `type`, `name`, `profileUrl`, `headline`. - For company authors: `type`, `name`, `companyUrl`. - `reposter` – person or company that reshared the post. Non-null only when `type` is `repost`. - For person reposters: `type`, `name`, `profileUrl`, `headline`. - For company reposters: `type`, `name`, `companyUrl`. - `text` – original author's post text, if available. - `repostText` – text added by the reposter on a repost with comment, if available. - `hashtags` – array of hashtags found in the post text, without leading `#`. - `mentions` – array of person and company profile URLs found in the post text. - `externalLinks` – array of outbound URLs found in the post text. - `images` – array of up to 3 preview image URLs, if available. - `documentSlides` – array of carousel or document slide image URLs, if available. - `hasVideo` – boolean indicating if the post contains a video. - `videoThumbnail` – URL of the video thumbnail, if available. - `hasPoll` – boolean indicating if the post contains a poll. - `reactionsCount` – number of reactions on the post. - `commentsCount` – number of comments on the post. - `repostsCount` – number of reposts on the post. - `then` – results of child actions execution. ## st.retrievePersonComments This action allows you to retrieve comments left by a person. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePersonComments", "label": "person1Comments", "limit": 10, "since": "2023-01-01T00:00:00Z" } ``` - `limit` (optional) – number of comments to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter comments made after the specified time. - `label` (optional) – custom label for tracking this action in workflow completion. **Note**: both `limit` and `since` conditions are applied simultaneously. For example: - If `limit` is 10, but there are more than 10 comments since the specified timestamp, only 10 reactions will be returned. - If `limit` is 15, but only 5 comments exist after the specified timestamp, only those 5 reactions will be returned. ## Result options This action always completes successfully. Even if the person has no comments, it returns an empty array. ```json { "actionType": "st.retrievePersonComments", "label": "person1Comments", "success": true, "data": [ { "postUrl": "https://www.linkedin.com/posts/example1", "time": "2023-01-02T10:30:00Z", "text": "Great work!", "image": null, "reactionsCount": 3 }, { "postUrl": "https://www.linkedin.com/posts/example2", "time": "2023-01-03T10:30:00Z", "text": "Feeling the same :)", "image": "https://static.linkedin.com/image1.jpg", "reactionsCount": 0 } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of person comments. - `postUrl` – URL of the post the comment belongs to. - `time` – timestamp when the comment was left. - `text` – text content of the comment, if available. - `image` – URL of the comment's image, if available. - `reactionsCount` – number of reactions on the comment. ## st.retrievePersonReactions This action allows you to retrieve reactions made by a person. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePersonReactions", "label": "person1Reactions", "limit": 10, "since": "2023-01-01T00:00:00Z" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` (optional) – number of reaction to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter reactions made after the specified time. **Note**: both `limit` and `since` conditions are applied simultaneously. For example: - If `limit` is 10, but there are more than 10 reactions since the specified timestamp, only 10 reactions will be returned. - If `limit` is 15, but only 5 reactions exist after the specified timestamp, only those 5 reactions will be returned. ## Result options This action always completes successfully. Even if the person has no reactions, it returns an empty array. ```json { "actionType": "st.retrievePersonReactions", "label": "person1Reactions", "success": true, "data": [ { "postUrl": "https://www.linkedin.com/posts/example1", "time": "2023-01-02T12:30:00Z", "type": "like" }, { "postUrl": "https://www.linkedin.com/posts/example2", "time": "2023-01-03T12:30:00Z", "type": "celebrate" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of person reactions. - `postUrl` – URL of the post the reaction belongs to. - `time` – timestamp when the reaction was made. - `type` – enum describing the reaction type. May take one of the following values: - `like` – standard "like". - `celebrate` – celebrates an achievement. - `support` – shows support. - `love` – expresses love or admiration. - `insightful` – appreciates insightful content. - `funny` – reacts to something humorous. ## st.openPost This action allows you to open a post to retrieve its data and perform additional post-related actions if needed. ## Constraints > ⏺️ **Root Start:** allowed, when `postUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.doForPosts](/docs/action-st-do-for-posts). > ⬇️ **Child Actions:** [st.reactToPost](/docs/action-st-react-to-post), [st.commentOnPost](/docs/action-st-comment-on-post), [st.retrievePostComments](/docs/action-st-retrieve-post-comments), [st.retrievePostReactions](/docs/action-st-retrieve-post-reactions). ## Parameters ```json { "actionType": "st.openPost", "label": "post1", "postUrl": "https://www.linkedin.com/posts/post1", "basicInfo": true, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `postUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the post. - `basicInfo` (optional, default: `false`) – when set to `true`, the action includes basic post information in the results. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful post opening with `basicInfo` set to `true`:** ```json { "actionType": "st.openPost", "label": "post1", "success": true, "data": { "url": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789", "activityUrn": "urn:li:activity:1234567890123456789", "time": "2023-01-02T12:30:00Z", "type": "original", "author": { "type": "person", "name": "Example Person", "profileUrl": "https://www.linkedin.com/in/example-person", "headline": "Product Marketing Lead" }, "reposter": null, "text": "Check out our latest product launch!", "repostText": null, "hashtags": ["product", "launch"], "mentions": ["https://www.linkedin.com/company/example-company"], "externalLinks": ["https://example.com/launch"], "images": [ "https://static.linkedin.com/image1.jpg", "https://static.linkedin.com/image2.jpg" ], "documentSlides": [], "hasVideo": false, "videoThumbnail": null, "hasPoll": false, "reactionsCount": 27, "commentsCount": 8, "repostsCount": 12, "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data` – basic information about the post and results of child actions execution. - `url` – URL of the post. - `activityUrn` – LinkedIn activity or UGC URN of the post, if available. - `time` – timestamp when the post was published. - `type` – type of the post. Enum with possible values: - `original` – for original posts. - `repost` – for reposts. - `author` – original content creator. Can be `null` if the actor cannot be parsed. - For person authors: `type`, `name`, `profileUrl`, `headline`. - For company authors: `type`, `name`, `companyUrl`. - `reposter` – person or company that reshared the post. Non-null only when `type` is `repost`. - For person reposters: `type`, `name`, `profileUrl`, `headline`. - For company reposters: `type`, `name`, `companyUrl`. - `text` – original author's post text, if available. - `repostText` – text added by the reposter on a repost with comment, if available. - `hashtags` – array of hashtags found in the post text, without leading `#`. - `mentions` – array of person and company profile URLs found in the post text. - `externalLinks` – array of outbound URLs found in the post text. - `images` – array of up to 3 preview image URLs, if available. - `documentSlides` – array of carousel or document slide image URLs, if available. - `hasVideo` – boolean indicating if the post contains a video. - `videoThumbnail` – URL of the video thumbnail, if available. - `hasPoll` – boolean indicating if the post contains a poll. - `reactionsCount` – number of reactions on the post. - `commentsCount` – number of comments on the post. - `repostsCount` – number of reposts on the post. - `then` – results of child actions execution. 2. **Successful repost opening with `basicInfo` set to `true`:** ```json { "actionType": "st.openPost", "label": "post1", "success": true, "data": { "url": "https://www.linkedin.com/feed/update/urn:li:activity:2345678901234567890", "activityUrn": "urn:li:activity:2345678901234567890", "time": "2023-01-03T09:15:00Z", "type": "repost", "author": { "type": "company", "name": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company" }, "reposter": { "type": "person", "name": "Example Reposter", "profileUrl": "https://www.linkedin.com/in/example-reposter", "headline": null }, "text": "Original post content about the webinar.", "repostText": "A useful summary for anyone planning a launch.", "hashtags": [], "mentions": [], "externalLinks": [], "images": [], "documentSlides": [], "hasVideo": true, "videoThumbnail": "https://media.licdn.com/dms/image/video-cover.jpg", "hasPoll": false, "reactionsCount": 6, "commentsCount": 0, "repostsCount": 1, "then": { ... } } } ``` 3. **Successful post opening with `basicInfo` set to `false`:** ```json { "actionType": "st.openPost", "label": "post1", "success": true, "data": { "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data.then` – results of child actions execution. 4. **Failed post opening:** ```json { "actionType": "st.openPost", "label": "post1", "success": false, "error": { "type": "postNotFound", "message": "The provided URL is not an existing LinkedIn post." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `postNotFound` – provided URL is not an existing LinkedIn post. ## st.createPost This action allows you to create a post on LinkedIn. You can post as yourself or on behalf of a company page you have admin access to. The action supports text content with optional media attachments including images, videos, and documents. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.createPost", "label": "post1", "text": "Excited to share my latest insights on building scalable APIs!", "attachments": [ { "url": "https://example.com/images/team-photo.jpg", "type": "image" } ], "companyUrl": "https://www.linkedin.com/company/microsoft" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `text` (required) – post text, must be up to **3000** characters. - `attachments` (optional) – array of media attachments. Maximum **9** items. - `url` (required) – publicly accessible URL of the file to attach. - `type` (required) – type of attachment: `image`, `video`, or `document`. - `name` (required when type is `document`) – display name for the document. - `companyUrl` (optional) – LinkedIn company page URL. When provided, the post will be published on behalf of the company. Requires content admin access to the company page. ### Attachment limits - Maximum **9 images** per post (JPEG, PNG, GIF, WebP, max 8mb). - Maximum **1 video** per post (MP4, MOV, WebM, max 200 mb). - Maximum **1 document** per post (PDF, max 100 mb). - Cannot mix different attachment types (e.g., images and video together). ## Result options 1. **Successful post creation:** ```json { "actionType": "st.createPost", "label": "post1", "success": true, "data": { "postUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890" } } ``` - `label` – included only if specified in the action parameters. - `data.postUrl` – URL of the created post. 2. **Failed post creation:** ```json { "actionType": "st.createPost", "label": "weeklyUpdate1", "success": false, "error": { "type": "companyNotFound", "message": "Provided URL is not an existing LinkedIn company." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `textTooLong` – post text exceeds 3000 characters limit. - `companyNotFound` – specified company page was not found on LinkedIn. - `noPostingPermission` – no permission to post on behalf of this company. - `unsupportedAttachmentType` – attachment type is not supported. Use `image`, `video`, or `document`. - `missingDocumentName` – document attachment requires a `name` field. - `urlNotAccessible` – attachment URL is not publicly accessible or returned an error. - `fileTooLarge` – attachment file exceeds the maximum allowed size. - `unsupportedMimeType` – file's MIME type is not supported by LinkedIn. - `tooManyAttachments` – more than 9 attachments were provided. - `mixingNotAllowed` – cannot mix different attachment types (e.g., images with video). ## st.reactToPost This action allows you to react to a post using any available reaction type. ## Constraints > ⏺️ **Root Start:** allowed, when `postUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPost](/docs/action-st-open-post), [st.doForPosts](/docs/action-st-do-for-posts). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.reactToPost", "postUrl": "https://www.linkedin.com/posts/post1", "companyUrl": "https://www.linkedin.com/company/company1", "type": "like", "label": "post1" } ``` - `postUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the post. - `type` – enum describing the reaction type. - `like` – standard "like". - `celebrate` – to celebrate an achievement. - `support` – to show support. - `love` – to express love or admiration. - `insightful` – to appreciate insightful content. - `funny` – to react to something humorous. - `companyUrl` (optional) – LinkedIn company page URL. When provided, the reaction will be posted on behalf of the company. Requires content admin access to the company page. - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options 1. **Successful post reaction:** ```json { "actionType": "st.reactToPost", "success": true, "label": "post1" } ``` - `label` – included only if specified in the action parameters. 2. **Failed post reaction:** ```json { "actionType": "st.reactToPost", "success": false, "label": "post1", "error": { "type": "postNotFound", "message": "The provided URL is not an existing LinkedIn post." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `postNotFound` – provided URL is not an existing LinkedIn post. - `noPostingPermission` – no permission to react on behalf of this company. ## st.commentOnPost This action allows you to leave a comment on a post. ## Constraints > ⏺️ **Root Start:** allowed, when `postUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPost](/docs/action-st-open-post), [st.doForPosts](/docs/action-st-do-for-posts). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.commentOnPost", "label": "post1", "postUrl": "https://www.linkedin.com/posts/post1", "companyUrl": "https://www.linkedin.com/company/company1", "text": "I completely agree with your point." } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `postUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the post. - `text` – comment text, must be up to **1000** characters. - `companyUrl` (optional) – LinkedIn company page URL. When provided, the comment will be posted on behalf of the company. Requires content admin access to the company page. ## Result options 1. **Successful post commenting:** ```json { "actionType": "st.commentOnPost", "label": "post1", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,9876543210)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)" } } ``` - `label` – included only if specified in the action parameters. - `data` – information about the created comment. - `commentUrn` – LinkedIn URN of the created comment, if available. - `commentUrl` – canonical deep-link URL of the created comment, if available. It can be used as the `commentUrl` input for [st.openComment](/docs/action-st-open-comment), [st.reactToComment](/docs/action-st-react-to-comment), or [st.replyToComment](/docs/action-st-reply-to-comment). 2. **Failed post commenting:** ```json { "actionType": "st.commentOnPost", "label": "post1", "success": false, "error": { "type": "commentingNotAllowed", "message": "Commenting is not allowed on this post." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `postNotFound` – provided URL is not an existing LinkedIn post. - `commentingNotAllowed` – commenting is not allowed on this post. This could be due to the post author's privacy settings, LinkedIn restrictions on commenting, or because the post type does not support comments. - `noPostingPermission` – no permission to comment on behalf of this company. ## st.retrievePostComments This action allows you to retrieve comments for a certain post. ## Constraints > ⏺️ **Root Start:** allowed, when `postUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPost](/docs/action-st-open-post), [st.doForPosts](/docs/action-st-do-for-posts). > ⬇️ **Child Actions:** [st.doForComments](/docs/action-st-do-for-comments). ## Parameters ```json { "actionType": "st.retrievePostComments", "postUrl": "https://www.linkedin.com/posts/post1", "replies": true, "limit": 10, "sort": "mostRelevant", "label": "post1Comments", "then": { ... } } ``` - `postUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the post. - `replies` (optional, default: `true`) – when set to `true`, the action includes replies to the comments in the results. - `limit` (optional) – number of comments to retrieve. Also applies to the replies if `replies` set to `true`. Defaults to **10**, with a maximum value of **500**. - `sort` (optional, default: `mostRelevant`) – enum representing comments sorting. Options: - `mostRelevant` – show most relevant comments first. - `mostRecent` – show most recent comments first. - `label` (optional) – custom label for tracking this action in workflow completion. - `then` (optional) – object or array of child actions to be executed for each retrieved comment (see [st.doForComments](/docs/action-st-do-for-comments)). ## Result options 1. **Successful comments retrieval:** ```json { "actionType": "st.retrievePostComments", "label": "post1Comments", "success": true, "data": [ { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,9876543210)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "commenterUrl": "https://www.linkedin.com/in/john-doe", "commenterName": "John Doe", "commenterHeadline": "Product Manager", "commenterType": "person", "time": "3d", "text": "Great work!", "image": null, "isReply": false, "reactionsCount": 3, "repliesCount": 2 }, { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,9876543211)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543211%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "commenterUrl": "https://www.linkedin.com/in/person1", "commenterName": "Jane Doe", "commenterHeadline": "Helping companies to use AI", "commenterType": "person", "time": "3d", "text": "Agree with you!", "image": null, "isReply": true, "reactionsCount": 0, "repliesCount": 0 }, { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,9876543212)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543212%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "commenterUrl": "https://www.linkedin.com/company/company1", "commenterName": "CloseAI", "commenterHeadline": "2548 followers", "commenterType": "company", "time": "1w", "text": null, "image": "https://static.linkedin.com/image1.jpg", "isReply": false, "reactionsCount": 8, "repliesCount": 4 } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of comments. - `commentUrn` – LinkedIn URN of the comment, if available. It can be used to build a `commentUrl` for comment-scoped actions. - `commentUrl` – canonical deep-link URL of the comment, if available. It can be used as the `commentUrl` input for [st.openComment](/docs/action-st-open-comment), [st.reactToComment](/docs/action-st-react-to-comment), or [st.replyToComment](/docs/action-st-reply-to-comment). - `commenterUrl` – public URL of the person or company. - `commenterName` – full name of the person or company. - `commenterHeadline` – headline of the person or company. - `commenterType` – commenter type. Enum with the following values: - `person` – commenter is a person. - `company` – commenter is a company. - `time` – relative time when the comment was posted, as shown by LinkedIn (e.g. `3d`, `1w`). - `text` – text of the comment, if available. - `image` – URL of an image attached to the comment, if available. - `isReply` – boolean indicating whether the comment is a reply to another comment. - `reactionsCount` – number of reactions on the comment. - `repliesCount` – number of replies to the comment. - `then` – results of child actions execution, present only when child actions are applied via [st.doForComments](/docs/action-st-do-for-comments). 2. **Failed comments retrieval:** ```json { "actionType": "st.retrievePostComments", "success": false, "label": "post1", "error": { "type": "postNotFound", "message": "The provided URL is not an existing LinkedIn post." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `postNotFound` – provided URL is not an existing LinkedIn post. ## st.openComment This action allows you to open a comment to react to it or reply to it. ## Constraints > ⏺️ **Root Start:** allowed, when `commentUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.doForComments](/docs/action-st-do-for-comments). > ⬇️ **Child Actions:** [st.reactToComment](/docs/action-st-react-to-comment), [st.replyToComment](/docs/action-st-reply-to-comment). ## Parameters ```json { "actionType": "st.openComment", "label": "comment1", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `commentUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the comment. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful comment opening:** ```json { "actionType": "st.openComment", "label": "comment1", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,9876543210)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data` – information about the opened comment and results of child actions execution. - `commentUrn` – LinkedIn URN of the comment, if available. - `commentUrl` – canonical deep-link URL of the comment, if available. - `then` – results of child actions execution. 2. **Failed comment opening:** ```json { "actionType": "st.openComment", "label": "comment1", "success": false, "error": { "type": "commentNotFound", "message": "The comment could not be opened or no longer exists." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `commentNotFound` – the comment could not be opened or no longer exists. ## st.reactToComment This action allows you to react to a comment using any available reaction type. ## Constraints > ⏺️ **Root Start:** allowed, when `commentUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.doForComments](/docs/action-st-do-for-comments), [st.openComment](/docs/action-st-open-comment). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.reactToComment", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "type": "like", "label": "comment1" } ``` - `commentUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the comment. - `type` (optional, default: `like`) – enum describing the reaction type. - `like` – standard "like". - `celebrate` – to celebrate an achievement. - `support` – to show support. - `love` – to express love or admiration. - `insightful` – to appreciate insightful content. - `funny` – to react to something humorous. - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options 1. **Successful comment reaction:** ```json { "actionType": "st.reactToComment", "success": true, "label": "comment1" } ``` - `label` – included only if specified in the action parameters. 2. **Failed comment reaction:** ```json { "actionType": "st.reactToComment", "success": false, "label": "comment1", "error": { "type": "commentNotFound", "message": "The comment could not be opened or no longer exists." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `commentNotFound` – the comment could not be opened or no longer exists. ## st.replyToComment This action allows you to reply to a comment. ## Constraints > ⏺️ **Root Start:** allowed, when `commentUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.doForComments](/docs/action-st-do-for-comments), [st.openComment](/docs/action-st-open-comment). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.replyToComment", "label": "comment1", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "text": "Thanks for sharing your thoughts!" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `commentUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the comment. - `text` – reply text. ## Result options 1. **Successful comment reply:** ```json { "actionType": "st.replyToComment", "label": "comment1", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,1122334455)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(1122334455%2Curn%3Ali%3Aactivity%3A1234567890123456789)" } } ``` - `label` – included only if specified in the action parameters. - `data` – information about the created reply. - `commentUrn` – LinkedIn URN of the created reply, if available. - `commentUrl` – canonical deep-link URL of the created reply, if available. 2. **Failed comment reply:** ```json { "actionType": "st.replyToComment", "label": "comment1", "success": false, "error": { "type": "commentNotFound", "message": "The comment could not be opened or no longer exists." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `commentNotFound` – the comment could not be opened or no longer exists. - `replyingNotAllowed` – replying is not allowed on this comment. ## st.retrievePostReactions This action allows you to retrieve reactions for a certain post. ## Constraints > ⏺️ **Root Start:** allowed, when `postUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPost](/docs/action-st-open-post), [st.doForPosts](/docs/action-st-do-for-posts). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePostReactions", "postUrl": "https://www.linkedin.com/posts/post1", "limit": 100, "label": "post1Reactions" } ``` - `postUrl` (required for root start, forbidden for parent start) – LinkedIn URL of the post. - `limit` (optional) – number of reactions to retrieve. Defaults to **10**, with a maximum value of **500**. - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options 1. **Successful reactions retrieval:** ```json { "actionType": "st.retrievePostReactions", "label": "post1Reactions", "success": true, "data": [ { "engagerUrl": "https://www.linkedin.com/in/john-doe", "engagerName": "John Doe", "engagerHeadline": "Product Manager", "engagerType": "person", "type": "like" }, { "engagerUrl": "https://www.linkedin.com/in/person1", "engagerName": "Jane Doe", "engagerHeadline": "Helping companies to use AI", "engagerType": "person", "type": "support" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of reactions. - `engagerUrl` – URL of the person or company. - `engagerName` – full name of the person or company. - `engagerHeadline` – headline of the person or company. - `engagerType` – the engager type. Enum with the following values: - `person` – the engager is the person. - `company` – the engager is the company. 2. **Failed reactions retrieval:** ```json { "actionType": "st.retrievePostReactions", "success": false, "label": "post1", "error": { "type": "postNotFound", "message": "The provided URL is not an existing LinkedIn post." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `postNotFound` – provided URL is not an existing LinkedIn post. ## st.openJob This action allows you to open a LinkedIn job and optionally retrieve its details. ## Constraints > ⏺️ **Root Start:** allowed, when `jobUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.doForJobs](/docs/action-st-do-for-jobs). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.openJob", "label": "job1", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "basicInfo": true } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `jobUrl` (required for root start, omitted for parent start) – LinkedIn job URL. When `st.openJob` is used under [`st.doForJobs`](/docs/action-st-do-for-jobs), the URL is injected from the parent search result. - `basicInfo` (optional, default: `false`) – when set to `true`, the action includes job details in the result. ## Result options 1. **Successful job opening with `basicInfo` set to `true`:** ```json { "actionType": "st.openJob", "label": "job1", "success": true, "data": { "jobId": "4416248954", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "title": "Senior Product Manager", "companyName": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company", "location": "San Francisco, CA", "postedDate": "1w", "applicantsCount": 84, "workplaceType": "remote", "employmentType": "Full-time", "salary": { "currency": "usd", "minAmount": 140000, "maxAmount": 180000, "period": "yearly" }, "description": "Example job description text.", "applyUrl": "https://www.linkedin.com/jobs/view/4416248954/apply/", "easyApply": true } } ``` - `label` – included only if specified in the action parameters. - `data` – job details. - `jobId` – LinkedIn job identifier. - `jobUrl` – normalized LinkedIn job URL. - `title` – job title. - `companyName` – company name, if available. - `companyUrl` – normalized LinkedIn company URL, if available. - `location` – free-form job location, if available. - `postedDate` – compact relative date, such as `1w`, `3d`, or `2mo`, if available. - `applicantsCount` – number of applicants, if LinkedIn shows it. - `workplaceType` – workplace type label as shown by LinkedIn (such as `remote`, `hybrid`, or `on-site`), if available. Not one of the `workplaceTypes` filter values. - `employmentType` – employment type label as shown by LinkedIn (such as `Full-time`), if available. Not one of the `employmentTypes` filter values. - `salary` – parsed salary range, if LinkedIn shows one. - `currency` – lowercase currency code, such as `usd`, `eur`, or `gbp`. - `minAmount` – minimum amount in the parsed range. - `maxAmount` – maximum amount in the parsed range. - `period` – salary period. Possible values are `yearly`, `monthly`, and `hourly`. - `description` – job description text, if available. - `applyUrl` – Easy Apply or external application URL, if available. - `easyApply` – boolean indicating whether the job uses LinkedIn Easy Apply. 2. **Successful job opening with `basicInfo` set to `false`:** ```json { "actionType": "st.openJob", "label": "job1", "success": true } ``` - `label` – included only if specified in the action parameters. - The action opens the job but does not include job details in the result. 3. **Failed job opening:** ```json { "actionType": "st.openJob", "label": "job1", "success": false, "error": { "type": "jobNotFound", "message": "The job could not be opened or no longer exists." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `jobNotFound` – the job could not be opened or no longer exists. ## st.doForCompanies This action allows you to apply the actions specified in its `then` parameter to each company provided by the parent action. > There is a limit of 20 companies that this action can be applied to. If the parent action provides more than 20 companies, only the first 20 will be processed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.searchCompanies](/docs/action-st-search-companies). > ⬇️ **Child Actions:** [st.openCompanyPage](/docs/action-st-open-company-page). ## Parameters ```json { "actionType": "st.doForCompanies", "then": { ... } } ``` - `then` – object or array of child actions to apply to each company from the parent action. ## Result options This action doesn't produce its own distinct result. The effects are visible in the companies that were involved, as they will contain a `then` field with the results of the applied actions. See usage examples for more details. ## Usage examples 1. **Retrieving basic information about searched companies:** **Workflow:** ```json { "actionType": "st.searchCompanies", "term": "Tech Inc", "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Robotics Engineering"] }, "then": { "actionType": "st.doForCompanies", "then": { "actionType": "st.openCompanyPage", "basicInfo": true } } } ``` **Completion:** ```json { "actionType": "st.searchCompanies", "success": true, "data": [ { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "industry": "Information Technology", "location": "California", "then": { "actionType": "st.openCompanyPage", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "California", "headquarters": "US", "industry": "Information Technology", "specialties": "Cloud Computing, AI, Software Development", "website": "https://techcorp.com", "employeeCount": 500, "yearFounded": 2019, "ventureFinancing": true, "jobsCount": 12 } } }, { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "industry": "Software Development", "location": "Mountain View", "then": { "actionType": "st.openCompanyPage", "success": true, "data": { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "description": "TechLife is a worldwide leader in software development for sustainability.", "location": "Mountain View", "headquarters": "US", "industry": "Software Development", "specialties": "Software Development, AI", "website": "https://techlife.com", "employeeCount": 163, "yearFounded": 2016, "ventureFinancing": false, "jobsCount": 4 } } } ] } ``` 2. **Retrieving decision makers of searched companies:** **Workflow:** ```json { "actionType": "st.searchCompanies", "term": "Tech Inc", "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York", "London"], "industries": ["Software Development", "Robotics Engineering"] }, "then": { "actionType": "st.doForCompanies", "then": { "actionType": "st.openCompanyPage", "then": { "actionType": "st.retrieveCompanyDMs" } } } } ``` **Completion:** ```json { "actionType": "st.searchCompanies", "success": true, "data": [ { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "industry": "Information Technology", "location": "California", "then": { "actionType": "st.openCompanyPage", "success": true, "data": { "then": { "actionType": "st.retrieveCompanyDMs", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Founder of TechCorp", "location": "New York, USA", "countryCode": "US" }, { "name": "Jane Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "CEO at TechCorp", "location": "San Francisco, USA", "countryCode": "US" } ] } } } }, { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "industry": "Software Development", "location": "Mountain View", "then": { "actionType": "st.openCompanyPage", "success": true, "data": { "then": { "actionType": "st.retrieveCompanyDMs", "success": true, "data": [ { "name": "Row Pitterson", "publicUrl": "https://www.linkedin.com/in/robby", "headline": "Founder of AweSome", "location": "London, UK", "countryCode": "UK" }, { "name": "Angela White", "publicUrl": "https://www.linkedin.com/in/anglwiteh", "headline": "CEO at FunnyFace Inc.", "location": "San Francisco, USA", "countryCode": "US" } ] } } } } ] } ``` ## st.doForPeople This action allows you to apply the actions specified in its `then` parameter to each person provided by the parent action. > There is a limit of 20 people that this action can be applied to. If the parent action provides more than 20 people, only the first 20 will be processed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.searchPeople](/docs/action-st-search-people), [st.retrieveCompanyEmployees](/docs/action-st-retrieve-company-employees), [st.retrieveCompanyDMs](/docs/action-st-retrieve-company-dms). > ⬇️ **Child Actions:** [st.openPersonPage](/docs/account-api/action-st-open-person-page). ## Parameters ```json { "actionType": "st.doForPeople", "then": { ... } } ``` - `then` – object or array of child actions to apply to each person from the parent action. ## Result options This action doesn't produce its own distinct result. The effects are visible in the people that were involved, as they will contain a `then` field with the results of the applied actions. See usage examples for more details. ## Usage examples 1. **Retrieving basic information about the first 2 people from the search results:** **Workflow:** ```json { "actionType": "st.searchPeople", "term": "John Doe", "limit": 2, "filter": { "position": "CEO", "locations": ["New York", "San Francisco", "London"] }, "then": { "actionType": "st.doForPeople", "then": { "actionType": "st.openPersonPage", "basicInfo": true } } } ``` **Completion:** ```json { "actionType": "st.searchPeople", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "then": { "actionType": "st.openPersonPage", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "countryCode": "US", "position": "Software Engineer", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678" } } }, { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johnyde", "headline": "Product Manager at TechCorp", "location": "New York, USA", "then": { "actionType": "st.openPersonPage", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johnyd", "hashedUrl": "https://www.linkedin.com/in/Hs763KsjdB153e", "headline": "Product Manager at TechCorp", "location": "New York, USA", "countryCode": "US", "position": "Product Manager", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678" } } } ] } ``` 2. **Sending connection requests to all the company's decision makers:** **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/techcorp", "then": { "actionType": "st.retrieveCompanyDMs", "then": { "actionType": "st.doForPeople", "then": { "actionType": "st.openPersonPage", "then": { "actionType": "st.sendConnectionRequest" } } } } } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { "then": { "actionType": "st.retrieveCompanyDMs", "success": true, "data": [ { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "headline": "CEO at TechCorp", "location": "New York, USA", "then": { "actionType": "st.openPersonPage", "success": true, "data": { "then": { "actionType": "st.sendConnectionRequest", "success": true } } } }, { "name": "CTO Smith", "publicUrl": "https://www.linkedin.com/in/janesmith", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "then": { "actionType": "st.openPersonPage", "success": true, "data": { "then": { "actionType": "st.sendConnectionRequest", "success": true } } } } ] } } } ``` ## st.doForPosts This action allows you to apply the actions specified in its `then` parameter to each post provided by the parent action. > There is a limit of 20 posts that this action can be applied to. If the parent action provides more than 20 posts, only the first 20 will be processed. > Post-scoped actions can be nested directly under `st.doForPosts`, or wrapped in [st.openPost](/docs/action-st-open-post) first when you also need the post's data or want to run several post-scoped actions together for each post. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.retrieveCompanyPosts](/docs/action-st-retrieve-company-posts), [st.retrievePersonPosts](/docs/action-st-retrieve-person-posts). > ⬇️ **Child Actions:** [st.openPost](/docs/action-st-open-post), [st.reactToPost](/docs/action-st-react-to-post), [st.commentOnPost](/docs/action-st-comment-on-post), [st.retrievePostComments](/docs/action-st-retrieve-post-comments), [st.retrievePostReactions](/docs/action-st-retrieve-post-reactions). ## Parameters ```json { "actionType": "st.doForPosts", "then": { ... } } ``` - `then` – object or array of child actions to apply to each post from the parent action. ## Result options This action doesn't produce its own distinct result. The effects are visible in the posts that were involved, as they will contain a `then` field with the results of the applied actions. See usage examples for more details. ## Usage examples 1. **Liking the latest 3 posts from the company:** **Workflow:** ```json { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/company1", "then": { "actionType": "st.retrieveCompanyPosts", "limit": 3, "then": { "actionType": "st.doForPosts", "then": { "actionType": "st.openPost", "basicInfo": false, "then": { "actionType": "st.reactToPost", "type": "like" } } } } } ``` **Completion:** ```json { "actionType": "st.openCompanyPage", "success": true, "data": { ... "then": { "actionType": "st.retrieveCompanyPosts", "success": true, "data": [ { ... "then": { "actionType": "st.openPost", "success": true, "data": { "then": { "actionType": "st.reactToPost", "success": true } } } }, { ... "then": { "actionType": "st.openPost", "success": true, "data": { "then": { "actionType": "st.reactToPost", "success": true } } } }, { ... "then": { "actionType": "st.openPost", "success": true, "data": { "then": { "actionType": "st.reactToPost", "success": true } } } } ] } } } ``` 2. **Commenting on the person's latest post:** **Workflow:** ```json { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/person1", "then": { "actionType": "st.retrievePersonPosts", "limit": 1, "then": { "actionType": "st.doForPosts", "then": { "actionType": "st.openPost", "basicInfo": false, "then": { "actionType": "st.commentOnPost", "text": "Great post! Thanks for sharing." } } } } } ``` **Completion:** ```json { "actionType": "st.openPersonPage", "success": true, "data": { ... "then": { "actionType": "st.retrievePersonPosts", "success": true, "data": [ { ... "then": { "actionType": "st.openPost", "success": true, "data": { "then": { "actionType": "st.commentOnPost", "success": true } } } } ] } } } ``` 3. **Liking the latest 3 posts from a person directly, without opening them:** **Workflow:** ```json { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/person1", "then": { "actionType": "st.retrievePersonPosts", "limit": 3, "then": { "actionType": "st.doForPosts", "then": { "actionType": "st.reactToPost", "type": "like" } } } } ``` **Completion:** ```json { "actionType": "st.openPersonPage", "success": true, "data": { ... "then": { "actionType": "st.retrievePersonPosts", "success": true, "data": [ { ... "then": { "actionType": "st.reactToPost", "success": true } } ] } } } ``` ## st.doForComments This action allows you to apply the actions specified in its `then` parameter to each comment provided by the parent action. > The comments this action iterates over are the ones returned by the parent [st.retrievePostComments](/docs/action-st-retrieve-post-comments) action, so their number is controlled by its `limit` parameter. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.retrievePostComments](/docs/action-st-retrieve-post-comments). > ⬇️ **Child Actions:** [st.openComment](/docs/action-st-open-comment), [st.reactToComment](/docs/action-st-react-to-comment), [st.replyToComment](/docs/action-st-reply-to-comment). ## Parameters ```json { "actionType": "st.doForComments", "then": { ... } } ``` - `then` – object or array of child actions to apply to each comment from the parent action. ## Result options This action doesn't produce its own distinct result. The effects are visible in the comments that were involved, as they will contain a `then` field with the results of the applied actions. See usage examples for more details. ## Usage examples 1. **Liking the 5 most relevant comments on a post:** **Workflow:** ```json { "actionType": "st.retrievePostComments", "postUrl": "https://www.linkedin.com/posts/post1", "limit": 5, "sort": "mostRelevant", "then": { "actionType": "st.doForComments", "then": { "actionType": "st.reactToComment", "type": "like" } } } ``` **Completion:** ```json { "actionType": "st.retrievePostComments", "success": true, "data": [ { ... "then": { "actionType": "st.reactToComment", "success": true } }, { ... "then": { "actionType": "st.reactToComment", "success": true } } ] } ``` 2. **Reacting to and replying to each comment through the `st.openComment` wrapper:** **Workflow:** ```json { "actionType": "st.retrievePostComments", "postUrl": "https://www.linkedin.com/posts/post1", "limit": 3, "then": { "actionType": "st.doForComments", "then": { "actionType": "st.openComment", "then": [ { "actionType": "st.reactToComment", "type": "celebrate" }, { "actionType": "st.replyToComment", "text": "Well said, thanks for sharing!" } ] } } } ``` **Completion:** ```json { "actionType": "st.retrievePostComments", "success": true, "data": [ { ... "then": { "actionType": "st.openComment", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,9876543210)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(9876543210%2Curn%3Ali%3Aactivity%3A1234567890123456789)", "then": [ { "actionType": "st.reactToComment", "success": true }, { "actionType": "st.replyToComment", "success": true, "data": { "commentUrn": "urn:li:comment:(urn:li:activity:1234567890123456789,1122334455)", "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A(1122334455%2Curn%3Ali%3Aactivity%3A1234567890123456789)" } } ] } } } ] } ``` ## st.doForJobs This action allows you to apply the actions specified in its `then` parameter to each job provided by the parent action. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [st.searchJobs](/docs/action-st-search-jobs). > ⬇️ **Child Actions:** [st.openJob](/docs/action-st-open-job). ## Parameters ```json { "actionType": "st.doForJobs", "then": { ... } } ``` - `then` – object or array of child actions to apply to each job from the parent action. ## Result options This action doesn't produce its own distinct result. The effects are visible in the jobs that were involved, as they will contain a `then` field with the results of the applied actions. See usage examples for more details. ## Usage examples 1. **Retrieving details for jobs from search results:** **Workflow:** ```json { "actionType": "st.searchJobs", "term": "product manager", "limit": 2, "then": { "actionType": "st.doForJobs", "then": { "actionType": "st.openJob", "basicInfo": true } } } ``` **Completion:** ```json { "actionType": "st.searchJobs", "success": true, "data": [ { "jobId": "4416248954", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "title": "Senior Product Manager", "companyName": "Example Company", "location": "San Francisco, CA", "workplaceType": "remote", "salary": { "currency": "usd", "minAmount": 140000, "maxAmount": 180000, "period": "yearly" }, "easyApply": true, "isPromoted": false, "then": { "actionType": "st.openJob", "success": true, "data": { "jobId": "4416248954", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "title": "Senior Product Manager", "companyName": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company", "location": "San Francisco, CA", "postedDate": "1w", "applicantsCount": 84, "workplaceType": "remote", "employmentType": "Full-time", "salary": { "currency": "usd", "minAmount": 140000, "maxAmount": 180000, "period": "yearly" }, "description": "Example job description text.", "applyUrl": "https://www.linkedin.com/jobs/view/4416248954/apply/", "easyApply": true } } } ] } ``` ## st.retrieveSSI This action allows you to retrieve your current SSI (Social Selling Index). ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrieveSSI", "label": "ssi" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options This action always completes successfully. ```json { "actionType": "st.retrieveSSI", "label": "ssi", "success": true, "data": { "ssi": 78, "industryTop": 15, "networkTop": 10 } } ``` - `label` – included only if specified in the action parameters. - `data.ssi` – number (1-100) representing your current Social Selling Index. - `data.industryTop` – percentage (1-100) showing your industry ranking by SSI score. For example, a value of 5 means you are in the top 5% of your industry. - `data.networkTop` – percentage (1-100) showing your network ranking by SSI score. For example, a value of 10 means you are in the top 10% of your network. ## st.retrievePerformance This action allows you to retrieve performance analytics from your [LinkedIn dashboard](https://www.linkedin.com/dashboard/). ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrievePerformance", "label": "performance" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. ## Result options This action always completes successfully. ```json { "actionType": "st.retrievePerformance", "label": "performance", "success": true, "data": { "followersCount": 1250, "postViewsLast7Days": 523, "profileViewsLast90Days": 342, "searchAppearancesPreviousWeek": 89 } } ``` - `label` – included only if specified in the action parameters. - `data.followersCount` – total number of your followers. - `data.postViewsLast7Days` – number of views on your posts in the last 7 days. - `data.profileViewsLast90Days` – number of views on your profile in the last 90 days. - `data.searchAppearancesPreviousWeek` – number of times your profile appeared in LinkedIn searches during the previous week. ## st.retrieveFeed This action allows you to retrieve posts from your own LinkedIn home feed. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "st.retrieveFeed", "label": "feed", "limit": 20 } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` (optional) – number of posts to retrieve. Defaults to **20**, with a maximum value of **100**. ## Result options 1. **Successful retrieval:** ```json { "actionType": "st.retrieveFeed", "label": "feed", "success": true, "data": [ { "url": "https://www.linkedin.com/feed/update/urn:li:activity:1234567890123456789", "activityUrn": "urn:li:activity:1234567890123456789", "time": "2023-01-02T12:30:00Z", "type": "original", "author": { "type": "person", "name": "Example Person", "profileUrl": "https://www.linkedin.com/in/example-person", "headline": "Product Marketing Lead" }, "reposter": null, "text": "Check out our latest product launch!", "repostText": null, "hashtags": ["product", "launch"], "mentions": ["https://www.linkedin.com/company/example-company"], "externalLinks": ["https://example.com/launch"], "images": [ "https://static.linkedin.com/image1.jpg", "https://static.linkedin.com/image2.jpg" ], "documentSlides": [], "hasVideo": false, "videoThumbnail": null, "hasPoll": false, "reactionsCount": 27, "commentsCount": 8, "repostsCount": 12, "feedContext": "Example Reactor reacted to this" }, { "url": "https://www.linkedin.com/feed/update/urn:li:activity:2345678901234567890", "activityUrn": "urn:li:activity:2345678901234567890", "time": "2023-01-01T09:15:00Z", "type": "repost", "author": { "type": "company", "name": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company" }, "reposter": { "type": "person", "name": "Example Reposter", "profileUrl": "https://www.linkedin.com/in/example-reposter", "headline": null }, "text": "Original post content about the webinar.", "repostText": "A useful summary for anyone planning a launch.", "hashtags": [], "mentions": [], "externalLinks": [], "images": [], "documentSlides": [], "hasVideo": true, "videoThumbnail": "https://media.licdn.com/dms/image/video-cover.jpg", "hasPoll": false, "reactionsCount": 6, "commentsCount": 0, "repostsCount": 1, "feedContext": "Example Reposter reposted this" } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of posts from your home feed. Sponsored (promoted) posts are included as regular posts. - `url` – URL of the post. - `activityUrn` – LinkedIn activity or UGC URN of the post, if available. - `time` – timestamp when the post was published. - `type` – type of the post. Enum with possible values: - `original` – for original posts. - `repost` – for reposts. - `author` – original content creator. Can be `null` if the actor cannot be parsed. - For person authors: `type`, `name`, `profileUrl`, `headline`. - For company authors: `type`, `name`, `companyUrl`. - `reposter` – person or company that reshared the post. Non-null only when `type` is `repost`. - For person reposters: `type`, `name`, `profileUrl`, `headline`. - For company reposters: `type`, `name`, `companyUrl`. - `text` – original author's post text, if available. - `repostText` – text added by the reposter on a repost with comment, if available. - `hashtags` – array of hashtags found in the post text, without leading `#`. - `mentions` – array of person and company profile URLs found in the post text. - `externalLinks` – array of outbound URLs found in the post text. - `images` – array of up to 3 preview image URLs, if available. - `documentSlides` – array of carousel or document slide image URLs, if available. - `hasVideo` – boolean indicating if the post contains a video. - `videoThumbnail` – URL of the video thumbnail, if available. - `hasPoll` – boolean indicating if the post contains a poll. - `reactionsCount` – number of reactions on the post. - `commentsCount` – number of comments on the post. - `repostsCount` – number of reposts on the post. - `feedContext` – verbatim localized context line explaining why the post appears in your feed (for example, "Example Reactor reacted to this", "Example Reposter reposted this", or "Promoted"). `null` when no context line is present. 2. **Failed retrieval:** ```json { "actionType": "st.retrieveFeed", "label": "feed", "success": false, "error": { "type": "unexpectedError", "message": "An unexpected error occurred during action execution." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `unexpectedError` – no parseable posts could be retrieved from the feed, for example due to a slow connection or a temporary rendering issue. Try running the action again. ## nv.sendMessage This action allows you to send a message to a person in Sales Navigator. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` or `threadId` parameter is provided. > ⬆️ **Parent Actions:** [nv.openPersonPage](/docs/action-nv-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "nv.sendMessage", "label": "person1", "personUrl": "https://www.linkedin.com/in/person1", "text": "Hi! I'd love to connect and discuss some ideas.", "subject": "Let's Connect!" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start unless `threadId` is provided, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to send a message to. - `threadId` (optional) – identifier of an existing conversation thread to reply into, as returned by [inbox polling](/docs/monitoring-inbox) or [conversation polling](/docs/working-with-conversations), or read from the address bar of an open conversation — the `` in `linkedin.com/sales/inbox/`. Provide either `personUrl` or `threadId`; if both are given, `threadId` takes precedence. - `text` – message text, must be up to **1900** characters. - `subject` – subject line, must be up to **80** characters. Required when starting a new conversation; ignored when replying into an existing thread via `threadId`. ## Result options 1. **Successful message sending:** ```json { "actionType": "nv.sendMessage", "label": "person1", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **Failed message sending:** ```json { "actionType": "nv.sendMessage", "label": "person1", "success": false, "error": { "type": "personNotFound", "message": "The provided URL is not an existing LinkedIn person." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `personNotFound` – provided URL is not an existing LinkedIn person. - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `messagingNotAllowed` – sending a message to the person is not allowed. This could happen for several reasons: - Your monthly Sales Navigator message limit has been reached. - LinkedIn has restricted your ability to send messages to the person, for example, due the person’s privacy settings. ## nv.syncConversation This action allows you to sync a conversation in Sales Navigator so you can [start polling](/docs/working-with-conversations) it. ## Constraints > ⏺️ **Root Start:** allowed, when `personUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openPersonPage](/docs/action-st-open-person-page), [nv.openPersonPage](/docs/action-nv-open-person-page). > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "nv.syncConversation", "label": "sync1", "personUrl": "https://www.linkedin.com/in/person1", "days": 14 } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personUrl` (required for root start, forbidden for parent start) – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person whose conversation you want to sync. - `days` (optional) – how many days the conversation stays synchronized, from 1 to 90. Defaults to 30. ## Syncing period Synchronization is not permanent. The conversation is updated until the `syncUntil` moment returned by this action, counted from the moment the action starts running. After that moment the conversation stops being updated, but nothing is deleted: [polling](/docs/working-with-conversations) keeps returning the messages collected so far, together with the past `syncUntil` value. To resume updates, run this action again for the same person — the accumulated history is preserved, and a new period starts. A reply from the other person does not extend the period. ## Result options 1. **Successful syncing:** ```json { "actionType": "nv.syncConversation", "label": "sync1", "success": true, "data": { "syncUntil": "2026-09-09T09:55:31.582Z" } } ``` - `label` – included only if specified in the action parameters. - `data.syncUntil` – moment when synchronization of this conversation stops. 2. **Failed syncing:** ```json { "actionType": "nv.syncConversation", "label": "sync1", "success": false, "error": { "type": "noSalesNavigator", "message": "Your account does not have Sales Navigator subscription." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `personNotFound` – provided URL is not an existing LinkedIn person. ## nv.syncInbox This action enables **whole-inbox monitoring** for your Sales Navigator inbox so you can [poll the inbox](/docs/monitoring-inbox) for messages across every conversation. Unlike [`nv.syncConversation`](/docs/action-nv-sync-conversation), which watches a single person's conversation, `nv.syncInbox` starts tracking **all** incoming Sales Navigator threads. Run it once per account. After it succeeds, new messages become available through the [inbox polling endpoint](/docs/monitoring-inbox). > 💡 Only messages that arrive **after** inbox sync is enabled are captured. History predating the moment you enable it is not imported. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "nv.syncInbox", "label": "enableNvInbox" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. This action takes no other parameters. ## Result options 1. **Successful enabling:** ```json { "actionType": "nv.syncInbox", "label": "enableNvInbox", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **Failed enabling:** ```json { "actionType": "nv.syncInbox", "label": "enableNvInbox", "success": false, "error": { "type": "noSalesNavigator", "message": "Your account does not have Sales Navigator subscription." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `noSalesNavigator` – your account does not have Sales Navigator subscription. ## nv.manageConversation This action allows you to manage a conversation thread in Sales Navigator — archive or unarchive it — by its `threadId`. ## Constraints > ⏺️ **Root Start:** allowed, when `threadId` parameter is provided. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** N/A. ## Parameters ```json { "actionType": "nv.manageConversation", "label": "thread1", "threadId": "2-Zjhm...", "operation": "archive" } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `threadId` (required) – identifier of the conversation thread to manage, as returned by [inbox polling](/docs/monitoring-inbox) or [conversation polling](/docs/working-with-conversations). You can also read it from the address bar of an open conversation — it is the `` in `linkedin.com/sales/inbox/`. - `operation` – operation to apply to the thread. One of: - `archive` / `unarchive` – archive or unarchive the conversation. ## Result options 1. **Successful operation:** ```json { "actionType": "nv.manageConversation", "label": "thread1", "success": true } ``` - `label` – included only if specified in the action parameters. 2. **Failed operation:** ```json { "actionType": "nv.manageConversation", "label": "thread1", "success": false, "error": { "type": "threadNotFound", "message": "The provided thread identifier does not match an existing conversation." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `threadNotFound` – provided `threadId` does not match an existing conversation. ## nv.searchCompanies This action allows you to search for companies in Sales Navigator applying various filtering criteria. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** [nv.doForCompanies](/docs/action-nv-do-for-companies). ## Parameters ```json { "actionType": "nv.searchCompanies", "label": "techIncSearch1", "term": "Tech Inc", "limit": 2, "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Robotics Engineering"], "annualRevenue": { "min": "0", "max": "2.5" } }, "customSearchUrl": "https://www.linkedin.com/sales/search/company?query=(spellCorrectionEnabled%3Atrue%2Ckeywords%3ALinked%2520API)", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `term` (optional) – keyword or phrase to search. Either `term` or `customSearchUrl` must be provided; a request with neither (for example, with `filter` only) is rejected. - `limit` (optional) – number of search results to return. Defaults to **25**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for companies. When multiple filter fields are specified, they are combined using `AND` logic. - `sizes` (optional) – array of enums representing employee count ranges. Matches if company’s size falls within any of the listed ranges. Options: - `1-10`. - `11-50`. - `51-200`. - `201-500`. - `501-1000`. - `1001-5000`. - `5001-10000`. - `10001+`. - `locations` (optional) – array of free-form strings representing locations. Matches if company is headquartered in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if company operates in any of the listed industries. Takes specific values available in the LinkedIn interface. - `annualRevenue` (optional) – object representing company annual revenue range in million USD: - `min` – enum with options: - `0`. - `0.5`. - `1`. - `2.5`. - `5`. - `10`. - `20`. - `50`. - `100`. - `500`. - `1000`. - `max` – enum with options: - `customSearchUrl` (optional) – URL copied from Sales Navigator search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in Sales Navigator. Either `term` or `customSearchUrl` must be provided. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful search:** ```json { "actionType": "nv.searchCompanies", "label": "techIncSearch1", "success": true, "data": [ { "name": "TechCorp", "hashedUrl": "https://www.linkedin.com/company/12345678", "industry": "Information Technology", "employeesCount": 500, "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQE/company-logo_100_100/0/1700000000000", "then": { ... } }, { "name": "Techical Life", "hashedUrl": "https://www.linkedin.com/company/87654321", "industry": "Software Development", "employeesCount": 120, "logoUrl": null, "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of search outputs with results of child actions execution. - `name` – name of the company. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `employeesCount` – total number of employees associated with the company. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. - `then` – results of child actions execution. 2. **Failed search:** ```json { "actionType": "nv.searchCompanies", "label": "techIncSearch1", "success": false, "error": { "type": "noSalesNavigator", "message": "Your account does not have Sales Navigator subscription." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## nv.searchPeople This action allows you to search for people in Sales Navigator applying various filtering criteria. ## Constraints > ⏺️ **Root Start:** allowed. > ⬆️ **Parent Actions:** N/A. > ⬇️ **Child Actions:** [nv.doForPeople](/docs/action-nv-do-for-people). ## Parameters ```json { "actionType": "nv.searchPeople", "label": "johnDoeSearch1", "term": "John Doe", "limit": 2, "filter": { "firstName": "John", "lastName": "Doe", "position": "CEO", "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "currentCompanies": ["Tech Solutions", "Innovatech"], "previousCompanies": ["FutureCorp"], "schools": ["Harvard University", "MIT"], "yearsOfExperience": ["lessThanOne", "oneToTwo", "threeToFive"] }, "customSearchUrl": "https://www.linkedin.com/sales/search/people?query=(recentSearchParam%3A(doLogHistory%3Atrue)%2CspellCorrectionEnabled%3Atrue%2Ckeywords%3ABill%2520Gates)", "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `term` (optional) – keyword or phrase to search. Either `term` or `customSearchUrl` must be provided; a request with neither (for example, with `filter` only) is rejected. - `limit` (optional) – number of search results to return. Defaults to **25**, with a maximum value of **2500**. - `filter` (optional) – object that specifies filtering criteria for people. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of person. - `lastName` (optional) – last name of person. - `position` (optional) – job position of person. - `locations` (optional) – array of free-form strings representing locations. Matches if person is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if person works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `currentCompanies` (optional) – array of company names. Matches if person currently works at any of the listed companies. - `previousCompanies` (optional) – array of company names. Matches if person previously worked at any of the listed companies. - `schools` (optional) – array of institution names. Matches if person currently attends or previously attended any of the listed institutions. - `yearsOfExperience` (optional) – array of enums representing professional experience. Matches if person’s experience falls within any of the listed ranges. Options: - `lessThanOne` – less than 1 year. - `oneToTwo` – 1 to 2 years. - `threeToFive` – 3 to 5 years. - `sixToTen` – 6 to 10 years. - `moreThanTen` – more than 10 years. - `customSearchUrl` (optional) – URL copied from Sales Navigator search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in Sales Navigator. Either `term` or `customSearchUrl` must be provided. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful search:** ```json { "actionType": "nv.searchPeople", "label": "johnDoeSearch1", "success": true, "data": [ { "name": "John Doe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8OeJoj0mrkxx7Jiuy0", "position": "Founder & CEO", "location": "London", "avatarUrl": "https://media.licdn.com/dms/image/v2/D5603AQE/profile-displayphoto-shrink_100_100/0/1700000000000", "then": { ... } }, { "name": "John Doe", "hashedUrl": "https://www.linkedin.com/in/ACoAA2DdsSV83B48UDvgO5jPoу3Gho0o", "position": "Product Manager", "location": "New York", "avatarUrl": null, "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of search outputs with results of child actions execution. - `name` – full name of the person. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `position` – job position of the person. - `location` – free-form string indicating the person's location. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. - `then` – results of child actions execution. 2. **Failed search:** ```json { "actionType": "nv.searchPeople", "label": "johnDoeSearch1", "success": false, "error": { "type": "noSalesNavigator", "message": "Your account does not have Sales Navigator subscription." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## nv.openCompanyPage This action allows you to open a company page in Sales Navigator to retrieve its basic information and perform additional company-related actions if needed. ## Constraints > ⏺️ **Root Start:** allowed, when `companyHashedUrl` parameter is provided. > ⬆️ **Parent Actions:** [st.openCompanyPage](/docs/action-st-open-company-page), [nv.doForCompanies](/docs/action-nv-do-for-companies). > ⬇️ **Child Actions:** [nv.retrieveCompanyEmployees](/docs/action-nv-retrieve-company-employees), [nv.retrieveCompanyDMs](/docs/action-nv-retrieve-company-dms), [st.openCompanyPage](/docs/action-st-open-company-page). ## Parameters ```json { "actionType": "nv.openCompanyPage", "label": "company1", "companyHashedUrl": "https://www.linkedin.com/company/12345678", "basicInfo": true, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `companyHashedUrl` (required for root start, forbidden for parent start) – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `basicInfo` (optional, default: `false`) – when set to `true`, the action includes basic company information in the results. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful page opening with `basicInfo` set to `true`:** ```json { "actionType": "nv.openCompanyPage", "label": "company1", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/company1", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "Cupertino, California", "headquarters": "US", "industry": "Information Technology", "website": "https://techcorp.com", "employeesCount": 500, "yearFounded": 2019, "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQE/company-logo_200_200/0/1700000000000", "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data` – basic information about the company and results of child actions execution. - `name` – name of the company. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `description` – description of the company. - `location` – free-form string representing the company headquarters location. - `headquarters` – two-character country code (e.g., "US", "UK") representing headquarters location. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. 2. **Successful page opening with `basicInfo` set to `false`:** ```json { "actionType": "nv.openCompanyPage", "label": "company1", "success": true, "data": { "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data.then` – results of child actions execution. 3. **Failed page opening:** ```json { "actionType": "nv.openCompanyPage", "label": "company1", "success": false, "error": { "type": "companyNotFound", "message": "The provided URL is not an existing LinkedIn company." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `companyNotFound` – provided URL is not an existing LinkedIn company. ## nv.retrieveCompanyEmployees This action allows you to retrieve company employees from Sales Navigator and perform additional person-related actions if needed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [nv.openCompanyPage](/docs/action-nv-open-company-page). > ⬇️ **Child Actions:** [nv.doForPeople](/docs/action-nv-do-for-people). ## Parameters ```json { "actionType": "nv.retrieveCompanyEmployees", "label": "company1Employees", "limit": 300, "filter": { "firstName": "John", "lastName": "Doe", "positions": ["Manager", "Executive"], "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "schools": ["Harvard University", "MIT"], "yearsOfExperiences": ["threeToFive", "sixToTen"] }, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` – (optional) maximum number of employees to retrieve. Defaults to **25**, with a maximum value of **2500**. - `filter` (optional) – object that specifies filtering criteria for employees. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of employee. - `lastName` (optional) – last name of employee. - `positions` (optional) – array of job position names. Matches if employee's current position is any of the listed options. - `locations` (optional) – array of free-form strings representing locations. Matches if employee is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if employee works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `schools` (optional) – array of institution names. Matches if employee currently attends or previously attended any of the listed institutions. - `yearsOfExperiences` (optional) – array of enums representing professional experience. Matches if employee’s experience falls within any of the listed ranges. Options: - `lessThanOne` – less than 1 year. - `oneToTwo` – 1 to 2 years. - `threeToFive` – 3 to 5 years. - `sixToTen` – 6 to 10 years. - `moreThanTen` – more than 10 years. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful retrieval:** ```json { "actionType": "nv.retrieveCompanyEmployees", "label": "company1Employees", "success": true, "data": [ { "name": "John Doe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8OeJoj0mrkxx7Jiuy0", "position": "Manager", "location": "New York, USA", "then": { ... } }, { "name": "Jane Smith", "hashedUrl": "https://www.linkedin.com/in/ACoAA2DdsSV83B48UDvgO5jPoу3Gho0o", "position": "Software Engineer", "location": "San Francisco, USA", "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of company employees with results of child actions execution. - `name` – full name of the employee. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the employee. - `position` – job position of the employee. - `location` – free-form string indicating the employee's location. - `then` – results of child actions execution. 2. **Failed retrieval:** ```json { "actionType": "nv.retrieveCompanyEmployees", "label": "company1Employees", "success": false, "error": { "type": "retrievingNotAllowed", "message": "LinkedIn has blocked performing the retrieval." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## nv.retrieveCompanyDMs This action allows you to retrieve company decision makers from Sales Navigator and perform additional person-related actions if needed. > Decision makers are ranked by seniority, starting with the highest-level positions. For example, founders and C-level executives are returned first, followed by VPs, directors, and so on. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [nv.openCompanyPage](/docs/action-nv-open-company-page). > ⬇️ **Child Actions:** [nv.doForPeople](/docs/action-nv-do-for-people). ## Parameters ```json { "actionType": "nv.retrieveCompanyDMs", "label": "company1DMs", "limit": 3, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `limit` (optional) – maximum number of decision makers to retrieve. Defaults to **20**, with a maximum value of **20**. If a company has fewer decision makers than specified, only the available ones will be returned. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful retrieval:** ```json { "actionType": "nv.retrieveCompanyDMs", "label": "company1DMs", "success": true, "data": [ { "name": "John Doe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8OeJoj0mrkxx7Jiuy0", "position": "Founder", "location": "New York, USA", "countryCode": "US", "then": { ... } }, { "name": "Jane Smith", "hashedUrl": "https://www.linkedin.com/in/ACoAA2DdsSV83B48UDvgO5jPoу3Gho0o", "position": "CEO", "location": "San Francisco, USA", "countryCode": "US", "then": { ... } } ] } ``` - `label` – included only if specified in the action parameters. - `data` – array of decision makers with results of child actions execution. - `name` – full name of the decision-maker. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the decision-maker. - `position` – job position of the decision-maker. - `location` – free-form string indicating the decision-maker's location. - `countryCode` – two-character code of the decision-maker's country. - `then` – results of child actions execution. 2. **Failed retrieval:** ```json { "actionType": "nv.retrieveCompanyDMs", "label": "company1DMs", "success": false, "error": { "type": "retrievingNotAllowed", "message": "LinkedIn has blocked performing the retrieval." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## nv.openPersonPage This action allows you to open a person page in Sales Navigator to retrieve their basic information and perform additional person-related actions if needed. ## Constraints > ⏺️ **Root Start:** allowed, when `personHashedUrl` parameter is provided. > ⬆️ **Parent Actions:** [nv.doForPeople](/docs/action-nv-do-for-people), [st.openPersonPage](/docs/action-st-open-person-page). > ⬇️ **Child Actions:** [nv.sendMessage](/docs/action-nv-send-message), [nv.syncConversation](/docs/action-nv-sync-conversation), [st.openPersonPage](/docs/action-st-open-person-page). ## Parameters ```json { "actionType": "nv.openPersonPage", "label": "person1", "personHashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "basicInfo": true, "then": { ... } } ``` - `label` (optional) – custom label for tracking this action in workflow completion. - `personHashedUrl` (required for root start, forbidden for parent start) – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `basicInfo` (optional, default: `false`) – when set to `true`, the action includes basic person information in the results. - `then` (optional) – object or array of child actions to be executed within this action. ## Result options 1. **Successful page opening with `basicInfo` set to `true`:** ```json { "actionType": "nv.openPersonPage", "label": "person1", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/person1", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "countryCode": "US", "position": "Software Engineer", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678", "avatarUrl": "https://media.licdn.com/dms/image/v2/D5603AQE/profile-displayphoto-shrink_100_100/0/1700000000000", "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data` – basic information about the person and results of child actions execution. - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. - `countryCode` – two-character code of the person's country. - `position` – current job position of the person. - `companyName` – name of the person's current company. - `companyHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person's current company. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. 2. **Successful page opening with `basicInfo` set to `false`:** ```json { "actionType": "nv.openPersonPage", "label": "person1", "success": true, "data": { "then": { ... } } } ``` - `label` – included only if specified in the action parameters. - `data.then` – results of child actions execution. 3. **Failed page opening:** ```json { "actionType": "nv.openPersonPage", "label": "person1", "success": false, "error": { "type": "personNotFound", "message": "The provided URL is not an existing LinkedIn person." } } ``` - `label` – included only if specified in the action parameters. - `error.type` – enum with the following possible values: - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `personNotFound` – provided URL is not an existing LinkedIn person. ## nv.doForCompanies This action allows you to apply the actions specified in its `then` parameter to each company provided by the parent action in Sales Navigator. > There is a limit of 20 companies that this action can be applied to. If the parent action provides more than 20 companies, only the first 20 will be processed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [nv.searchCompanies](/docs/action-nv-search-companies). > ⬇️ **Child Actions:** [nv.openCompanyPage](/docs/action-nv-open-company-page). ## Parameters ```json { "actionType": "nv.doForCompanies", "then": { ... } } ``` - `then` – object or array of child actions to apply to each company from the parent action. ## Result options This action doesn't produce its own distinct result. The effects are visible in the companies that were involved, as they will contain a `then` field with the results of the applied actions. See usage examples for more details. ## Usage examples 1. **Retrieving basic information about searched companies in Sales Navigator:** **Workflow:** ```json { "actionType": "nv.searchCompanies", "term": "Tech Inc", "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Robotics Engineering"], "annualRevenue": { "min": "0", "max": "2.5" } }, "then": { "actionType": "nv.doForCompanies", "then": { "actionType": "nv.openCompanyPage", "basicInfo": true } } } ``` **Completion:** ```json { "actionType": "nv.searchCompanies", "success": true, "data": [ { "name": "TechCorp", "hashedUrl": "https://www.linkedin.com/company/12345678", "industry": "Information Technology", "employeeCount": 500, "then": { "actionType": "nv.openCompanyPage", "success": true, "data": { "name": "TechCorp", "publicUrl": "https://www.linkedin.com/company/techcorp", "description": "TechCorp is a leading provider of innovative technology solutions for businesses worldwide.", "location": "Cupertino, California", "headquarters": "US", "industry": "Information Technology", "website": "https://techcorp.com", "employeeCount": 500, "yearFounded": 2019 } } }, { "name": "Techical Life", "hashedUrl": "https://www.linkedin.com/company/12345678", "industry": "Software Development", "employeeCount": 230, "then": { "actionType": "nv.openCompanyPage", "success": true, "data": { "name": "Techical Life", "publicUrl": "https://www.linkedin.com/company/techlife", "description": "TechLife is a worldwide leader in software development for sustainability.", "location": "Mountain View", "headquarters": "US", "industry": "Software Development", "website": "https://techlife.com", "employeeCount": 230, "yearFounded": 2016 } } } ] } ``` 2. **Retrieving decision makers of searched companies in Sales Navigator:** **Workflow:** ```json { "actionType": "nv.searchCompanies", "term": "Tech Inc", "filter": { "sizes": ["51-200", "2001-500"], "locations": ["San Francisco", "New York", "London"], "industries": ["Software Development", "Robotics Engineering"], "annualRevenue": { "min": "0", "max": "2.5" } }, "then": { "actionType": "nv.doForCompanies", "then": { "actionType": "nv.openCompanyPage", "then": { "actionType": "nv.retrieveCompanyDMs" } } } } ``` **Completion:** ```json { "actionType": "nv.searchCompanies", "success": true, "data": [ { "name": "TechCorp", "hashedUrl": "https://www.linkedin.com/company/12345678", "industry": "Information Technology", "employeeCount": 500, "then": { "actionType": "nv.openCompanyPage", "success": true, "data": { "then": { "actionType": "nv.retrieveCompanyDMs", "success": true, "data": [ { "name": "John Doe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8OeJoj0mrkxx7Jiuy0", "position": "Founder", "location": "New York, USA", "countryCode": "US" }, { "name": "Jane Smith", "hashedUrl": "https://www.linkedin.com/in/ACoAA2DdsSV83B48UDvgO5jPoу3Gho0o", "position": "CEO", "location": "San Francisco, USA", "countryCode": "US" } ] } } } }, { "name": "Techical Life", "hashedUrl": "https://www.linkedin.com/company/12345678", "industry": "Software Development", "employeeCount": 230, "then": { "actionType": "nv.openCompanyPage", "success": true, "data": { "then": { "actionType": "nv.retrieveCompanyDMs", "success": true, "data": [ { "name": "Row Pitterson", "hashedUrl": "https://www.linkedin.com/in/S0mrkxx7JiuyInQBmjJ015eLr8OeJoj0", "position": "Founder", "location": "London, UK", "countryCode": "UK" }, { "name": "Angela White", "hashedUrl": "https://www.linkedin.com/in/48UDvgO5jPoу3Gho0oACoAA2DdsSV83B", "position": "CEO", "location": "San Francisco, USA", "countryCode": "US" } ] } } } } ] } ``` ## nv.doForPeople This action allows you to apply the actions specified in its `then` parameter to each person provided by the parent action in Sales Navigator. > There is a limit of 20 people that this action can be applied to. If the parent action provides more than 20 people, only the first 20 will be processed. ## Constraints > ⏺️ **Root Start:** not allowed. > ⬆️ **Parent Actions:** [nv.searchPeople](/docs/action-nv-search-people), [nv.retrieveCompanyEmployees](/docs/action-nv-retrieve-company-employees), [nv.retrieveCompanyDMs](/docs/action-nv-retrieve-company-dms). > ⬇️ **Child Actions:** [nv.openPersonPage](/docs/action-nv-open-person-page). ## Parameters ```json { "actionType": "nv.doForPeople", "then": { ... } } ``` - `then` – object or array of child actions to apply to each person from the parent action. ## Result options This action doesn't produce its own distinct result. The effects are visible in the people that were involved, as they will contain a `then` field with the results of the applied actions. See usage examples for more details. ## Usage examples 1. **Retrieving basic information about the first 2 people from the Sales Navigator search results:** **Workflow:** ```json { "actionType": "nv.searchPeople", "term": "John Doe", "limit": 2, "filter": { "position": "CEO", "locations": ["New York", "San Francisco", "London"] }, "then": { "actionType": "nv.doForPeople", "then": { "actionType": "nv.openPersonPage", "basicInfo": true } } } ``` **Completion:** ```json { "actionType": "nv.searchPeople", "success": true, "data": [ { "name": "John Doe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "position": "Founder & CEO", "location": "San Francisco, USA", "then": { "actionType": "nv.openPersonPage", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johndoe", "hashedUrl": "https://www.linkedin.com/in/SInQBmjJ015eLr8", "headline": "Software Engineer at TechCorp", "location": "San Francisco, USA", "countryCode": "US", "position": "Software Engineer", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678" } } }, { "name": "John Doe", "hashedUrl": "https://www.linkedin.com/in/CoAA2DdsSV83B48", "position": "Product Manager", "location": "New York, USA", "then": { "actionType": "nv.openPersonPage", "success": true, "data": { "name": "John Doe", "publicUrl": "https://www.linkedin.com/in/johnyd", "hashedUrl": "https://www.linkedin.com/in/Hs763KsjdB153e", "headline": "Product Manager at TechCorp", "location": "New York, USA", "countryCode": "US", "position": "Product Manager", "companyName": "TechCorp", "companyHashedUrl": "https://www.linkedin.com/company/12345678" } } } ] } ``` --- # Node.js SDK ## Core concepts This page covers the fundamental concepts and design principles behind Linked API, providing the foundation you need to build effective integrations. ## Workflows as basis Linked API is built around the **concept of workflows**. This means that any automation you want to perform (such as sending a connection request, commenting on a post, retrieving company data, etc.) must be executed as a workflow. ## Request timing Since Linked API [fully emulates](/safety) actions of a real LinkedIn user, requests are not executed instantly. For example, simple workflows like visiting a person's page and liking their latest post might take around 20 seconds, while more complex workflows, such as retrieving detailed company information including a list of its employees, can take several minutes. ## Sequential execution Linked API executes workflows sequentially. While you can send multiple workflow requests in quick succession, they **will not be executed in parallel**. Each workflow request is **accepted immediately**, but its execution will not start until the previous workflow is completed. For example, if you request a workflow to [check a connection status](/sdks/check-connection-status) and immediately request another workflow to [send a message](/sdks/send-message), the second workflow will begin **only after the first workflow is completed**. Workflow start responses and in-progress status responses (`pending` or `running`) include a `message` field that explains whether the workflow is queued or how long it usually takes. Use this message when you need to display progress to users. This approach ensures alignment with realistic user behavior on LinkedIn, as it mirrors how a real user would interact with the platform. ## Limits management LinkedIn limits vary depending on your account's age, social selling index, subscription (e.g., Sales Navigator), and other factors. > It is your responsibility to understand and follow the limits appropriate for your account. To help you stay within safe boundaries, Linked API allows you to configure action limits for each connected account directly from the platform dashboard. When aworkflow contains an action that would exceed a configured limit, that action will return a `limitExceeded` error instead of executing. You can also [manage limits programmatically](/docs/admin-limits) via the Admin API, [monitor your activity](/docs/checking-api-usage-statistics), [check SSI](/docs/action-st-retrieve-ssi), and review our [guide on LinkedIn limits](/guides/understanding-linkedin-limits). ## URL normalization Linked API normalizes all LinkedIn URLs in responses, regardless of the format provided in action parameters. Normalized URLs consistently follow these rules: - Use `https` protocol. - Include `www` subdomain. - Exclude trailing slash (`/`). For example, if action parameters include a URL like `http://linkedin.com/in/person1/`, the response will return it as `https://www.linkedin.com/in/person1`. Consider this behavior when implementing URL comparisons or storage in your integration. ## Timezone normalization All date and time values returned by Linked API are in the **UTC** **timezone**. To display these values correctly in your integration, you should apply the appropriate offset for your user's local timezone. ## Handling missing values When certain data is unavailable on LinkedIn, some fields in action results may return as `null` or empty array (`[]`). Missing fields are always **explicitly included** in the response rather than being omitted. ## Installation, authorization This page covers the installation and authorization process for Linked API SDKs. ## Installation To install the SDK, run following command for your package manager: ```bash lang=ts npm install -S @linkedapi/node ``` ```bash lang=python pip install linkedapi ``` ## Authorization To authorize your requests, you need to initialize the SDK with 2 tokens: ```typescript const linkedApi = new LinkedApi({ linkedApiToken: "your-linked-api-token", identificationToken: "your-identifiaction-token", }); ``` ```python from linkedapi import LinkedApi, LinkedApiConfig linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) ``` - `linkedApiToken` – your main token that enables overall Linked API access. - `identificationToken` – unique token specific to each managed LinkedIn account. You can obtain these tokens through [our platform](https://app.linkedapi.io/), as demonstrated below: ![](/images/docs/tokens-3.webp) ### Admin API To manage your subscription, accounts, and limits programmatically, use the separate `LinkedApiAdmin` class. It requires only the `linkedApiToken`: ```typescript import { LinkedApiAdmin } from '@linkedapi/node'; const admin = new LinkedApiAdmin({ linkedApiToken: 'your-linked-api-token', }); const status = await admin.subscription.getStatus(); const { accounts } = await admin.accounts.getAll(); ``` ```python from linkedapi import LinkedApiAdmin, AdminConfig admin = LinkedApiAdmin( AdminConfig(linked_api_token="your-linked-api-token") ) status = admin.subscription.get_status() accounts = admin.accounts.get_all().accounts ``` See the [Admin overview](/sdks/admin-overview) for details. ### Multiple LinkedIn accounts If you need to manage **multiple LinkedIn accounts**, simply initialize a separate SDK instance for each account's identification token. ```typescript const firstLinkedInAccount = new LinkedApi({ linkedApiToken: "your-linked-api-token", identificationToken: "your-first-identifiaction-token", }); const secondLinkedInAccount = new LinkedApi({ linkedApiToken: "your-linked-api-token", identificationToken: "your-second-identifiaction-token", }) ``` ```python from linkedapi import LinkedApi, LinkedApiConfig first_linkedin_account = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-first-identification-token", ) ) second_linkedin_account = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-second-identification-token", ) ) ``` ## Predefined vs. custom workflows There are two primary ways to execute automations with the SDKs: using **predefined methods** or building your own **custom workflows**. ## Predefined workflows Predefined workflows are the SDK's standard, ready-to-use methods, such as `linkedApi.sendMessage()` or `linkedApi.fetchPerson()`. Each of these helper methods automatically creates and runs a corresponding, pre-configured workflow. > A complete list of all available methods can be explored in the side navigation menu. ### When to use them You should always start with predefined workflows. They are the fastest way to integrate Linked API because they are **strongly typed**. Both the input parameters and the result objects have defined types, which provides auto-completion in your IDE and helps prevent errors. As long as a predefined method meets your needs, it's the recommended approach. ```typescript // Predefined method to send a message const sendMessageWorkflow = await linkedapi.sendMessage.execute({ personUrl: "https://www.linkedin.com/in/john-doe", text: "Hello! I saw your post about AI and wanted to connect.", }); const sendMessageResult = await linkedapi.sendMessage.result(sendMessageWorkflow.workflowId); // Predefined method to check connection status const connectionStatusWorkflow = await linkedapi.checkConnectionStatus.execute({ personUrl: "https://www.linkedin.com/in/john-doe", }); const connectionStatusResult = await linkedapi.checkConnectionStatus.result(connectionStatusWorkflow.workflowId); ``` ```python from linkedapi import CheckConnectionStatusParams, SendMessageParams # Predefined method to send a message send_message_workflow = linkedapi.send_message.execute( SendMessageParams( person_url="https://www.linkedin.com/in/john-doe", text="Hello! I saw your post about AI and wanted to connect.", ) ) send_message_result = linkedapi.send_message.result(send_message_workflow.workflow_id) # Predefined method to check connection status connection_status_workflow = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url="https://www.linkedin.com/in/john-doe") ) connection_status_result = linkedapi.check_connection_status.result( connection_status_workflow.workflow_id ) ``` ## Custom workflows For tasks requiring more flexibility or complex, multi-step logic, you can build a custom workflow. This approach allows you to directly interact with the Linked API workflow engine by constructing an **object that defines the workflow's steps and logic**. You are responsible for correctly structuring the **workflow definition object** and then parsing the resulting **completion object** returned upon execution. The [building workflows](/docs/building-workflows) page provides the complete specification for both of these object structures. ### When to use them Switch to custom workflows when the predefined methods are not sufficient for your use case. For example: ```typescript // Custom workflow to find managers at a company, // retrieve their education, and send connection requests const customWorkflow = await linkedapi.customWorkflow.execute({ "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/techcorp", "basicInfo": true, "then": { "actionType": "st.retrieveCompanyEmployees", "filter": { "position": "Manager" }, "then": { "actionType": "st.doForPeople", "then": { "actionType": "st.openPersonPage", "basicInfo": true, "then": [ { "actionType": "st.retrievePersonEducation" }, { "actionType": "st.sendConnectionRequest" } ] } } } }); const customWorkflowResult = await linkedapi.customWorkflow.result(customWorkflow.workflowId); ``` ```python # Custom workflow to find managers at a company, # retrieve their education, and send connection requests custom_workflow = linkedapi.custom_workflow.execute( { "actionType": "st.openCompanyPage", "companyUrl": "https://www.linkedin.com/company/techcorp", "basicInfo": True, "then": { "actionType": "st.retrieveCompanyEmployees", "filter": { "position": "Manager", }, "then": { "actionType": "st.doForPeople", "then": { "actionType": "st.openPersonPage", "basicInfo": True, "then": [ { "actionType": "st.retrievePersonEducation", }, { "actionType": "st.sendConnectionRequest", }, ], }, }, }, } ) custom_workflow_result = linkedapi.custom_workflow.result(custom_workflow.workflow_id) ``` ## Handling results and errors When you initiate any workflow, the `execute()` method immediately returns workflow tracking details: - `workflowId` – unique workflow identifier. - `workflowStatus` – initial workflow status, either `pending` or `running`. - `pendingReason` – why the workflow has not started yet, when it is pending. See [working hours](/docs/working-hours). - `message` – human-readable queue or duration message, when available. You can then use the `result()` method with this ID to get the workflow results when it's complete. ```typescript const workflow = await linkedapi.checkConnectionStatus.execute({ personUrl: "https://www.linkedin.com/in/john-doe" }); const result = await linkedapi.checkConnectionStatus.result(workflow.workflowId); ``` ```python from linkedapi import CheckConnectionStatusParams workflow = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url="https://www.linkedin.com/in/john-doe") ) result = linkedapi.check_connection_status.result(workflow.workflow_id) ``` The structure of the object returned by `result()` depends on whether you use a [predefined workflow](/sdks/predefined-vs-custom-workflows) or a [custom workflow](/sdks/predefined-vs-custom-workflows). ## For predefined workflows When using a standard, predefined method, `result()` returns a convenient object with two key fields: - `data` – typed object containing the successful result of the workflow. - `errors` – array of non-critical **workflow execution errors**, that may have occurred during the run, but did not stop the entire workflow. Each error object contains `type` and `message` fields. ```typescript const workflow = await linkedapi.checkConnectionStatus.execute({ personUrl: "https://www.linkedin.com/in/john-doe" }); const result = await linkedapi.checkConnectionStatus.result(workflow.workflowId); const data = result.data; const errors = result.errors; ``` ```python from linkedapi import CheckConnectionStatusParams workflow = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url="https://www.linkedin.com/in/john-doe") ) result = linkedapi.check_connection_status.result(workflow.workflow_id) data = result.data errors = result.errors ``` > For each predefined method, the specific structure of its `data` object and a list of possible `errors` are detailed on the corresponding page in the side navigation menu. ## For custom workflows When using `customWorkflow`, the `result()` method returns the raw **completion object**. This object contains the full execution context of the workflow, and you are responsible for parsing it according to the specification on the [building workflows](/docs/building-workflows) page. ```typescript const workflow = await linkedapi.customWorkflow.execute({ "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/john-doe", "basicInfo": true, "then": { "actionType": "st.checkConnectionStatus", } }); const workflowCompletion = await linkedapi.customWorkflow.result(workflow.workflowId); ``` ```python workflow = linkedapi.custom_workflow.execute( { "actionType": "st.openPersonPage", "personUrl": "https://www.linkedin.com/in/john-doe", "basicInfo": True, "then": { "actionType": "st.checkConnectionStatus", }, } ) workflow_completion = linkedapi.custom_workflow.result(workflow.workflow_id) ``` ## Checking status without waiting Use `status()` when you want to check a workflow once without waiting for it to finish. If the workflow is still `pending` or `running`, the SDK returns its current `workflowStatus`, its `pendingReason` and, when provided, `message`. The message can change between polling requests, for example when a workflow moves from `pending` to `running`. ```typescript const workflowId = "YOUR_WORKFLOW_ID"; const status = await linkedapi.checkConnectionStatus.status(workflowId); if ('workflowStatus' in status) { showProgress(status.workflowStatus, status.message); } ``` ```python from linkedapi import WorkflowInProgressResponse workflow_id = "YOUR_WORKFLOW_ID" status = linkedapi.check_connection_status.status(workflow_id) if isinstance(status, WorkflowInProgressResponse): show_progress(status.workflow_status, status.message) ``` ### Pending reasons A pending workflow is not always simply queued. `pendingReason` tells the two cases apart: - `queued` – waiting its turn behind other work on the same account. Minutes, typically. - `outsideWorkingHours` – parked until the account [working hours](/docs/working-hours) reopen, which can be the next working day. - `null` – the workflow is not pending. Back off your polling when the reason is `outsideWorkingHours`; the `message` names the time the window opens. ```typescript import { LINKED_API_WORKFLOW_PENDING_REASON } from '@linkedapi/node'; const status = await linkedapi.checkConnectionStatus.status(workflowId); if ('workflowStatus' in status) { if (status.pendingReason === LINKED_API_WORKFLOW_PENDING_REASON.outsideWorkingHours) { notifyUser(status.message); } else { scheduleNextPoll(); } } ``` ```python status = linkedapi.check_connection_status.status(workflow_id) if isinstance(status, WorkflowInProgressResponse): if status.pending_reason == "outsideWorkingHours": notify_user(status.message) else: schedule_next_poll() ``` Two error types relate to working hours as well: - `outsideWorkingHours` – raised on submission when the account rejects off-hours requests. - `workingHoursWaitExpired` – raised as a workflow failure when a parked workflow was never started within the wait limit. ## Receiving results via webhooks Instead of polling `result()` or `status()`, you can register a webhook and let Linked API notify your endpoint when a workflow completes – and when a connected account changes status. This is the better fit for long-running workflows and for reacting to account events (like a reconnection becoming required) that no single `result()` call would surface. Each delivery is an HTTP `POST` with a typed event in its body. Parse the raw request body with `parseWebhookEvent` / `parse_webhook_event`, then branch on the event's `type`: ```typescript import { parseWebhookEvent } from '@linkedapi/node'; const event = parseWebhookEvent(rawRequestBody); if (event.type === 'workflow.completed') { console.log(event.data.workflowId, event.data.status, event.data.result); } ``` ```python from linkedapi import parse_webhook_event event = parse_webhook_event(raw_request_body) if event.type == "workflow.completed": print(event.data.workflow_id, event.data.status, event.data.result) ``` `workflow.completed` carries the full workflow result in `fat` payload mode; in `thin` mode fetch it with `result()` using `data.workflowId`. Deliveries are at-least-once, so deduplicate on the event `id`. See [Webhook Events](/docs/webhooks) for the full event model, and [Webhooks](/sdks/webhooks) to register and manage one with the SDK. ## Handling critical errors Separate from the non-critical `errors` array [returned by pre-defined workflows](/sdks/handling-results-and-errors), the SDK can throw **critical errors**. These are exceptions that immediately stop execution and indicate a fundamental problem with the request or your account. These exceptions, always of the type `LinkedApiError`, can be thrown when calling `execute()` to initiate a workflow or when calling `result()` to get the results. You must handle them using a `try...catch` block to inspect the error's `type` field and determine the cause. ```typescript try { const workflow = await linkedapi.checkConnectionStatus.execute({ personUrl: "https://www.linkedin.com/in/john-doe" }); const { data, errors } = await linkedapi.checkConnectionStatus.result(workflow.workflowId); // 1. Handling non-critical execution errors if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:', errors); } // Handling the successful result if (data) { console.log('Connection Status:', data.connectionStatus); } } catch (e) { // 2. Handling critical errors (exceptions) if (e instanceof LinkedApiError) { if (e.type === LINKED_API_ERROR.invalidLinkedApiToken) { console.error('Authentication failed. Please check your API token.'); } else if (e.type === LINKED_API_ERROR.subscriptionRequired) { console.error('Invalid parameters in request:', e.message); } else { // Handle other types of critical API errors console.error(`A different API error occurred: ${e.type}`); } } else { // Handle other unexpected errors (e.g., network issues) console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import CheckConnectionStatusParams, LinkedApiError try: workflow = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url="https://www.linkedin.com/in/john-doe") ) result = linkedapi.check_connection_status.result(workflow.workflow_id) data = result.data errors = result.errors # 1. Handling non-critical execution errors if errors: print("Workflow completed with execution errors:", errors) # Handling the successful result if data: print("Connection Status:", data.connection_status) except LinkedApiError as e: # 2. Handling critical errors (exceptions) if e.type == "invalidLinkedApiToken": print("Authentication failed. Please check your API token.") elif e.type == "subscriptionRequired": print("Invalid parameters in request:", e.message) else: print(f"A different API error occurred: {e.type}") except Exception as error: # Handle other unexpected errors (e.g., network issues) print("An unexpected, non-API error occurred:", error) ``` **Here are all possible critical errors:** - `invalidLinkedApiToken` – the provided `linked-api-token` is invalid. - `invalidIdentificationToken` – the provided `identification-token` is invalid. - `subscriptionRequired` – no purchased subscription seats available for this LinkedIn account. - `plusPlanRequired` – to execute this workflow you need the [Plus plan](/pricing). - `invalidWorkflow` (only for custom workflows) – workflow configuration is not valid due to violated [action constraints](/docs/actions-overview) or invalid [action parameters](/docs/actions-overview): {validation_details}. - `invalidRequestPayload` – invalid request body/parameters: {validation_details}. - `tooManyRequests` – too many requests have been made in the last minute. - `linkedinAccountSignedOut` – your LinkedIn account has been signed out in our cloud browser. This occasionally happens as LinkedIn may sign out accounts after an extended period. You'll need to visit [our platform](https://app.linkedapi.io/) and reconnect your account. - `languageNotSupported` – your LinkedIn account uses a language other than English, which is currently the only supported option. If you encounter this issue, please contact our support. We prioritize adding new languages based on user requests, so your feedback is important. ## Persisting and cancelling workflows This page explains how to persist a workflow for later restoration and how to cancel its execution while it is running. ## Persisting workflows You can save a workflow ID to retrieve its result later. This is especially useful for long-running automations that might be interrupted by an application restart. The process involves these steps: 1. When you initiate a workflow with `execute()`, save the returned `workflowId`. 2. After a restart, retrieve the saved `workflowId`. 3. Call the `result()` method on the same SDK method, passing the saved `workflowId` to get the result. ```typescript // --- Step 1 & 2: Initiate a workflow and save its references --- async function startAndSaveWorkflow(taskId) { try { const { workflowId } = await linkedapi.checkConnectionStatus.execute({ personUrl: "https://www.linkedin.com/in/john-doe" }); // Save the workflow ID to your database await db.saveTask({ taskId: taskId, workflowId: workflowId }); console.log(`Workflow ${workflowId} started and saved for task ${taskId}.`); } catch (e) { console.error('Failed to start workflow:', e.message); } } // --- Step 3 & 4: Restore the workflow and get the result --- async function restoreAndGetResult(taskId) { try { // Retrieve the saved workflow ID from your database const task = await db.getTask(taskId); if (!task || !task.workflowId) { console.log(`No pending workflow found for task ${taskId}.`); return; } // Get the result using the saved workflow ID // You call the same method you used to start the workflow const { data, errors } = await linkedapi.checkConnectionStatus.result(task.workflowId); if (data) { console.log(`Task ${taskId} complete. Result:`, data); await db.updateTaskStatus(taskId, 'complete'); } } catch (e) { console.error(`Failed to get result for task ${taskId}:`, e.message); await db.updateTaskStatus(taskId, 'failed'); } } ``` ```python from linkedapi import CheckConnectionStatusParams, LinkedApiError # --- Step 1 & 2: Initiate a workflow and save its references --- def start_and_save_workflow(task_id): try: workflow = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url="https://www.linkedin.com/in/john-doe") ) # Save the workflow ID to your database db.save_task(task_id=task_id, workflow_id=workflow.workflow_id) print(f"Workflow {workflow.workflow_id} started and saved for task {task_id}.") except LinkedApiError as e: print("Failed to start workflow:", e.message) # --- Step 3 & 4: Restore the workflow and get the result --- def restore_and_get_result(task_id): try: # Retrieve the saved workflow ID from your database task = db.get_task(task_id) if not task or not task.workflow_id: print(f"No pending workflow found for task {task_id}.") return # Get the result using the saved workflow ID # You call the same method you used to start the workflow result = linkedapi.check_connection_status.result(task.workflow_id) if result.data: print(f"Task {task_id} complete. Result:", result.data) db.update_task_status(task_id, "complete") except LinkedApiError as e: print(f"Failed to get result for task {task_id}:", e.message) db.update_task_status(task_id, "failed") ``` ## Cancelling workflows You can cancel any running workflow when you no longer need its results. This is useful for stopping long-running automations or when conditions in your application change. To cancel a workflow, call the `cancel()` function with the `workflowId`: ```typescript // --- Start a workflow and then cancel it --- async function startAndCancelWorkflow() { try { // Step 1: Initiate the workflow const { workflowId } = await linkedapi.checkConnectionStatus.execute({ personUrl: "https://www.linkedin.com/in/john-doe" }); console.log(`Workflow ${workflowId} started.`); // Step 2: Cancel the workflow using its ID // You call cancel() on the same method you used to start the workflow const result = await linkedapi.checkConnectionStatus.cancel(workflowId); if (result.cancelled) { console.log(`Workflow ${workflowId} cancelled successfully.`); } } catch (e) { console.error('Failed to process workflow:', e.message); } } ``` ```python from linkedapi import CheckConnectionStatusParams, LinkedApiError # --- Start a workflow and then cancel it --- def start_and_cancel_workflow(): try: # Step 1: Initiate the workflow workflow = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url="https://www.linkedin.com/in/john-doe") ) print(f"Workflow {workflow.workflow_id} started.") # Step 2: Cancel the workflow using its ID # You call cancel() on the same method you used to start the workflow cancelled = linkedapi.check_connection_status.cancel(workflow.workflow_id) if cancelled: print(f"Workflow {workflow.workflow_id} cancelled successfully.") except LinkedApiError as e: print("Failed to process workflow:", e.message) ``` **Important notes:** - **Partial execution.** The workflow is cancelled at its current execution point. Any actions that have already been completed cannot be undone. For example, if the workflow has already sent messages or connection requests from your LinkedIn account, these actions will remain executed. - **No intermediate data.** Once a workflow is cancelled, no data is preserved or returned. You will not receive any intermediate results or partial data that may have been collected before cancellation. ## sendMessage This method allows you to send a message to a person. ```typescript try { const workflow = await linkedapi.sendMessage.execute({ personUrl: "https://www.linkedin.com/in/john-doe", text: "Hi John! How are you?", manageConversation: { operation: "archive" } }); const { errors } = await linkedapi.sendMessage.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Message sent successfully.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import SendMessageParams, SendMessageManageConversation, LinkedApiError try: workflow = linkedapi.send_message.execute( SendMessageParams( person_url="https://www.linkedin.com/in/john-doe", text="Hi John! How are you?", manage_conversation=SendMessageManageConversation(operation="archive"), ) ) result = linkedapi.send_message.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Message sent successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to send a message to. - `threadId` (optional) – identifier of an existing conversation thread to reply into, as returned by [`pollInbox`](/sdks/poll-inbox) or [`pollConversations`](/sdks/poll-conversations). Provide either `personUrl` or `threadId`; if both are given, `threadId` takes precedence. - `text` – message text, must be up to **1900** characters. - `manageConversation` (optional) – manage the conversation right after the message is sent. Provide `{ operation }` (one of `archive`, `unarchive`, `star`, `unstar`, `mute`, `unmute`); it acts on the same thread, so no `threadId` is needed. > Replying by `threadId` sends the message directly into a known conversation, which is convenient when reacting to an inbox event without resolving the person's profile URL first. ## Data The method doesn't return any data. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. - `messagingNotAllowed` – sending a message to the person is not allowed. This could happen for several reasons: - You are not connected to the person. - LinkedIn has restricted your ability to send messages to the person, for example, due to reaching message limits or the person’s privacy settings. ## syncConversation This method allows you to sync a conversation so you can start polling it for new messages. ```typescript try { const workflow = await linkedapi.syncConversation.execute({ personUrl: "https://www.linkedin.com/in/john-doe", days: 14 }); const { data, errors } = await linkedapi.syncConversation.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log(`Workflow completed successfully. Syncing until ${data.syncUntil}`); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import SyncConversationParams, LinkedApiError try: workflow = linkedapi.sync_conversation.execute( SyncConversationParams(person_url="https://www.linkedin.com/in/john-doe", days=14) ) result = linkedapi.sync_conversation.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print(f"Workflow completed successfully. Syncing until {result.data.sync_until}") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person whose conversation you want to synchronize. - `days` (optional) – how many days the conversation stays synchronized, from 1 to 90. Defaults to 30. ## Data - `syncUntil` – moment when synchronization of this conversation stops. Once it passes, polling keeps returning the messages collected so far, but they are no longer updated. Call this method again for the same person to start a new period; the accumulated history is preserved. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. ## syncInbox This method enables **whole-inbox monitoring** for your account so you can [poll the inbox](/sdks/poll-inbox) for messages across every conversation. Run it once per account. > Unlike [`syncConversation`](/sdks/sync-conversation), which watches a single person, `syncInbox` tracks your entire inbox but only captures messages that arrive **after** it is enabled. ```typescript try { const workflow = await linkedapi.syncInbox.execute(); const { errors } = await linkedapi.syncInbox.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Inbox monitoring enabled.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.sync_inbox.execute() result = linkedapi.sync_inbox.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Inbox monitoring enabled.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params This method takes no parameters. ## Data The method doesn't return any data. ## manageConversation This method allows you to manage a conversation thread by archiving, starring, or muting it. ```typescript try { const workflow = await linkedapi.manageConversation.execute({ threadId: "2-Zjhm...", operation: "archive" }); const { errors } = await linkedapi.manageConversation.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Conversation updated successfully.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import ManageConversationParams, LinkedApiError try: workflow = linkedapi.manage_conversation.execute( ManageConversationParams( thread_id="2-Zjhm...", operation="archive", ) ) result = linkedapi.manage_conversation.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Conversation updated successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `threadId` – identifier of the conversation thread to manage, as returned by [`pollInbox`](/sdks/poll-inbox) or [`pollConversations`](/sdks/poll-conversations), or read from the address bar of an open conversation — the `` in `linkedin.com/messaging/thread/`. - `operation` – operation to apply to the thread: `archive`, `unarchive`, `star`, `unstar`, `mute`, or `unmute`. ## Data The method doesn't return any data. ## Errors - `threadNotFound` – provided `threadId` does not match an existing conversation. ## checkConnectionStatus This method allows you to check the connection status between your account and another person. ```typescript try { const workflow = await linkedapi.checkConnectionStatus.execute({ personUrl: "https://www.linkedin.com/in/john-doe" }); const { data, errors } = await linkedapi.checkConnectionStatus.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Connection status:', data.connectionStatus); console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import CheckConnectionStatusParams, LinkedApiError try: workflow = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url="https://www.linkedin.com/in/john-doe") ) result = linkedapi.check_connection_status.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Connection status:", data.connection_status) print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to check a connection status with. ## Data - `connectionStatus` – current connection status with the person. Possible values: - `connected` – your account is connected with the person. - `notConnected` – your account is not connected with the person. - `pending` – your account has a pending connection request to the person. - `incoming` – the person has sent your account a connection request that is awaiting your response. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. ## sendConnectionRequest This method allows you to send a connection request to a person. ```typescript try { const workflow = await linkedapi.sendConnectionRequest.execute({ personUrl: "https://www.linkedin.com/in/john-doe", note: "Hello! I'd love to connect and discuss opportunities.", email: "john.doe@example.com", // Optional: required by some people }); const { errors } = await linkedapi.sendConnectionRequest.result( workflow.workflowId, ); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn("Workflow completed with execution errors:"); errors.forEach((error) => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log("Connection request sent successfully."); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error("An unexpected, non-API error occurred:", e); } } ``` ```python from linkedapi import SendConnectionRequestParams, LinkedApiError try: workflow = linkedapi.send_connection_request.execute( SendConnectionRequestParams( person_url="https://www.linkedin.com/in/john-doe", note="Hello! I'd love to connect and discuss opportunities.", email="john.doe@example.com", # Optional: required by some people ) ) result = linkedapi.send_connection_request.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Connection request sent successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to send a connection request to. - `note` (optional) – note to include with the connection request. LinkedIn limits note length and may limit how many personalized invitation notes a free account can send. - `email` (optional) – email address required by some people for sending connection requests to them. If it is required and not provided, the connection request will fail. ## Data The method doesn't return any data. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. - `alreadyPending` – connection request to this person has already been sent and is still pending. - `alreadyConnected` – your LinkedIn account is already connected with this person. - `emailRequired` – person requires an email address to send a connection request. - `noteTooLong` – the note exceeds the character limit allowed by LinkedIn. Free accounts are limited to 200 characters, Premium accounts to 300. - `noteLimitExceeded` – your LinkedIn account has reached the limit for personalized invitation notes. Send the connection request without a note, or buy LinkedIn Premium to include one. - `requestNotAllowed` – LinkedIn has restricted sending a connection request to this person. This can happen for the following reasons: - The person has disabled connection requests in their privacy settings. - You recently sent and withdrew a connection request to this person. - You have reached LinkedIn's daily, weekly, or monthly limits for sending connection requests. ## withdrawConnectionRequest This method allows you to withdraw the connection request sent to a person. ```typescript try { const workflow = await linkedapi.withdrawConnectionRequest.execute({ personUrl: "https://www.linkedin.com/in/john-doe", unfollow: true // Optional: whether to unfollow the person (default: true) }); const { errors } = await linkedapi.withdrawConnectionRequest.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Connection request withdrawn successfully.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import WithdrawConnectionRequestParams, LinkedApiError try: workflow = linkedapi.withdraw_connection_request.execute( WithdrawConnectionRequestParams( person_url="https://www.linkedin.com/in/john-doe", unfollow=True, # Optional: whether to unfollow the person (default: True) ) ) result = linkedapi.withdraw_connection_request.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Connection request withdrawn successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to withdraw the connection request from. - `unfollow` – (optional) boolean indicating whether you want to unfollow the person when withdrawing the request. Default value is **true**. ## Data The method doesn't return any data. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. - `notPending` – there is no pending connection request to this person. ## acceptInvitation This method accepts an incoming invitation by type and target URL. ```typescript try { const workflow = await linkedapi.acceptInvitation.execute({ invitationType: "connect", personUrl: "https://www.linkedin.com/in/john-doe", }); const { errors } = await linkedapi.acceptInvitation.result( workflow.workflowId, ); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn("Workflow completed with execution errors:"); for (const error of errors) { console.warn(` - Type: ${error.type}, Message: ${error.message}`); } } else { console.log("Invitation accepted successfully."); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error("An unexpected, non-API error occurred:", e); } } ``` ```python from linkedapi import AcceptInvitationParams, LinkedApiError try: workflow = linkedapi.accept_invitation.execute( AcceptInvitationParams( invitation_type="connect", person_url="https://www.linkedin.com/in/john-doe", ) ) result = linkedapi.accept_invitation.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Invitation accepted successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `invitationType` – required invitation type: `connect`, `companyFollow`, or `newsletterSubscribe`. - `personUrl` – required only for `connect`. - `companyUrl` – required only for `companyFollow`. - `newsletterUrl` – required only for `newsletterSubscribe`. Provide exactly one URL matching `invitationType`. ## Data The method doesn't return any data. ## Errors - `noPendingRequest` – there is no matching pending invitation for the supplied type and URL. ## ignoreInvitation This method ignores an incoming invitation by type and target URL. ```typescript try { const workflow = await linkedapi.ignoreInvitation.execute({ invitationType: "newsletterSubscribe", newsletterUrl: "https://www.linkedin.com/newsletters/example-1234567890", }); const { errors } = await linkedapi.ignoreInvitation.result( workflow.workflowId, ); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn("Workflow completed with execution errors:"); for (const error of errors) { console.warn(` - Type: ${error.type}, Message: ${error.message}`); } } else { console.log("Invitation ignored successfully."); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error("An unexpected, non-API error occurred:", e); } } ``` ```python from linkedapi import IgnoreInvitationParams, LinkedApiError try: workflow = linkedapi.ignore_invitation.execute( IgnoreInvitationParams( invitation_type="newsletterSubscribe", newsletter_url="https://www.linkedin.com/newsletters/example-1234567890", ) ) result = linkedapi.ignore_invitation.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Invitation ignored successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `invitationType` – required invitation type: `connect`, `companyFollow`, or `newsletterSubscribe`. - `personUrl` – required only for `connect`. - `companyUrl` – required only for `companyFollow`. - `newsletterUrl` – required only for `newsletterSubscribe`. Provide exactly one URL matching `invitationType`. ## Data The method doesn't return any data. ## Errors - `noPendingRequest` – there is no matching pending invitation for the supplied type and URL. ## retrievePendingRequests This method allows you to retrieve pending connection requests sent from your account. ```typescript try { const workflow = await linkedapi.retrievePendingRequests.execute(); const { data, errors } = await linkedapi.retrievePendingRequests.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Pending requests count:', data.requests.length); data.forEach(request => { console.log(`Request to: ${request.name} (${request.personUrl})`); console.log(`Sent: ${request.sentTime}`); }); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.retrieve_pending_requests.execute() result = linkedapi.retrieve_pending_requests.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Pending requests count:", len(data)) for request in data: print(f"Request to: {request.name} ({request.public_url})") print(f"Sent: {request.sent_time}") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params The method doesn't require any parameters. ## Data Array of pending requests. Each pending request contains: - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `sentTime` – time since the connection request was sent (e.g., "1 month ago"). ## Errors The method has no execution errors (`errors` is always `[]`). ## retrieveInvitations This method retrieves all incoming invitations from the invitation manager. ```typescript try { const workflow = await linkedapi.retrieveInvitations.execute(); const { data, errors } = await linkedapi.retrieveInvitations.result( workflow.workflowId, ); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn("Workflow completed with execution errors:"); for (const error of errors) { console.warn(` - Type: ${error.type}, Message: ${error.message}`); } } // The structure of the 'data' object is below if (data) { console.log("Incoming invitations count:", data.length); for (const invitation of data) { console.log(`${invitation.invitationType}: ${invitation.name}`); if (invitation.invitationType === "connect") { console.log(invitation.publicUrl, invitation.headline, invitation.note); } else if (invitation.invitationType === "companyFollow") { console.log(invitation.companyName, invitation.companyUrl); } else { console.log(invitation.newsletterName, invitation.newsletterUrl); } } } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error("An unexpected, non-API error occurred:", e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.retrieve_invitations.execute() result = linkedapi.retrieve_invitations.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Incoming invitations count:", len(data)) for invitation in data: print(f"{invitation.invitation_type}: {invitation.name}") if invitation.invitation_type == "connect": print(invitation.public_url, invitation.headline, invitation.note) elif invitation.invitation_type == "companyFollow": print(invitation.company_name, invitation.company_url) else: print(invitation.newsletter_name, invitation.newsletter_url) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params The method doesn't require any parameters. ## Data Array of incoming invitations. Every item contains: - `invitationType` – `connect`, `companyFollow`, or `newsletterSubscribe`. - `name` – name of the person who sent the invitation. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of that person. Type-specific fields: - `connect`: `headline` and `note`, each nullable. - `companyFollow`: `companyUrl` and nullable `companyName`. - `newsletterSubscribe`: `newsletterUrl` and nullable `newsletterName`. ## Errors The method has no execution errors (`errors` is always `[]`). ## retrieveConnections This method allows you to retrieve your connections applying various filtering criteria. ```typescript try { const workflow = await linkedapi.retrieveConnections.execute({ filter: { firstName: "John", position: "Engineer", locations: ["San Francisco", "New York"] }, limit: 100 }); const { data, errors } = await linkedapi.retrieveConnections.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Retrieved connections:', data.length); data.forEach(connection => { console.log(`${connection.name} - ${connection.headline}`); console.log(`Location: ${connection.location}`); console.log(`Profile: ${connection.publicUrl}`); }); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import RetrieveConnectionsParams, LinkedApiError try: workflow = linkedapi.retrieve_connections.execute( RetrieveConnectionsParams( filter={ "first_name": "John", "position": "Engineer", "locations": ["San Francisco", "New York"], }, limit=100, ) ) result = linkedapi.retrieve_connections.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Retrieved connections:", len(data)) for connection in data: print(f"{connection.name} - {connection.headline}") print(f"Location: {connection.location}") print(f"Profile: {connection.public_url}") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `limit` (optional) – number of connections to return. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for people. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of person. - `lastName` (optional) – last name of person. - `position` (optional) – job position of person. - `locations` (optional) – array of free-form strings representing locations. Matches if person is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if person works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `currentCompanies` (optional) – array of company names. Matches if person currently works at any of the listed companies. - `previousCompanies` (optional) – array of company names. Matches if person previously worked at any of the listed companies. - `schools` (optional) – array of institution names. Matches if person currently attends or previously attended any of the listed institutions. ## Data Array of connections. Each connection contains: - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. ## Errors - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## removeConnection This method allows you to remove a person from your connections. ```typescript try { const workflow = await linkedapi.removeConnection.execute({ personUrl: "https://www.linkedin.com/in/john-doe" }); const { data, errors } = await linkedapi.removeConnection.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Connection removed successfully'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import RemoveConnectionParams, LinkedApiError try: workflow = linkedapi.remove_connection.execute( RemoveConnectionParams(person_url="https://www.linkedin.com/in/john-doe") ) result = linkedapi.remove_connection.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Connection removed successfully") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to remove from your connections. ## Data The method doesn't return any data. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `connectionNotFound` – person is not in your connections. ## syncNetwork This method enables **network monitoring** for your account so you can [poll the network](/sdks/poll-network) for connection events as they happen. Run it once per account. > Instead of repeatedly retrieving connections and pending requests and comparing the results, `syncNetwork` watches the account's network in the background and captures every change. Only changes that happen **after** it is enabled are captured — existing connections and pending invitations are used as a silent baseline and never emitted. ```typescript try { const workflow = await linkedapi.syncNetwork.execute(); const { errors } = await linkedapi.syncNetwork.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Network monitoring enabled.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.sync_network.execute() result = linkedapi.sync_network.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Network monitoring enabled.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params This method takes no parameters. ## Data The method doesn't return any data. ## searchCompanies This method allows you to search for companies applying various filtering criteria. ```typescript try { const workflow = await linkedapi.searchCompanies.execute({ term: "Technology", limit: 10, filter: { sizes: ["51-200", "201-500"] as TSearchCompanySize[], locations: ["San Francisco", "New York"], industries: ["Software Development", "Information Technology"] }, customSearchUrl: "https://www.linkedin.com/search/results/companies/?companySize=%5B%22B%22%5D&keywords=Linked%20API" }); const { data, errors } = await linkedapi.searchCompanies.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import SearchCompaniesParams, LinkedApiError try: workflow = linkedapi.search_companies.execute( SearchCompaniesParams( term="Technology", limit=10, filter={ "sizes": ["51-200", "201-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Information Technology"], }, custom_search_url="https://www.linkedin.com/search/results/companies/?companySize=%5B%22B%22%5D&keywords=Linked%20API", ) ) result = linkedapi.search_companies.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `term` (optional) – keyword or phrase to search. - `limit` (optional) – number of search results to return. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for companies. When multiple filter fields are specified, they are combined using `AND` logic. - `sizes` (optional) – array of enums representing employee count ranges. Matches if company’s size falls within any of the listed ranges. Options: - `1-10`. - `11-50`. - `51-200`. - `201-500`. - `501-1000`. - `1001-5000`. - `5001-10000`. - `10001+`. - `locations` (optional) – array of free-form strings representing locations. Matches if company is headquartered in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if company operates in any of the listed industries. Takes specific values available in the LinkedIn interface. - `customSearchUrl` (optional) – URL copied from LinkedIn search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in the LinkedIn interface. ## Data Array of search results. Each search result contains: - `name` – name of the company. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `location` – free-form string representing the company headquarters location. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. ## Errors - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## searchPeople This method allows you to search for people applying various filtering criteria. ```typescript try { const workflow = await linkedapi.searchPeople.execute({ term: "John Doe", limit: 10, filter: { firstName: "John", lastName: "Doe", position: "CEO", locations: ["New York", "San Francisco", "London"], industries: ["Software Development", "Professional Services"], currentCompanies: ["Tech Solutions", "Innovatech"], previousCompanies: ["FutureCorp"], schools: ["Harvard University", "MIT"] }, customSearchUrl: "https://www.linkedin.com/search/results/people/?geoUrn=%5B%22103644278%22%5D&keywords=Bill%20Gates" }); const { data, errors } = await linkedapi.searchPeople.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import SearchPeopleParams, LinkedApiError try: workflow = linkedapi.search_people.execute( SearchPeopleParams( term="John Doe", limit=10, filter={ "first_name": "John", "last_name": "Doe", "position": "CEO", "locations": ["New York", "San Francisco", "London"], "industries": ["Software Development", "Professional Services"], "current_companies": ["Tech Solutions", "Innovatech"], "previous_companies": ["FutureCorp"], "schools": ["Harvard University", "MIT"], }, custom_search_url="https://www.linkedin.com/search/results/people/?geoUrn=%5B%22103644278%22%5D&keywords=Bill%20Gates", ) ) result = linkedapi.search_people.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `term` (optional) – keyword or phrase to search. - `limit` (optional) – number of search results to return. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for people. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of person. - `lastName` (optional) – last name of person. - `position` (optional) – job position of person. - `locations` (optional) – array of free-form strings representing locations. Matches if person is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if person works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `currentCompanies` (optional) – array of company names. Matches if person currently works at any of the listed companies. - `previousCompanies` (optional) – array of company names. Matches if person previously worked at any of the listed companies. - `schools` (optional) – array of institution names. Matches if person currently attends or previously attended any of the listed institutions. - `customSearchUrl` (optional) – URL copied from LinkedIn search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in the LinkedIn interface. ## Data Array of search results. Each search result contains: - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. ## Errors - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## searchJobs This method allows you to search for jobs applying various filtering criteria. ```typescript try { const workflow = await linkedapi.searchJobs.execute({ term: "product manager", limit: 10, filter: { location: "San Francisco, California, United States", datePosted: "pastWeek", experienceLevels: ["midSeniorLevel", "director"], employmentTypes: ["fullTime"], workplaceTypes: ["remote", "hybrid"], companies: ["Example Company"], industries: ["Software Development"], jobFunctions: ["Product Management"], easyApply: true, hasVerifications: true, under10Applicants: false, inYourNetwork: false, fairChanceEmployer: false }, customSearchUrl: "https://www.linkedin.com/jobs/search/?keywords=product%20manager" }); const { data, errors } = await linkedapi.searchJobs.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import SearchJobsParams, LinkedApiError try: workflow = linkedapi.search_jobs.execute( SearchJobsParams( term="product manager", limit=10, filter={ "location": "San Francisco, California, United States", "date_posted": "pastWeek", "experience_levels": ["midSeniorLevel", "director"], "employment_types": ["fullTime"], "workplace_types": ["remote", "hybrid"], "companies": ["Example Company"], "industries": ["Software Development"], "job_functions": ["Product Management"], "easy_apply": True, "has_verifications": True, "under_10_applicants": False, "in_your_network": False, "fair_chance_employer": False, }, custom_search_url="https://www.linkedin.com/jobs/search/?keywords=product%20manager", ) ) result = linkedapi.search_jobs.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `term` (optional) – keyword or phrase to search. If omitted, starts from a broad jobs search and applies the filters. - `limit` (optional) – number of search results to return. Defaults to **10**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for jobs. When multiple filter fields are specified, they are combined using `AND` logic. - `location` (optional) – free-form location string. - `datePosted` (optional) – one of `anyTime`, `past24Hours`, `pastWeek`, `pastMonth`. - `experienceLevels` (optional) – array of `internship`, `entryLevel`, `associate`, `midSeniorLevel`, `director`, `executive`. - `employmentTypes` (optional) – array of `fullTime`, `partTime`, `contract`, `temporary`, `volunteer`, `internship`, `other`. - `workplaceTypes` (optional) – array of `onSite`, `remote`, `hybrid`. - `companies` (optional) – array of company names. - `industries` (optional) – array of industry names. - `jobFunctions` (optional) – array of job function names. - `easyApply` (optional) – when `true`, only jobs with Easy Apply. - `hasVerifications` (optional) – when `true`, only jobs with verification signals. - `under10Applicants` (optional) – when `true`, only jobs with fewer than 10 applicants. - `inYourNetwork` (optional) – when `true`, only jobs from your network. - `fairChanceEmployer` (optional) – when `true`, only fair chance employer jobs. - `customSearchUrl` (optional) – URL copied from a LinkedIn jobs search page after configuring filters. When specified, overrides `term` and `filter`. ## Data Array of search results. Each result contains: - `jobId` – LinkedIn job identifier, when it can be extracted. - `jobUrl` – LinkedIn job URL, when `jobId` can be extracted. - `title` – job title. - `companyName` – company name, if available. - `location` – free-form job location, if available. - `workplaceType` – workplace type label as shown by LinkedIn, if available. - `salary` – parsed salary range, if LinkedIn shows one. - `currency` – lowercase currency code, such as `usd`, `eur`, or `gbp`. - `minAmount` – minimum amount in the range. - `maxAmount` – maximum amount in the range. - `period` – one of `yearly`, `monthly`, `hourly`. - `easyApply` – whether the card mentions Easy Apply. - `isPromoted` – whether the card is promoted. ## Errors - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## fetchCompany This method allows you to retrieve various data about a company: basic information, employees, posts, and decision makers. ```typescript try { const workflow = await linkedapi.fetchCompany.execute({ companyUrl: "https://www.linkedin.com/company/microsoft", retrieveEmployees: true, retrievePosts: true, retrieveDMs: true, employeesRetrievalConfig: { limit: 25, filter: { firstName: "John", lastName: "Smith", position: "engineer", locations: ["United States", "Canada"], industries: ["Software Development"], currentCompanies: ["Microsoft", "Google"], previousCompanies: ["Apple", "Amazon"], schools: ["Stanford University", "MIT"], }, }, postsRetrievalConfig: { limit: 10, since: "2024-01-01T00:00:00Z", }, dmsRetrievalConfig: { limit: 5, }, }) const { data, errors } = await linkedapi.fetchCompany.result( workflow.workflowId, ) // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn("Workflow completed with execution errors:") errors.forEach((error) => { console.warn(` - Type: ${error.type}, Message: ${error.message}`) }) } // The structure of the 'data' object is below if (data) { console.log("Company name:", data.name) console.log("Description:", data.description) console.log("Location:", data.location) console.log("Industry:", data.industry) console.log("Employees count:", data.employeesCount) console.log("Employees:", data.employees) console.log("Posts:", data.posts) console.log("Decision makers:", data.dms) console.log("Workflow completed successfully. Data:", data) } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`) } else { console.error("An unexpected, non-API error occurred:", e) } } ``` ```python from linkedapi import FetchCompanyParams, LinkedApiError try: workflow = linkedapi.fetch_company.execute( FetchCompanyParams( company_url="https://www.linkedin.com/company/microsoft", retrieve_employees=True, retrieve_posts=True, retrieve_dms=True, employees_retrieval_config={ "limit": 25, "filter": { "first_name": "John", "last_name": "Smith", "position": "engineer", "locations": ["United States", "Canada"], "industries": ["Software Development"], "schools": ["Stanford University", "MIT"], }, }, posts_retrieval_config={"limit": 10, "since": "2024-01-01T00:00:00Z"}, dms_retrieval_config={"limit": 5}, ) ) result = linkedapi.fetch_company.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Company name:", data.name) print("Description:", data.description) print("Location:", data.location) print("Industry:", data.industry) print("Employees count:", data.employees_count) print("Employees:", data.employees) print("Posts:", data.posts) print("Decision makers:", data.dms) print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `companyUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company you want to retrieve data for. - `retrieveEmployees` (optional) – when set to `true`, includes company employees with their profiles in the results. - `retrieveDMs` (optional) – when set to `true`, includes decision makers and key personnel in the results. - `retrievePosts` (optional) – when set to `true`, includes recent company posts and updates in the results. - `employeesRetrievalConfig` (optional) – configuration for retrieving employees. Available only if `retrieveEmployees` is `true`. - `limit` – (optional) maximum number of employees to retrieve. Defaults to **500**, with a maximum value of **500**. - `filter` (optional) – object that specifies filtering criteria for employees. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of employee. - `lastName` (optional) – last name of employee. - `position` (optional) – job position of employee. - `locations` (optional) – array of free-form strings representing locations. Matches if employee is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if employee works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `schools` (optional) – array of institution names. Matches if employee currently attends or previously attended any of the listed institutions. - `dmsRetrievalConfig` (optional) – configuration for retrieving decision makers. Available only if `retrieveDMs` is `true`. - `limit` – number of decision makers to retrieve. Defaults to **20**, with a maximum value of **20**. If a company has fewer decision makers than specified, only the available ones will be returned. - `postsRetrievalConfig` (optional) – configuration for retrieving posts. Available only if `retrievePosts` is `true`. - `limit` (optional) – number of posts to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter posts published after the specified time. ## Data - `name` – name of the company. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `description` – description of the company. - `location` – free-form string representing the company headquarters location. - `headquarters` – two-character country code (e.g., "US", "UK") representing headquarters location. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `specialties` – comma-separated list of company's specialties. - `website` – company's official website URL. - `employeesCount` – total number of employees associated with the company. - `yearFounded` – year the company was established, if available. - `ventureFinancing` – boolean indicating whether the company has received venture financing. - `jobsCount` – number of current job vacancies posted by the company. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. - `employees` – array of employee profiles (included only if `retrieveEmployees` is `true`). - `name` – full name of the employee. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the employee. - `headline` – headline of the employee. - `location` – free-form string indicating the employee's location. - `dms` – array of decision makers (included only if `retrieveDMs` is `true`). - `name` – full name of the decision-maker. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the decision-maker. - `headline` – headline of the decision-maker. - `location` – free-form string indicating the decision-maker's location. - `countryCode` – two-character code of the decision-maker's country. - `posts` – array of company posts (included only if `retrievePosts` is `true`). - `url` – URL of the post. - `activityUrn` – LinkedIn activity or UGC URN of the post, if available. - `time` – timestamp when the post was published. - `type` – type of the post. Enum with possible values: - `original` – for original posts. - `repost` – for reposts. - `author` – original content creator. Can be `null` if the actor cannot be parsed. - For person authors: `type`, `name`, `profileUrl`, `headline`. - For company authors: `type`, `name`, `companyUrl`. - `reposter` – person or company that reshared the post. Non-null only when `type` is `repost`. - For person reposters: `type`, `name`, `profileUrl`, `headline`. - For company reposters: `type`, `name`, `companyUrl`. - `text` – original author's post text, if available. - `repostText` – text added by the reposter on a repost with comment, if available. - `hashtags` – array of hashtags found in the post text, without leading `#`. - `mentions` – array of person and company profile URLs found in the post text. - `externalLinks` – array of outbound URLs found in the post text. - `images` – array of up to 3 preview image URLs, if available. - `documentSlides` – array of carousel or document slide image URLs, if available. - `hasVideo` – boolean indicating if the post contains a video. - `videoThumbnail` – URL of the video thumbnail, if available. - `hasPoll` – boolean indicating if the post contains a poll. - `reactionsCount` – number of reactions on the post. - `commentsCount` – number of comments on the post. - `repostsCount` – number of reposts on the post. ## Errors - `companyNotFound` – provided URL is not an existing LinkedIn company. - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## fetchPerson This method allows you to retrieve various data about a person: basic information, experience, education, skills, languages, posts, comments, and reactions. ```typescript try { const workflow = await linkedapi.fetchPerson.execute({ personUrl: "https://www.linkedin.com/in/john-doe", retrieveExperience: true, retrieveEducation: true, retrieveSkills: true, retrieveLanguages: true, retrievePosts: true, retrieveComments: true, retrieveReactions: true, postsRetrievalConfig: { limit: 20, since: "2024-01-01T00:00:00Z" }, commentsRetrievalConfig: { limit: 15, since: "2024-01-01T00:00:00Z" }, reactionsRetrievalConfig: { limit: 10, since: "2024-01-01T00:00:00Z" } }); const { data, errors } = await linkedapi.fetchPerson.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Person name:', data.name); console.log('Headline:', data.headline); console.log('Location:', data.location); console.log('Experience:', data.experience); console.log('Education:', data.education); console.log('Skills:', data.skills); console.log('Languages:', data.languages); console.log('Posts:', data.posts); console.log('Comments:', data.comments); console.log('Reactions:', data.reactions); console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import FetchPersonParams, LinkedApiError try: workflow = linkedapi.fetch_person.execute( FetchPersonParams( person_url="https://www.linkedin.com/in/john-doe", retrieve_experience=True, retrieve_education=True, retrieve_skills=True, retrieve_languages=True, retrieve_posts=True, retrieve_comments=True, retrieve_reactions=True, posts_retrieval_config={"limit": 20, "since": "2024-01-01T00:00:00Z"}, comments_retrieval_config={"limit": 15, "since": "2024-01-01T00:00:00Z"}, reactions_retrieval_config={"limit": 10, "since": "2024-01-01T00:00:00Z"}, ) ) result = linkedapi.fetch_person.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Person name:", data.name) print("Headline:", data.headline) print("Location:", data.location) print("Experience:", data.experiences) print("Education:", data.education) print("Skills:", data.skills) print("Languages:", data.languages) print("Posts:", data.posts) print("Comments:", data.comments) print("Reactions:", data.reactions) print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to retrieve data for. - `retrieveExperience` (optional) – when set to `true`, includes work experience and job history in the results. - `retrieveEducation` (optional) – when set to `true`, includes educational background and degrees in the results. - `retrieveSkills` (optional) – when set to `true`, includes skills and endorsements in the results. - `retrieveLanguages` (optional) – when set to `true`, includes languages and proficiency levels in the results. - `retrievePosts` (optional) – when set to `true`, includes recent posts and articles in the results. - `retrieveComments` (optional) – when set to `true`, includes comments made by the person in the results. - `retrieveReactions` (optional) – when set to `true`, includes reactions/likes given by the person in the results. - `postsRetrievalConfig` (optional) – configuration for retrieving posts. Available only if `retrievePosts` is `true`. - `limit` (optional) – number of posts to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter posts published after the specified time. - `commentsRetrievalConfig` (optional) – configuration for retrieving comments. Available only if `retrieveComments` is `true`. - `limit` (optional) – number of comments to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter comments made after the specified time. - `reactionsRetrievalConfig` (optional) – configuration for retrieving reactions. Available only if `retrieveReactions` is `true`. - `limit` (optional) – number of reaction to retrieve. Defaults to **20**, with a maximum value of **20**. - `since` (optional) – timestamp to filter reactions made after the specified time. ## Data - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. - `countryCode` – two-character code of the person's country. - `position` – current job position of the person. - `companyName` – name of the person's current company. - `companyHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person's current company. - `about` – "About" section text from the person's profile. - `followersCount` – number of followers the person has. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. - `experiences` – array of work experience entries (included only if `retrieveExperience` is `true`). - `position` – job position held by the person. - `companyName` – name of the company where the person worked. - `companyHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company where the person worked. - `employmentType` – type of employment. Enum with the following values: - `fullTime` – full-time employment. - `partTime` – part-time employment. - `selfEmployed` – self-employed work. - `freelance` – freelance work. - `contract` – contract-based employment. - `internship` – internship position. - `apprenticeship` – apprenticeship program. - `seasonal` – seasonal employment. - `locationType` – type of location. Enum with the following values: - `remote` – position is fully remote. - `onSite` – position requires on-site work. - `hybrid` – position is a mix of remote and on-site work. - `description` – description of the job or responsibilities. - `duration` – number of months the person worked in the position. - `startTime` – timestamp of **the first day of the month** when the person started the position. - `endTime` – timestamp **of the last day of the month** when the person ended the position. Returns `null` if the person is still working in this position. - `location` – free-form string indicating the location of the position. - `education` – array of education entries (included only if `retrieveEducation` is `true`). - `schoolName` – name of the institution. - `schoolHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the institution. - `details` – information about the person's education, such as the degree, major, field of study, and other related details. - `skills` – array of skills (included only if `retrieveSkills` is `true`). - `name` – name of the skill. - `languages` – array of languages with proficiency levels (included only if `retrieveLanguages` is `true`). - `name` – name of the language. - `proficiency` – proficiency level in the language. Enum with the following possible values: - `elementary` – basic understanding. - `limitedWorking` – limited ability for routine tasks. - `professionalWorking` – effective in professional settings. - `fullProfessional` – near-native proficiency. - `nativeOrBilingual` – fluent, like a native speaker. - `posts` – array of recent posts and articles (included only if `retrievePosts` is `true`). - `url` – URL of the post. - `activityUrn` – LinkedIn activity or UGC URN of the post, if available. - `time` – timestamp when the post was published. - `type` – type of the post. Enum with possible values: - `original` – for original posts. - `repost` – for reposts. - `author` – original content creator. Can be `null` if the actor cannot be parsed. - For person authors: `type`, `name`, `profileUrl`, `headline`. - For company authors: `type`, `name`, `companyUrl`. - `reposter` – person or company that reshared the post. Non-null only when `type` is `repost`. - For person reposters: `type`, `name`, `profileUrl`, `headline`. - For company reposters: `type`, `name`, `companyUrl`. - `text` – original author's post text, if available. - `repostText` – text added by the reposter on a repost with comment, if available. - `hashtags` – array of hashtags found in the post text, without leading `#`. - `mentions` – array of person and company profile URLs found in the post text. - `externalLinks` – array of outbound URLs found in the post text. - `images` – array of up to 3 preview image URLs, if available. - `documentSlides` – array of carousel or document slide image URLs, if available. - `hasVideo` – boolean indicating if the post contains a video. - `videoThumbnail` – URL of the video thumbnail, if available. - `hasPoll` – boolean indicating if the post contains a poll. - `reactionsCount` – number of reactions on the post. - `commentsCount` – number of comments on the post. - `repostsCount` – number of reposts on the post. - `comments` – array of person comments (included only if `retrieveComments` is `true`). - `postUrl` – URL of the post the comment belongs to. - `time` – timestamp when the comment was left. - `text` – text content of the comment, if available. - `image` – URL of the comment's image, if available. - `reactionsCount` – number of reactions on the comment. - `reactions` – array of person reactions (included only if `retrieveReactions` is `true`). - `postUrl` – URL of the post the reaction belongs to. - `time` – timestamp when the reaction was made. - `type` – enum describing the reaction type. May take one of the following values: - `like` – standard "like". - `celebrate` – celebrates an achievement. - `support` – shows support. - `love` – expresses love or admiration. - `insightful` – appreciates insightful content. - `funny` – reacts to something humorous. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `selfProfileNotAllowed` – action cannot be performed on your own profile. ## fetchPost This method allows you to retrieve post data. ```typescript try { const workflow = await linkedapi.fetchPost.execute({ postUrl: "https://www.linkedin.com/posts/username_activity-id", retrieveComments: true, retrieveReactions: true, commentsRetrievalConfig: { limit: 20, replies: true, sort: 'mostRecent' }, reactionsRetrievalConfig: { limit: 50, }, }); const { data, errors } = await linkedapi.fetchPost.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import FetchPostParams, LinkedApiError try: workflow = linkedapi.fetch_post.execute( FetchPostParams( post_url="https://www.linkedin.com/posts/username_activity-id", retrieve_comments=True, retrieve_reactions=True, comments_retrieval_config={"limit": 20, "replies": True, "sort": "mostRecent"}, reactions_retrieval_config={"limit": 50}, ) ) result = linkedapi.fetch_post.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `postUrl` – LinkedIn URL of the post. - `retrieveComments` (optional) – when set to `true`, includes comments on the post in the results. - `retrieveReactions` (optional) – when set to `true`, includes reactions on the post in the results. - `commentsRetrievalConfig` (optional) – configuration for retrieving comments. Available only if `retrieveComments` is `true`. - `replies` (optional, default: `false`) – when set to `true`, the action includes replies to the comments in the results. - `limit` (optional) – number of comments to retrieve. Also applies to the replies if `replies` set to `true`. Defaults to **10**, with a maximum value of **500**. - `sort` (optional, default: `mostRelevant`) – enum representing comments sorting. Options: - `mostRelevant` – show most relevant comments first. - `mostRecent` – show most recent comments first. - `reactionsRetrievalConfig` (optional) – configuration for retrieving reactions. Available only if `retrieveReactions` is `true`. - `limit` (optional) – number of reactions to retrieve. Defaults to **10**, with a maximum value of **500**. ## Data - `url` – URL of the post. - `activityUrn` – LinkedIn activity or UGC URN of the post, if available. - `time` – timestamp when the post was published. - `type` – type of the post. Enum with possible values: - `original` – for original posts. - `repost` – for reposts. - `author` – original content creator. Can be `null` if the actor cannot be parsed. - For person authors: `type`, `name`, `profileUrl`, `headline`. - For company authors: `type`, `name`, `companyUrl`. - `reposter` – person or company that reshared the post. Non-null only when `type` is `repost`. - For person reposters: `type`, `name`, `profileUrl`, `headline`. - For company reposters: `type`, `name`, `companyUrl`. - `text` – original author's post text, if available. - `repostText` – text added by the reposter on a repost with comment, if available. - `hashtags` – array of hashtags found in the post text, without leading `#`. - `mentions` – array of person and company profile URLs found in the post text. - `externalLinks` – array of outbound URLs found in the post text. - `images` – array of up to 3 preview image URLs, if available. - `documentSlides` – array of carousel or document slide image URLs, if available. - `hasVideo` – boolean indicating if the post contains a video. - `videoThumbnail` – URL of the video thumbnail, if available. - `hasPoll` – boolean indicating if the post contains a poll. - `reactionsCount` – number of reactions on the post. - `commentsCount` – number of comments on the post. - `repostsCount` – number of reposts on the post. - `comments` – array of post comments (included only if `retrieveComments` is `true`). - `commentUrn` – LinkedIn URN of the comment, if available. - `commentUrl` – canonical deep-link URL of the comment, if available. It can be used as the `commentUrl` input for `reactToComment` or `replyToComment`. - `commenterUrl` – public URL of the person or company. - `commenterName` – full name of the person or company. - `commenterHeadline` – headline of the person or company. - `commenterType` – commenter type. Enum with the following values: - `person` – commenter is a person. - `company` – commenter is a company. - `time` – free-form string indicating comment time. For example: `6d`, `2w` or `1y`. - `text` – text content of the comment, if available. - `image` – URL of the comment's image, if available. - `isReply` – boolean value indicates if this comment is a reply to another comment. - `reactionsCount` – number of reactions on the comment. - `repliesCount` – number of replies on the comment. Always returns `0` if `isReply` is true. - `reactions` – array of post reactions (included only if `retrieveReactions` is `true`). - `engagerUrl` – URL of the person or company. - `engagerName` – full name of the person or company. - `engagerHeadline` – headline of the person or company. - `engagerType` – the engager type. Enum with the following values: - `person` – the engager is the person. - `company` – the engager is the company. - `type` – enum describing the reaction type. Possible values: - `like` – standard "like". - `celebrate` – to celebrate an achievement. - `support` – to show support. - `love` – to express love or admiration. - `insightful` – to appreciate insightful content. - `funny` – to react to something humorous. ## Errors - `postNotFound` – provided URL is not an existing LinkedIn post. ## fetchJob This method allows you to retrieve job data. ```typescript try { const workflow = await linkedapi.fetchJob.execute({ jobUrl: "https://www.linkedin.com/jobs/view/4416248954/" }); const { data, errors } = await linkedapi.fetchJob.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import FetchJobParams, LinkedApiError try: workflow = linkedapi.fetch_job.execute( FetchJobParams( job_url="https://www.linkedin.com/jobs/view/4416248954/", ) ) result = linkedapi.fetch_job.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `jobUrl` – LinkedIn URL of the job. ## Data - `jobId` – LinkedIn job identifier. - `jobUrl` – normalized LinkedIn job URL. - `title` – job title. - `companyName` – company name, if available. - `companyUrl` – normalized LinkedIn company URL, if available. - `location` – free-form job location, if available. - `postedDate` – compact relative date, such as `1w`, `3d`, or `2mo`, if available. - `applicantsCount` – number of applicants, if LinkedIn shows it. - `workplaceType` – workplace type label as shown by LinkedIn, if available. - `employmentType` – employment type label as shown by LinkedIn, if available. - `salary` – parsed salary range, if LinkedIn shows one. - `currency` – lowercase currency code, such as `usd`, `eur`, or `gbp`. - `minAmount` – minimum amount in the range. - `maxAmount` – maximum amount in the range. - `period` – one of `yearly`, `monthly`, `hourly`. - `description` – job description text, if available. - `applyUrl` – Easy Apply or external application URL, if available. - `easyApply` – whether the job uses LinkedIn Easy Apply. ## Errors - `jobNotFound` – provided URL is not an existing LinkedIn job. ## reactToPost This method allows you to react to a post using any available reaction type. ```typescript try { const workflow = await linkedapi.reactToPost.execute({ postUrl: "https://www.linkedin.com/posts/username_activity-id", type: "like", companyUrl: "https://www.linkedin.com/company/company1" }); const { errors } = await linkedapi.reactToPost.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Workflow completed successfully.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import ReactToPostParams, LinkedApiError try: workflow = linkedapi.react_to_post.execute( ReactToPostParams( post_url="https://www.linkedin.com/posts/username_activity-id", type="like", company_url="https://www.linkedin.com/company/company1", ) ) result = linkedapi.react_to_post.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Workflow completed successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `postUrl` – LinkedIn URL of the post to react. - `type` – enum describing the reaction type. - `like` – standard "like". - `celebrate` – to celebrate an achievement. - `support` – to show support. - `love` – to express love or admiration. - `insightful` – to appreciate insightful content. - `funny` – to react to something humorous. - `companyUrl` (optional) – LinkedIn company page URL. When provided, the reaction will be posted on behalf of the company. Requires content admin access to the company page. ## Data The method doesn't return any data. ## Errors - `postNotFound` – provided URL is not an existing LinkedIn post. - `noPostingPermission` – no permission to react on behalf of this company. ## commentOnPost This method allows you to leave a comment on a post. ```typescript try { const workflow = await linkedapi.commentOnPost.execute({ postUrl: "https://www.linkedin.com/posts/username_activity-id", text: "Great insights! I completely agree with your perspective on this topic.", companyUrl: "https://www.linkedin.com/company/company1" }); const { data, errors } = await linkedapi.commentOnPost.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import CommentOnPostParams, LinkedApiError try: workflow = linkedapi.comment_on_post.execute( CommentOnPostParams( post_url="https://www.linkedin.com/posts/username_activity-id", text="Great insights! I completely agree with your perspective on this topic.", company_url="https://www.linkedin.com/company/company1", ) ) result = linkedapi.comment_on_post.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `postUrl` – LinkedIn URL of the post to comment. - `text` – comment text, must be up to **1000** characters. - `companyUrl` (optional) – LinkedIn company page URL. When provided, the comment will be posted on behalf of the company. Requires content admin access to the company page. ## Data - `commentUrn` – LinkedIn URN of the created comment, if available. - `commentUrl` – canonical deep-link URL of the created comment, if available. It can be used as the `commentUrl` input for `reactToComment` or `replyToComment`. ## Errors - `postNotFound` – provided URL is not an existing LinkedIn post. - `commentingNotAllowed` – commenting is not allowed on this post. This could be due to the post author's privacy settings, LinkedIn restrictions on commenting, or because the post type does not support comments. - `noPostingPermission` – no permission to comment on behalf of this company. ## reactToComment This method allows you to react to a comment using any available reaction type. ```typescript try { const workflow = await linkedapi.reactToComment.execute({ commentUrl: "https://www.linkedin.com/feed/update/urn:li:activity:123/?dashCommentUrn=urn:li:fsd_comment:(456,urn:li:activity:123)", type: "like" }); const { errors } = await linkedapi.reactToComment.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Workflow completed successfully.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import ReactToCommentParams, LinkedApiError try: workflow = linkedapi.react_to_comment.execute( ReactToCommentParams( comment_url="https://www.linkedin.com/feed/update/urn:li:activity:123/?dashCommentUrn=urn:li:fsd_comment:(456,urn:li:activity:123)", type="like", ) ) result = linkedapi.react_to_comment.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Workflow completed successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `commentUrl` – deep-link URL of the comment to react to. You can obtain it from the `commentUrl` field returned by `commentOnPost`, `replyToComment`, or a comment retrieved via `fetchPost`. - `type` (optional, default: `like`) – enum describing the reaction type. - `like` – standard "like". - `celebrate` – to celebrate an achievement. - `support` – to show support. - `love` – to express love or admiration. - `insightful` – to appreciate insightful content. - `funny` – to react to something humorous. ## Data The method doesn't return any data. ## Errors - `commentNotFound` – the comment could not be opened or no longer exists. ## replyToComment This method allows you to reply to a comment. ```typescript try { const workflow = await linkedapi.replyToComment.execute({ commentUrl: "https://www.linkedin.com/feed/update/urn:li:activity:123/?dashCommentUrn=urn:li:fsd_comment:(456,urn:li:activity:123)", text: "Totally agree — thanks for adding this!" }); const { data, errors } = await linkedapi.replyToComment.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import ReplyToCommentParams, LinkedApiError try: workflow = linkedapi.reply_to_comment.execute( ReplyToCommentParams( comment_url="https://www.linkedin.com/feed/update/urn:li:activity:123/?dashCommentUrn=urn:li:fsd_comment:(456,urn:li:activity:123)", text="Totally agree — thanks for adding this!", ) ) result = linkedapi.reply_to_comment.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `commentUrl` – deep-link URL of the comment to reply to. You can obtain it from the `commentUrl` field returned by `commentOnPost`, `replyToComment`, or a comment retrieved via `fetchPost`. - `text` – reply text. ## Data - `commentUrn` – LinkedIn URN of the created reply, if available. - `commentUrl` – canonical deep-link URL of the created reply, if available. It can be used as the `commentUrl` input for `reactToComment` or `replyToComment`. ## Errors - `commentNotFound` – the comment could not be opened or no longer exists. - `replyingNotAllowed` – replying is not allowed on this comment. ## retrieveSSI This method allows you to retrieve your current SSI (Social Selling Index). ```typescript try { const workflow = await linkedapi.retrieveSSI.execute(); const { data } = await linkedapi.retrieveSSI.result(workflow.workflowId); // The structure of the 'data' object is below if (data) { console.log('Your SSI score:', data.ssi); console.log('Industry ranking:', data.industryTop + '%'); console.log('Network ranking:', data.networkTop + '%'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.retrieve_ssi.execute() result = linkedapi.retrieve_ssi.result(workflow.workflow_id) data = result.data # The structure of the 'data' object is below if data: print("Your SSI score:", data.ssi) print("Industry ranking:", f"{data.industry_top}%") print("Network ranking:", f"{data.network_top}%") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params The method doesn't require any parameters. ## Data - `ssi` – number (1-100) representing your current Social Selling Index. - `industryTop` – percentage (1-100) showing your industry ranking by SSI score. For example, a value of 5 means you are in the top 5% of your industry. - `networkTop` – percentage (1-100) showing your network ranking by SSI score. For example, a value of 10 means you are in the top 10% of your network. ## Errors The method has no execution errors (`errors` is always `[]`). ## retrievePerformance This method allows you to retrieve performance analytics from your [LinkedIn dashboard](https://www.linkedin.com/dashboard/). ```typescript try { const workflow = await linkedapi.retrievePerformance.execute(); const { data, errors } = await linkedapi.retrievePerformance.result(workflow.workflowId); // The structure of the 'data' object is below if (data) { console.log('Followers count:', data.followersCount); console.log('Post views (last 7 days):', data.postViewsLast7Days); console.log('Profile views (last 90 days):', data.profileViewsLast90Days); console.log('Search appearances (previous week):', data.searchAppearancesPreviousWeek); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.retrieve_performance.execute() result = linkedapi.retrieve_performance.result(workflow.workflow_id) data = result.data # The structure of the 'data' object is below if data: print("Followers count:", data.followers_count) print("Post views (last 7 days):", data.post_views_last_7_days) print("Profile views (last 90 days):", data.profile_views_last_90_days) print("Search appearances (previous week):", data.search_appearances_previous_week) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params The method doesn't require any parameters. ## Data - `followersCount` – total number of your followers. - `postViewsLast7Days` – number of views on your posts in the last 7 days. - `profileViewsLast90Days` – number of views on your profile in the last 90 days. - `searchAppearancesPreviousWeek` – number of times your profile appeared in LinkedIn searches during the previous week. ## Errors The method has no execution errors (`errors` is always `[]`). ## retrieveFeed Retrieve posts from the home feed of the account associated with the current API tokens. ```typescript const workflow = await linkedapi.retrieveFeed.execute({ limit: 20 }); const { data, errors } = await linkedapi.retrieveFeed.result(workflow.workflowId); if (errors.length > 0) { console.warn(errors); } for (const post of data ?? []) { console.log(post.url, post.author?.name, post.feedContext); } ``` ```python from linkedapi import RetrieveFeedParams workflow = linkedapi.retrieve_feed.execute(RetrieveFeedParams(limit=20)) result = linkedapi.retrieve_feed.result(workflow.workflow_id) if result.errors: print(result.errors) for post in result.data or []: print(post.url, post.author.name if post.author else None, post.feed_context) ``` ## Params - `limit` (optional) – maximum number of posts to retrieve. Defaults to **20** and accepts values from **1** to **100**. The home feed is algorithmic rather than chronological, so this method has no `since` parameter. It can return fewer posts than requested when the feed stops loading. ## Data Returns an array of posts using the standard post shape, plus `feedContext`. The context is LinkedIn's localized explanation of why the post is shown, such as a connection reacting to it, and is `null` when no context line is present. See the [`st.retrieveFeed` action reference](/docs/action-st-retrieve-feed) for the complete response contract and error list. ## nvSendMessage This method allows you to send a message to a person in Sales Navigator. ```typescript try { const workflow = await linkedapi.nvSendMessage.execute({ personUrl: "https://www.linkedin.com/in/john-doe", text: "Hi John! I'd love to connect and discuss some opportunities.", subject: "Let's Connect!" }); const { errors } = await linkedapi.nvSendMessage.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Sales Navigator message sent successfully.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NvSendMessageParams, LinkedApiError try: workflow = linkedapi.nv_send_message.execute( NvSendMessageParams( person_url="https://www.linkedin.com/in/john-doe", text="Hi John! I'd love to connect and discuss some opportunities.", subject="Let's Connect!", ) ) result = linkedapi.nv_send_message.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Sales Navigator message sent successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person you want to send a message to. - `threadId` (optional) – identifier of an existing conversation thread to reply into, as returned by [`pollInbox`](/sdks/poll-inbox) or [`pollConversations`](/sdks/poll-conversations). Provide either `personUrl` or `threadId`; if both are given, `threadId` takes precedence. - `text` – message text, must be up to **1900** characters. - `subject` – subject line, must be up to **80** characters. Required when starting a new conversation; ignored when replying into an existing thread via `threadId`. ## Data The method doesn't return any data. ## Errors - `personNotFound` – provided URL is not an existing LinkedIn person. - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `messagingNotAllowed` – sending a message to the person is not allowed. This could happen for several reasons: - Your monthly Sales Navigator message limit has been reached. - LinkedIn has restricted your ability to send messages to the person, for example, due to the person's privacy settings. ## nvSyncConversation This method allows you to sync a conversation in Sales Navigator so you can [start polling](/docs/working-with-conversations) it. ```typescript try { const workflow = await linkedapi.nvSyncConversation.execute({ personUrl: "https://www.linkedin.com/in/john-doe", days: 14 }); const { data, errors } = await linkedapi.nvSyncConversation.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log(`Sales Navigator conversation synced. Syncing until ${data.syncUntil}`); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NvSyncConversationParams, LinkedApiError try: workflow = linkedapi.nv_sync_conversation.execute( NvSyncConversationParams(person_url="https://www.linkedin.com/in/john-doe", days=14) ) result = linkedapi.nv_sync_conversation.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print(f"Sales Navigator conversation synced. Syncing until {result.data.sync_until}") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person whose conversation you want to sync. - `days` (optional) – how many days the conversation stays synchronized, from 1 to 90. Defaults to 30. ## Data - `syncUntil` – moment when synchronization of this conversation stops. Once it passes, polling keeps returning the messages collected so far, but they are no longer updated. Call this method again for the same person to start a new period; the accumulated history is preserved. ## Errors - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `personNotFound` – provided URL is not an existing LinkedIn person. ## nvSyncInbox This method enables **whole-inbox monitoring** for your Sales Navigator inbox so you can [poll the inbox](/sdks/poll-inbox) for messages across every conversation. Run it once per account. > Unlike [`nvSyncConversation`](/sdks/nv-sync-conversation), which watches a single person, `nvSyncInbox` tracks your entire Sales Navigator inbox but only captures messages that arrive **after** it is enabled. ```typescript try { const workflow = await linkedapi.nvSyncInbox.execute(); const { errors } = await linkedapi.nvSyncInbox.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Sales Navigator inbox monitoring enabled.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.nv_sync_inbox.execute() result = linkedapi.nv_sync_inbox.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Sales Navigator inbox monitoring enabled.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params This method takes no parameters. ## Data The method doesn't return any data. ## Errors - `noSalesNavigator` – your account does not have Sales Navigator subscription. ## nvManageConversation This method allows you to manage a conversation thread in Sales Navigator by archiving or unarchiving it. ```typescript try { const workflow = await linkedapi.nvManageConversation.execute({ threadId: "2-Zjhm...", operation: "archive" }); const { errors } = await linkedapi.nvManageConversation.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } else { console.log('Sales Navigator conversation updated successfully.'); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NvManageConversationParams, LinkedApiError try: workflow = linkedapi.nv_manage_conversation.execute( NvManageConversationParams( thread_id="2-Zjhm...", operation="archive", ) ) result = linkedapi.nv_manage_conversation.result(workflow.workflow_id) errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") else: print("Sales Navigator conversation updated successfully.") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `threadId` – identifier of the conversation thread to manage, as returned by [`pollInbox`](/sdks/poll-inbox) or [`pollConversations`](/sdks/poll-conversations), or read from the address bar of an open conversation — the `` in `linkedin.com/sales/inbox/`. - `operation` – operation to apply to the thread: `archive` or `unarchive`. ## Data The method doesn't return any data. ## Errors - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `threadNotFound` – provided `threadId` does not match an existing conversation. ## nvSearchCompanies This method allows you to search for companies in Sales Navigator applying various filtering criteria. ```typescript try { const workflow = await linkedapi.nvSearchCompanies.execute({ term: "TechCorp", limit: 10, filter: { sizes: ["51-200", "201-500"] as TSearchCompanySize[], locations: ["San Francisco", "New York"], industries: ["Software Development", "Technology"], annualRevenue: { min: "10", max: "100" } }, customSearchUrl: "https://www.linkedin.com/sales/search/company?query=(spellCorrectionEnabled%3Atrue%2Ckeywords%3ALinked%2520API)" }); const { data, errors } = await linkedapi.nvSearchCompanies.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } if (data) { console.log('Search completed successfully.'); console.log('Found companies:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NvSearchCompaniesParams, LinkedApiError try: workflow = linkedapi.nv_search_companies.execute( NvSearchCompaniesParams( term="TechCorp", limit=10, filter={ "sizes": ["51-200", "201-500"], "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Technology"], "annual_revenue": {"min": "10", "max": "100"}, }, custom_search_url="https://www.linkedin.com/sales/search/company?query=(spellCorrectionEnabled%3Atrue%2Ckeywords%3ALinked%2520API)", ) ) result = linkedapi.nv_search_companies.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") if data: print("Search completed successfully.") print("Found companies:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `term` – (optional) keyword or phrase to search for. - `limit` – (optional) number of search results to return. Defaults to **25**, with a maximum value of **1000**. - `filter` (optional) – object that specifies filtering criteria for companies. When multiple filter fields are specified, they are combined using `AND` logic. - `sizes` (optional) – array of enums representing employee count ranges. Matches if company’s size falls within any of the listed ranges. Options: - `1-10`. - `11-50`. - `51-200`. - `201-500`. - `501-1000`. - `1001-5000`. - `5001-10000`. - `10001+`. - `locations` (optional) – array of free-form strings representing locations. Matches if company is headquartered in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if company operates in any of the listed industries. Takes specific values available in the LinkedIn interface. - `annualRevenue` (optional) – object representing company annual revenue range in million USD: - `min` – enum with options: - `0`. - `0.5`. - `1`. - `2.5`. - `5`. - `10`. - `20`. - `50`. - `100`. - `500`. - `1000`. - `max` – enum with options: - `0.5`. - `1`. - `2.5`. - `5`. - `10`. - `20`. - `50`. - `100`. - `500`. - `1000`. - `1000+`. - `customSearchUrl` (optional) – URL copied from Sales Navigator search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in Sales Navigator. ## Data Array of search results. Each search result contains: - `name` – name of the company. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `employeesCount` – total number of employees associated with the company. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. ## Errors - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## nvSearchPeople This method allows you to search for people in Sales Navigator applying various filtering criteria. ```typescript try { const workflow = await linkedapi.nvSearchPeople.execute({ term: "Product Manager", limit: 10, filter: { firstName: "John", lastName: "Doe", position: "Product Manager", locations: ["San Francisco", "New York"], industries: ["Software Development", "Technology"], currentCompanies: ["Google", "Microsoft"], previousCompanies: ["Apple"], schools: ["Stanford University", "MIT"], yearsOfExperience: ["threeToFive", "sixToTen"] }, customSearchUrl: "https://www.linkedin.com/sales/search/people?query=(recentSearchParam%3A(doLogHistory%3Atrue)%2CspellCorrectionEnabled%3Atrue%2Ckeywords%3ABill%2520Gates)" }); const { data, errors } = await linkedapi.nvSearchPeople.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } if (data) { console.log('Search completed successfully.'); console.log('Found people:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NvSearchPeopleParams, LinkedApiError try: workflow = linkedapi.nv_search_people.execute( NvSearchPeopleParams( term="Product Manager", limit=10, filter={ "first_name": "John", "last_name": "Doe", "position": "Product Manager", "locations": ["San Francisco", "New York"], "industries": ["Software Development", "Technology"], "current_companies": ["Google", "Microsoft"], "previous_companies": ["Apple"], "schools": ["Stanford University", "MIT"], "years_of_experience": ["threeToFive", "sixToTen"], }, custom_search_url="https://www.linkedin.com/sales/search/people?query=(recentSearchParam%3A(doLogHistory%3Atrue)%2CspellCorrectionEnabled%3Atrue%2Ckeywords%3ABill%2520Gates)", ) ) result = linkedapi.nv_search_people.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") if data: print("Search completed successfully.") print("Found people:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `term` – (optional) keyword or phrase to search for. - `limit` – (optional) number of search results to return. Defaults to **25**, with a maximum value of **2500**. - `filter` (optional) – object that specifies filtering criteria for people. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of person. - `lastName` (optional) – last name of person. - `position` (optional) – job position of person. - `locations` (optional) – array of free-form strings representing locations. Matches if person is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if person works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `currentCompanies` (optional) – array of company names. Matches if person currently works at any of the listed companies. - `previousCompanies` (optional) – array of company names. Matches if person previously worked at any of the listed companies. - `schools` (optional) – array of institution names. Matches if person currently attends or previously attended any of the listed institutions. - `yearsOfExperience` (optional) – array of enums representing professional experience. Matches if person’s experience falls within any of the listed ranges. Options: - `lessThanOne` – less than 1 year. - `oneToTwo` – 1 to 2 years. - `threeToFive` – 3 to 5 years. - `sixToTen` – 6 to 10 years. - `moreThanTen` – more than 10 years. - `customSearchUrl` (optional) – URL copied from Sales Navigator search results page after configuring desired filters. When specified, overrides term and filter parameters. Allows using any search configuration available in Sales Navigator. ## Data Array of search results. Each search result contains: - `name` – full name of the person. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `position` – job position of the person. - `location` – free-form string indicating the person's location. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. ## Errors - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `searchingNotAllowed` – LinkedIn has blocked performing the search due to exceeding limits or other restrictions. ## nvFetchCompany This method allows you to retrieve various data about a company from Sales Navigator: basic information, employees, and decision makers. ```typescript try { const workflow = await linkedapi.nvFetchCompany.execute({ companyHashedUrl: "https://www.linkedin.com/company/12345678", retrieveEmployees: true, retrieveDMs: true, employeesRetrievalConfig: { limit: 25, filter: { firstName: "John", lastName: "Smith", positions: ["engineer", "manager"], locations: ["United States", "Canada"], industries: ["Software Development"], schools: ["Stanford University", "MIT"], yearsOfExperiences: ["threeToFive", "sixToTen"] } }, dmsRetrievalConfig: { limit: 5 } }); const { data, errors } = await linkedapi.nvFetchCompany.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Company name:', data.name); console.log('Description:', data.description); console.log('Location:', data.location); console.log('Industry:', data.industry); console.log('Employee count:', data.employeesCount); console.log('Employees:', data.employees); console.log('Decision makers:', data.dms); console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NvFetchCompanyParams, LinkedApiError try: workflow = linkedapi.nv_fetch_company.execute( NvFetchCompanyParams( company_hashed_url="https://www.linkedin.com/company/12345678", retrieve_employees=True, retrieve_dms=True, employees_retrieval_config={ "limit": 25, "filter": { "first_name": "John", "last_name": "Smith", "positions": ["engineer", "manager"], "locations": ["United States", "Canada"], "industries": ["Software Development"], "schools": ["Stanford University", "MIT"], "years_of_experiences": ["threeToFive", "sixToTen"], }, }, dms_retrieval_config={"limit": 5}, ) ) result = linkedapi.nv_fetch_company.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Company name:", data.name) print("Description:", data.description) print("Location:", data.location) print("Industry:", data.industry) print("Employee count:", data.employees_count) print("Employees:", data.employees) print("Decision makers:", data.dms) print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `companyHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url)LinkedIn URL of the company. - `retrieveEmployees` (optional) – when set to `true`, includes company employees with their profiles in the results. - `retrieveDMs` (optional) – when set to `true`, includes decision makers and key personnel in the results. - `employeesRetrievalConfig` (optional) – configuration for retrieving employees. Available only if `retrieveEmployees` is `true`. - `limit` – (optional) maximum number of employees to retrieve. Defaults to **25**, with a maximum value of **2500**. - `filter` (optional) – object that specifies filtering criteria for employees. When multiple filter fields are specified, they are combined using `AND` logic. - `firstName` (optional) – first name of employee. - `lastName` (optional) – last name of employee. - `positions` (optional) – array of job position names. Matches if employee's current position is any of the listed options. - `locations` (optional) – array of free-form strings representing locations. Matches if employee is located in any of the listed locations. - `industries` (optional) – array of enums representing industries. Matches if employee works in any of the listed industries. Takes specific values available in the LinkedIn interface. - `schools` (optional) – array of institution names. Matches if employee currently attends or previously attended any of the listed institutions. - `yearsOfExperiences` (optional) – array of enums representing professional experience. Matches if employee’s experience falls within any of the listed ranges. Options: - `lessThanOne` – less than 1 year. - `oneToTwo` – 1 to 2 years. - `threeToFive` – 3 to 5 years. - `sixToTen` – 6 to 10 years. - `moreThanTen` – more than 10 years. - `dmsRetrievalConfig` (optional) – configuration for retrieving decision makers. Available only if `retrieveDMs` is `true`. - `limit` (optional) – number of decision makers to retrieve. Defaults to **20**, with a maximum value of **20**. If a company has fewer decision makers than specified, only the available ones will be returned. ## Data - `name` – name of the company. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the company. - `description` – description of the company. - `location` – free-form string representing the company headquarters location. - `headquarters` – two-character country code (e.g., "US", "UK") representing headquarters location. - `industry` – enum representing the company industry. Takes specific values available in the LinkedIn interface. - `website` – company's official website URL. - `employeesCount` – total number of employees associated with the company. - `yearFounded` – year the company was established, if available. - `logoUrl` – URL of the company's logo, or `null` if the company has no logo. - `employees` – array of employee profiles (included only if `retrieveEmployees` is `true`). - `name` – full name of the employee. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the employee. - `headline` – headline of the employee. - `location` – free-form string indicating the employee's location. - `dms` – array of decision makers (included only if `retrieveDMs` is `true`). - `name` – full name of the decision-maker. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the decision-maker. - `position` – job position of the decision-maker. - `location` – free-form string indicating the decision-maker's location. - `countryCode` – two-character code of the decision-maker's country. ## Errors - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `companyNotFound` – provided URL is not an existing LinkedIn company. - `retrievingNotAllowed` – LinkedIn has blocked performing the retrieval due to exceeding limits or other restrictions. ## nvFetchPerson This method allows you to retrieve basic information about a person from Sales Navigator. ```typescript try { const workflow = await linkedapi.nvFetchPerson.execute({ personHashedUrl: "https://www.linkedin.com/in/SInQBmjJ015eLr8" }); const { data, errors } = await linkedapi.nvFetchPerson.result(workflow.workflowId); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } // The structure of the 'data' object is below if (data) { console.log('Person name:', data.name); console.log('Headline:', data.headline); console.log('Location:', data.location); console.log('Position:', data.position); console.log('Company:', data.companyName); console.log('Workflow completed successfully. Data:', data); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NvOpenPersonPageParams, LinkedApiError try: workflow = linkedapi.nv_fetch_person.execute( NvOpenPersonPageParams(person_hashed_url="https://www.linkedin.com/in/SInQBmjJ015eLr8") ) result = linkedapi.nv_fetch_person.result(workflow.workflow_id) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") # The structure of the 'data' object is below if data: print("Person name:", data.name) print("Headline:", data.headline) print("Location:", data.location) print("Position:", data.position) print("Company:", data.company_name) print("Workflow completed successfully. Data:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `personHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. ## Data - `name` – full name of the person. - `publicUrl` – [public](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `hashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person. - `headline` – headline of the person. - `location` – free-form string indicating the person's location. - `countryCode` – two-character code of the person's country. - `position` – current job position of the person. - `companyName` – name of the person's current company. - `companyHashedUrl` – [hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person's current company. - `avatarUrl` – URL of the person's profile photo, or `null` if the person has no photo. ## Errors - `noSalesNavigator` – your account does not have Sales Navigator subscription. - `personNotFound` – provided URL is not an existing LinkedIn person. ## customWorkflow This method allows you to execute a custom Linked API workflow using a raw workflow definition for complex multi-step operations. ```typescript try { const workflow = await linkedapi.customWorkflow.execute({ actionType: "st.searchCompanies", term: "AI startup", filter: { locations: ["San Francisco", "New York"], sizes: ["11-50", "51-200"] }, limit: 10, then: { actionType: "st.doForCompanies", then: { actionType: "st.openCompanyPage", basicInfo: true, then: { actionType: "st.retrieveCompanyEmployees", limit: 5, filter: { position: "manager" }, then: { actionType: "st.doForPeople", then: { actionType: "st.sendConnectionRequest", note: "Hi! I'd love to connect and discuss opportunities." } } } } } }); const { data } = await linkedapi.customWorkflow.result(workflow.workflowId); console.log('Custom workflow executed successfully.'); console.log('Workflow result:', data); } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: workflow = linkedapi.custom_workflow.execute( { "actionType": "st.searchCompanies", "term": "AI startup", "filter": { "locations": ["San Francisco", "New York"], "sizes": ["11-50", "51-200"], }, "limit": 10, "then": { "actionType": "st.doForCompanies", "then": { "actionType": "st.openCompanyPage", "basicInfo": True, "then": { "actionType": "st.retrieveCompanyEmployees", "limit": 5, "filter": { "position": "manager", }, "then": { "actionType": "st.doForPeople", "then": { "actionType": "st.sendConnectionRequest", "note": "Hi! I'd love to connect and discuss opportunities.", }, }, }, }, }, } ) result = linkedapi.custom_workflow.result(workflow.workflow_id) data = result.data print("Custom workflow executed successfully.") print("Workflow result:", data) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params Pass a workflow definition object that follows our [HTTP API workflow format](/docs/building-workflows). ## Data Completion object that matches our [HTTP API completion format](/docs/executing-workflows). ## Errors The method has no execution errors (`errors` is always `[]`). ## Iterating over post comments Reacting to a single comment is available directly via [`reactToComment`](/sdks/react-to-comment) and [`replyToComment`](/sdks/reply-to-comment). To react or reply to **every** comment on a post without knowing their URLs upfront, use a custom workflow: retrieve the comments with `st.retrievePostComments`, iterate over them with `st.doForComments`, and either react/reply directly or open each comment first with `st.openComment` to run several comment-scoped actions. ```typescript const workflow = await linkedapi.customWorkflow.execute({ actionType: "st.retrievePostComments", postUrl: "https://www.linkedin.com/posts/username_activity-id", limit: 3, sort: "mostRelevant", then: { actionType: "st.doForComments", then: { actionType: "st.openComment", then: [ { actionType: "st.reactToComment", type: "celebrate" }, { actionType: "st.replyToComment", text: "Great point, thanks for sharing!" } ] } } }); ``` ```python workflow = linkedapi.custom_workflow.execute( { "actionType": "st.retrievePostComments", "postUrl": "https://www.linkedin.com/posts/username_activity-id", "limit": 3, "sort": "mostRelevant", "then": { "actionType": "st.doForComments", "then": { "actionType": "st.openComment", "then": [ {"actionType": "st.reactToComment", "type": "celebrate"}, {"actionType": "st.replyToComment", "text": "Great point, thanks for sharing!"}, ], }, }, } ) ``` > The `st.openComment` wrapper is optional — `st.reactToComment` and `st.replyToComment` can be nested directly under `st.doForComments`. Similarly, `st.doForPosts` accepts `st.reactToPost`, `st.commentOnPost`, `st.retrievePostComments`, and `st.retrievePostReactions` directly, so you can act on each iterated post without wrapping it in `st.openPost`. ## pollConversations This method allows you to poll multiple conversations to retrieve message history and monitor for new messages across both standard and Sales Navigator conversations. > Before polling conversations, you must first sync each conversation using [`syncConversation`](/sdks/sync-conversation) or [`nvSyncConversation`](/sdks/nv-sync-conversation) methods. ```typescript try { // Poll multiple conversations for new messages const { data, errors } = await linkedapi.pollConversations([ { personUrl: "https://www.linkedin.com/in/john-doe", type: "st", // Standard LinkedIn conversation since: "2024-01-01T00:00:00Z" // Optional: only get messages since this date }, { personUrl: "https://www.linkedin.com/sales/people/ABC123", type: "nv", // Sales Navigator conversation since: "2024-01-15T10:30:00Z" }, { personUrl: "https://www.linkedin.com/in/jane-smith", type: "st" // No 'since' parameter = retrieve entire conversation history } ]); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } if (data) { console.log('Successfully retrieved conversations'); data.forEach(conversation => { console.log(`\nConversation with ${conversation.personUrl}:`); console.log(`Type: ${conversation.type}`); console.log(`Messages: ${conversation.messages.length}`); conversation.messages.forEach(message => { const sender = message.sender === 'us' ? 'You' : 'Them'; console.log(`${sender} (${message.time}): ${message.text}`); }); }); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import ConversationPollRequest, LinkedApiError try: # Poll multiple conversations for new messages result = linkedapi.poll_conversations( [ ConversationPollRequest( person_url="https://www.linkedin.com/in/john-doe", type="st", # Standard LinkedIn conversation since="2024-01-01T00:00:00Z", # Optional: only get messages since this date ), ConversationPollRequest( person_url="https://www.linkedin.com/sales/people/ABC123", type="nv", # Sales Navigator conversation since="2024-01-15T10:30:00Z", ), ConversationPollRequest( person_url="https://www.linkedin.com/in/jane-smith", type="st", # No 'since' parameter = retrieve entire conversation history ), ] ) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") if data: print("Successfully retrieved conversations") for conversation in data: print(f"\nConversation with {conversation.person_url}:") print(f"Type: {conversation.type}") print(f"Messages: {len(conversation.messages)}") for message in conversation.messages: sender = "You" if message.sender == "us" else "Them" print(f"{sender} ({message.time}): {message.text}") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params Pass an array of conversation objects each containing the following: - `personUrl` – [public or hashed](/faq/what-are-hashed-url-and-public-url) LinkedIn URL of the person whose conversation you want to poll. - `since` (optional) – timestamp indicating the starting point for retrieving messages. If not provided, the entire conversation history will be returned. - `type` – enum indicating the conversation type: - `st` – for standard conversations. - `nv` – for Sales Navigator conversations. ## Data Array of conversations. Each conversation contains: - `personUrl` – LinkedIn URL of the person. - `since` – timestamp that was used for filtering (if provided). - `type` – conversation type (`st` or `nv`). - `syncUntil` – moment when synchronization of this conversation stops. Once it is in the past, the messages below are still returned but no longer updated — [sync the conversation](/sdks/sync-conversation) again to resume updates. - `messages` – array of messages. - `id` – unique identifier for the message. - `threadId` – identifier of the conversation thread. Pass it to [`sendMessage`](/sdks/send-message) / [`nvSendMessage`](/sdks/nv-send-message) to reply into the thread. May be `null` for messages captured before a thread identifier was known. - `sender` – enum indicating who sent the message. Possible values: - `us` – message was sent by you. - `them` – message was sent by the person. - `text` – message text. - `time` – timestamp when the message was sent or received. ## Errors - `conversationsNotSynced` – conversations must be [synced](/sdks/sync-conversation) before polling: {conversations_list}. --- **Recommended flow for integrations:** If you integrate this method into a frontend application, we recommend the following approach: 1. Leave `since` empty to retrieve the full conversation history. 2. For subsequent requests, pass the **timestamp of the last message** you received as `since` to retrieve only new messages. 3. Continue using `since` to fetch new messages periodically. If new messages are returned, update the `since` timestamp to the latest message time. This flow helps keep conversations up to date and allows your app to handle new message events (e.g., display notifications, play sounds, and so on). ## pollInbox This method polls your monitored inbox to retrieve captured messages and receive new ones across **every** conversation, for both standard and Sales Navigator inboxes. > Before polling, enable inbox monitoring once with [`syncInbox`](/sdks/sync-inbox) or [`nvSyncInbox`](/sdks/nv-sync-inbox). ```typescript try { const { data, errors } = await linkedapi.pollInbox({ since: "2024-01-01T00:00:00Z", // Optional: only messages after this timestamp type: "st", // Optional: "st" | "nv"; omit for both threadId: "2-abc123..." // Optional: restrict to a single thread }); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } if (data) { console.log(`Retrieved ${data.messages.length} messages`); data.messages.forEach(message => { const sender = message.sender === 'us' ? 'You' : 'Them'; console.log(`[${message.type}] ${sender} (${message.time}): ${message.text}`); }); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import InboxPollRequest, LinkedApiError try: result = linkedapi.poll_inbox( InboxPollRequest( since="2024-01-01T00:00:00Z", # Optional: only messages after this timestamp type="st", # Optional: "st" | "nv"; omit for both thread_id="2-abc123...", # Optional: restrict to a single thread ) ) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") if data: print(f"Retrieved {len(data.messages)} messages") for message in data.messages: sender = "You" if message.sender == "us" else "Them" print(f"[{message.type}] {sender} ({message.time}): {message.text}") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params All fields are optional: - `since` – timestamp indicating the starting point for retrieving messages. If not provided, all captured messages are returned. - `type` – enum to filter by inbox type; omit for both: - `st` – standard inbox messages only. - `nv` – Sales Navigator inbox messages only. - `threadId` – restrict the result to a single conversation thread. ## Data - `messages` – flat array of messages across all threads, ordered newest first. - `id` – unique identifier for the message. - `type` – inbox type the message belongs to (`st` or `nv`). - `threadId` – identifier of the conversation thread. Pass it to [`sendMessage`](/sdks/send-message) / [`nvSendMessage`](/sdks/nv-send-message) to reply into the thread. - `personUrl` – LinkedIn URL of the other participant. - `sender` – enum indicating who sent the message. Possible values: - `us` – message was sent by you (through the LinkedIn UI or the Linked API). - `them` – message was sent by the other person. - `text` – message text. - `time` – timestamp when the message was sent or received. --- **Recommended flow for integrations:** 1. Leave `since` empty on the first request to retrieve everything captured so far. 2. For subsequent requests, pass the **timestamp of the most recent message** you received as `since` to fetch only newer messages. 3. Continue polling with an advancing `since`. Whenever new messages arrive, update `since` to the latest message time. For push-based delivery instead of polling, subscribe to the inbox [webhook events](/sdks/webhooks). ## pollNetwork This method polls your monitored network to retrieve captured connection events and receive new ones across the account's connection graph. > Before polling, enable network monitoring once with [`syncNetwork`](/sdks/sync-network). ```typescript try { const { data, errors } = await linkedapi.pollNetwork({ since: "2024-01-01T00:00:00Z", // Optional: only events after this timestamp type: "connectionAccepted" // Optional: filter by a single event type }); // The list of possible execution errors is below if (errors && errors.length > 0) { console.warn('Workflow completed with execution errors:'); errors.forEach(error => { console.warn(` - Type: ${error.type}, Message: ${error.message}`); }); } if (data) { console.log(`Retrieved ${data.events.length} events`); data.events.forEach(event => { console.log(`[${event.type}] ${event.personUrl} (${event.detectedAt})`); }); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import NetworkPollRequest, LinkedApiError try: result = linkedapi.poll_network( NetworkPollRequest( since="2024-01-01T00:00:00Z", # Optional: only events after this timestamp type="connectionAccepted", # Optional: filter by a single event type ) ) data = result.data errors = result.errors # The list of possible execution errors is below if errors: print("Workflow completed with execution errors:") for error in errors: print(f" - Type: {error.type}, Message: {error.message}") if data: print(f"Retrieved {len(data.events)} events") for event in data.events: print(f"[{event.type}] {event.person_url} ({event.detected_at})") except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params All fields are optional: - `since` – timestamp indicating the starting point for retrieving events. If not provided, all captured events are returned. - `type` – filter by a single event type; omit for all: - `connectionRequestReceived` – someone sent you a connection request. - `connectionAccepted` – a new connection that matches a request you sent. - `connectionAdded` – a new connection not attributable to a request you sent. ## Data - `events` – array of connection events, ordered newest first. - `id` – unique identifier for the event. - `type` – one of `connectionRequestReceived`, `connectionAccepted`, or `connectionAdded`. - `personUrl` – LinkedIn URL of the other person. - `detectedAt` – timestamp when Linked API observed the event. --- **Recommended flow for integrations:** 1. Leave `since` empty on the first request to retrieve everything captured so far. 2. For subsequent requests, pass the **timestamp of the most recent event** you received as `since` to fetch only newer events. 3. Continue polling with an advancing `since`. Whenever new events arrive, update `since` to the latest `detectedAt`. Events are retained for **90 days** — poll (or receive webhooks) often enough to consume them within that window, as older events are pruned and will no longer be returned. For push-based delivery instead of polling, subscribe to the network [webhook events](/sdks/webhooks). ## getApiUsage This method allows you to retrieve Linked API usage statistics so you can monitor your limits and stay within their [recommended values](/guides/understanding-linkedin-limits). > In addition to monitoring usage, you can configure action limits for each account on the [platform](https://app.linkedapi.io/). When a limit is reached, actions will automatically return a `limitExceeded` error instead of executing. ```typescript try { // Get usage stats for the last 7 days const endDate = new Date(); const startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000); const { data } = await linkedapi.getApiUsage({ start: startDate.toISOString(), end: endDate.toISOString() }); if (data) { console.log('Usage statistics retrieved successfully'); console.log('Total actions executed:', statsResponse.length); // Analyze the statistics const successfulActions = statsResponse.result?.filter(action => action.success); const failedActions = statsResponse.result?.filter(action => !action.success); console.log('Successful actions:', successfulActions?.length); console.log('Failed actions:', failedActions?.length); } } catch (e) { // A list of all critical errors can be found here: // https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from datetime import datetime, timedelta, timezone from linkedapi import ApiUsageParams, LinkedApiError try: # Get usage stats for the last 7 days end_date = datetime.now(timezone.utc) start_date = end_date - timedelta(days=7) result = linkedapi.get_api_usage( ApiUsageParams( start=start_date.isoformat(), end=end_date.isoformat(), ) ) data = result.data if data: print("Usage statistics retrieved successfully") print("Total actions executed:", len(data)) # Analyze the statistics successful_actions = [action for action in data if action.success] failed_actions = [action for action in data if not action.success] print("Successful actions:", len(successful_actions)) print("Failed actions:", len(failed_actions)) except LinkedApiError as e: # A list of all critical errors can be found here: # https://linkedapi.io/sdks/handling-results-and-errors/#handling-critical-errors print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ## Params - `start` – timestamp from which the statistics will be retrieved. - `end` – timestamp up to which the statistics will be retrieved. > The difference between `start` and `end` must not exceed 30 days. ## Data Array of [HTTP API actions](/docs/actions-overview). Each action contains: - `actionType` – type of the action (e.g., `st.sendMessage`, `st.openCompanyPage`). - `success` – boolean indicating whether the action executed successfully. - `time` – timestamp when the action was executed. ## Errors The method has no execution errors (`errors` is always `[]`). ## Overview The `LinkedApiAdmin` class lets you manage your Linked API subscription status, seats, connected LinkedIn accounts, rate limits, and webhook programmatically. ## Initialization Unlike the main SDK, the Admin class requires only your `linkedApiToken` – no `identificationToken` needed: ```typescript import { LinkedApiAdmin } from '@linkedapi/node'; const admin = new LinkedApiAdmin({ linkedApiToken: 'your-linked-api-token', }); ``` ```python from linkedapi import LinkedApiAdmin, AdminConfig admin = LinkedApiAdmin( AdminConfig(linked_api_token="your-linked-api-token") ) ``` The class exposes four namespaces matching the [Admin API](/docs/admin-overview) resources: - `admin.subscription` – manage subscription status and seats - `admin.accounts` – connect, disconnect, and monitor LinkedIn accounts - `admin.limits` – configure and monitor rate limits - `admin.webhooks` – register and inspect your outbound [webhook](/sdks/webhooks) ## Error handling Admin methods use direct request-response (not workflows), so there is no `execute()` / `result()` pattern. Methods return data directly and throw `LinkedApiError` on failure: ```typescript try { const status = await admin.subscription.getStatus(); console.log(status.status); // 'active', 'trialing', etc. } catch (e) { if (e instanceof LinkedApiError) { console.error(`Error: ${e.type} – ${e.message}`); } } ``` ```python from linkedapi import LinkedApiError try: status = admin.subscription.get_status() print(status.status) # 'active', 'trialing', etc. except LinkedApiError as e: print(f"Error: {e.type} – {e.message}") ``` **Common critical errors:** - `linkedApiTokenRequired` – missing `linked-api-token` - `invalidLinkedApiToken` – token is invalid or expired - `tooManyRequests` – rate limit exceeded (100 requests per 60 seconds) For the complete HTTP API reference, see the [Admin API docs](/docs/admin-overview). ## Subscription Manage your Linked API subscription: check status and adjust seats. See the [Admin overview](/sdks/admin-overview) for initialization. ## Get status ```typescript try { const status = await admin.subscription.getStatus(); console.log(status.status); // 'active' | 'trialing' | 'past_due' | 'canceled' | undefined console.log(status.eligibleForTrial); // boolean console.log(status.cancelAtPeriodEnd); // boolean } catch (e) { if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: status = admin.subscription.get_status() print(status.status) # 'active' | 'trialing' | 'past_due' | 'canceled' | None print(status.eligible_for_trial) # bool print(status.cancel_at_period_end) # bool except LinkedApiError as e: print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ### Data - `status` – `active`, `trialing`, `past_due`, `canceled`, or `undefined` if no subscription. - `eligibleForTrial` – whether a 7-day free trial is available. - `cancelAtPeriodEnd` – whether the subscription is scheduled to cancel at the end of the current billing period. ## Get seats ```typescript const { seats } = await admin.subscription.getSeats(); for (const seat of seats) { console.log(`${seat.seatType} × ${seat.quantity} (${seat.billingPeriod})`); } ``` ```python seats = admin.subscription.get_seats().seats for seat in seats: print(f"{seat.seat_type} × {seat.quantity} ({seat.billing_period})") ``` ### Data Array of seat objects: - `seatType` – `core` or `plus`. The `plus` tier unlocks Sales Navigator actions (`nv.*`). - `quantity` – number of seats. Each seat allows one connected LinkedIn account. - `billingPeriod` – `month` or `year`. ## Set seats New users can start with a 7-day free trial. ```typescript const result = await admin.subscription.setSeats({ quantity: 5, seatType: 'plus', billingPeriod: 'year', }); if (result.status === 'processing') { // No active subscription – redirect user to checkout console.log('Complete payment:', result.paymentLink); } else { console.log('Seats updated'); } ``` ```python from linkedapi import SetSeatsParams result = admin.subscription.set_seats( SetSeatsParams( quantity=5, seat_type="plus", billing_period="year", ) ) if result.status == "processing": # No active subscription – redirect user to checkout print("Complete payment:", result.payment_link) else: print("Seats updated") ``` ### Params - `quantity` – number of seats (1–1000). - `seatType` – `core` or `plus`. - `billingPeriod` – `month` or `year`. ### Data - `status` – `complete` (subscription updated) or `processing` (checkout required). - `paymentLink` – Stripe checkout URL (only when `status` is `processing`). > **Note:** When reducing seats below the number of connected accounts, excess accounts will be automatically frozen. ## Errors All subscription methods may throw: - `linkedApiTokenRequired` – missing token. - `invalidLinkedApiToken` – invalid or expired token. - `tooManyRequests` – rate limit exceeded. For the complete HTTP API reference, see [Admin API: Subscription](/docs/admin-subscription). ## Accounts Manage your connected LinkedIn accounts programmatically. See the [Admin overview](/sdks/admin-overview) for initialization. ## Get all accounts ```typescript try { const { accounts, pendingConnectionSessions } = await admin.accounts.getAll(); for (const account of accounts) { console.log(`${account.name} (${account.status}) – ${account.id}`); console.log(`Profile: ${account.url}`); console.log(`Headline: ${account.headline ?? 'not parsed yet'}`); if (account.reconnectionLink) { console.log(`Reconnect: ${account.reconnectionLink}`); } } for (const session of pendingConnectionSessions) { console.log(`Pending session: ${session.sessionId} (${session.status})`); } } catch (e) { if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: result = admin.accounts.get_all() accounts = result.accounts pending_connection_sessions = result.pending_connection_sessions for account in accounts: print(f"{account.name} ({account.status}) – {account.id}") print(f"Profile: {account.url}") print(f"Headline: {account.headline or 'not parsed yet'}") if account.reconnection_link: print(f"Reconnect: {account.reconnection_link}") for session in pending_connection_sessions: print(f"Pending session: {session.session_id} ({session.status})") except LinkedApiError as e: print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ### Data - `accounts` – array of connected accounts, each containing: - `id` – account UUID. - `name` – LinkedIn account name. - `url` – public LinkedIn profile URL for the connected account. - `avatarUrl` / `avatar_url` – LinkedIn profile image URL, or `null` / `None` when it has not been parsed yet. - `headline` – LinkedIn headline shown below the account name, or `null` / `None` when it has not been parsed yet. - `countryCode` – country code selected during connection. - `identificationToken` – token used in the `identification-token` header for Account API calls. - `status` – `active`, `frozen`, or `reconnection_required`. - `connectedAt` – ISO 8601 timestamp. - `reconnectionSessionId` / `reconnection_session_id` – present when `status` is `reconnection_required`. - `reconnectionLink` / `reconnection_link` – present when `status` is `reconnection_required`. - `pendingConnectionSessions` – array of pending sessions, each containing `sessionId` and `status`. For `reconnection_required` accounts, `getAll` creates a new reconnection session automatically when no active reconnection session exists. ## Refresh account profile info Use `reparseAccountInfo` to refresh the stored LinkedIn profile URL, avatar URL, headline, and name for a connected account. The method starts a background workflow and returns its `workflowId`; call `getAll` after the workflow completes to read the updated account fields. ```typescript const { workflowId } = await admin.accounts.reparseAccountInfo({ accountId: 'f9b4346a-...', }); console.log('Reparse workflow:', workflowId); ``` ```python from linkedapi import ReparseAccountInfoParams result = admin.accounts.reparse_account_info( ReparseAccountInfoParams(account_id="f9b4346a-...") ) print("Reparse workflow:", result.workflow_id) ``` ### Params - `accountId` / `account_id` – UUID of the account to refresh. ### Result - `workflowId` / `workflow_id` – workflow ID for the background reparse operation. `avatarUrl` / `avatar_url` and `headline` can remain empty when LinkedIn does not render those values or the account session needs reconnection. ## Connect a new account Connecting a LinkedIn account is a multi-step process: ```typescript // 1. Create a connection session const { sessionId, connectionLink } = await admin.accounts.createConnectionSession(); console.log('Open this link to connect:', connectionLink); // 2. Poll until the session completes const POLL_INTERVAL_MS = 3000; const MAX_WAIT_MS = 2 * 60 * 1000; const deadline = Date.now() + MAX_WAIT_MS; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); let session; do { if (Date.now() >= deadline) { throw new Error('Timed out waiting for connection session completion'); } await sleep(POLL_INTERVAL_MS); session = await admin.accounts.getConnectionSession({ sessionId }); } while (session.session.status === 'pending' || session.session.status === 'preparing' || session.session.status === 'serving' || session.session.status === 'streaming'); if (session.session.status === 'success') { console.log('Account connected successfully!'); // 3. Get the new account details const { accounts } = await admin.accounts.getAll(); console.log('Total accounts:', accounts.length); } ``` ```python import time from linkedapi import GetConnectionSessionParams # 1. Create a connection session created = admin.accounts.create_connection_session() session_id = created.session_id print("Open this link to connect:", created.connection_link) # 2. Poll until the session completes POLL_INTERVAL_SECONDS = 3 MAX_WAIT_SECONDS = 2 * 60 deadline = time.monotonic() + MAX_WAIT_SECONDS in_progress_statuses = {"pending", "preparing", "serving", "streaming"} while True: if time.monotonic() >= deadline: raise TimeoutError("Timed out waiting for connection session completion") time.sleep(POLL_INTERVAL_SECONDS) session = admin.accounts.get_connection_session( GetConnectionSessionParams(session_id=session_id) ) if session.session.status not in in_progress_statuses: break if session.session.status == "success": print("Account connected successfully!") # 3. Get the new account details accounts = admin.accounts.get_all().accounts print("Total accounts:", len(accounts)) ``` ### createConnectionSession data - `sessionId` – session UUID for tracking. - `connectionLink` – URL to open in a browser to complete the LinkedIn login. ### createConnectionSession errors - `noAvailableSeats` – no available seats. All seats are occupied by active accounts or pending connection sessions. - `dailyConnectionAttemptsExceeded` – too many connection attempts in the last 24 hours. ## Reconnect an account Use `createReconnectionSession` for accounts with `status` equal to `reconnection_required`. The method cancels the previous active reconnection session, if one exists, and returns a fresh reconnection link. If an in-progress reconnection session is applying a pending proxy change, Linked API returns that existing session instead so the proxy change state is preserved. ```typescript const { reconnectionSessionId, reconnectionLink } = await admin.accounts.createReconnectionSession({ accountId: 'f9b4346a-...', }); console.log('Open this link to reconnect:', reconnectionLink); ``` ```python from linkedapi import CreateReconnectionSessionParams created = admin.accounts.create_reconnection_session( CreateReconnectionSessionParams(account_id="f9b4346a-...") ) print("Open this link to reconnect:", created.reconnection_link) ``` ### createReconnectionSession params - `accountId` / `account_id` – UUID of the account to reconnect. ### createReconnectionSession data - `reconnectionSessionId` / `reconnection_session_id` – session UUID for tracking. - `reconnectionLink` / `reconnection_link` – URL to open in a browser to complete LinkedIn reconnection. ### createReconnectionSession errors - `invalidRequestPayload` – the account is not in `reconnection_required` status or cannot be reconnected. ### getConnectionSession params - `sessionId` – session UUID. ### Session statuses | Status | Description | |--------|-------------| | `pending` | Session created, waiting for user to open the link | | `preparing` | Browser is being provisioned | | `serving` | Browser is ready, waiting for user to connect | | `streaming` | User is connected and logging in | | `success` | Login completed, account is being created | | `expired` | Session timed out | | `error` | An error occurred | | `cancelled` | Session was cancelled | ## Cancel connection session ```typescript await admin.accounts.cancelConnectionSession({ sessionId: '990eef7a-...' }); ``` ```python from linkedapi import CancelConnectionSessionParams admin.accounts.cancel_connection_session( CancelConnectionSessionParams(session_id="990eef7a-...") ) ``` ### Params - `sessionId` – session UUID. ## Disconnect account ```typescript await admin.accounts.disconnect({ accountId: 'f9b4346a-...' }); ``` ```python from linkedapi import DisconnectParams admin.accounts.disconnect(DisconnectParams(account_id="f9b4346a-...")) ``` ### Params - `accountId` – UUID of the account to disconnect. > **Warning:** This action is irreversible. The account must be reconnected from scratch. ## Regenerate identification token ```typescript const { token } = await admin.accounts.regenerateIdentificationToken({ accountId: 'f9b4346a-...', }); console.log('New token:', token); ``` ```python from linkedapi import RegenerateTokenParams result = admin.accounts.regenerate_identification_token( RegenerateTokenParams(account_id="f9b4346a-...") ) print("New token:", result.token) ``` ### Params - `accountId` – UUID of the account. ### Data - `token` – the new identification token. > **Important:** Update the `identificationToken` in all your SDK instances immediately after regeneration. ## Errors All account methods may throw: - `linkedApiTokenRequired` – missing token. - `invalidLinkedApiToken` – invalid or expired token. - `accountNotFound` – account does not exist or does not belong to you. - `tooManyRequests` – rate limit exceeded. For the complete HTTP API reference, see [Admin API: Accounts](/docs/admin-accounts) and [Connection Sessions](/docs/admin-connection-sessions). ## Limits Configure and monitor rate limits for your LinkedIn accounts. See the [Admin overview](/sdks/admin-overview) for initialization. ## Get defaults Get the system default limits. These are applied when an account has no custom limits or after resetting. ```typescript try { const { limits } = await admin.limits.getDefaults(); for (const limit of limits) { console.log(`${limit.category} (${limit.period}): max ${limit.maxValue}, enabled: ${limit.isEnabled}`); } } catch (e) { if (e instanceof LinkedApiError) { console.error(`Critical Error - Type: ${e.type}, Message: ${e.message}`); } else { console.error('An unexpected, non-API error occurred:', e); } } ``` ```python from linkedapi import LinkedApiError try: limits = admin.limits.get_defaults().limits for limit in limits: print(f"{limit.category} ({limit.period}): max {limit.max_value}, enabled: {limit.is_enabled}") except LinkedApiError as e: print(f"Critical Error - Type: {e.type}, Message: {e.message}") except Exception as error: print("An unexpected, non-API error occurred:", error) ``` ### Data Array of limit objects: - `category` – limit category (see table below). - `period` – `daily`, `weekly`, or `monthly`. - `maxValue` – maximum allowed actions. - `isEnabled` – whether this limit is enforced. ### Limit categories | Category | Description | |----------|-------------| | `stPersonProfileViews` | Standard LinkedIn profile views | | `stCompanyPageViews` | Standard LinkedIn company page views | | `stConnectionRequests` | Connection requests sent | | `stMessages` | Messages sent | | `stSearchQueries` | LinkedIn search queries | | `stReactions` | Post reactions | | `stComments` | Post comments | | `stPosts` | Posts created | | `nvPersonProfileViews` | Sales Navigator profile views | | `nvCompanyPageViews` | Sales Navigator company page views | | `nvMessages` | Sales Navigator messages | ## Get account limits ```typescript const { limits } = await admin.limits.get({ accountId: 'f9b4346a-...' }); ``` ```python from linkedapi import GetLimitsParams limits = admin.limits.get(GetLimitsParams(account_id="f9b4346a-...")).limits ``` ### Params - `accountId` – account UUID. ## Get usage Check current usage against configured limits. ```typescript const { usage } = await admin.limits.getUsage({ accountId: 'f9b4346a-...' }); for (const entry of usage) { const remaining = entry.maxValue - entry.currentValue; console.log(`${entry.category} (${entry.period}): ${entry.currentValue}/${entry.maxValue} (${remaining} remaining)`); } ``` ```python from linkedapi import GetLimitsUsageParams usage = admin.limits.get_usage(GetLimitsUsageParams(account_id="f9b4346a-...")).usage for entry in usage: remaining = entry.max_value - entry.current_usage print(f"{entry.category} ({entry.period}): {entry.current_usage}/{entry.max_value} ({remaining} remaining)") ``` ### Params - `accountId` – account UUID. ### Data Array of usage objects: - `category` – limit category. - `period` – `daily`, `weekly`, or `monthly`. - `maxValue` – maximum allowed actions. - `currentValue` – actions performed in the current period. - `isEnabled` – whether this limit is enforced. ## Set limits Set or update limits for an account. Only the specified limits are created or updated; other limits remain unchanged. ```typescript await admin.limits.set({ accountId: 'f9b4346a-...', limits: [ { category: 'stMessages', period: 'daily', maxValue: 25, isEnabled: true, }, { category: 'stConnectionRequests', period: 'weekly', maxValue: 30, }, ], }); ``` ```python from linkedapi import SetLimitEntry, SetLimitsParams admin.limits.set( SetLimitsParams( account_id="f9b4346a-...", limits=[ SetLimitEntry( category="stMessages", period="daily", max_value=25, is_enabled=True, ), SetLimitEntry( category="stConnectionRequests", period="weekly", max_value=30, ), ], ) ) ``` ### Params - `accountId` – account UUID. - `limits` – array of limit configurations: - `category` – limit category. - `period` – `daily`, `weekly`, or `monthly`. - `maxValue` – maximum allowed actions (>= 0). - `isEnabled` – whether this limit is enforced (default: `true`). ## Delete limits Remove specific limits from an account. The account will fall back to default behavior for removed limits. ```typescript await admin.limits.delete({ accountId: 'f9b4346a-...', limits: [ { category: 'stMessages', period: 'daily' }, { category: 'stConnectionRequests', period: 'weekly' }, ], }); ``` ```python from linkedapi import DeleteLimitEntry, DeleteLimitsParams admin.limits.delete( DeleteLimitsParams( account_id="f9b4346a-...", limits=[ DeleteLimitEntry(category="stMessages", period="daily"), DeleteLimitEntry(category="stConnectionRequests", period="weekly"), ], ) ) ``` ### Params - `accountId` – account UUID. - `limits` – array of limits to remove, each with `category` and `period`. ## Reset to defaults Reset all limits for an account to the system defaults. ```typescript await admin.limits.resetToDefaults({ accountId: 'f9b4346a-...' }); ``` ```python from linkedapi import ResetLimitsParams admin.limits.reset_to_defaults(ResetLimitsParams(account_id="f9b4346a-...")) ``` ### Params - `accountId` – account UUID. ## Errors All limit methods may throw: - `linkedApiTokenRequired` – missing token. - `invalidLinkedApiToken` – invalid or expired token. - `accountNotFound` – account does not exist or does not belong to you. - `tooManyRequests` – rate limit exceeded. For the complete HTTP API reference, see [Admin API: Limits](/docs/admin-limits). ## Webhooks Manage your outbound webhook and parse incoming deliveries into typed events. See the [Admin overview](/sdks/admin-overview) for how to create the `admin` client, and the [Webhooks API](/docs/webhooks) for the underlying event model. Webhook management lives on the admin client (`admin.webhooks`); event parsing is a standalone helper exported from the package root. ## Register a webhook A client may hold **one active webhook**. It receives every event Linked API emits. ```typescript const webhook = await admin.webhooks.set({ url: 'https://example.com/hooks/linkedapi', payloadMode: 'fat', // 'fat' inlines the workflow result; 'thin' sends a reference only }); console.log(`Webhook ${webhook.id} → ${webhook.url}`); ``` ```python from linkedapi import SetWebhookParams webhook = admin.webhooks.set( SetWebhookParams( url="https://example.com/hooks/linkedapi", payload_mode="fat", # 'fat' inlines the workflow result; 'thin' sends a reference only ) ) print(f"Webhook {webhook.id} → {webhook.url}") ``` ### Params - `url` – HTTPS endpoint that will receive deliveries. - `payloadMode` / `payload_mode` – optional, `"fat"` (default) or `"thin"`. ### Data - `id` – webhook subscription id. - `url` – the registered destination. - `payloadMode` / `payload_mode` – current payload mode. - `isActive` / `is_active` – whether the webhook is active. - `createdAt` / `created_at` – ISO 8601 timestamp. There is no update method – to change the destination, `delete` the webhook and `set` a new one. ## Get the active webhook ```typescript const webhooks = await admin.webhooks.get(); console.log(webhooks[0]?.url ?? 'no active webhook'); ``` ```python webhooks = admin.webhooks.get() print(webhooks[0].url if webhooks else "no active webhook") ``` Returns an array with at most one entry. ## Change payload mode ```typescript await admin.webhooks.setPayloadMode({ id: 'whs-...', payloadMode: 'thin' }); ``` ```python from linkedapi import SetWebhookPayloadModeParams admin.webhooks.set_payload_mode( SetWebhookPayloadModeParams(id="whs-...", payload_mode="thin") ) ``` ## Delete the webhook Soft delete: the delivery history is preserved and pending deliveries are dropped. You can register a fresh webhook afterwards. ```typescript await admin.webhooks.delete({ id: 'whs-...' }); ``` ```python from linkedapi import DeleteWebhookParams admin.webhooks.delete(DeleteWebhookParams(id="whs-...")) ``` ## Inspect deliveries Return the most recent deliveries (newest first) as a debug feed. ```typescript const deliveries = await admin.webhooks.deliveries(); for (const delivery of deliveries) { console.log(`${delivery.eventType} → ${delivery.status} (attempts: ${delivery.attempts})`); if (delivery.lastError) { console.log(` last error: ${delivery.lastError} (HTTP ${delivery.responseStatusCode})`); } } ``` ```python deliveries = admin.webhooks.deliveries() for delivery in deliveries: print(f"{delivery.event_type} → {delivery.status} (attempts: {delivery.attempts})") if delivery.last_error: print(f" last error: {delivery.last_error} (HTTP {delivery.response_status_code})") ``` ### Data Each delivery has the following shape: ```typescript interface TWebhookDelivery { id: string; // delivery identifier, passed to replayDelivery eventType: TWebhookEventType; // the event type that was delivered eventId: string; // the envelope id of the delivered event status: 'pending' | 'delivering' | 'success' | 'failed'; attempts: number; // delivery attempts made so far responseStatusCode: number | null; // HTTP status your endpoint returned on the last attempt lastError: string | null; // error text from the last failed attempt createdAt: string; // ISO 8601 updatedAt: string; // ISO 8601 } ``` ```python class WebhookDelivery: id: str # delivery identifier, passed to replay_delivery event_type: WebhookEventType # the event type that was delivered event_id: str # the envelope id of the delivered event status: str # "pending" | "delivering" | "success" | "failed" attempts: int # delivery attempts made so far response_status_code: int | None # HTTP status your endpoint returned on the last attempt last_error: str | None # error text from the last failed attempt created_at: str # ISO 8601 updated_at: str # ISO 8601 ``` ## Replay a delivery Re-arm an already-settled delivery. The same event id is reused, so your idempotency handling still applies. ```typescript await admin.webhooks.replayDelivery({ deliveryId: 'whd-...' }); ``` ```python from linkedapi import ReplayWebhookDeliveryParams admin.webhooks.replay_delivery(ReplayWebhookDeliveryParams(delivery_id="whd-...")) ``` ## Send a test event Emit a synthetic `webhook.test` event to verify a freshly registered endpoint end-to-end. ```typescript await admin.webhooks.sendTest(); ``` ```python admin.webhooks.send_test() ``` ## Receiving and parsing events Use `parseWebhookEvent` / `parse_webhook_event` to turn a raw request body into a typed, discriminated event. Pass the **raw** body exactly as received, then branch on `type` to narrow `data`. ```typescript import express from 'express'; import { parseWebhookEvent } from '@linkedapi/node'; const app = express(); app.use(express.raw({ type: 'application/json' })); app.post('/hooks/linkedapi', (req, res) => { const event = parseWebhookEvent(req.body as Buffer); switch (event.type) { case 'workflow.completed': console.log(`Workflow ${event.data.workflowId} finished: ${event.data.status}`); console.log('Result:', event.data.result); // present in 'fat' mode only break; case 'account.reconnectionRequired': console.log(`Account ${event.data.accountId} needs reconnection.`); break; case 'inbox.messageReceived': console.log(`New message from ${event.data.personUrl}: ${event.data.text}`); break; case 'inbox.messageSent': console.log(`Outgoing message in thread ${event.data.threadId}`); break; case 'network.connectionAccepted': console.log(`${event.data.personUrl} accepted your connection request`); break; case 'network.connectionRequestReceived': console.log(`New connection request from ${event.data.personUrl}`); break; case 'webhook.test': console.log('Test event:', event.data.message); break; } // Acknowledge fast. A non-2xx response makes Linked API retry with backoff. res.sendStatus(200); }); ``` ```python from fastapi import FastAPI, Request, Response from linkedapi import parse_webhook_event app = FastAPI() @app.post("/hooks/linkedapi") async def receive(request: Request) -> Response: event = parse_webhook_event(await request.body()) if event.type == "workflow.completed": print(f"Workflow {event.data.workflow_id} finished: {event.data.status}") print("Result:", event.data.result) # present in 'fat' mode only elif event.type == "account.reconnectionRequired": print(f"Account {event.data.account_id} needs reconnection.") elif event.type == "inbox.messageReceived": print(f"New message from {event.data.person_url}: {event.data.text}") elif event.type == "inbox.messageSent": print(f"Outgoing message in thread {event.data.thread_id}") elif event.type == "network.connectionAccepted": print(f"{event.data.person_url} accepted your connection request") elif event.type == "network.connectionRequestReceived": print(f"New connection request from {event.data.person_url}") elif event.type == "webhook.test": print("Test event:", event.data.message) # Acknowledge fast. A non-2xx response makes Linked API retry with backoff. return Response(status_code=200) ``` `parseWebhookEvent` / `parse_webhook_event` throws when the body is not valid JSON or is missing the `id` / `type` envelope fields. Deduplicate on `event.id` – deliveries are at-least-once, so the same event can arrive more than once. The returned union (`TWebhookEvent` in Node, `WebhookEvent` in Python) covers workflow events, account events, [inbox message events](/docs/webhooks#inbox-events), [network events](/docs/webhooks#network-events), and the test event. See the [Webhooks API](/docs/webhooks) for the full event catalog. ## Errors All webhook methods throw `LinkedApiError` on failure – see [Admin overview](/sdks/admin-overview#error-handling). For the complete HTTP reference, see the [Webhooks API docs](/docs/webhooks). --- # CLI ## LinkedIn CLI LinkedIn CLI gives AI agents and developers full LinkedIn capabilities through simple shell commands. Built for Claude Code, Cursor, Codex, and any AI agent that can execute shell commands. > Source code available on [GitHub](https://github.com/Linked-API/linkedin-cli). ## Quick start ```bash # 1. Install npm install -g @linkedapi/linkedin-cli # 2. Save your tokens (get them at app.linkedapi.io) linkedin setup # 3. Fetch a profile linkedin person fetch https://www.linkedin.com/in/vprudnikoff --json # 4. Search for people linkedin person search --term "revops engineer" --current-companies "Linked API" --json # 5. Send a message linkedin message send https://www.linkedin.com/in/vprudnikoff "Hey, loved your latest post!" ``` ## Install ```bash npm install -g @linkedapi/linkedin-cli ``` ## Setup ```bash linkedin setup ``` The CLI will ask for your **Linked API Token** and **Identification Token**. Get them at [app.linkedapi.io](https://app.linkedapi.io). For non-interactive environments (CI, scripts): ```bash linkedin setup --linked-api-token=xxx --identification-token=yyy ``` | Flag | Description | | --- | --- | | `--linked-api-token` | Linked API Token (skips prompt) | | `--identification-token` | Identification Token (skips prompt) | ## Multiple accounts Run `linkedin setup` again with different tokens to add more accounts. The last added account becomes active. ```bash # List all accounts (* marks active) linkedin account list # Switch active account linkedin account switch "Vlad" # Update tokens for active account (e.g. after regenerating tokens on the dashboard) linkedin account update # Update tokens for a specific account linkedin account update "Vlad" # Rename a saved account linkedin account rename "Vlad" --name "My Work Account" # Use a specific account for one command linkedin person fetch https://www.linkedin.com/in/... --account "Vlad" # Remove active account (auto-switches to next) linkedin reset # Remove all accounts linkedin reset --all ``` ## Updating tokens If your tokens were regenerated on the [dashboard](https://app.linkedapi.io), update them in the CLI without removing the account: ```bash linkedin account update ``` This prompts for new tokens, verifies them, and updates the saved account in place. To update a specific account: ```bash linkedin account update "Account Name" ``` For non-interactive environments: ```bash linkedin account update --linked-api-token=xxx --identification-token=yyy ``` | Flag | Description | | --- | --- | | `--linked-api-token` | New Linked API Token (skips prompt) | | `--identification-token` | New Identification Token (skips prompt) | ## How it works Under the hood, [Linked API](https://linkedapi.io) runs a dedicated cloud browser instance for each connected LinkedIn account. When a command is executed, the CLI sends the request to Linked API's infrastructure, where an isolated browser with a residential IP performs the action on LinkedIn – simulating human-like mouse movements, keyboard input, and natural browsing patterns. No local browsers, no proxies, no Selenium, no infrastructure to manage. [Learn more about safety](/safety). ## Global flags Every command supports these flags: | Flag | Description | | --- | --- | | `--json` | Structured JSON output to stdout | | `--fields name,url,...` | Select specific fields in output | | `--quiet` / `-q` | Suppress stderr progress | | `--no-color` | Disable colors | | `--account "Name"` | Use a specific account for this command | ## Output format **JSON mode** (`--json` or non-TTY stdout): ```json {"success": true, "data": {"name": "Vlad Prudnikov", "headline": "CEO at Linked API"}} ``` ```json {"success": false, "error": {"type": "personNotFound", "message": "Person not found"}} ``` **Human mode** (TTY stdout): key-value pairs for objects, tables for arrays. ## Exit codes | Code | Meaning | | --- | --- | | `0` | Success (check `success` field – action may have returned an error like "person not found") | | `1` | General/unexpected error | | `2` | Missing or invalid tokens | | `3` | Subscription/plan required | | `4` | LinkedIn account issue | | `5` | Invalid arguments | | `6` | Rate limited | | `7` | Network error | | `8` | Workflow timeout (workflowId returned for recovery) | ## Important notes - **Sequential execution.** Linked API executes all workflows sequentially per account. You can send multiple requests, but they queue and run one at a time. - **Not instant.** Simple operations (fetch a profile) take ~10-20 seconds. Complex operations (search with filters) can take longer. This is because a real cloud browser navigates LinkedIn on your behalf. - **Timestamps in UTC.** All dates and times returned by the API are in UTC. - **URL normalization.** All LinkedIn URLs in responses are normalized to `https://www.linkedin.com/...` format without trailing slashes. - **Null fields.** Fields that are unavailable are returned as `null` or `[]`, not omitted. - **Action limits.** Configurable per-account action limits on the [platform](https://app.linkedapi.io) prevent LinkedIn policy violations. When a limit is reached, you get a `limitExceeded` error. - **Admin commands.** Manage your subscription, accounts, and limits with `linkedin admin`. See [Administration](/cli/administration). - **Webhooks.** Receive workflow and account events on your own endpoint instead of polling. See [Webhook Events](/docs/webhooks). ## Person ## person fetch Fetch a LinkedIn person profile with optional additional data sections. ```bash linkedin person fetch [flags] ``` | Arg | Required | Description | | --- | --- | --- | | `url` | yes | LinkedIn profile URL | | Flag | Type | Description | | --- | --- | --- | | `--experience` | boolean | Include work experience | | `--education` | boolean | Include education history | | `--skills` | boolean | Include skills | | `--languages` | boolean | Include languages | | `--posts` | boolean | Include recent posts | | `--comments` | boolean | Include recent comments | | `--reactions` | boolean | Include recent reactions | | `--posts-limit` | integer | Max posts to retrieve (requires `--posts`) | | `--posts-since` | string | Posts since ISO timestamp (requires `--posts`) | | `--comments-limit` | integer | Max comments to retrieve (requires `--comments`) | | `--comments-since` | string | Comments since ISO timestamp (requires `--comments`) | | `--reactions-limit` | integer | Max reactions to retrieve (requires `--reactions`) | | `--reactions-since` | string | Reactions since ISO timestamp (requires `--reactions`) | ```bash # Basic profile info linkedin person fetch https://www.linkedin.com/in/vprudnikoff # Full profile with experience and education linkedin person fetch https://www.linkedin.com/in/vprudnikoff --experience --education --json # Profile with recent posts (last 5) linkedin person fetch https://www.linkedin.com/in/vprudnikoff --posts --posts-limit 5 # Everything linkedin person fetch https://www.linkedin.com/in/vprudnikoff \ --experience --education --skills --languages \ --posts --comments --reactions --json ``` ## person search Search for people on LinkedIn with filters. ```bash linkedin person search [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--term` | string | Search keyword or phrase | | `--limit` | integer | Max results to return | | `--first-name` | string | Filter by first name | | `--last-name` | string | Filter by last name | | `--position` | string | Filter by job position | | `--locations` | string | Comma-separated locations | | `--industries` | string | Comma-separated industries | | `--current-companies` | string | Comma-separated current company names | | `--previous-companies` | string | Comma-separated previous company names | | `--schools` | string | Comma-separated school names | ```bash linkedin person search --term "revops engineer" --locations "San Francisco" linkedin person search --current-companies "Linked API" --position "Engineer" --json linkedin person search --schools "MIT" --industries "Software Development" --limit 20 --json ``` ## Company ## company fetch Fetch a LinkedIn company profile with optional employees, decision makers, and posts. ```bash linkedin company fetch [flags] ``` | Arg | Required | Description | | --- | --- | --- | | `url` | yes | LinkedIn company URL | | Flag | Type | Description | | --- | --- | --- | | `--employees` | boolean | Include employee data | | `--dms` | boolean | Include decision makers | | `--posts` | boolean | Include company posts | | `--employees-limit` | integer | Max employees to retrieve | | `--employees-first-name` | string | Filter employees by first name | | `--employees-last-name` | string | Filter employees by last name | | `--employees-position` | string | Filter employees by position | | `--employees-locations` | string | Filter employees by locations, comma-separated | | `--employees-industries` | string | Filter employees by industries, comma-separated | | `--employees-schools` | string | Filter employees by schools, comma-separated | | `--dms-limit` | integer | Max decision makers to retrieve | | `--posts-limit` | integer | Max posts to retrieve | | `--posts-since` | string | Posts since ISO timestamp | ```bash # Basic company info linkedin company fetch https://www.linkedin.com/company/flutterwtf # Company with employees and decision makers linkedin company fetch https://www.linkedin.com/company/flutterwtf --employees --dms --json # Filter employees by position and location linkedin company fetch https://www.linkedin.com/company/flutterwtf \ --employees --employees-position "Engineer" --employees-locations "United States" # Company posts from last month linkedin company fetch https://www.linkedin.com/company/flutterwtf \ --posts --posts-since 2024-12-01T00:00:00Z --json ``` ## company search Search for companies on LinkedIn with filters. ```bash linkedin company search [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--term` | string | Search keyword or phrase | | `--limit` | integer | Max results to return | | `--sizes` | string | Company sizes, comma-separated (`1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+`) | | `--locations` | string | Comma-separated locations | | `--industries` | string | Comma-separated industries | ```bash linkedin company search --term "fintech" --sizes "11-50,51-200" --json linkedin company search --industries "Software Development" --locations "Berlin" --json ``` ## Jobs ## jobs search Search for jobs on LinkedIn with filters. ```bash linkedin jobs search [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--term` | string | Search keyword or phrase | | `--limit` | integer | Max results to return | | `--location` | string | Location filter | | `--date-posted` | string | One of `anyTime`, `past24Hours`, `pastWeek`, `pastMonth` | | `--experience-levels` | string | Comma-separated: `internship`, `entryLevel`, `associate`, `midSeniorLevel`, `director`, `executive` | | `--employment-types` | string | Comma-separated: `fullTime`, `partTime`, `contract`, `temporary`, `volunteer`, `internship`, `other` | | `--workplace-types` | string | Comma-separated: `onSite`, `remote`, `hybrid` | | `--companies` | string | Comma-separated company names | | `--industries` | string | Comma-separated industries | | `--job-functions` | string | Comma-separated job functions | | `--easy-apply` | boolean | Only jobs with Easy Apply | | `--has-verifications` | boolean | Only jobs with verification signals | | `--under-10-applicants` | boolean | Only jobs with fewer than 10 applicants | | `--in-your-network` | boolean | Only jobs from your network | | `--fair-chance-employer` | boolean | Only fair chance employer jobs | ```bash linkedin jobs search --term "product manager" --location "San Francisco" --limit 20 linkedin jobs search --term "engineer" --workplace-types "remote,hybrid" --easy-apply --json ``` ## jobs fetch Fetch details for a single LinkedIn job. ```bash linkedin jobs fetch ``` | Arg | Required | Description | | --- | --- | --- | | `url` | yes | LinkedIn job URL | ```bash linkedin jobs fetch https://www.linkedin.com/jobs/view/4416248954/ --json ``` ## Messages ## message send Send a message to a LinkedIn connection. ```bash linkedin message send ``` | Arg | Required | Description | | --- | --- | --- | | `person-url` | conditional | LinkedIn profile URL of the recipient. Optional when `--thread-id` is provided | | `text` | yes | Message text (up to 1900 characters) | | Flag | Type | Description | | --- | --- | --- | | `--thread-id` | string | Reply into an existing conversation thread instead of passing `person-url` | | `--manage` | string | Manage the conversation right after sending, acting on the same thread. One of `archive`, `unarchive`, `star`, `unstar`, `mute`, `unmute` | ```bash linkedin message send https://www.linkedin.com/in/vprudnikoff "Hey, loved your latest post!" linkedin message send --thread-id 2-abc123... "Sounds good, talk soon!" linkedin message send https://www.linkedin.com/in/vprudnikoff "Hey, loved your latest post!" --manage archive ``` ## message get Get conversation messages with a LinkedIn person. ```bash linkedin message get [flags] ``` | Arg | Required | Description | | --- | --- | --- | | `person-url` | yes | LinkedIn profile URL | | Flag | Type | Description | | --- | --- | --- | | `--since` | string | Only retrieve messages since this ISO timestamp | ```bash linkedin message get https://www.linkedin.com/in/vprudnikoff --json linkedin message get https://www.linkedin.com/in/vprudnikoff --since 2024-01-15T10:30:00Z ``` > The first call for a conversation triggers a background sync, which may take longer. Subsequent calls use cached data and are faster. ## message manage Archive, star, or mute a conversation thread. ```bash linkedin message manage ``` | Arg | Required | Description | | --- | --- | --- | | `thread-id` | yes | Conversation thread identifier, as returned by `inbox get` / `message get`, or the `` in `linkedin.com/messaging/thread/` of an open conversation | | `operation` | yes | One of `archive`, `unarchive`, `star`, `unstar`, `mute`, `unmute` | ```bash linkedin message manage 2-abc123... archive linkedin message manage 2-abc123... unmute ``` ## inbox sync Enable whole-inbox monitoring for the account. Run once — afterwards new messages across every conversation become available via `inbox get`. ```bash linkedin inbox sync # standard inbox linkedin inbox sync --nv # Sales Navigator inbox ``` | Flag | Type | Description | | --- | --- | --- | | `--nv` | bool | Enable monitoring for the Sales Navigator inbox instead of the standard one | > Only messages that arrive after this runs are captured. ## inbox get Get messages from the monitored inbox across all conversations. ```bash linkedin inbox get [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--since` | string | Only retrieve messages after this ISO timestamp | | `--type` | string | Filter by inbox type: `st` or `nv` (default: both) | | `--thread-id` | string | Restrict to a single conversation thread | ```bash linkedin inbox get --json linkedin inbox get --since 2024-01-15T10:30:00Z --type nv ``` > Requires inbox monitoring to be enabled once with `inbox sync`. ## Connections ## connection status Check connection status with a LinkedIn person. ```bash linkedin connection status ``` ```bash linkedin connection status https://www.linkedin.com/in/vprudnikoff ``` ## connection send Send a connection request. ```bash linkedin connection send [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--note` | string | Personalized note to include with the request | | `--email` | string | Email address (required by some profiles to connect) | ```bash linkedin connection send https://www.linkedin.com/in/vprudnikoff linkedin connection send https://www.linkedin.com/in/vprudnikoff --note "Love to connect!" linkedin connection send https://www.linkedin.com/in/vprudnikoff --email vlad@example.com ``` ## connection list List your LinkedIn connections with optional filters. ```bash linkedin connection list [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--limit` | integer | Max connections to return | | `--since` | string | Only connections made since this ISO timestamp | | `--first-name` | string | Filter by first name | | `--last-name` | string | Filter by last name | | `--position` | string | Filter by job position | | `--locations` | string | Comma-separated locations | | `--industries` | string | Comma-separated industries | | `--current-companies` | string | Comma-separated current company names | | `--previous-companies` | string | Comma-separated previous company names | | `--schools` | string | Comma-separated school names | > `--since` only works when no filter flags are used. ```bash linkedin connection list --limit 50 --json linkedin connection list --current-companies "Linked API" --position "Engineer" --json linkedin connection list --since 2024-01-01T00:00:00Z --json ``` ## connection pending List pending outgoing connection requests. ```bash linkedin connection pending --json ``` ## connection invitations List incoming connection, company-follow, and newsletter-subscription invitations. ```bash linkedin connection invitations --json ``` ## connection accept Accept an incoming invitation. The type must match the target URL. ```bash linkedin connection accept ``` ```bash linkedin connection accept connect https://www.linkedin.com/in/vprudnikoff linkedin connection accept companyFollow https://www.linkedin.com/company/example linkedin connection accept newsletterSubscribe https://www.linkedin.com/newsletters/example-1234567890 ``` ## connection ignore Ignore an incoming invitation. The type must match the target URL. ```bash linkedin connection ignore ``` ```bash linkedin connection ignore connect https://www.linkedin.com/in/vprudnikoff linkedin connection ignore companyFollow https://www.linkedin.com/company/example linkedin connection ignore newsletterSubscribe https://www.linkedin.com/newsletters/example-1234567890 ``` ## connection withdraw Withdraw a pending connection request. ```bash linkedin connection withdraw [flags] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--unfollow` / `--no-unfollow` | boolean | `true` | Also unfollow the person | ```bash linkedin connection withdraw https://www.linkedin.com/in/vprudnikoff linkedin connection withdraw https://www.linkedin.com/in/vprudnikoff --no-unfollow ``` ## connection remove Remove an existing connection. ```bash linkedin connection remove ``` ```bash linkedin connection remove https://www.linkedin.com/in/vprudnikoff ``` ## network sync Enable network monitoring for the account. Run once — afterwards connection events (accepted requests, new connections, incoming requests) become available via `network events`. ```bash linkedin network sync ``` > Only changes that happen after this runs are captured. ## network events Get connection events from the monitored network. ```bash linkedin network events [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--since` | string | Only retrieve events after this ISO timestamp | | `--type` | string | Filter by event type: `connectionRequestReceived`, `connectionAccepted`, or `connectionAdded` (default: all) | ```bash linkedin network events --json linkedin network events --since 2024-01-15T10:30:00Z --type connectionAccepted ``` > Requires network monitoring to be enabled once with `network sync`. Events are retained for 90 days. ## Feed ## feed retrieve Retrieve posts from the home feed of the active account. ```bash linkedin feed retrieve [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--limit` | integer | Maximum posts to retrieve (1–100, default 20) | ```bash linkedin feed retrieve --limit 50 --json ``` Each post uses the standard post result shape and includes `feedContext`, localized explanation of why the post appears in the feed. ## Posts ## post fetch Fetch a LinkedIn post with optional comments and reactions. ```bash linkedin post fetch [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--comments` | boolean | Include comments | | `--reactions` | boolean | Include reactions | | `--comments-limit` | integer | Max comments to retrieve | | `--comments-sort` | string | Sort order: `mostRelevant` or `mostRecent` | | `--comments-replies` | boolean | Include replies to comments | | `--reactions-limit` | integer | Max reactions to retrieve | > With `--comments`, each returned comment includes `commentUrn` and `commentUrl`. Pass a `commentUrl` to `comment react` / `comment reply` to engage with that comment. ```bash linkedin post fetch https://www.linkedin.com/posts/vprudnikoff_activity-123 # With comments sorted by most recent, including replies linkedin post fetch https://www.linkedin.com/posts/vprudnikoff_activity-123 \ --comments --comments-sort mostRecent --comments-replies --json # With reactions linkedin post fetch https://www.linkedin.com/posts/vprudnikoff_activity-123 \ --comments --reactions --json ``` ## post create Create a LinkedIn post with optional media attachments. ```bash linkedin post create [flags] ``` | Arg | Required | Description | | --- | --- | --- | | `text` | yes | Post text (up to 3000 characters) | | Flag | Type | Description | | --- | --- | --- | | `--company-url` | string | Post on behalf of a company page (requires admin access) | | `--attachments` | string | Attachments as `url:type` or `url:type:name`. Types: `image`, `video`, `document`. Can be specified multiple times. | > Attachment limits: up to 9 images, or 1 video, or 1 document. Cannot mix types. ```bash linkedin post create "Excited to share our latest update!" # With an image linkedin post create "Check this out" \ --attachments "https://example.com/photo.jpg:image" # With a document linkedin post create "Our Q4 report" \ --attachments "https://example.com/report.pdf:document:Q4 Report" # Post as a company linkedin post create "Company announcement" \ --company-url https://www.linkedin.com/company/flutterwtf ``` ## post react React to a LinkedIn post. ```bash linkedin post react --type [flags] ``` | Flag | Type | Required | Description | | --- | --- | --- | --- | | `--type` | string | yes | Reaction type: `like`, `love`, `support`, `celebrate`, `insightful`, `funny` | | `--company-url` | string | no | React on behalf of a company page | ```bash linkedin post react https://www.linkedin.com/posts/vprudnikoff_activity-123 --type like linkedin post react https://www.linkedin.com/posts/vprudnikoff_activity-123 --type celebrate \ --company-url https://www.linkedin.com/company/flutterwtf ``` ## post comment Comment on a LinkedIn post. ```bash linkedin post comment [flags] ``` | Arg | Required | Description | | --- | --- | --- | | `url` | yes | LinkedIn post URL | | `text` | yes | Comment text (up to 1000 characters) | | Flag | Type | Description | | --- | --- | --- | | `--company-url` | string | Comment on behalf of a company page | ```bash linkedin post comment https://www.linkedin.com/posts/vprudnikoff_activity-123 "Great insights!" linkedin post comment https://www.linkedin.com/posts/vprudnikoff_activity-123 "Well said!" \ --company-url https://www.linkedin.com/company/flutterwtf ``` ## comment react React to a comment on a LinkedIn post. ```bash linkedin comment react --type ``` | Arg | Required | Description | | --- | --- | --- | | `commentUrl` | yes | Deep-link URL of the comment, from `post fetch --comments` or a `comment reply` result | | Flag | Type | Required | Description | | --- | --- | --- | --- | | `--type` | string | yes | Reaction type: `like`, `love`, `support`, `celebrate`, `insightful`, `funny` | ```bash linkedin comment react "https://www.linkedin.com/feed/update/urn:li:activity:123/?dashCommentUrn=urn:li:fsd_comment:(456,urn:li:activity:123)" --type like ``` ## comment reply Reply to a comment on a LinkedIn post. ```bash linkedin comment reply [flags] ``` | Arg | Required | Description | | --- | --- | --- | | `commentUrl` | yes | Deep-link URL of the comment to reply to | | `text` | yes | Reply text | The reply's `commentUrn` and `commentUrl` are printed on success (use `--json` for the full object). ```bash linkedin comment reply "https://www.linkedin.com/feed/update/urn:li:activity:123/?dashCommentUrn=urn:li:fsd_comment:(456,urn:li:activity:123)" "Great point, thanks for sharing!" --json ``` ## Statistics ## stats ssi Retrieve your LinkedIn Social Selling Index. ```bash linkedin stats ssi [flags] ``` ```bash linkedin stats ssi --json ``` ## stats performance Retrieve your LinkedIn performance analytics (profile views, post impressions, search appearances). ```bash linkedin stats performance [flags] ``` ```bash linkedin stats performance --json ``` ## stats usage Retrieve Linked API usage statistics for a date range. ```bash linkedin stats usage --start --end [flags] ``` | Flag | Type | Required | Description | | --- | --- | --- | --- | | `--start` | string | yes | Start date in ISO timestamp | | `--end` | string | yes | End date in ISO timestamp | ```bash linkedin stats usage --start 2024-01-01T00:00:00Z --end 2024-01-31T00:00:00Z --json ``` ## Sales Navigator Requires a LinkedIn Sales Navigator subscription. ## navigator person fetch Fetch a person profile via Sales Navigator. ```bash linkedin navigator person fetch ``` | Arg | Required | Description | | --- | --- | --- | | `hashed-url` | yes | Hashed LinkedIn profile URL | ```bash linkedin navigator person fetch https://www.linkedin.com/in/ACwAAA... ``` ## navigator person search Search for people via Sales Navigator with advanced filters. ```bash linkedin navigator person search [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--term` | string | Search keyword or phrase | | `--limit` | integer | Max results to return | | `--first-name` | string | Filter by first name | | `--last-name` | string | Filter by last name | | `--position` | string | Filter by job position | | `--locations` | string | Comma-separated locations | | `--industries` | string | Comma-separated industries | | `--current-companies` | string | Comma-separated current company names | | `--previous-companies` | string | Comma-separated previous company names | | `--schools` | string | Comma-separated school names | | `--years-of-experience` | string | Comma-separated experience ranges: `lessThanOne`, `oneToTwo`, `threeToFive`, `sixToTen`, `moreThanTen` | ```bash linkedin navigator person search --term "VP Marketing" --locations "United States" linkedin navigator person search --years-of-experience "moreThanTen" --position "CEO" --json ``` ## navigator company fetch Fetch a company profile via Sales Navigator with optional employees and decision makers. ```bash linkedin navigator company fetch [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--employees` | boolean | Include employee data | | `--dms` | boolean | Include decision makers | | `--employees-limit` | integer | Max employees to retrieve | | `--employees-first-name` | string | Filter employees by first name | | `--employees-last-name` | string | Filter employees by last name | | `--employees-positions` | string | Filter employees by positions, comma-separated | | `--employees-locations` | string | Filter employees by locations, comma-separated | | `--employees-industries` | string | Filter employees by industries, comma-separated | | `--employees-schools` | string | Filter employees by schools, comma-separated | | `--employees-years-of-experience` | string | Filter employees by experience ranges, comma-separated | | `--dms-limit` | integer | Max decision makers to retrieve | ```bash linkedin navigator company fetch https://www.linkedin.com/sales/company/97ural --employees --dms linkedin navigator company fetch https://www.linkedin.com/sales/company/97ural \ --employees --employees-positions "Engineer,Designer" --employees-locations "Europe" ``` ## navigator company search Search for companies via Sales Navigator with advanced filters including revenue. ```bash linkedin navigator company search [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--term` | string | Search keyword or phrase | | `--limit` | integer | Max results to return | | `--sizes` | string | Company sizes, comma-separated | | `--locations` | string | Comma-separated locations | | `--industries` | string | Comma-separated industries | | `--revenue-min` | string | Min annual revenue in M USD: `0`, `0.5`, `1`, `2.5`, `5`, `10`, `20`, `50`, `100`, `500`, `1000` | | `--revenue-max` | string | Max annual revenue in M USD: `0.5`, `1`, `2.5`, `5`, `10`, `20`, `50`, `100`, `500`, `1000`, `1000+` | ```bash linkedin navigator company search --term "fintech" --sizes "11-50,51-200" linkedin navigator company search --revenue-min 10 --revenue-max 100 --locations "United States" --json ``` ## navigator message send Send a message via Sales Navigator (InMail). ```bash linkedin navigator message send --subject ``` | Arg | Required | Description | | --- | --- | --- | | `person-url` | yes | LinkedIn profile URL of the recipient | | `text` | yes | Message text (up to 1900 characters) | | Flag | Type | Required | Description | | --- | --- | --- | --- | | `--subject` | string | yes | Message subject line (up to 80 characters) | ```bash linkedin navigator message send https://www.linkedin.com/in/vprudnikoff \ "Would love to chat about API integrations" --subject "Partnership Opportunity" ``` ## navigator message get Get Sales Navigator conversation messages. ```bash linkedin navigator message get [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `--since` | string | Only retrieve messages since this ISO timestamp | ```bash linkedin navigator message get https://www.linkedin.com/in/vprudnikoff linkedin navigator message get https://www.linkedin.com/in/vprudnikoff --since 2024-01-15T10:30:00Z ``` ## navigator message manage Archive or unarchive a Sales Navigator conversation thread. ```bash linkedin navigator message manage ``` | Arg | Required | Description | | --- | --- | --- | | `thread-id` | yes | Conversation thread identifier, as returned by `inbox get --type nv` / `navigator message get`, or the `` in `linkedin.com/sales/inbox/` of an open conversation | | `operation` | yes | One of `archive`, `unarchive` | ```bash linkedin navigator message manage 2-abc123... archive ``` ## Custom Workflows ## workflow run Execute a custom workflow definition. Accepts JSON from a file or stdin. The command starts the workflow and returns the workflow ID, current status, and message. ```bash linkedin workflow run [flags] ``` | Flag | Type | Description | | --- | --- | --- | | `-f` / `--file` | string | Path to workflow JSON file | ```bash # From file linkedin workflow run --file workflow.json # From stdin cat workflow.json | linkedin workflow run # Inline echo '{"actions":[...]}' | linkedin workflow run ``` See [Building Workflows](/docs/building-workflows) for the workflow JSON schema. ## workflow status Check status of a running workflow or wait for its completion. In-progress responses include `status` (`pending` or `running`) and `message`. ```bash linkedin workflow status [flags] ``` | Arg | Required | Description | | --- | --- | --- | | `id` | yes | Workflow ID | | Flag | Type | Description | | --- | --- | --- | | `--wait` | boolean | Block until the workflow completes | ```bash # Check current status linkedin workflow status abc123 # Wait for completion linkedin workflow status abc123 --wait --json ``` ## Administration Manage your Linked API subscription status, seats, connected accounts, and rate limits from the terminal. > Admin commands use your **Linked API Token** and do not require an active account. They work even if no LinkedIn accounts are connected. ## Subscription ```bash # Check subscription status linkedin admin subscription status # View current seats linkedin admin subscription seats # Set seats (updates existing or returns checkout link) linkedin admin subscription set-seats --quantity 5 --type plus --period year ``` ## Accounts ```bash # List all connected accounts and pending sessions linkedin admin accounts list # Connect a new LinkedIn account (opens connection link) linkedin admin accounts connect # Create a fresh reconnection link for an account linkedin admin accounts reconnect # Refresh stored profile URL, avatar, headline, and name linkedin admin accounts reparse # Check connection session status linkedin admin accounts session # Cancel a pending connection session linkedin admin accounts cancel-session # Disconnect an account (irreversible) linkedin admin accounts disconnect # Regenerate identification token linkedin admin accounts regenerate-token ``` ### Connection flow ```bash # 1. Start a connection session linkedin admin accounts connect # → Opens connection link, displays sessionId # 2. User logs into LinkedIn in the opened page # 3. Check session status (or wait for the command above to complete) linkedin admin accounts session --json # → {"status": "success"} when done # 4. Verify the account appeared linkedin admin accounts list ``` ### Reconnection flow ```bash # 1. Find accounts that require reconnection linkedin admin accounts list # → reconnection_required accounts include reconnectionLink when available # 2. Create a fresh reconnection session if you need to replace the link linkedin admin accounts reconnect # → Displays reconnectionSessionId and reconnectionLink # 3. Open the reconnectionLink and complete LinkedIn login # 4. Check session status linkedin admin accounts session --json ``` ## Webhooks Register an endpoint that receives [workflow and account events](/docs/webhooks), inspect recent deliveries, and verify the endpoint end-to-end. A client may hold one active webhook at a time. ```bash # Register the webhook (fat inlines the workflow result; thin sends a reference) linkedin admin webhook set https://example.com/hooks/linkedapi linkedin admin webhook set https://example.com/hooks --payload-mode thin # Show the active webhook linkedin admin webhook get # Switch payload mode linkedin admin webhook set-payload-mode thin # Delete the webhook (soft-delete: delivery history is kept) linkedin admin webhook delete # Inspect the most recent deliveries (debug feed, newest first) linkedin admin webhook deliveries # Re-arm an already-settled delivery for redelivery (reuses the same event id) linkedin admin webhook replay # Emit a synthetic webhook.test event to verify the endpoint linkedin admin webhook send-test ``` ## Limits ```bash # View system default limits linkedin admin limits defaults # View current limits for an account linkedin admin limits get # View current usage against limits linkedin admin limits usage # Set limits for an account linkedin admin limits set \ --category stMessages --period daily --max 25 # Delete specific limits (falls back to defaults) linkedin admin limits delete \ --category stMessages --period daily # Reset all limits to defaults linkedin admin limits reset ``` ### Limit categories | Category | Description | | --- | --- | | `stPersonProfileViews` | Standard LinkedIn profile views | | `stCompanyPageViews` | Standard LinkedIn company page views | | `stConnectionRequests` | Connection requests sent | | `stMessages` | Messages sent | | `stSearchQueries` | LinkedIn search queries | | `stReactions` | Post reactions | | `stComments` | Post comments | | `stPosts` | Posts created | | `nvPersonProfileViews` | Sales Navigator profile views | | `nvCompanyPageViews` | Sales Navigator company page views | | `nvMessages` | Sales Navigator messages | Periods: `daily`, `weekly`, `monthly`. For the complete HTTP API reference, see the [Admin API docs](/docs/admin-overview). --- # MCP Server ## Linked API MCP Linked API MCP server connects your LinkedIn account to AI assistants like Claude, Claude Code, ChatGPT, Codex, Cursor, VS Code, Windsurf, and more. Ask them to search for leads, send messages, analyze profiles, and much more – they'll handle it through our cloud browser, safely and automatically. > Source code available on [GitHub](https://github.com/Linked-API/linkedapi-mcp). ## Common use cases - **Sales automation assistant**. Ask your AI to find leads, check their profiles, and draft personalized outreach. It can search for "software engineers at companies with 50-200 employees in San Francisco", analyze their backgrounds, and suggest connection messages that actually make sense. - **Recruitment assistant**. Let your assistant search for candidates with specific skills, review their experience, and send initial outreach. It handles the time-consuming parts while you focus on actually talking to people. - **Conversation assistant**. Your AI can read your existing LinkedIn conversations and help you respond naturally. It understands the context of your chats, suggests relevant replies, and can even send follow-up messages. - **Market research assistant**. Need competitor analysis? Your assistant can gather data about companies, their employees, and recent activities. Get insights about industry trends without spending hours on LinkedIn. ## How to get started To start using Linked API MCP, spend 2 minutes reading these essential guides: 1. [Installation](/mcp/installation) – set up MCP in Claude, Claude Code, ChatGPT, Codex, Cursor, VS Code, Windsurf, and more. 2. [Available tools](/mcp/available-tools) – explore all the LinkedIn tools your assistant can call. 3. [Usage examples](/mcp/usage-examples) – see real-world examples to get you started quickly. ## Installation The MCP server needs tokens to access your LinkedIn account. Here's how to get them: 1. Sign up at [Linked API Platform](https://app.linkedapi.io/). 2. Choose your plan and start your 7-day free trial. 3. Connect your LinkedIn account (it takes about 2 minutes). 4. Copy your tokens from the dashboard (as shown below): ![](/images/docs/tokens-4.webp) **Multiple LinkedIn accounts:** If you have multiple LinkedIn accounts, you'll get a separate identification token for each one, and you can create multiple MCP server instances – one for each account. ## Claude To connect Linked API to Claude, you'll need to add it as a **custom connector**: 1. Open Claude's settings. 2. Navigate to the "Connectors" section. 3. Click on "Add custom connector". 4. In the URL field, paste this link (replace with your actual tokens): ```text https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=claude ``` This works for both [claude.ai](https://claude.ai) and the Claude Desktop app. Connectors sync across both. ## Claude Desktop If you prefer running the MCP server locally instead of using the connector above, add this to your Claude Desktop config file: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "linkedapi": { "command": "npx", "args": ["-y", "@linkedapi/mcp"], "env": { "LINKED_API_TOKEN": "{YOUR_LINKED_API_TOKEN}", "IDENTIFICATION_TOKEN": "{YOUR_IDENTIFICATION_TOKEN}" } } } } ``` Restart Claude Desktop after saving. ## Claude Code Add the remote MCP server with a single command: ```bash claude mcp add linkedapi \ --transport http \ "https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=claude-code" ``` Or add it locally via NPX: ```bash claude mcp add linkedapi \ -e LINKED_API_TOKEN={YOUR_LINKED_API_TOKEN} \ -e IDENTIFICATION_TOKEN={YOUR_IDENTIFICATION_TOKEN} \ -- npx -y @linkedapi/mcp ``` > **Tip:** For Claude Code, you may find it more convenient to use our [LinkedIn CLI](/cli/getting-started) or [Agent Skills](/skills) instead of MCP – they provide a more native integration with the agentic coding workflow. ## ChatGPT To connect Linked API to ChatGPT, you'll need to add it as a **connector**: 1. Navigate to your ChatGPT settings. 2. Find the "Connectors" section. 3. Click on "Add connector". 4. In the URL field, paste this link (replace with your actual tokens): ```text https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=chatgpt ``` ## Codex Add the MCP server to your Codex config file at `~/.codex/config.toml`: ```toml [mcp_servers.linkedapi] url = "https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=codex" ``` Or add it via CLI: ```bash codex mcp add linkedapi \ --url "https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=codex" ``` > **Tip:** For Codex, you may find it more convenient to use our [LinkedIn CLI](/cli/getting-started) instead of MCP – it gives you shell-native LinkedIn commands that work naturally in any terminal-based agent. ## Cursor To connect Linked API to Cursor, follow these steps: 1. Navigate to "Cursor Settings" > "Tools & MCP". 2. Click the "+ Add New MCP Server" button. 3. Paste the following configuration: ```json { "mcpServers": { "linkedapi": { "url": "https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=cursor" } } } ``` You can also add this to `.cursor/mcp.json` in your project root for project-level configuration. ## VS Code VS Code has built-in MCP support (no extension required). Add a `.vscode/mcp.json` file to your project: ```json { "servers": { "linkedapi": { "type": "http", "url": "https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=vscode" } } } ``` Alternatively, add it to your `settings.json`: ```json { "mcp": { "servers": { "linkedapi": { "type": "http", "url": "https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=vscode" } } } } ``` ## Windsurf Add this to your Windsurf configuration file at `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "linkedapi": { "serverUrl": "https://mcp.linkedapi.io?linked-api-token={YOUR_LINKED_API_TOKEN}&identification-token={YOUR_IDENTIFICATION_TOKEN}&client=windsurf" } } } ``` You can also configure it through the UI: open the Cascade panel, click the tools icon, then click "Configure" to edit the configuration file. ## Local installation For clients that support local MCP servers, you can run Linked API MCP server directly on your device using NPX: ```json { "mcpServers": { "linkedapi": { "command": "npx", "args": ["-y", "@linkedapi/mcp"], "env": { "LINKED_API_TOKEN": "{YOUR_LINKED_API_TOKEN}", "IDENTIFICATION_TOKEN": "{YOUR_IDENTIFICATION_TOKEN}" } } } } ``` ## Available tools The MCP server provides these tools for interacting with LinkedIn: ## Standard interface | Tool | Description | | --- | --- | | `send_message` | Send message to person, or reply into an existing thread by `threadId`, with optional archive, star, or mute of the conversation right after sending | | `get_conversation` | Get a conversation with person | | `sync_inbox` | Enable whole-inbox monitoring so every incoming conversation can be polled | | `get_inbox` | Get messages from the monitored inbox across all conversations | | `manage_conversation` | Archive, star, or mute a conversation thread by `threadId` | | `check_connection_status` | Check connection status with person | | `send_connection_request` | Send connection request with optional note | | `withdraw_connection_request` | Withdraw pending connection request | | `accept_invitation` | Accept an incoming invitation by type and matching person, company, or newsletter URL | | `ignore_invitation` | Ignore an incoming invitation by type and matching person, company, or newsletter URL | | `retrieve_pending_requests` | Get all pending connection requests | | `retrieve_invitations` | Get all received connection, company-follow, and newsletter-subscription invitations | | `retrieve_connections` | Get your connections with filtering | | `retrieve_feed` | Get posts from your personalized home feed, with an optional limit from 1 to 100 | | `remove_connection` | Remove person from connections | | `sync_network` | Enable network monitoring so connection events can be polled | | `get_network` | Get connection events from the monitored network | | `search_companies` | Search for companies with advanced filtering | | `search_people` | Search for people with advanced filtering | | `search_jobs` | Search for jobs with advanced filtering | | `fetch_company` | Get company information with optional employees, decision makers, posts | | `fetch_person` | Get person page information with optional experience, education, skills, posts | | `fetch_post` | Get post information and engagement metrics | | `fetch_job` | Get job details such as company, location, salary, description | | `react_to_post` | React to post (like, love, support, celebrate, insightful, funny) | | `comment_on_post` | Leave comment on post; returns the new comment's URN and URL | | `react_to_comment` | React to a comment by `commentUrl` (like, love, support, celebrate, insightful, funny) | | `reply_to_comment` | Reply to a comment by `commentUrl`; returns the reply's URN and URL | | `retrieve_ssi` | Get current SSI (Social Selling Index) | | `retrieve_performance` | Get LinkedIn dashboard analytics | ## Sales Navigator | Tool | Description | | --- | --- | | `nv_send_message` | Send message to person via Sales Navigator, or reply into an existing thread by `threadId` | | `nv_get_conversation` | Get a Sales Navigator conversation with person | | `nv_sync_inbox` | Enable whole-inbox monitoring in Sales Navigator so every incoming conversation can be polled | | `nv_manage_conversation` | Archive or unarchive a Sales Navigator conversation thread by `threadId` | | `nv_search_companies` | Search for companies with advanced filtering via Sales Navigator | | `nv_search_people` | Search for people with advanced filtering via Sales Navigator | | `nv_fetch_company` | Get company information with optional employees and decision makers from Sales Navigator | | `nv_fetch_person` | Get person page information from Sales Navigator | ## Other tools | Tool | Description | | --- | --- | | `execute_custom_workflow` | Execute custom workflow definition | | `get_workflow_result` | Get workflow result by ID | | `get_api_usage` | Get Linked API usage statistics | ## Administration | Tool | Description | | --- | --- | | `admin_get_subscription_status` | Get current subscription status and trial eligibility | | `admin_get_seats` | Get active subscription seats | | `admin_set_seats` | Set number of subscription seats | | `admin_get_accounts` | Get all connected LinkedIn accounts, pending sessions, and reconnection links | | `admin_connect_account` | Create connection session for a new LinkedIn account | | `admin_reparse_account_info` | Refresh stored profile URL, avatar, headline, and name for a connected account | | `admin_create_reconnection_session` | Create a fresh reconnection session and link for an account that requires reconnection | | `admin_get_connection_session` | Get connection or reconnection session status | | `admin_cancel_connection_session` | Cancel a pending connection or reconnection session | | `admin_disconnect_account` | Disconnect a LinkedIn account | | `admin_regenerate_token` | Regenerate identification token for an account | | `admin_get_limits_defaults` | Get system default rate limits | | `admin_get_limits` | Get configured rate limits for an account | | `admin_get_limits_usage` | Get current usage against configured limits | | `admin_set_limits` | Set rate limits for an account | | `admin_delete_limits` | Delete specific account rate limits | | `admin_reset_limits` | Reset all limits to system defaults | ## Usage examples With Linked API MCP, you can ask your AI-assistant things like: > Find all decision makers at Acme Corp and send them connection requests. > Search for product managers at fintech companies in New York with 50-200 employees. > Pull all contacts labeled 'follow-up' from my CRM, review our conversation history, and send each person a personalized follow-up message. > Tell me about 'https://linkedin.com/in/jane-doe' including their work history and experience. > Send a connection request to 'https://linkedin.com/in/jane-doe' mentioning their recent article about AI in healthcare. > Get all my pending connection requests and withdraw each one of them. > Fetch the comments on 'https://linkedin.com/posts/jane-doe_activity-123', then like each one and reply to the most relevant with a thoughtful follow-up. These are just basic examples. Since your assistant can execute custom workflows combining multiple actions, the automation potential is truly limitless. --- # Agent Skills Ready-to-run LinkedIn workflows your AI agent installs in one command and runs in plain language. ## LinkedIn Growth Your agent finds leads from a search you define, qualifies each one against your criteria, invites the matches on a schedule, tracks who accepts, and withdraws what goes unanswered. Install: `npx @linkedapi/skills add linkedin-growth`. Works with Claude Code, Codex, Cursor, Windsurf. ### A loop that runs itself A continuous cycle, not a one-off blast. Set it once and it keeps going. - **Define your target** — A LinkedIn or Sales Navigator search plus a plain-language profile of your ideal lead. - **Auto-qualify every candidate** — Every profile is checked against your criteria; matches are kept, the rest dropped, with a reason recorded. - **Invite on a schedule** — Matches are spread across your accounts and invited during active hours, within safe limits. - **Track who accepts** — Connected, pending, and declined stay up to date on their own. - **Clean up automatically** — Unanswered invites past your threshold are withdrawn, and can be retried from another account. After that, inviting, tracking, and cleaning up run on a schedule on their own. To add new people, you run another import whenever you want. ### Safety Growth sends real connection requests, so pacing comes first. You stay inside LinkedIn-safe limits without thinking about it. - A per-account daily limit and a minimum gap between invites. - Invites only during the active hours you set. - Automatic backoff when LinkedIn signals a limit. - Stale pending requests withdrawn to keep your weekly budget clean. ### FAQ **Does it keep finding new people on its own?** No. You run an import when you want to add people – a search plus your criteria. After that, sending invites, tracking who accepts, and cleaning up run on a schedule on their own. Import again whenever you want more. **How does qualification work?** Each person from your search is checked against your criteria. Matches are kept, the rest are dropped, and a short reason is recorded for each one so you can see why. **Where does it run?** On the machine where you install the skill. It keeps a local database and sends invites on a schedule – no external services beyond LinkedIn CLI and Linked API. **Can I use more than one LinkedIn account?** Yes. People are spread across your connected accounts, each paced within its own limits. If a request goes unanswered, it can be retried from another account. **Is this safe for my account?** Yes. Every invite goes through a dedicated cloud browser that is matched to your account and behaves like a real person, with natural timing. Because Growth sends real connection requests, pacing comes first: invites stay within the daily limit and minimum gap you set, go out only during your active hours, and if LinkedIn signals a limit the account backs off until its next scheduled run. Stale, unanswered requests are withdrawn so your pending list never piles up. [See how we keep accounts safe](/safety). **Can I change my criteria later?** Any time. The search and your criteria are plain language – update them whenever you want, and your next import uses the new rules. **What do I need to get started?** Node.js 20+, the LinkedIn CLI, and your [Linked API tokens](https://app.linkedapi.io). The installer checks for these and offers to set them up. ### Pricing Runs on your existing Linked API plan. Invites count as normal actions – there is no separate fee for the skill. ## LinkedIn General Your agent handles LinkedIn in plain language – fetching profiles, searching people and companies, messaging, connecting, posting, and pulling analytics. Install: `npx @linkedapi/skills add linkedin`. Works with Claude Code, Codex, Cursor, Windsurf. ### Everything your agent can do One skill, the full surface of LinkedIn. Ask in plain language and each action runs in a real cloud browser. - **People** — Fetch full profiles, Search with filters, Posts, comments, reactions - **Companies** — Fetch company pages, Employees & decision-makers, Search by size & industry - **Messaging** — Send messages, Read conversations, InMail via Sales Navigator - **Connections** — Send & withdraw requests, Check status, List & filter your network - **Posts & engagement** — Create posts with media, React and comment, Post as a company - **Insights** — Social Selling Index, Profile & post analytics, API usage stats ### Safety Every action runs in an isolated cloud browser that behaves like a real person, paced within the per-account limits you set. - Each account gets its own isolated cloud browser. - Human-like timing, mouse, and typing on every action. - Per-account daily, weekly, and monthly limits. - Your password is never seen or stored. ### FAQ **What can the LinkedIn skill do?** It covers the full surface of LinkedIn: fetch profiles, search people and companies, send messages, manage connections, create posts, react and comment, and pull analytics. Sales Navigator search and InMail are included too. **How do I control it?** Just describe what you want in plain language. Your agent translates the request into the right LinkedIn actions and runs them – no flags or scripts to learn, though the exact CLI commands are there if you want them. **How fast is it?** Actions are not instant. Each one drives a real cloud browser, so expect anywhere from 30 seconds to a few minutes, and actions for one account run one at a time. **Is this safe for my account?** Yes. Every action runs through a dedicated cloud browser that is matched to your account – the same device fingerprint, language, and location each time – and behaves like a real person, with natural timing and mouse movement. Actions are physically performed in a real browser at human-like speed. On top of that, you cap how much it does with daily, weekly, and monthly limits per account, and your password is never seen or stored. [See how we keep accounts safe](/safety). **Can I use more than one LinkedIn account?** Yes. Connect as many accounts as your plan allows and tell your agent which one to act as – each is paced within its own limits. **Do I need Sales Navigator?** Only for the Sales Navigator features – advanced search and InMail. Everything else works on a standard LinkedIn account. **What do I need to get started?** Node.js 20+, the LinkedIn CLI, and your [Linked API tokens](https://app.linkedapi.io). The installer checks for these and offers to set them up. ### Pricing Runs on your existing Linked API plan. Each action counts as a normal action – there is no separate fee for the skill. --- # n8n Integration ## Linked API + n8n Linked API for n8n connects your LinkedIn account to n8n's automation platform, so you can build visual workflows that search for prospects, send messages, manage connections, extract data, and more – all with [industry-leading safety](/safety). > Our n8n node is a **verified community node** – just open the **nodes panel**, search for "Linked API", and install it with a click. Alternatively, install it manually: go to **Settings** → **Community Nodes**, enter `n8n-nodes-linked-api`, and click **Install**. ### Common use cases - **Lead generation pipeline**.Search for prospects matching your criteria, automatically send connection requests, wait for acceptance, then send personalized follow-up messages. Connect the results to your CRM to track everything in one place. - **CRM synchronization**. When a new lead enters your CRM, automatically find their LinkedIn profile, send a connection request, and update the CRM with their LinkedIn data. Keep your sales pipeline enriched with real-time LinkedIn information. - **Content engagement**. Monitor when your target accounts post on LinkedIn, automatically react or comment on their posts, then log the engagement in your tracking spreadsheet. Build relationships at scale without losing the personal touch. - **Recruitment workflows**. Search for candidates with specific skills, send personalized InMails through Sales Navigator, track responses in your ATS, and automatically follow up with interested candidates. Streamline your entire recruitment funnel. - **Multi-channel outreach.** Combine LinkedIn with email, Slack, and other channels. When someone accepts your connection request, trigger an email sequence, notify your team in Slack, and create a task in your project management tool. ### How to get started To build LinkedIn automations with n8n, read these essential guides: 1. [Creating credential](/integrations/n8n/creating-credential) – get your API tokens and connect to n8n. 2. [Building workflows](/integrations/n8n/building-workflows) – learn how to use Linked API actions in your workflows. 3. [Available actions](/integrations/n8n/available-actions) – explore all LinkedIn actions you can automate. ## Creating credential Setting up Linked API in n8n requires two steps: **getting your API tokens** and **creating credential**. This guide walks you through both steps. ## Getting your tokens 1. Sign up at [Linked API Platform](https://app.linkedapi.io/). 2. Choose your plan and start your 7-day free trial. 3. Connect your LinkedIn account (it takes about 2 minutes). 4. Copy your tokens from the dashboard (as shown below): ![](/images/docs/tokens.webp) ## Creating n8n credential 1. In your n8n workflow, add any Linked API action. 2. Click the "Create new credential" in the action settings. ![](/images/docs/add-credentials-n8n.webp) 3. Paste your tokens into the displayed fields. ![](/images/docs/paste-tokens-n8n.webp) 4. Click "Save". Your credential is now ready to use. ## Multiple LinkedIn accounts If you manage multiple LinkedIn accounts, create a separate credential for each one. Every account has its own **Identification Token**, while the **Linked API Token** remains the same. You can then switch between accounts by selecting different credentials. ## Building workflows Once your credential is set up, you can start building LinkedIn automations in n8n. To add Linked API functionality to your workflows: 1. Search for "Linked API" in the node search panel when building your workflow. ![](/images/docs/node-search-n8n.png) 2. Select the action that matches what you want to do: send messages, search for people, retrieve company data, etc. See the full [list of available actions](/integrations/n8n/available-actions). 3. Configure the module with your specific parameters and select the credential [you created earlier](/integrations/n8n/creating-credential). ![](/images/docs/select-credential-n8n.png) 4. Search for "Wait" in the node search and add it to your workflow. 5. In the Wait node, find the "Resume" field and select "On Webhook Call". ![](/images/docs/wait-node-config-n8n.png) 6. Connect your Wait node to the Linked API node. ![](/images/docs/wait-demo-n8n.png) > The Wait node is required for [all actions](/integrations/n8n/available-actions) except **Cancel workflow**, **Get actions statistics**, **Get workflow result**, **Poll conversations**, **Poll inbox**, and **Poll network** – these return results directly. ### Combining with other tools The real power comes from combining Linked API actions with other apps in your n8n workflows. You can connect LinkedIn actions with: - **CRM systems** like HubSpot, Salesforce, or Pipedrive: automatically add new LinkedIn connections to your CRM or trigger LinkedIn messages when deals reach certain stages. - **Databases** like Airtable, Google Sheets, or PostgreSQL: store LinkedIn search results, track outreach campaigns, or pull prospect lists for automated connection requests. - **Communication tools** like Slack, Gmail, or Microsoft Teams: get notifications about LinkedIn responses or trigger LinkedIn actions from team messages. - **Webhooks and APIs**: integrate with any custom system or trigger LinkedIn workflows from external events. ## Available actions Linked API provides these n8n actions for automating your LinkedIn flows: ## Standard interface | Action | Description | | --- | --- | | **Accept invitation** | Accept a connection, company-follow, or newsletter-subscription invitation | | **Check connection status** | Check connection status with person | | **Comment on post** | Leave comment on post; returns the new comment's URN and URL | | **Fetch company** | Get company information with optional employees, decision makers, posts | | **Fetch person** | Get person page information with optional experience, education, skills, posts | | **Fetch post** | Get post information and engagement metrics | | **Fetch job** | Get job details such as company, location, salary, description | | **Ignore invitation** | Ignore a connection, company-follow, or newsletter-subscription invitation | | **Manage conversation** | Archive, star, or mute a conversation thread by thread ID | | **React to comment** | React to a comment by comment URL (like, love, support, celebrate, insightful, funny) | | **React to post** | React to post (like, love, support, celebrate, insightful, funny) | | **Remove connection** | Remove person from connections | | **Reply to comment** | Reply to a comment by comment URL; returns the reply's URN and URL | | **Retrieve invitations** | Get all received connection, company-follow, and newsletter-subscription invitations | | **Retrieve connections** | Get your connections with filtering | | **Retrieve feed** | Get posts from your home feed | | **Retrieve pending requests** | Get all pending connection requests | | **Retrieve performance** | Get LinkedIn dashboard analytics | | **Retrieve SSI** | Get current SSI (Social Selling Index) | | **Search companies** | Search for companies with advanced filtering | | **Search people** | Search for people with advanced filtering | | **Search jobs** | Search for jobs with advanced filtering | | **Send connection request** | Send connection request with optional note | | **Send message** | Send message to person, or reply into an existing thread by thread ID, with optional archive, star, or mute of the conversation right after sending | | **Sync conversation** | Sync conversation for polling later | | **Sync inbox** | Enable whole-inbox monitoring so every incoming conversation can be polled | | **Sync network** | Enable network monitoring so connection events can be polled | | **Withdraw connection request** | Withdraw pending connection request | The accept and ignore actions require an invitation type and its matching URL: `personUrl` for `connect`, `companyUrl` for `companyFollow`, or `newsletterUrl` for `newsletterSubscribe`. ## Sales Navigator | Action | Description | | --- | --- | | **Fetch company in Sales Navigator** | Get company information with optional employees and decision makers from Sales Navigator | | **Fetch person in Sales Navigator** | Get person page information from Sales Navigator | | **Manage conversation in Sales Navigator** | Archive or unarchive a conversation thread by thread ID via Sales Navigator | | **Search companies in Sales Navigator** | Search for companies with advanced filtering via Sales Navigator | | **Search people in Sales Navigator** | Search for people with advanced filtering via Sales Navigator | | **Send message in Sales Navigator** | Send message to person via Sales Navigator, or reply into an existing thread by thread ID | | **Sync conversation in Sales Navigator** | Sync conversation in Sales Navigator for polling later | | **Sync inbox in Sales Navigator** | Enable whole-inbox monitoring in Sales Navigator so every incoming conversation can be polled | ## Other actions | Action | Description | | --- | --- | | **Execute custom workflow** | Execute custom workflow by raw definition | | **Cancel workflow** | Cancel prevously started workflow | | **Get actions statistics** | Get statistics about previously executed actions | | **Get workflow result** | Get result by workflow id | | **Poll conversations** | Poll conversations for new messages | | **Poll inbox** | Poll the monitored inbox for new messages across all conversations | | **Poll network** | Poll the monitored network for new connection events | ## Administration | Action | Description | | --- | --- | | **Get Subscription Status** | Check current subscription status and trial eligibility | | **Get Seats** | Get active subscription seats | | **Set Seats** | Set number of subscription seats | | **Get Accounts** | List connected LinkedIn accounts with profile URL, avatar, headline, pending sessions, and reconnection links | | **Connect Account** | Create connection session for a new LinkedIn account | | **Reparse Account Info** | Refresh stored profile URL, avatar, headline, and name for an account | | **Reconnect Account** | Create reconnection session for an account that requires reconnection | | **Get Connection Session** | Get connection or reconnection session status | | **Cancel Connection Session** | Cancel a pending connection or reconnection session | | **Disconnect Account** | Disconnect a LinkedIn account | | **Regenerate Token** | Regenerate identification token for an account | | **Get Limits Defaults** | Get system default rate limits | | **Get Limits** | Get configured rate limits for an account | | **Get Limits Usage** | Check current usage against configured limits | | **Set Limits** | Configure rate limits for an account | | **Delete Limits** | Delete specific account rate limits | | **Reset Limits** | Reset all limits to system defaults | ## Usage examples We collect the following examples to show how Linked API can be used with n8n to automate LinkedIn-related operations. They’re intended to inspire practical ways to reduce manual steps in your business processes. > 💡 We also have [n8n templates](/templates) that you can import and use right away, or customize for your specific case. ## Content warming & outreach Here is the scenario example of how you can use Linked API & n8n integration for social selling through gradual engagement before sending requests for connect. This workflow warms up prospects automatically: first it likes and comments on their latest posts, then sends a connection request, checks if it’s accepted, and withdraws unaccepted ones after 7 days. All actions are scheduled and logged in a Google Sheet, so you can monitor who’s at each stage. **Modules:** Google Sheets, Linked API (fetchPerson, reactToPost, commentOnPost, sendConnectionRequest, checkConnectionStatus, withdrawConnectionRequest), Slack. ![Example: Content warming & outreach](/images/docs/Workflow-1.webp) ## Form → CRM → LinkedIn/Email Here is the scenario example of how you can use Linked API & n8n to instantly turn new leads into personalized outreach at the moment they submit a form. When someone fills out your lead form (Typeform, website, ad campaign, etc.), the scenario instantly adds them to your CRM (e.g. Pipedrive), searches for their LinkedIn profile, and decides what to do next: send a connection request and log their LinkedIn URL or send a follow-up email instead. Every new lead will get a timely, personalized touch without manual lookup. **Modules:** Typeform (Trigger), Pipedrive, Linked API (searchPeople, sendConnectionRequest), Chat GPT, Email. ![Example: Form → CRM → LinkedIn/Email](/images/docs/3.webp) ## LinkedIn SMM tracker Here is the scenario example of how you can use Linked API & n8n to get automated insights into your LinkedIn growth and engagement. Once a week, the workflow collects your LinkedIn metrics (SSI score, profile performance, and post stats), saves them to Google Sheets, compares with last week’s data, and sends a Slack summary with what’s up or down. **Modules:** Google Sheets, Linked API (retrieveSSI, retrievePerformance, fetchPerson, fetchPost), Slack. ![Example: LinkedIn SMM tracker](/images/docs/--------------------------2025-10-24----14.03.26.webp) ## Recruiting automation Here is the scenario example of how you can use Linked API & n8n to automate candidate search, profile validation, and connection requests from ATS (Airtable) to LinkedIn. When a new record is added, it searches LinkedIn for a matching profile, validates fit using ChatGPT, sends a connection request, and tracks its status. The system updates candidate records daily, withdraws unaccepted requests after 7 days, and notifies the team in Slack when connections are accepted. **Modules:** Airtable, Linked API (searchPeople, sendConnectionRequest, checkConnectionStatus, withdrawConnectionRequest), ChatGPT, Slack. ![Example: Recruiting automation](/images/docs/5-1.webp) ## ERP lead warming Here is an example of how you can use Linked API and n8n to automate lead enrichment and engagement directly from your ERP system. Each company record is used to find up to three decision-makers on LinkedIn, fetch their profiles, and define if they are Active or Inactive based on posting activity over the last 90 days. The workflow continues only with active contacts, generating a short relevant comment with ChatGPT, and then sending a connection request. **Modules:** ERP (HubSpot, or other), Linked API (fetchCompany, fetchPerson, fetchPost, commentOnPost, sendConnectionRequest), ChatGPT. ![Example: ERP lead warming](/images/docs/2.webp) ## Standard Outreach Sequence Automate your LinkedIn outreach with this n8n workflow powered by [Linked API](https://linkedapi.io/). Sends connection requests, monitors acceptance, delivers personalized messages, and follows up automatically – all tracked in Google Sheets (you can adjust to something else). ### What it does - Sends connection requests to new leads - Monitors when connections are accepted - Sends your message sequences from Google Sheet (up to 3 messages per lead) - Follows up automatically if no reply - Marks leads as declined/expired/no response when appropriate - Tracks all leads and statuses in Google Sheets ### Setup - [Use this template in n8n](https://n8n.io/workflows/12915-manage-linkedin-outreach-sequences-with-linked-api-and-google-sheets/) - Copy the [Google Sheet template](https://docs.google.com/spreadsheets/d/141fJskisAQ7H8AxtojQ7LZrnd14EOyB26RdDq5aczEU/copy) - Connect credentials in n8n: - Google Sheets (OAuth2) - Linked API (get keys at [app.linkedapi.io](https://app.linkedapi.io)) - Set `DOCUMENT_LINK` and `SHEET_NAME` in the `Config` node (your spreadsheet URL after copying the template) - Add leads with `Status` = `NEW` and fill in: - `Connection note` (optional) - `Message 1`, `Message 2`, `Message 3` (required) - Activate the workflow ### Configuration | Setting | Default | Description | | --- | --- | --- | | DOCUMENT_LINK | – | URL to your Google Sheet | | SHEET_NAME | Leads | Name of the sheet with leads | | DAILY_CONNECTION_LIMIT | 25 | Max connection requests per day | | HOURS_TO_CHECK_IF_CONNECTION_ACCEPTED | 24 | Check frequency for connection acceptance | | HOURS_TO_CHECK_IF_REPLIED | 4 | Check frequency for message replies | | HOURS_DELAY_AFTER_CONNECTION_ACCEPTED | 24 | Delay before first message | | DAYS_DELAY_BETWEEN_MESSAGES | 2 | Delay between follow-ups | | DAYS_WAIT_FOR_CONNECTION_ACCEPTANCE | 10 | Timeout for connection requests | | DAYS_WAIT_AFTER_LAST_MESSAGE | 4 | Days to wait after last message before marking as no response | --- # Make Integration ## Linked API + Make Linked API for Make connects your LinkedIn account to Make's automation platform, so you can build visual scenarios that search for prospects, send messages, manage connections, extract data, and more – all with [industry-leading safety](/safety). > Our app is available in the **Make Apps Marketplace** – just search for "Linked API" when adding a new module to your scenario. ### Common use cases - **Lead generation pipeline**.Search for prospects matching your criteria, automatically send connection requests, wait for acceptance, then send personalized follow-up messages. Connect the results to your CRM to track everything in one place. - **CRM synchronization**. When a new lead enters your CRM, automatically find their LinkedIn profile, send a connection request, and update the CRM with their LinkedIn data. Keep your sales pipeline enriched with real-time LinkedIn information. - **Content engagement**. Monitor when your target accounts post on LinkedIn, automatically react or comment on their posts, then log the engagement in your tracking spreadsheet. Build relationships at scale without losing the personal touch. - **Recruitment workflows**. Search for candidates with specific skills, send personalized InMails through Sales Navigator, track responses in your ATS, and automatically follow up with interested candidates. Streamline your entire recruitment funnel. - **Multi-channel outreach.** Combine LinkedIn with email, Slack, and other channels. When someone accepts your connection request, trigger an email sequence, notify your team in Slack, and create a task in your project management tool. ### How to get started To build LinkedIn automations with Make, read these essential guides: 1. [Creating connection](/integrations/make/creating-connection) – get your API tokens and connect to Make. 2. [Building scenarios](/integrations/make/building-scenarios) – learn how to use Linked API modules in your scenarios. 3. [Available modules](/integrations/make/available-modules) – explore all LinkedIn actions you can automate. ## Creating connection Setting up Linked API in Make requires two steps: **getting your API tokens** and **creating a connection**. This guide walks you through both steps. ## Getting your tokens 1. Sign up at [Linked API Platform](https://app.linkedapi.io/). 2. Choose your plan and start your 7-day free trial. 3. Connect your LinkedIn account (it takes about 2 minutes). 4. Copy your tokens from the dashboard (as shown below): ![](/images/docs/tokens.webp) ## Creating Make connection 1. In your Make scenario, add any Linked API module. 2. Click the "Create a connection" button in the module settings. ![](/images/docs/create-connection-button.webp) 3. Paste your tokens into the displayed fields. ![](/images/docs/paste-tokens.webp) 4. Click "Save". Your connection is now ready to use. ## Multiple LinkedIn accounts If you manage multiple LinkedIn accounts, create a separate connection for each one. Every account has its own **Identification Token**, while the **Linked API Token** remains the same. You can then switch between accounts by selecting different connections. ## Building scenarios Once your connection is set up, you can start building LinkedIn automations in Make. To add Linked API functionality to your scenarios: 1. Search for "Linked API" in the module search bar when building your scenario. 2. Select the module that matches what you want to do: send messages, search for people, retrieve company data, etc. See the [full list of available modules](/integrations/make/available-modules). 3. Configure the module with your specific parameters and select the connection [you created earlier](/integrations/make/creating-connection). ![](/images/docs/make-modules.webp) ### Combining with other tools The real power comes from combining Linked API modules with other apps in your Make scenarios. You can connect LinkedIn actions with: - **CRM systems** like HubSpot, Salesforce, or Pipedrive: automatically add new LinkedIn connections to your CRM or trigger LinkedIn messages when deals reach certain stages. - **Databases** like Airtable, Google Sheets, or PostgreSQL: store LinkedIn search results, track outreach campaigns, or pull prospect lists for automated connection requests. - **Communication tools** like Slack, Gmail, or Microsoft Teams: get notifications about LinkedIn responses or trigger LinkedIn actions from team messages. - **Webhooks and APIs**: integrate with any custom system or trigger LinkedIn workflows from external events. ## Available modules Linked API provides these Make modules for automating your LinkedIn flows: ## Standard interface | Module | Description | | --- | --- | | **Accept an Invitation** | Accept a connection, company-follow, or newsletter-subscription invitation | | **Check Connection Status** | Check connection status with person | | **Comment on Post** | Leave comment on post; returns the new comment's URN and URL | | **Fetch Company** | Get company information with optional employees, decision makers, posts | | **Fetch Person** | Get person page information with optional experience, education, skills, posts | | **Fetch Post** | Get post information and engagement metrics | | **Fetch Job** | Get job details such as company, location, salary, description | | **Ignore an Invitation** | Ignore a connection, company-follow, or newsletter-subscription invitation | | **Manage Conversation** | Archive, star, or mute a conversation thread by thread ID | | **React to Comment** | React to a comment by comment URL (like, love, support, celebrate, insightful, funny) | | **React to Post** | React to post (like, love, support, celebrate, insightful, funny) | | **Remove Connection** | Remove person from connections | | **Reply to Comment** | Reply to a comment by comment URL; returns the reply's URN and URL | | **Retrieve Incoming Invitations** | Get all received connection, company-follow, and newsletter-subscription invitations | | **Retrieve Connections** | Get your connections with filtering | | **Retrieve Feed Posts** | Get posts from your home feed | | **Retrieve Pending Requests** | Get all pending connection requests | | **Retrieve Performance** | Get LinkedIn dashboard analytics | | **Retrieve SSI** | Get current SSI (Social Selling Index) | | **Search Companies** | Search for companies with advanced filtering | | **Search Jobs** | Search for jobs with advanced filtering | | **Search People** | Search for people with advanced filtering | | **Send Connection Request** | Send connection request with optional note | | **Send Message** | Send message to person, or reply into an existing thread by thread ID, with optional archive, star, or mute of the conversation right after sending | | **Sync Conversation** | Sync conversation for polling later | | **Sync Inbox** | Enable whole-inbox monitoring so every incoming conversation can be polled | | **Sync Network** | Enable network monitoring so connection events can be polled | | **Withdraw Connection Request** | Withdraw pending connection request | The accept and ignore modules require an invitation type and its matching URL: `personUrl` for `connect`, `companyUrl` for `companyFollow`, or `newsletterUrl` for `newsletterSubscribe`. ## Sales Navigator | Module | Description | | --- | --- | | **Fetch Company in Sales Navigator** | Get company information with optional employees and decision makers from Sales Navigator | | **Fetch Person in Sales Navigator** | Get person page information from Sales Navigator | | **Manage Conversation in Sales Navigator** | Archive or unarchive a conversation thread by thread ID via Sales Navigator | | **Search Companies in Sales Navigator** | Search for companies with advanced filtering via Sales Navigator | | **Search People in Sales Navigator** | Search for people with advanced filtering via Sales Navigator | | **Send Message in Sales Navigator** | Send message to person via Sales Navigator, or reply into an existing thread by thread ID | | **Sync Conversation in Sales Navigator** | Sync conversation in Sales Navigator for polling later | | **Sync Inbox in Sales Navigator** | Enable whole-inbox monitoring in Sales Navigator so every incoming conversation can be polled | ## Other modules | Module | Description | | --- | --- | | **Execute Custom Workflow** | Execute custom workflow by raw definition | | **Get Actions Statistics** | Get statistics about previously executed actions | | **Poll Conversations** | Poll conversations for new messages | | **Poll Inbox** | Poll the monitored inbox for new messages across all conversations | | **Poll Network** | Poll the monitored network for new connection events | ## Administration | Module | Description | | --- | --- | | **Get Subscription Status** | Check current subscription status and trial eligibility | | **Get Seats** | Get active subscription seats | | **Set Seats** | Set number of subscription seats | | **Get Accounts** | List connected LinkedIn accounts with profile URL, avatar, headline, pending sessions, and reconnection links | | **Connect Account** | Create connection session for a new LinkedIn account | | **Reparse Account Info** | Refresh stored profile URL, avatar, headline, and name for an account | | **Reconnect Account** | Create reconnection session for an account that requires reconnection | | **Get Connection Session** | Get connection or reconnection session status | | **Cancel Connection Session** | Cancel a pending connection or reconnection session | | **Disconnect Account** | Disconnect a LinkedIn account | | **Regenerate Token** | Regenerate identification token for an account | | **Get Limits Defaults** | Get system default rate limits | | **Get Limits** | Get configured rate limits for an account | | **Get Limits Usage** | Check current usage against configured limits | | **Set Limits** | Configure rate limits for an account | | **Delete Limits** | Delete specific account rate limits | | **Reset Limits** | Reset all limits to system defaults | ## Usage examples We collect the following examples to show how Linked API can be used with Make to automate LinkedIn-related operations. They’re intended to inspire practical ways to reduce manual steps in your business processes. ## Content warming & outreach Here is the scenario example of how you can use Linked API & Make integration for social selling through gradual engagement before sending requests for connect. This workflow warms up prospects automatically: first it likes and comments on their latest posts, then sends a connection request, checks if it’s accepted, and withdraws unaccepted ones after 7 days. All actions are scheduled and logged in a Google Sheet, so you can monitor who’s at each stage. **Modules:** Google Sheets, Linked API (Fetch aPerson, React to a Post, Comment on a Post, Send a Connection Request, Check a Connection Status, Withdraw a Connection Request), Slack. ![Example: Content warming & outreach workflow](/images/docs/Content-warming---outreach-example.webp) ## Form → CRM → LinkedIn/Email Here is the scenario example of how you can use Linked API & Make to instantly turn new leads into personalized outreach at the moment they submit a form. When someone fills out your lead form (Typeform, website, ad campaign, etc.), the scenario instantly adds them to your CRM (e.g. Pipedrive), searches for their LinkedIn profile, and decides what to do next: send a connection request and log their LinkedIn URL or send a follow-up email instead. Every new lead will get a timely, personalized touch without manual lookup. **Modules:** Typeform (Webhook), Pipedrive, Linked API (Search People, Send a Connection Request), Email. ![Example: Form → CRM → LinkedIn/Email workflow](/images/docs/Form-----CRM-----LinkedIn-example-2.webp) ## LinkedIn SMM tracker Here is the scenario example of how you can use Linked API & Make to get automated insights into your LinkedIn growth and engagement. Once a week, the workflow collects your LinkedIn metrics (SSI score, profile performance, and post stats), saves them to Google Sheets, compares with last week’s data, and sends a Slack summary with what’s up or down. **Modules:** Google Sheets, Linked API (Retrieve SSI, Retrieve Performance, Fetch a Person, Fetch a Post), set of variables, Slack. ![Example: LinkedIn SMM tracker workflow](/images/docs/LinkedIn-SMM-tracker-example.webp) ## Recruiting automation Here is the scenario example of how you can use Linked API & Make to automate candidate search, profile validation, and connection requests from ATS (Airtable) to LinkedIn. When a new record is added, it searches LinkedIn for a matching profile, validates fit using ChatGPT, sends a connection request, and tracks its status. The system updates candidate records daily, withdraws unaccepted requests after 7 days, and notifies the team in Slack when connections are accepted. **Modules:** Airtable, Linked API (Search People, Check a Connection Status, Send/Withdraw a Connection Request), ChatGPT, Slack. ![Example: Recruiting automation workflow](/images/docs/-Recruiting-Automation-example.webp) ## ERP lead warming Here is an example of how you can use Linked API and Make to automate lead enrichment and engagement directly from your ERP system. Each company record is used to find up to three decision-makers on LinkedIn, fetch their profiles, and define if they are Active or Inactive based on posting activity over the last 90 days. The workflow continues only with active contacts, generating a short relevant comment with ChatGPT, and then sending a connection request. **Modules:** ERP (HubSpot, or other), Linked API (Fetch a Company, Fetch a Person, Comment on a Post, Send a Connection Request), ChatGPT. ![Example: ERP lead warming](/images/docs/ERP-Lead-Warming--company-lists--2.webp) --- # Guides ## What 1st, 2nd and 3rd Mean on LinkedIn: Degrees of Connection Explained LinkedIn marks most names with a small label – 1st, 2nd or 3rd – and it quietly decides what you are allowed to do next. Whether you can message someone, whether the Connect button appears, even how much of their profile you get to see, all follow from that one number. People Search adds a broader 3rd+ filter on top of those. > **The short version.** The number is your network distance. **1st** means you are directly connected – LinkedIn lets you message them. **2nd** means they are connected to one of your 1st-degree connections, and you can send an invitation with the Connect button. **3rd** means they are connected to one of your 2nd-degrees. **"LinkedIn Member (Out of Network)"** covers people outside LinkedIn's listed network categories, whose profiles show you limited information and who you can reach with an InMail where that option is available. What the label does **not** distinguish is where you stand right now: the same non-1st badge appears whether neither side has an active invitation, your invitation is already waiting with them, or theirs is waiting with you – and those need three different actions. ## What 1st, 2nd and 3rd mean on LinkedIn | Label | What it is | Can you message them | Can you invite them | Profile visibility | | --- | --- | --- | --- | --- | | **1st** | Directly connected | Yes | Already connected | Full | | **2nd** | Connected to one of your 1st-degrees | Not directly | Yes, via Connect | Full | | **3rd** | Connected to one of your 2nd-degrees | Not directly | Usually, where Connect is offered | Full | | **3rd+** | A People Search filter, not a relationship type | Not directly | Only where Connect is offered | Varies | | **LinkedIn Member (Out of Network)** | Outside LinkedIn's listed network categories | InMail where available, or free if they have Open Profile | Rarely offered | Some fields limited | ## What "1st" means on LinkedIn A 1st-degree connection is mutual and it is always the result of an accepted invitation. Per [LinkedIn's help page](https://www.linkedin.com/help/linkedin/answer/a545636/your-network-and-degrees-of-connection), these are "people you're directly connected to because you've accepted their invitation to connect, or they've accepted your invitation" – and you can "contact them by sending a message on LinkedIn." Two consequences worth knowing: - **It is the only relationship LinkedIn creates in both directions at once.** You appear in their network exactly as they appear in yours, and either side can end it. - **There is a ceiling.** LinkedIn allows a "maximum of 30,000 1st-degree connections" per account. Followers, which are one-way, are not counted against it. ## What "2nd" means on LinkedIn A 2nd-degree connection is, in [LinkedIn's words](https://www.linkedin.com/help/linkedin/answer/a545636/your-network-and-degrees-of-connection), one of the "people who are connected to your 1st-degree connections" – and you "can send them an invitation by clicking the Connect button on their profile page." The useful part is the path rather than the definition. Because a 2nd-degree relationship exists only through someone you already know, LinkedIn shows you the shared connection on their profile. That gives you two options a cold approach does not have: mention the mutual contact in your invitation note, or ask that contact to introduce you. It is the difference between arriving as a stranger and arriving as a colleague of someone they trust. ## What "3rd", "3rd+" and "LinkedIn Member (Out of Network)" mean These three are not three rungs on one ladder, and treating them that way is where most explanations go wrong. **3rd-degree is a relationship. 3rd+ is a search filter. Out of Network is LinkedIn's label for members outside its listed network categories.** **3rd-degree** members are, per [LinkedIn Help](https://www.linkedin.com/help/linkedin/answer/a545636/your-network-and-degrees-of-connection), the "people who are connected to your 2nd-degree connections" – a friend of a friend of a friend. In practice the Connect button is usually still there. **3rd+** is not a separate kind of relationship, and LinkedIn does not document it as a profile label. Where it is defined is the Connections filter in People Search, and the definition is broad: [filtering by 3rd+](https://www.linkedin.com/help/sales-navigator/answer/a528043) "will include everyone that falls in your search criteria." Treat it as a search bucket rather than a degree. **LinkedIn Member (Out of Network)** is the label for people "who fall outside the categories listed above." Two things change here. First, visibility: LinkedIn notes that "some fields of profiles out of your network have limited visibility," which is why you sometimes see an abbreviated name instead of a full one. Second, the route: LinkedIn names InMail as the way to "introduce yourself" to these members, where that option is available to you. **Followers are not a degree.** Following is one-way: someone who follows you "chooses to follow your public updates" without any connection existing. It creates no degree, changes nothing about who can message whom, and does not count toward the 30,000 cap. ## The badge shows distance, not state Here is what none of this tells you: **where you actually stand with that person right now.** The badge answers a question about the network graph – how many steps away they are. It does not distinguish what has already happened between you. A profile showing "2nd" looks the same in three situations that call for three different actions: - **Neither side has an active invitation.** The invitation still has to be sent. - **Your invitation is already sitting with them,** unanswered. Sending another does nothing; your real options are to wait or to withdraw it. - **Their invitation is already sitting with you.** The right move is to accept it – and if you send your own request instead, you are doing extra work to reach a state you could have had with one click. LinkedIn does surface the difference elsewhere: the profile's own buttons change, and both invitation queues live under My Network. What stays identical across all three is the badge itself. That is a small annoyance when you are looking at one profile and can check your queues by hand, and a real problem the moment anything runs across a list, because the correct action differs in each case and the label you are reading does not. This is why an API answers a different question than the badge does. Instead of a degree, [Linked API](/)'s `checkConnectionStatus` returns the relationship state, and there are four of them: `connected`, `notConnected`, `pending` and `incoming`. Note what `notConnected` does and does not mean: no connection and no active invitation in either direction right now – not a guarantee that you have never been in touch before. And `incoming` is the state that is easy to miss if you look only at the badge: it means the other person invited you first. The product is built to expect this. If you skip the check and send a connection request to someone whose invitation is already waiting, Linked API does not quietly send a duplicate – it returns the error `invitationAlreadyReceived`, whose message is exactly the advice you needed: *"This person has already sent you a connection request. Accept it instead of sending a new one."* Useful, but it costs you a failed action and an error to handle, when one status check would have routed you to the right branch in the first place. ### Checking connection state in code The whole pattern is one call and four branches. Install the SDK with `npm install -S @linkedapi/node` or `pip install linkedapi`, then: ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const NOTE = 'Hi! We share a few contacts in this space - would love to connect.'; async function reach(personUrl: string, text: string): Promise { const check = await linkedapi.checkConnectionStatus.execute({ personUrl }); const { data } = await linkedapi.checkConnectionStatus.result(check.workflowId); switch (data?.connectionStatus) { case 'connected': await linkedapi.sendMessage.execute({ personUrl, text }); return; case 'incoming': // They invited you first - accept, do not send your own request. await linkedapi.acceptInvitation.execute({ invitationType: 'connect', personUrl }); return; case 'pending': // Your invitation is already with them: wait, or withdrawConnectionRequest. return; case 'notConnected': await linkedapi.sendConnectionRequest.execute({ personUrl, note: NOTE }); return; } } ``` ```python from linkedapi import ( LinkedApi, LinkedApiConfig, CheckConnectionStatusParams, SendMessageParams, SendConnectionRequestParams, AcceptInvitationParams, ) linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) NOTE = "Hi! We share a few contacts in this space - would love to connect." def reach(person_url: str, text: str) -> None: check = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url=person_url) ) status = linkedapi.check_connection_status.result(check.workflow_id).data.connection_status if status == "connected": linkedapi.send_message.execute(SendMessageParams(person_url=person_url, text=text)) elif status == "incoming": # They invited you first - accept, do not send your own request. linkedapi.accept_invitation.execute( AcceptInvitationParams(invitation_type="connect", person_url=person_url) ) elif status == "pending": # Your invitation is already with them: wait, or withdraw_connection_request. pass elif status == "notConnected": linkedapi.send_connection_request.execute( SendConnectionRequestParams(person_url=person_url, note=NOTE) ) ``` Note that `acceptInvitation` takes the invitation type alongside the URL – `invitationType: 'connect'` with a `personUrl` – because the same action also accepts company-follow and newsletter invitations. What happens after the request is sent – the `emailRequired` edge, withdrawing invitations that went stale, detecting the moment one is accepted – is its own lifecycle, covered in [How to Automate LinkedIn Connection Requests](/guides/how-to-automate-linkedin-connection-requests). Once someone is connected, [automating the message](/guides/how-to-automate-linkedin-messages) is the next step. ![Diagram: resolving LinkedIn connection state before acting. From a person URL, checkConnectionStatus returns one of four states. Connected leads to sending a message. NotConnected leads to sending a connection request, or a Sales Navigator InMail to skip the acceptance wait. Pending means your invitation is already with them, so wait or withdraw it. Incoming means they invited you first, so accept the invitation you already have. A footer notes that the visible profile badge is identical across the notConnected, pending and incoming branches.](/images/guides/linkedin-connection-state-router.webp) ## How to reach someone at each degree | Route | Who it reaches | Best for | Cost | | --- | --- | --- | --- | | Message | 1st-degree | Anyone already connected | Free | | Connection request (± note) | 2nd; 3rd where Connect is offered | Building an ongoing relationship rather than a one-off contact | Free; weekly invitation limits apply | | Sales Navigator InMail | 2nd, 3rd, out of network | Reaching someone without waiting for an invitation to be accepted | One credit; refunded if they accept, decline or reply within 90 days | | Open Profile message | Premium members who enabled Open Profile | Contacting a non-connection without using a credit | Free, including from free accounts | | Follow | Anyone who allows followers | Seeing someone's posts without a connection | Free; creates no degree | | Shared-connection introduction | 2nd | Warm outreach at low volume | Free; manual | | Programmatically, on your own account | The same eligible people reached by the supported routes: 1st-degree messages, Connect where it is offered, and Sales Navigator InMail. It unlocks nobody new | Running those routes across a list, or wiring them into your own product, CRM or agent | Paid, flat per seat; LinkedIn's own limits and credits still apply | Where a 3rd-degree profile offers no Connect button, InMail is the paid fallback – but check the free routes first: a Premium member with Open Profile can be messaged at no cost, and a shared group can open a direct route too. How InMail credits, caps and refunds work is covered in the [LinkedIn InMail guide](/guides/linkedin-inmail), including that Open Profile exception. That last row is worth one caveat. Doing this programmatically does not move anyone closer to you: degrees still apply, invitation limits still apply, InMail still costs a credit. What changes is that the routing happens in your code instead of your afternoon. Linked API runs each action on your own account through a human-paced cloud browser and surfaces LinkedIn's own limit signals rather than pushing past them, which is what keeps volume sane – our [connection limit guide](/guides/linkedin-connection-limit-2026) and [limits overview](/guides/understanding-linkedin-limits) have the numbers, and the [safety model](/safety) has the approach. You can build it in with the REST API, the Node and Python SDKs or the [shell CLI](/cli/getting-started), or get it out of the box through the [MCP server](/mcp/overview), the AI-agent-friendly CLI in Claude Code, Cursor and Codex, or a ready-made [skill](/skills). ## Frequently Asked Questions (FAQ) #### Do LinkedIn connection requests expire? Yes. An invitation nobody answers expires after six months, and LinkedIn sends up to two reminders before that happens. Once it has expired you are free to send a new one. Withdrawing an invitation yourself is different: the recipient is not notified, but you cannot re-invite the same person for up to three weeks. Our guide to [pending invitations](/guides/linkedin-pending-invitations) covers both queues and how to clear them. --- Building connection logic into your own product, CRM or agent? [Start with Linked API](/pricing) – check the real relationship state and act on it through the [API and SDKs](/sdks/installation), the [CLI](/cli/getting-started), the [MCP server](/mcp/overview) or ready-made [skills](/skills), on your own account and at a human pace. ## LinkedIn Sales Navigator Cost in 2026: Pricing by Plan Sales Navigator is priced per licence, and the published number is only half the decision. The other half is which tier that licence should be, and whether a given person on your team needs one at all. > **The short version.** Sales Navigator starts at US$119.99 per licence per month, or US$1,079.88 per year, for Core. Advanced starts at US$159.99 per month or US$1,799.88 per year. Advanced Plus has no published price and is quoted per customer. Core and Advanced give you the same search and InMail allocation – what the step up buys is buyer intelligence, warm-path tooling and team administration. And the annual discount is uneven: LinkedIn takes 25% off Core but only about 6% off Advanced, which makes the gap between the two tiers wider on annual billing than on monthly. ## How much does LinkedIn Sales Navigator cost in 2026? Per [LinkedIn's compare-plans page](https://business.linkedin.com/sell/sales-navigator/compare-plans), in US dollars: | Plan | Monthly, per licence | Annual, per licence | Effective monthly on annual | LinkedIn's "best for" | |---|---|---|---|---| | **Core** | US$119.99 | US$1,079.88 | US$89.99 | Individual sellers | | **Advanced** | US$159.99 | US$1,799.88 | US$149.99 | Sales teams | | **Advanced Plus** | Contact sales | Contact sales | – | Sales teams using integrated CRM | Both published tiers say "starts at", and pricing is per licence rather than per company, so a five-person team on Core is five times the number above. LinkedIn attaches a caveat to those figures worth reading before you build a budget on them: "prices listed above are estimates and may exclude Value-Added Tax (VAT), Goods & Services Tax (GST), and/or promotional discounts. Pricing is subject to change. Slight variations may occur across device types." ## Does annual billing actually save you money? Yes, but by very different amounts depending on which tier you are on. LinkedIn states both discounts itself: "a 25% savings with annual billing" on Core, and "a 6% savings with annual billing" on Advanced. The arithmetic behind those two numbers: | Plan | 12 months at the monthly rate | Annual rate | Saving | Discount | |---|---|---|---|---| | **Core** | US$1,439.88 | US$1,079.88 | US$360.00 | **25.0%** | | **Advanced** | US$1,919.88 | US$1,799.88 | US$120.00 | **6.25%** | The consequence is the part that changes a decision. Because Core gets four times the discount rate Advanced does, the price gap between the two tiers is not constant – it widens when you commit annually: - Paying monthly, Advanced costs **US$480 more per licence per year** than Core. - Paying annually, Advanced costs **US$720 more per licence per year** than Core. ![Sales Navigator Core versus Advanced on monthly and annual billing: the annual per-licence gap widens from US$480 to US$720 because Core receives a 25% annual discount while Advanced receives 6.25%](/images/guides/linkedin-sales-navigator-cost-annual-gap.webp) Nothing reverses here – annual Advanced is still cheaper than monthly Advanced. But the upgrade itself gets 50% more expensive under annual billing, so if you are sizing a team where some people need Advanced and some do not, price the mix on the billing basis you will actually use, not on the monthly rate. ## What the Core → Advanced step actually buys Not more search. Core and Advanced carry an identical search-and-reach allocation: the same 50+ advanced search filters, the same 50 InMail per month, and the same Relationship Explorer, Relationship Map and Alerts. What the step adds is buyer intelligence, engagement tooling, and the machinery a team needs to run seats together. | Capability | Core | Advanced | Advanced Plus | |---|:--:|:--:|:--:| | Advanced Search Filters | ✓ | ✓ | ✓ | | InMail | 50/month | 50/month | 50/month | | Relationship Explorer | ✓ | ✓ | ✓ | | Relationship Map | ✓ | ✓ | ✓ | | Alerts | ✓ | ✓ | ✓ | | Account IQ & Lead IQ | – | ✓ | ✓ | | Message Assist (public beta) | – | ✓ | ✓ | | TeamLink | – | ✓ | ✓ | | Smart Links | – | ✓ | ✓ | | Buyer Intent | – | ✓ | ✓ | | Team seat management & centralized billing | – | ✓ | ✓ | | Team reporting | – | ✓ | ✓ | | Manage your book of business | – | ✓ | ✓ | | Dedicated account team & tailored training\* | – | ✓ | ✓ | | Advanced CRM Integrations | – | – | ✓ | | Lead/Contact Creation | – | – | ✓ | | CRM Embedded Experiences and Profiles | – | – | ✓ | \* LinkedIn footnotes this row as "if purchased through a sales representative". So the question to ask about the extra US$720 a year is not "do we need better search" – you already have the same search on Core. It is whether these five things are worth it to the individual seller: knowing which accounts are already showing interest (Buyer Intent), AI-written account and lead research (Account IQ and Lead IQ), warm introduction paths through colleagues' connections (TeamLink), trackable content links (Smart Links), and drafted outreach (Message Assist) – plus, for the manager, seat administration and team reporting. LinkedIn's own framing matches: Core is for "individual sellers who want to find high-quality leads and build client relationships", Advanced for "sales teams that need AI-powered lead and account research, actionable insights, and collaboration tools". ## How Advanced Plus pricing works Advanced Plus has no list price. LinkedIn quotes it per customer, and the variables it names are team size, CRM integration requirements, and onboarding and training. What you get for the quote is narrower than the tier name suggests. In LinkedIn's own comparison matrix, exactly three capabilities separate Advanced Plus from Advanced, and all three are CRM: - **Advanced CRM Integrations** – keeping the CRM current from Sales Navigator. - **Lead/Contact Creation** – writing leads and accounts into the CRM. - **CRM Embedded Experiences and Profiles** – LinkedIn surfaces rendered inside the CRM. That makes the decision unusually clean. If real CRM synchronisation is the thing you need, request the quote. If it is not, Advanced already contains everything else Advanced Plus does, and there is nothing else in the tier to buy. Published estimates for the quote do not agree with each other, which is a reason to treat all of them as indicative: GigRadar puts Advanced Plus at roughly US$1,300–1,600 per seat per year, while Factors states US$1,600 and up. Neither is LinkedIn, and LinkedIn does not publish a figure. ## Which accounts actually need a Sales Navigator licence? Before choosing a tier, it is worth asking whether a particular person needs a licence at all – and the answer turns on one rule that decides more budget than any tier comparison. **A Sales Navigator licence attaches to a user, not to a task.** You cannot detach the ordinary parts of someone's work to shrink their subscription. If a seller needs advanced filters, saved lead and account lists, Sales Navigator's 50-a-month InMail allocation, alerts, or any other Sales Navigator surface for *any* part of their job, they need a licence, and moving the rest of their work elsewhere does not reduce what you pay for them. Where the rule pays off is the account that never touches those surfaces at all. | The work | Sales Navigator status | |---|---| | Advanced-filter and saved-search prospecting | **Nav-exclusive** – needs a licence | | Saved lead and account lists, book of business | **Nav-exclusive** – needs a licence | | [InMail](/guides/linkedin-inmail) at 50 a month | **The allocation is Nav-exclusive** – Premium Business includes 15 a month | | Buyer Intent, Account IQ, Lead IQ, TeamLink, Smart Links | **Nav-exclusive** – needs a licence, on Advanced or above | | CRM sync of Sales Navigator data | **Nav-exclusive** – needs Advanced Plus | | Standard LinkedIn people and company search | Not Nav-exclusive by itself | | Profile, company and post data | Not Nav-exclusive by itself | | Connection requests and ordinary messages | Not Nav-exclusive by itself | Read the right-hand column carefully: "not Nav-exclusive by itself" is not the same as "free". It means only that this work does not *require* Sales Navigator. An account that does some of it alongside Nav-exclusive work still needs a licence. The case where this genuinely saves money is narrower and specific: an account whose workflow never needs a Nav-exclusive capability – a research or data-collection account, an operations account feeding a CRM, an account behind a product feature – can run standard LinkedIn work on no Sales Navigator subscription at all. Two honest qualifications. Standard LinkedIn search is meaningfully narrower than Sales Navigator search, not an equivalent with a different label: our own [standard people search](/docs/searching-for-people) exposes eight filter fields – first name, last name, position, locations, industries, current companies, previous companies and schools – against the 50+ filters LinkedIn markets Sales Navigator on. And a licence does not lift every ceiling: LinkedIn says a paid plan increases the number of profile searches and views, but the connection-invite cap is unchanged – see [search query limits](/guides/understanding-linkedin-limits#search-query-limits) and the [2026 connection limits guide](/guides/linkedin-connection-limit-2026). ## When Sales Navigator is the right purchase The eligibility question above is about whether an account needs a licence. This one is different: given that the work needs doing, when is Sales Navigator itself the better tool than any alternative, ours included? - **A person is doing the searching.** Sales Navigator's filter interface, saved searches, and lead and account lists are built for a human working a pipeline interactively. No API is a better experience for that job. - **You want warm paths, not just names.** TeamLink surfaces which colleague can introduce you. That mapping is proprietary to LinkedIn and cannot be reconstructed from outside. - **You want LinkedIn's own account research.** Account IQ and Lead IQ summarise accounts and leads inside the product, with LinkedIn's data behind them. - **You want buying signals.** Buyer Intent reflects activity LinkedIn observes and does not expose anywhere else. - **Your requirement is CRM synchronisation.** Advanced Plus, or an approved SNAP integration through your CRM vendor, will beat anything you assemble – see our [Sales Navigator API guide](/guides/linkedin-sales-navigator-api). - **You need one spreadsheet, once.** An export tool is the right answer, not a subscription decision at all. Our [Sales Navigator export guide](/guides/linkedin-sales-navigator-scraper) compares the routes. - **You need bulk records with email addresses or phone numbers.** Sales Navigator does not provide those, and neither do we. A dataset provider is the honest fit. ## Getting more out of a licence you already pay for If you are paying for licences, the cost per seat is fixed but the output is not – it is capped by how many hours a person can spend in the interface. Sales Navigator work can also run programmatically on the same licence you already own, which is where [Linked API](/pricing) fits. This does not replace the subscription and cannot: Sales Navigator actions run *through* your seat, so the seat has to exist. What changes is that searching, opening a lead, retrieving a company's employees or decision makers, sending a message and syncing the conversation become composable steps you can run on a schedule or from inside your own product. There are two first-class ways to reach it, and neither is a lesser version of the other. **Build it in.** Compose the primitives yourself through the [REST API, Node and Python SDKs, and the shell CLI](/sdks), and embed them in your product, backend or CRM. The full action set, including which actions are Sales Navigator-only, is in the [actions overview](/docs/actions-overview). **Get it out of the box.** Connect the [MCP server](/mcp), point an AI agent at the [CLI](/cli) from Claude Code, Cursor or Codex, or install ready-made [skills](/skills) with `npx @linkedapi/skills`. You describe the job in plain language and the agent composes and runs the same workflow. Nothing to wire, no blocks to drag. Both run on a dedicated cloud browser against your own account, at a human pace, with per-action limits enforced by the platform. Pricing is flat per seat, from $49 per month billed annually. One limit worth stating plainly, because it decides whether this fits at all: Sales Navigator result types carry no email address and no phone number. If contact data is what you need, this is not the route. ## Frequently Asked Questions (FAQ) #### Is Sales Navigator included in LinkedIn Premium? No – LinkedIn Premium does not include Sales Navigator; the inclusion runs the other way. LinkedIn states that "a Sales Navigator subscription includes access to Premium Business features". Premium Business gives you basic search filters and 15 InMail per month; Sales Navigator gives you 50+ advanced filters, 50 InMail per month, Relationship Map, TeamLink, alerts and CRM integrations on top of those Premium Business features. #### Is there a free trial, and will you be charged for it? There is a free trial, and LinkedIn asks for payment information up front. Its stated terms are that you can "cancel for any reason before your Sales Navigator trial ends to avoid being charged", and that it sends "an email reminder seven (7) days before your free trial expires". #### Are the listed prices what you actually pay? Not necessarily. LinkedIn describes its published figures as estimates that "may exclude Value-Added Tax (VAT), Goods & Services Tax (GST), and/or promotional discounts", adds that pricing is subject to change, and notes that "slight variations may occur across device types". Both published tiers are also quoted as "starts at" prices. #### Does a paid tier raise your LinkedIn connection or search limits? Those are two different answers. It does **not** raise the connection-invite ceiling – that cap is the same whether you pay or not, and the current figures are in the [2026 connection limits guide](/guides/linkedin-connection-limit-2026). It **does** increase search access: LinkedIn's [commercial use limit](https://www.linkedin.com/help/linkedin/answer/a564226) page says you can upgrade to Premium Business, Recruiter Lite or Sales Navigator "to increase the number of profile searches and views", while adding on the same page that "we are not able to display the exact number of searches or views you have left". So the allowance goes up, and LinkedIn does not publish what it goes up to. What Sales Navigator does publish is how much one search returns: "up to 2,500 lead results across 100 pages or 1,000 account results across 40 pages", per [LinkedIn Help](https://www.linkedin.com/help/linkedin/answer/a106030). Our own breakdown sits in [search query limits](/guides/understanding-linkedin-limits#search-query-limits) and [search results limits](/guides/understanding-linkedin-limits#search-results-limits). #### Does Sales Navigator give you email addresses or phone numbers? No. Sales Navigator surfaces LinkedIn profile and company data, not contact details. The same applies through Linked API: our Sales Navigator result types carry no email or phone field. If you need verified contact data, that is a separate category of tool. #### Do I need a Sales Navigator subscription to automate LinkedIn? Only for Sales Navigator actions. Anything that runs through the Sales Navigator interface requires the licence, because it works through your own seat. Standard LinkedIn people, company and post data, connection requests and ordinary messages do not require a Sales Navigator subscription – see [Sales Navigator API access](/guides/linkedin-sales-navigator-api) for how the routes differ. ## Price the tier, then price the seat Sales Navigator's list price is the easy part: US$119.99 or US$159.99 per licence per month, less 25% or 6% respectively if you commit for a year. The decisions that actually move the bill are which of your people need a Nav-exclusive capability at all, and whether the seats you keep are producing everything they could. If the answer to the second one is no, [Linked API](/pricing) runs Sales Navigator workflows on the licences you already own – built into your own product through the [REST API, SDKs and CLI](/sdks), or run out of the box by an agent through [MCP](/mcp), the [CLI](/cli) or ready-made [skills](/skills). Flat per seat, from $49 per month billed annually. *Facts verified 10 August 2026 – Sales Navigator prices, plan features, trial terms and Premium comparison checked against LinkedIn's live pages, and third-party Advanced Plus estimates checked against their live pages, on that date.* ## LinkedIn X-Ray Search: Operators, Examples, and What Google's Index Can't See (2026) LinkedIn X-ray search means using Google to find LinkedIn profiles instead of searching inside LinkedIn. You point Google at LinkedIn's profile pages with `site:linkedin.com/in/`, add the words you expect to find on the profile, and read the results while logged out. It is free, it needs no account, and it exists because there is no general-purpose LinkedIn people-search API open to ordinary developers – see [what LinkedIn's API actually gives you](/guides/linkedin-api-access). This guide covers the operators that genuinely work, strings you can copy, the boundary where Google's copy of LinkedIn stops being enough, and what to do at that point. > **The short version.** X-ray search uses Google operators – `site:linkedin.com/in/`, quotes and `-`, plus a few Google does not document – to find public LinkedIn profiles without logging in. It works because members' public profiles are visible to search tools, and it is genuinely free. Its ceiling is architectural rather than a bug: you are searching Google's copy of LinkedIn, so you get what members chose to make public, as Google last crawled it, matched on keywords. There is no reliable structured filter for current versus past employer. When that distinction matters, run the search inside LinkedIn instead. **X-ray search is not the same thing as LinkedIn boolean search.** Both use boolean logic, which is why the two get mixed up constantly, but they query different indexes. Boolean search runs **inside** LinkedIn, on your own account, using LinkedIn's operators and filter fields – a separate technique with [its own operators and rules](/guides/linkedin-boolean-search). X-ray runs **outside** LinkedIn, in Google, against public copies of profiles. Everything below is about the second one. ## What LinkedIn X-ray search is (and why it works at all) X-ray search works because LinkedIn members can expose their profile to search engines. LinkedIn's own help page tells members they can "control what sections of your profile are eligible for display to people who are not signed in to LinkedIn, or can be viewed on search tools such as Google or Bing" ([LinkedIn public profile visibility](https://www.linkedin.com/help/linkedin/answer/a518980)). That single setting is the whole foundation. Google crawls those public profiles, stores them, and lets you query its copy with ordinary search operators. Nothing is bypassed and nothing is unlocked – you are reading pages members published deliberately. The technique got its name because it feels like seeing through LinkedIn's own search, which caps results by account tier and biases toward your network. Google has no such view of you. What it does not give you is LinkedIn's structured fields – there is nothing to filter on for who currently works where, which is the point at which the method eventually runs out. ## The X-ray operators that work The table below carries only the operators that are useful for finding profiles. Whether Google documents an operator is a separate question from whether it helps here, so it is noted per row rather than used to decide what belongs in the table. **Documented by Google and useful here** ([Refine Google searches](https://support.google.com/websearch/answer/2466433)): | Operator | What it does | Constraint to know | |---|---|---| | `site:` | Restricts results to one site or path. `site:linkedin.com/in/` returns profiles only. | Retrieval is **not exhaustive** – see below. | | `"exact phrase"` | Matches the phrase as written. | Quote every multi-word title, or Google matches the words separately and independently. | | `-` | Excludes a term. | No space between the operator and the word: `-recruiter`, never `- recruiter`. | **Not documented by Google, but working in practice.** These four appear on every X-ray guide as though they were official. They are not on Google's operator help page, so treat them as best-effort: use them, then sanity-check the results rather than trusting them silently. | Operator | What it does | Constraint to know | |---|---|---| | `intitle:` | Matches text in the page title. A LinkedIn profile title carries the name and headline. | Best-effort; verify a sample of results. | | `inurl:` | Matches text in the URL. Useful for separating `/in/` from `/company/`. | Best-effort. | | `OR` | Either term matches. | Must be uppercase. Lowercase `or` is treated as a normal word. | | `*` | Stands in for a whole word. | A whole word, not a partial stem. Most reliable inside a quoted phrase: `"head of * marketing"`. | **Documented by Google but not useful for this job.** `filetype:` filters by document format, and LinkedIn profiles are ordinary HTML pages, so it narrows nothing here. `before:` and `after:` filter on Google's document dates – not on employment dates, and not a dependable signal for how fresh a profile is. If you have seen them recommended for X-ray, that is why they are absent from the tables above. One limit applies to every string you will write. Google states plainly that the `site:` operator "doesn't necessarily return all the URLs that are indexed under the prefix specified in the query" ([Google Search Central](https://developers.google.com/search/docs/monitor-debug/search-operators/all-search-site)). An X-ray search is a sample of what Google chose to show you, never a complete list of matching profiles. ### Common X-ray syntax mistakes - **A space after the operator.** Google is explicit: "Do not put spaces between the operator and your search term." - **Lowercase `or`.** It stops being an operator and becomes a search word. - **Unquoted multi-word titles.** `head of growth` matches the three words anywhere on the page; `"head of growth"` matches the title. - **`site:linkedin.com` when you wanted people.** That includes posts, jobs and company pages. Use `site:linkedin.com/in/` for profiles. ## X-ray search strings you can copy Each string is labelled with what it actually does, which is not always what it looks like it does. **By title and location** ``` site:linkedin.com/in/ "head of growth" "Berlin" ``` **Mentions a company – not a current-employer filter** ``` site:linkedin.com/in/ "product manager" ("Acme" OR "Globex") ``` This finds profiles where those company names appear anywhere: current role, a job from six years ago, or a recommendation someone wrote. A snippet often hints at which, but there is no structured employer field to filter on, so you sort it out by reading the results. **By school** ``` site:linkedin.com/in/ "data scientist" "Technical University of Munich" ``` **By localized LinkedIn host** ``` site:de.linkedin.com/in/ "vertriebsleiter" ``` This selects LinkedIn's German-language host. It is **not** a "people located in Germany" filter – plenty of people in Germany are served on `www.linkedin.com`, and this string will miss them. **Excluding the noise** ``` site:linkedin.com/in/ "software engineer" "Amsterdam" -recruiter -intern -"talent acquisition" ``` **Company pages instead of people** ``` site:linkedin.com/company/ "solar installation" "Netherlands" ``` ## What Google's index can't know X-ray is not broken, and the strings above genuinely work. But five limits are structural, and no amount of operator skill removes them. **1. The index lags, sometimes by months.** LinkedIn is direct about this: "After you make changes to your public profile, it can take several weeks or months at times for search tools like Google, Yahoo, or Bing to detect changes and refresh. LinkedIn doesn't control that refresh process" ([LinkedIn public profile visibility](https://www.linkedin.com/help/linkedin/answer/a518980)). A person who changed jobs in March may still read as their old title. ![Diagram of the X-ray refresh path: a member edits their profile, the change appears on their public LinkedIn profile immediately, then Google re-crawls the page on its own schedule taking weeks to months, and only then does the change appear in X-ray results. A separate short arrow shows native authenticated search reading LinkedIn directly with no lag.](/images/guides/linkedin-xray-refresh-path.webp) **2. No reliable structured current-versus-past employer filter.** Google matches words on a page, so a profile mentioning Acme may be a current role, a former one, or a line in someone's recommendation. A snippet often hints at which, but there is no field meaning "currently works here" that you can filter or sort on – so at any volume you are verifying by hand. **3. Google operators are not LinkedIn fields.** `intitle:` searches text. LinkedIn's own search has structured fields for position, industry, location, school and employer. These are different mechanisms, and the second is not reachable from Google. **4. You only ever see what members allowed.** Public-profile visibility is member-controlled per section. Someone who narrowed theirs is invisible to X-ray no matter how precise your string is. **5. Retrieval is not exhaustive.** As Google states above, `site:` does not necessarily return everything indexed under that prefix. You cannot know what you missed, which makes X-ray unsuitable as a coverage-complete source list. ## X-ray vs LinkedIn-native search: which to use when | | X-ray search (Google) | LinkedIn-native search (Linked API) | |---|---|---| | Where the search runs | Google's index of public profiles | LinkedIn itself, authenticated | | What it matches on | Keywords in a public snippet | LinkedIn's own structured fields | | Freshness | Google's crawl – weeks to months of lag | LinkedIn as it is now | | Current vs past employer | No reliable structured filter | `currentCompanies` and `previousCompanies`, separately | | Filters available | Google operators | Position, locations, industries, companies, schools | | Output | A page of links you copy by hand | Structured results your code can consume | | Login required | None – runs logged out | Your own LinkedIn account | | Cost | Free | Flat per seat, from $49/mo billed annually | | Best for | One-off lookups, no account, no budget | Repeatable searches, products, CRM enrichment | ## When the job outgrows Google: run the search inside LinkedIn At the point where the current-versus-past distinction matters, or you need the same search to run every week, the answer is to stop searching Google's copy and search LinkedIn. There are two ways in, and they are equally first-class – pick the one that matches how you work. ### Ask an agent (nothing to build) If you would rather not write code, you do not have to. Ask in plain language: > Find heads of growth in Berlin who currently work at Acme or Globex and give me their LinkedIn URLs. That runs through the [MCP server](/mcp/overview) in any MCP client, through the AI-agent-friendly [CLI](/cli/getting-started) in Claude Code, Cursor or Codex, or through a ready-made skill installed with `npx @linkedapi/skills`. Install it, ask, read the results. There are no blocks to drag and nothing to wire, and the agent composes the search from the same filters described below. ### Build it in (compose it yourself) To embed the search in your own product, backend or CRM, call it directly. Pass a keyword `term` alongside the structured `filter` – this is what makes the current-versus-past distinction available at all: ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const search = await linkedapi.searchPeople.execute({ term: 'growth', limit: 25, filter: { position: 'Head of Growth', locations: ['Berlin'], currentCompanies: ['Acme', 'Globex'], }, }); const { data: people } = await linkedapi.searchPeople.result(search.workflowId); for (const person of people ?? []) { console.log(person.name, person.headline, person.publicUrl); } ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, SearchPeopleFilter, SearchPeopleParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) search = linkedapi.search_people.execute( SearchPeopleParams( term="growth", limit=25, filter=SearchPeopleFilter( position="Head of Growth", locations=["Berlin"], current_companies=["Acme", "Globex"], ), ) ) people = linkedapi.search_people.result(search.workflow_id).data for person in people or []: print(person.name, person.headline, person.public_url) ``` Swap `currentCompanies` for `previousCompanies` and you have the search Google cannot express: people who used to work somewhere and no longer do. The same search runs from the shell: ```bash linkedin person search --term "growth" --limit 25 --position "Head of Growth" --locations "Berlin" --current-companies Acme,Globex --json ``` Two honest notes on the output. A search returns search results – name, profile URL, headline, location – and not full profiles; pulling experience, education, skills or posts is a separate `fetchPerson` call, covered in the [profile scraper guide](/guides/linkedin-profile-scraper). And results never include personal email addresses or phone numbers. Full parameters are in the [people search docs](/docs/searching-for-people) and [person data docs](/docs/retrieving-person-data); if you would rather build the query in LinkedIn's UI and hand over its URL, the [boolean search guide](/guides/linkedin-boolean-search) covers that route. If you source in Sales Navigator, the `nv` variants take the same shape with extra filters such as years of experience – see the [Sales Navigator scraper guide](/guides/linkedin-sales-navigator-scraper). For the wider picture of getting LinkedIn data without maintaining a scraper, start from [how to scrape LinkedIn](/guides/how-to-scrape-linkedin). ## When X-ray is still the right tool X-ray is not a worse version of API search. For several jobs it is simply the correct choice, and switching would be overkill: - **One-off lookups.** Finding one person once does not justify an account, a seat or a line of code. - **No LinkedIn account, or a locked-down one.** X-ray runs logged out. It never touches your account, so it consumes nothing and risks nothing. - **Zero budget.** It is free, permanently. - **Checking what is publicly visible.** If you want to see how someone appears to the outside world, X-ray shows exactly that, because that is all it can see. There is a third option worth naming honestly. If what you want is a finished interface with sourcing, sequences and campaign management already built in, a closed automation suite is a reasonable answer, and several exist. Linked API is the opposite trade: primitives you compose, embedded wherever you need them, or driven by an agent – which is the right shape when the search feeds your own product or workflow, and the wrong shape when you wanted a ready-made UI. ## Is X-ray search safe for your account? X-ray itself touches nothing. You are logged out and running a Google query, so LinkedIn sees no activity from you at all, and none of your account's search allowances are consumed. The [limits guide](/guides/understanding-linkedin-limits) covers what LinkedIn's own in-app search costs you by comparison. Running searches through the API is different, because that genuinely uses your account – so it is built to behave like you. Each account gets a real, dedicated cloud browser that you authorize once through LinkedIn's own sign-in flow; Linked API never stores your password. Actions run at a human pace, and you set the per-action ceilings yourself, per day, week or month. See [safety](/safety) for how that works in practice. ## Frequently Asked Questions (FAQ) #### What is LinkedIn X-ray search? It is the technique of finding LinkedIn profiles through Google instead of LinkedIn's own search, by restricting Google to LinkedIn's profile pages with `site:linkedin.com/in/` and adding the words you expect on the profile. It works on public profiles, while logged out, and requires no LinkedIn account. #### Is X-ray search the same as LinkedIn boolean search? No, though they are easy to confuse because both use boolean logic. Boolean search runs inside LinkedIn on your own account, using LinkedIn's operators and its filter fields. X-ray runs in Google, against public copies of profiles, using Google's operators. The difference is which index you are querying, not which logic you use – see the [boolean search guide](/guides/linkedin-boolean-search) for the in-LinkedIn technique. #### Is LinkedIn X-ray search free? Yes, entirely. It uses ordinary Google search with standard operators, so there is nothing to buy and no seat to hold. The cost is your time building strings and copying results by hand. #### Do I need a LinkedIn account to run an X-ray search? No. That is one of its real advantages. X-ray reads public profiles through Google while you are signed out, so it never touches a LinkedIn account and never consumes any account's search allowance. #### Why does my X-ray search return so few results? Usually one of four reasons: your quoted phrases are stricter than the wording on the profiles; the `site:` operator does not necessarily return everything Google has indexed under that path; some members have narrowed which sections of their public profile are visible to search tools; or Google has not re-crawled the profiles since they changed. There is no fixed result cap to work around – tighten or loosen your terms and compare. #### Does `intitle:` work in LinkedIn's own search box? No. `intitle:` is a Google operator and only works in Google. LinkedIn's own search has no `title:`-style field command – you use its boolean operators and its filter fields instead, which the [boolean search guide](/guides/linkedin-boolean-search) covers. #### Can X-ray search find email addresses or phone numbers? Only by accident. If a member published contact details in a public section of their profile and Google indexed that text, it can surface – but it is not a reliable contact source and most profiles will not yield one. Linked API does not return personal email addresses or phone numbers either; searches and profile fetches return LinkedIn profile data. #### Does X-ray search count toward LinkedIn's search limits? No. You are querying Google, not LinkedIn, so LinkedIn's commercial-use limit and tier-based result caps do not apply. Those limits apply to searching inside LinkedIn, whether by hand or through an API – the [limits guide](/guides/understanding-linkedin-limits) has the working numbers. #### Can I run LinkedIn searches programmatically instead? Yes. [Linked API](/sdks/installation) runs the search inside LinkedIn on your own account and returns structured results, with filters for position, location, industry, school, and current versus previous employer. You can call it from the REST API, the Node and Python SDKs or the [CLI](/cli/getting-started), or have an agent run it through the [MCP server](/mcp/overview) or a [ready-made skill](/skills). ## Start where the search stops Use X-ray for what it is genuinely good at – free, logged-out, one-off lookups on public profiles. When the search needs to be repeatable, current, or feed something you are building, run it inside LinkedIn instead: ask an agent through the [MCP server](/mcp/overview), the [CLI](/cli/getting-started) or a [ready-made skill](/skills), or build it in with the [REST API and SDKs](/sdks/installation). Every route runs on the same dedicated cloud browser, on an account you own, inside the limits you set. See [pricing](/pricing) for plans. *Facts verified 7 August 2026 – LinkedIn public-profile visibility and refresh statements checked against LinkedIn Help, and Google search-operator behaviour checked against Google Search Help and Google Search Central, on that date.* ## LinkedIn AI Agent: How to Give Your Agent Access to Your Account A LinkedIn AI agent is one that *does* things in your account rather than writing text for you to paste: it runs the search, opens the profiles, sends the connection request, replies in the thread. Getting one working turns out to be less about the agent than about what sits between it and LinkedIn, because that connector decides both what your agent can do and what it holds of yours. > **The short version.** An AI agent needs an authorized way to act on LinkedIn – typically a scoped access token or an authenticated account session. Compare connectors on three things: what authentication material they receive and retain, where the actions execute, and which actions and limits they expose. Those answers shape your exposure, your control and your capability. ## What is a LinkedIn AI agent? Two different things share the name, and mixing them up is why setup advice often does not match what you wanted. | | What it does | Where it runs | The ask that fits | |---|---|---|---| | **Writing assistant** | Drafts posts, comments and replies for you to publish yourself | In the chat window | "Write me a post about our launch" | | **Acting agent** | Runs actions in your account – search, read, connect, message | Through a connector holding an authorized session | "Find heads of sales in Berlin and connect with the ones who fit" | This guide covers the second kind. If all you want is better copy, a writing assistant is the cheaper answer and none of the setup below applies. ## How can an agent reach your account? An agent has no standing on LinkedIn by itself. Something has to carry authorization on your behalf, and the options differ on two axes: **what authentication material that connector receives and retains**, and **where the actions actually execute**. These are the common models, not an exhaustive taxonomy: | Model | Authentication material held | Where actions execute | What that means for you | |---|---|---|---| | Credential-based | Your LinkedIn password, used to obtain session cookies | Vendor side, varies by implementation | The connector holds your credentials | | Cookie-based | Your `li_at` session cookie | Vendor side, varies by implementation | The cookie expires, so you reconnect on a cycle | | Official tokens | An OAuth token, scoped to what LinkedIn grants | LinkedIn's public API | A narrow, well-defined surface – see [LinkedIn API access](/guides/linkedin-api-access) | | Local browser extension | Your own logged-in browser session | Your machine, your browser | Requires your browser and device to stay available; controls and pacing vary by implementation | | Wire-level client | Session material, held vendor side | Reverse-engineered calls to private endpoints | Traffic that is not a browser session | | Hosted browser session | An authorized session inside a browser bound to your account | That hosted browser | You connect the account once and the session persists | Two things are worth being precise about. First, every logged-in browser necessarily contains session state, so the useful question is never "cookies or no cookies" – it is who *receives and retains* what. Second, the third comparison dimension from the summary above, which actions and limits a connector exposes, is not in this table on purpose: it is answered by the next two sections. [Linked API](/) is one implementation of the last row. You sign in once through LinkedIn's own flow, the account gets its own dedicated cloud browser, and every action afterwards happens on LinkedIn's real pages at a human pace. Linked API never stores your password. ## What can an AI agent actually do on LinkedIn? The ceiling is set by the actions your connector exposes, not by how clever the model is. These are the operations available to an agent through Linked API, grouped by what the agent is trying to accomplish: | Group | Operations | Typical agent task | |---|---|---| | **Find** | `search_people`, `search_companies`, `search_jobs` | "Find product managers at fintech companies in Amsterdam" | | **Read** | `fetch_person`, `fetch_company`, `fetch_post`, `fetch_job`, `retrieve_connections`, `retrieve_feed`, `retrieve_ssi`, `retrieve_performance` | "Pull their experience and skills, and tell me who moved roles recently" | | **Act** | `send_connection_request`, `withdraw_connection_request`, `accept_invitation`, `send_message`, `react_to_post`, `comment_on_post`, `reply_to_comment` | "Connect with the ones who fit, with a note referencing their last post" | | **Monitor** | `sync_inbox` and `get_inbox`, `sync_network` and `get_network`, `check_connection_status`, `retrieve_pending_requests` | "Tell me who accepted and who replied since yesterday" | Each of these has a guide of its own with the depth this page skips: [profile data](/guides/linkedin-profile-scraper), [company data](/guides/linkedin-company-scraper), [messaging](/guides/how-to-automate-linkedin-messages), [connection requests](/guides/how-to-automate-linkedin-connection-requests), [posting](/guides/how-to-automate-linkedin-posts), [pending invitations](/guides/linkedin-pending-invitations) and [your SSI](/guides/linkedin-social-selling-index). The full list, including the Sales Navigator operations, is in [available tools](/mcp/available-tools). Sales Navigator has its own parallel set of operations, prefixed `nv_`. Those need a Sales Navigator seat on the account; everything in the table above does not. ## How do you connect an agent and run a first action? The sequence is the same whichever surface you pick, and the order matters – limits before the first run, reads before writes. **1. Connect the account once.** Sign in through the platform, which opens the account's dedicated cloud browser. This is the only step that touches LinkedIn credentials, and it happens on LinkedIn's own pages. **2. Choose a surface.** Two working modes – run it out of the box, or build it in – across four surfaces, all first-class. Pick by how you want to work, not by how much you want to build: | Surface | How you start | Best for | What to know | |---|---|---|---| | **Ready-made skills** | `npx @linkedapi/skills add linkedin`, or `add linkedin-growth` for hands-off network growth | You want the outcome, not the wiring – install and ask | Two published skills; needs Node.js 20+, the CLI and your tokens, and the installer checks for them | | **MCP server** | [Installation](/mcp/installation) | Claude, Claude Code, ChatGPT, Codex, Cursor, VS Code, Windsurf | Your agent has to speak MCP | | **Shell CLI** | `npm install -g @linkedapi/linkedin-cli`, then `linkedin setup` | Agents that run shell commands, plus your own scripts and CI | Shell-shaped output rather than a typed client | | **REST API and SDKs** | [Node and Python SDKs](/sdks) | Embedding the actions inside your own product or backend | You build and maintain the orchestration | ![The execution path for a LinkedIn AI agent: an agent such as Claude, Claude Code, ChatGPT, Codex or Cursor reaches Linked API through four surfaces – a ready-made skill, which runs on the shell CLI; the shell CLI directly; the MCP server; or the REST API and SDKs; inside Linked API the configured per-action limits gate every request before it reaches the account's dedicated cloud browser, which acts on LinkedIn](/images/guides/linkedin-ai-agent-execution-path.webp) **3. Set your tokens and your limits before the first run.** The surface needs your Linked API token and identification token. This is also the moment to configure per-action limits, covered in the next section. Set your own caps before the first run; otherwise that run uses the system defaults rather than your custom limits. **4. Test with a read.** Ask the agent for something that changes nothing: fetch a profile, run a search, pull your connections. You are checking that the account is connected, the output is what you expected, and the agent is calling the operation you thought it would. **5. Then approve a write.** Only once reads behave, let it send a connection request or a message – and read the copy it drafted before it goes out. ## What limits can you put on what the agent does? An agent will do exactly as much as you let it, which is a problem when the instruction was vague. Two distinct things help here, and they are worth keeping apart, because only the first is enforced by the platform. **Enforced by the platform.** You configure per-action limits on the account by category and period – 11 categories, including connection requests, messages, searches, reactions, comments, posts and profile views, each settable for a `daily`, `weekly` or `monthly` window. An action that would cross a cap returns a `limitExceeded` error instead of executing. An enforced ceiling makes the agent's maximum activity predictable by action category and period. The [Limits API](/docs/admin-limits) covers reading defaults, setting your own and resetting them. Worth stating plainly: limits govern the volume and pace you configured. They are a control you set, not a guarantee about anything else. **Your own operating practices.** These are not platform behaviour, they are how you work: read before you write on a new setup, review outbound copy before it sends while you are still learning what the agent drafts, and start below the volume you eventually want. For LinkedIn's own position on automation and account restrictions, see our guide to a [restricted LinkedIn account](/guides/linkedin-account-restricted); the working numbers live in [understanding LinkedIn limits](/guides/understanding-linkedin-limits). ## When an agent is the wrong route An agent driving a session is interactive and non-deterministic. That is the point when you are exploring, and a drawback when you are not: - **You need the same job to run identically every night.** Scheduled, repeatable, auditable work belongs in your own code, calling the same operations through the REST API or the SDKs. You give up the plain-language interface and get determinism and logs in exchange. - **Your volume exceeds what an account can do.** Everything here runs through one LinkedIn account at a human pace, whichever surface you choose – the API is not a faster lane. Work that needs to move faster than an account can act does not fit this model at all. - **You only want better copy.** A writing assistant drafts your posts and comments without any of this setup. ## Frequently Asked Questions (FAQ) #### What is a LinkedIn AI agent? An AI agent that performs actions in a LinkedIn account rather than only generating text – searching for people, reading profiles, sending connection requests and messages, reacting and commenting. It reaches the account through a connector that holds an authorized session or token, and what that connector exposes sets what the agent can do. #### Can ChatGPT or Claude use my LinkedIn account directly? Not on their own. A model has no standing on LinkedIn until you connect it to something that carries authorization on your behalf. In practice that means adding a connector – an MCP server, a command-line tool the agent can run, or an API your own code calls – and pointing your agent at it. #### Do I have to give an AI agent my LinkedIn password? That depends entirely on the connector, and it is worth checking before you pick one. Some models take your password or your session cookie; token-based routes take neither. With Linked API you sign in once through LinkedIn's own flow to authorize a dedicated cloud browser for the account, and your password is never stored. #### Which AI agents and clients work with Linked API? Two separate paths. Any MCP client connects through the MCP server – Claude, Claude Code, ChatGPT, Codex, Cursor, VS Code and Windsurf among them. Separately, any agent that can run shell commands works through the CLI, which is how Claude Code, Cursor and Codex are typically driven, and which also suits scripts and CI. #### What can an AI agent not do on LinkedIn? It cannot exceed what the connector exposes or what the account itself can do. It has no access to data LinkedIn does not show that account, it cannot outpace a human-paced session, and it does not return personal email addresses or phone numbers. Sales Navigator operations additionally require a Sales Navigator seat. #### How many actions per day should I let an agent run? You decide, per action category and per day, week or month, and the platform holds that ceiling. There is no published universal number that applies to every account, so treat the cap you configure as your control rather than looking for an official figure. The [limits guide](/guides/understanding-linkedin-limits) collects the working numbers. #### Do I need Sales Navigator for an AI agent to work? No. Standard search, profile and company data, messaging, connections, posts and analytics all work on a regular LinkedIn account. A Sales Navigator seat is needed only for the `nv_` operations, which run inside Sales Navigator itself. #### How much does it cost to run an AI agent on LinkedIn? Linked API is flat per seat, from $49 per month billed annually, with no per-lead or per-credit metering – the agent calling an operation a hundred times costs the same as calling it once. See [pricing](/pricing) for the current plans. ## Give your agent an account it can actually use Pick the route that matches how you work. Install a [ready-made skill](/skills) and ask in plain language, connect the [MCP server](/mcp/overview) to your assistant, drive the [CLI](/cli/getting-started) from Claude Code, Cursor or Codex, or build it into your own product with the [REST API and SDKs](/sdks). Every route runs on the same dedicated cloud browser, on an account you own, inside the limits you set. ## LinkedIn API Access: What Standard Developer Access Gets You LinkedIn API access begins the same way for everyone: you arrive at the developer portal with a specific job in mind, create an application, open the Products tab, and find the list of things you can switch on is far shorter than expected. This guide covers exactly what standard developer access grants, how to set it up, and what to do about the jobs it does not cover. > **The short version.** Only a small set of LinkedIn products can be enabled without review, and every one of them is scoped to the member who authenticates: reading that member's own name, headline, photo and primary email through Sign in with LinkedIn, and posting or commenting as that member through Share on LinkedIn. Verified on LinkedIn adds a self-serve Development tier for testing. There is no endpoint a standard developer account can call to search for arbitrary people, look up another member by profile URL, send ordinary member-to-member messages, or read a member's inbox. For those jobs you drive an account you already own, which is what [Linked API](/pricing) does. ## What you can enable self-serve today LinkedIn states the rule plainly in its [access documentation](https://learn.microsoft.com/en-us/linkedin/shared/authentication/getting-access): > "Most permissions and partner programs require explicit approval from LinkedIn. Open Permissions are the only permissions that are available to all developers without special approval." These are the Open Permissions, added through the Products tab on your own application: | Product | Permission | What it grants | |---|---|---| | Sign in with LinkedIn using OpenID Connect | `profile` | **Member Auth**: Retrieve authenticated member's name, headline, and photo. | | Sign in with LinkedIn using OpenID Connect | `email` | **Member Auth**: Retrieve authenticated member's primary email address. | | Share on LinkedIn | `w_member_social` | **Member Auth**: Post, comment and like posts on behalf of an authenticated member. | One more route is self-serve and often missed. **Verified on LinkedIn** offers a Development tier you can enable immediately, restricted to admin accounts and intended for testing, plus a Lite tier you request self-serve for consenting members, per [LinkedIn's Verified API documentation](https://www.linkedin.com/help/linkedin/answer/a9385085). Its endpoints take their own scopes: [`/identityMe`](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/verified-on-linkedin/api-reference/identity-me) requires `r_profile_basicinfo`, and [`/verificationReport`](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/verified-on-linkedin/api-reference/verification-report) requires `r_verify_details`, with the older `r_verify` still accepted. Read the pattern rather than the list. **Every self-serve permission acts on the member who just signed in.** You get their details because they authenticated, and you publish as them because they authorised it. Nothing in this set reaches outward to other members. ## How to get self-serve LinkedIn API access The self-serve path is short, and most of the friction is in details that are easy to get wrong. 1. **Create an application** in the LinkedIn Developer Portal. You will be asked to associate it with a LinkedIn Page, so have one ready or create it first – an application cannot exist without it. 2. **Enable your products** on the application's Products tab. Sign in with LinkedIn using OpenID Connect and Share on LinkedIn appear immediately. Verified on LinkedIn is requested from the same tab: accept the terms and the Development tier is provisioned automatically, while Lite is a self-serve request. Anything reviewed shows an application flow instead of a switch. 3. **Configure your redirect URL** on the Auth tab. It must match byte for byte what your application sends, including the scheme and any trailing path, per LinkedIn's [3-legged OAuth flow](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow). A mismatch fails the authorisation request rather than degrading. 4. **Request only the scopes your enabled products grant.** A scope your application has not been granted is rejected outright, so a request that mixes granted and ungranted scopes returns nothing at all. 5. **Send the member through the authorisation flow**, exchange the returned code for an access token, and make your first call as that member. ## What standard developer access does not reach Four jobs come up constantly, and none of them is reachable this way: ![Self-serve LinkedIn access covers you, not other people: included are your own name, headline and photo, your email address, posting as you, and your verified identity, all granted when you sign in; not included are finding people, reading a profile, sending a message and reading an inbox, which need an account you already own](/images/guides/linkedin-api-access-scope.webp) Some of these capabilities do exist inside narrowly scoped reviewed and partner integrations, which are bound to a product context rather than exposed as general-purpose endpoints and are [approval-gated](https://learn.microsoft.com/en-us/linkedin/shared/authentication/getting-access). If you work in Sales Navigator, that picture is covered in our [Sales Navigator API guide](/guides/linkedin-sales-navigator-api). The practical position is simpler than the documentation makes it look: **for these four jobs there is nothing a standard developer account can switch on.** ## What to build on instead The alternative is to work through an account you already have. The member is you or your team, the account is one you own, and the primitives are exposed programmatically. Both routes below are first-class – pick by how you want to work, not by how much you want to build. ### Build it in – REST, Node and Python SDKs, CLI Compose the primitives and embed them in your product. A search returns each person's `publicUrl`, which you pass straight into a fetch: ```typescript import { LinkedApi } from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: 'your-linked-api-token', identificationToken: 'your-identification-token', }); const workflow = await linkedapi.searchPeople.execute({ term: "Head of Engineering", limit: 10, filter: { locations: ["San Francisco"], industries: ["Software Development"], }, }); const { data, errors } = await linkedapi.searchPeople.result(workflow.workflowId); if (errors?.length) { errors.forEach((error) => console.warn(`${error.type}: ${error.message}`)); } if (data) { for (const person of data) { const detail = await linkedapi.fetchPerson.execute({ personUrl: person.publicUrl }); const result = await linkedapi.fetchPerson.result(detail.workflowId); if (result.data) { console.log(result.data.name, result.data.headline, result.data.location); } } } ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, SearchPeopleParams, FetchPersonParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) workflow = linkedapi.search_people.execute( SearchPeopleParams( term="Head of Engineering", limit=10, filter={ "locations": ["San Francisco"], "industries": ["Software Development"], }, ) ) result = linkedapi.search_people.result(workflow.workflow_id) for error in result.errors or []: print(f"{error.type}: {error.message}") if result.data: for person in result.data: detail = linkedapi.fetch_person.execute( FetchPersonParams(person_url=person.public_url) ) person_result = linkedapi.fetch_person.result(detail.workflow_id) if person_result.data: print( person_result.data.name, person_result.data.headline, person_result.data.location, ) ``` The same primitives cover the other three jobs. Messaging is `sendMessage`, and the inbox is a two-step pattern rather than a single read: `syncInbox` starts monitoring, then `pollInbox` retrieves what has arrived since. The same operations run from the shell as `linkedin person search`, `linkedin person fetch`, `linkedin message send`, `linkedin inbox sync` and `linkedin inbox get`. Each job has its own guide with the depth this page deliberately skips: [people search](/docs/searching-for-people), [profile data](/guides/linkedin-profile-scraper), and [messaging and inbox](/guides/how-to-automate-linkedin-messages). ### Run it out of the box – MCP, the AI-agent-friendly CLI, ready-made skills If you would rather not build the integration at all, an AI agent can do it. Connect the [MCP server](/mcp), point an agent at the [CLI](/cli) from Claude Code, Cursor or Codex, or install ready-made skills with `npx @linkedapi/skills`. You describe the job in plain language and the agent composes and runs the same workflows. Nothing to wire, no flows to configure. ## When the self-serve products are the right answer Reach for LinkedIn's own products, not an alternative, when the job actually matches what they do: - **You need members to sign in.** Sign in with LinkedIn using OpenID Connect is built for exactly this, and nothing else should be used for authentication. - **You need to publish as the member who authorised you.** Share on LinkedIn covers posting, commenting and liking on their behalf. - **You need identity verification signals.** Verified on LinkedIn is the route, starting on the Development tier. The account-based route earns its place when the job reaches beyond the authenticated member – reading data about other people, or acting continuously rather than at the moment someone clicks a button. ## Frequently Asked Questions (FAQ) #### What LinkedIn API access can I get without approval? The Open Permissions – `profile` and `email` through Sign in with LinkedIn using OpenID Connect, and `w_member_social` through Share on LinkedIn – plus Verified on LinkedIn, whose Development tier is provisioned automatically on accepting the terms and whose Lite tier is a self-serve request. Every one of these acts on the member who authenticated. #### How do I set up self-serve LinkedIn API access? Create an application in the Developer Portal and associate it with a LinkedIn Page, enable your products on the Products tab, set the redirect URL on the Auth tab so it matches your application exactly, request only the scopes your enabled products grant, then send the member through the authorisation flow and exchange the code for a token. #### Why can't I search for people with standard developer access? There is no general-purpose people-search endpoint available to a standard developer account. Search capabilities exist inside narrowly scoped reviewed and partner integrations tied to a product context, not as an open endpoint. The alternative is to run the search through an account you own. #### Can I read another member's profile with standard developer access? No. The self-serve permissions return the authenticated member's own details, not other members'. Reading another member's profile from a URL is not something a standard developer account can enable, which is why account-based routes exist. #### What do I use when self-serve products don't cover the job? An account-based API that drives a LinkedIn account you already own. Linked API exposes people search, profile fetch, messaging and inbox as programmable primitives through the REST API, Node and Python SDKs and the CLI, or out of the box through MCP, the AI-agent-friendly CLI and ready-made skills. ## Build on the account you already have If the self-serve products cover your job, use them. If your job needs to reach past the member who signed in, Linked API runs those operations on an account you own, through a dedicated cloud browser at a human pace. Embed it through the [REST API, SDKs and CLI](/sdks), or skip the build and let an agent run it through [MCP](/mcp) or [ready-made skills](/skills). Pricing is flat per seat – see [pricing](/pricing). *Facts verified 3 August 2026 – LinkedIn permission names, product availability and endpoint scopes checked against LinkedIn's official developer and help documentation on that date.* ## LinkedIn Sales Navigator API: What Exists, Who Can Use It, and What to Build On Sales Navigator holds the search filters, lead lists, and account data your team actually works from, and sooner or later someone asks whether the LinkedIn Sales Navigator API can drive all of it from code. The answer that circulates online is a muddle: some pages walk you through applying for access, others say applications are closed, and a few manage both on the same page. This guide states what the official API is, who can actually use it, and what the remaining routes give you. > **The short version.** LinkedIn does run an official Sales Navigator API – the Sales Navigator Application Platform, with three service families – but its own documentation says LinkedIn is **not currently accepting new partners**, adding that it reviews onboarding capacity periodically and will update the page if that changes. So the practical question is which of four situations you are in: you are already an approved partner; your CRM vendor already ships an approved integration; you need a one-off extraction, where an export tool beats any API; or you need repeatable automation on a Sales Navigator seat you own, which is where an account-based platform like [Linked API](/pricing) fits. ## Is there an official LinkedIn Sales Navigator API? Yes, and it is a real product with real documentation: the **Sales Navigator Application Platform (SNAP)**. It is also gated. LinkedIn's [SNAP documentation](https://learn.microsoft.com/en-us/linkedin/sales/) states plainly: > "We are not currently accepting new partners for access to the LinkedIn Sales Navigator API. We periodically review our onboarding capacity and will update this page if availability changes." That second sentence matters. This is a current state, not a permanent one, and any page telling you the programme is simply "closed" is as wrong as one walking you through an application form. SNAP is organised into three service families: | Service family | What it does | Shape | |---|---|---| | **Display Services** | UI modules embedded in your application via iframe or JavaScript SDK: view profiles and accounts, see recent activity and posts, connect and send messages including InMail, plus the Profile Associations API for photos and profile links | Embedded interface | | **Analytics Services** | Bulk export API for activities performed by sales teams, plus seat-holder metadata such as daily [Social Selling Index](/guides/linkedin-social-selling-index), total connections, and total leads saved | Bulk data export | | **Sync Services** | APIs built on top of CRM Sync, using matches between CRM records and LinkedIn to enable data-integrity workflows and CRM UI experiences | CRM integration | One distinction gets blurred constantly, so it is worth being precise. Display Services **does** let a message or InMail be sent – but by a **seller acting inside an embedded UI module** in your application. That is not a raw messaging endpoint, and it is not an automated outbound API. If you read "the Sales Navigator API can send messages" somewhere and pictured a POST request in a nightly job, that is not what is on offer. Use of these APIs is governed by the LinkedIn SNAP Terms of Use unless you have executed a separate signed partnership agreement. ## Which route fits your situation Most guides route everyone through "apply to become a partner". That is the least likely path to be relevant to you. Find your row first. | Your situation | The right route | What you get | |---|---|---| | **You are an approved SNAP partner** | The official Sales Navigator APIs | Display, Analytics, and Sync services under your partnership agreement. Nothing here beats them. | | **Your CRM already ships a SNAP integration** | Your CRM vendor's existing feature | Sales Navigator data inside the CRM you already pay for, with no development work at all | | **You need a one-off extraction** | An export tool or a dataset | A CSV of a saved search, or bulk records with no owned seat | | **You need repeatable automation on a seat you own** | An account-based automation platform | Search, fetch, message, and conversation sync as programmable primitives on your own seat | ## What each route actually gives you ### If you are an approved SNAP partner Use the official APIs and stop reading. You have Display Services for embedded LinkedIn surfaces, Analytics Services for bulk activity export, and Sync Services on top of CRM Sync, all under a signed agreement. No third-party route competes with sanctioned access on a supported contract. ### If your CRM already ships a SNAP integration This is the situation nearly every guide omits, and it covers far more readers than partnership does. You do not need to become a SNAP partner to benefit from SNAP – you need a vendor who already is one. Major CRM platforms ship Sales Navigator integrations built on exactly these services, which is what Sync Services exists for. Before you write any code, check whether the integration you want already exists in your CRM's marketplace. Building an account-based workflow to replicate a feature your CRM already ships is wasted effort. ### If you need a one-off extraction If what you actually need is a spreadsheet of one saved search, an export tool is the right answer and an API is overkill. Sales Navigator does not export leads or accounts to CSV or XLS natively, which is precisely why this tool category exists. Our [Sales Navigator export guide](/guides/linkedin-sales-navigator-scraper) compares the options. The trade-off is that you get a snapshot. There is no workflow, nothing to trigger, and nothing to act on. ### If you need repeatable automation on a seat you own This is the route for teams building Sales Navigator behaviour into a product, backend, or CRM of their own: search, open a lead, send a message, sync the conversation, then poll for the reply, all executed on a Sales Navigator seat you already pay for. More than one product serves this. Unipile covers the same ground on capability, per its own product documentation – search, sending, replies, inbox and conversation sync, and webhooks – so this is not a question of who can do it. The difference is architectural: Linked API drives your own seat through a dedicated real browser session with pacing and per-action limits enforced by the platform, while a reverse-engineered client constructs wire-level requests against LinkedIn's private interface and, per Unipile's own API documentation, its GA v1 leaves pacing to the caller while its v2 beta adds enforced rate limits – a structurally higher-risk position for the account, not a matter of configuration. The full side-by-side is in [Linked API vs Unipile](/vs/unipile). Neither model eliminates risk; automating a real account never does. Neither product is affiliated with, endorsed by, or sponsored by LinkedIn – Unipile states this plainly on its own site, and the same is true of Linked API. What differs is where the safety engineering lives: on [Linked API](/safety) it is the platform's job end to end, in a cloud browser that loads LinkedIn's own pages at a human pace. ## Two ways to run it Both routes below are first-class. Pick the one that matches how you work, not how much you want to build. ### Build it in – REST, Node and Python SDKs, CLI Compose the primitives yourself and embed them in your product. [`nvSearchPeople`](/sdks/nv-search-people) returns each person's `hashedUrl`, which is the durable key you pass to [`nvFetchPerson`](/sdks/nv-fetch-person): ```typescript const workflow = await linkedapi.nvSearchPeople.execute({ term: "Head of Engineering", limit: 10, filter: { locations: ["San Francisco"], industries: ["Software Development"], }, }); const { data, errors } = await linkedapi.nvSearchPeople.result(workflow.workflowId); if (errors && errors.length > 0) { errors.forEach((error) => console.warn(`${error.type}: ${error.message}`)); } if (data) { for (const person of data) { const detail = await linkedapi.nvFetchPerson.execute({ personHashedUrl: person.hashedUrl, }); const result = await linkedapi.nvFetchPerson.result(detail.workflowId); if (result.data) { console.log(result.data.name, result.data.position, result.data.companyName); } } } ``` ```python from linkedapi import NvSearchPeopleParams, NvOpenPersonPageParams workflow = linkedapi.nv_search_people.execute( NvSearchPeopleParams( term="Head of Engineering", limit=10, filter={ "locations": ["San Francisco"], "industries": ["Software Development"], }, ) ) result = linkedapi.nv_search_people.result(workflow.workflow_id) for error in result.errors or []: print(f"{error.type}: {error.message}") if result.data: for person in result.data: detail = linkedapi.nv_fetch_person.execute( NvOpenPersonPageParams(person_hashed_url=person.hashed_url) ) person_result = linkedapi.nv_fetch_person.result(detail.workflow_id) if person_result.data: print( person_result.data.name, person_result.data.position, person_result.data.company_name, ) ``` The same primitives are available over the [REST API](/docs/actions-overview) and the [shell CLI](/cli/sales-navigator) – `navigator person search`, `navigator person fetch`, `navigator message send`. Outreach itself is a separate job with its own rules; the messaging flow is covered in [How to Automate LinkedIn Messages](/guides/how-to-automate-linkedin-messages). ![Sales Navigator workflow architecture: nv.searchPeople feeds an nv.doForPeople iteration block, which opens each nv.openPersonPage, executed sequentially in a dedicated cloud browser on your own Sales Navigator seat and returning structured JSON, with conversation polling running outside the workflow](/images/guides/linkedin-sales-navigator-api-architecture.webp) Composed as a single workflow, [`nv.searchPeople`](/docs/action-nv-search-people) fans out through an [`nv.doForPeople`](/docs/action-nv-do-for-people) block into an [`nv.openPersonPage`](/docs/action-nv-open-person-page) per result, and the platform runs the whole tree sequentially on your seat. Replies are handled separately: send the message, sync the conversation, then poll for the reply later. ### Run it out of the box – MCP, the AI-agent-friendly CLI, ready-made skills If you would rather not build the integration at all, an AI agent can do it for you. Connect the [MCP server](/mcp) or point an agent at the [CLI](/cli) from Claude Code, Cursor, or Codex, or install ready-made skills with `npx @linkedapi/skills`. You describe the job in plain language – "find heads of engineering at Series B software companies in San Francisco and summarise their recent activity" – and the agent composes and runs the same workflows. Nothing to wire, no blocks to drag. ## When another route is the better choice - **You need one spreadsheet, once.** Use an export tool. An API is a worse answer to a question that ends when the CSV downloads. - **You need bulk records and own no Sales Navigator seat.** A dataset provider is the honest fit. Account-based routes require a seat, because they work through it. - **You want Sales Navigator surfaces inside a CRM UI.** If an approved SNAP integration exists for your CRM, it will beat anything you assemble, and it comes with vendor support. - **You are already an approved partner.** Use what you have. ## Frequently Asked Questions (FAQ) #### Is there an official LinkedIn Sales Navigator API? Yes. The Sales Navigator Application Platform (SNAP) offers Display, Analytics, and Sync services. It is gated to approved partners, and LinkedIn's own documentation states it is not currently accepting new partners, while noting it reviews onboarding capacity periodically and will update that page if availability changes. #### Can I apply to become a SNAP partner? Not at the moment. LinkedIn's SNAP documentation says it is not currently accepting new partners. Third-party guides that walk you through an application flow are describing a process that is not open, though LinkedIn does say it will update the page if capacity changes, so the state is worth re-checking rather than assuming permanence. #### Can the official Sales Navigator API send messages? Partly, and the detail matters. Display Services lets a seller send messages and InMail from inside an embedded UI module in your application. That is a person acting through an interface you have embedded, not a raw messaging endpoint and not an automated outbound API. #### Do I need a Sales Navigator subscription to automate it? Yes. Every route that works through your own seat requires that seat to exist. Sales Navigator is only needed for Sales Navigator actions – standard LinkedIn people, company, and post data does not require it. For what that seat costs per tier, and which accounts need one at all, see our [Sales Navigator cost guide](/guides/linkedin-sales-navigator-cost). #### Can I get emails or phone numbers from Sales Navigator? No. Linked API's Sales Navigator result types carry no email or phone field. Search results return name, hashed URL, position, location, and avatar URL, and a person fetch adds headline, position, and company name. #### What is the difference between a Sales Navigator export tool and an account-based API? An export tool produces a snapshot, usually a CSV, and the job ends there. An account-based API exposes Sales Navigator actions as composable primitives – search, fetch, message, sync – on your own seat, so you can build a repeatable workflow that reads and acts from inside your own code. ## Build it on a seat you own If the official API is not open to you and a CSV is not enough, Linked API runs Sales Navigator workflows on your own seat through a dedicated cloud browser, with pacing and limits enforced by the platform. Embed it through the [REST API, SDKs, and CLI](/sdks), or skip the build entirely and let an agent run it through [MCP](/mcp) or [ready-made skills](/skills). Pricing is flat per seat, from $49 per month billed annually – see [pricing](/pricing). *Facts verified 29 July 2026 – SNAP service descriptions and partner-availability wording checked against LinkedIn's official documentation, and competitor capability claims checked against their live pages, on that date.* ## LinkedIn Scraper in Python: Libraries, Code, and What Each Option Costs Most guides to building a LinkedIn scraper in Python compare the routes on how much code you write. That is the smaller half of the decision. The larger half is what runs your LinkedIn account while the code executes, because the routes differ far more there than they do in what they can extract. This guide covers all three practical routes, with the same extraction written twice so you can compare them directly, and with the current release and repository status of the libraries people usually recommend. One thing to know before you start reading around: a good share of the Python LinkedIn scraping guides you will find are published by companies that sell proxy and unblocking infrastructure – ScraperAPI, Scrapfly and Bright Data among them. They cover the routes that need that infrastructure well. The route that runs through an authenticated account is the one that tends to be missing, which is why it gets equal space here. > **The short version.** The Python routes differ less in what they extract than in what runs your LinkedIn account while they work, and that is what decides whether your account survives the project. Three practical routes. **Linked API's authenticated account API** drives a browser you signed into yourself, at human pace, under per-action limits you set, and returns structured data with no HTML parsing, no selector upkeep and no proxy pool. An **open-source library** is free and fastest to start, but it runs an automated browser on a session file you exported – or, in the Voyager case, calls LinkedIn's private endpoints directly – and you inherit its maintenance: the most popular scraper library rewrote its entire API in its last major version, and the widely referenced Voyager library has not shipped a release since November 2024. A **DIY browser script** gives maximum control and maximum upkeep, with pacing, session handling and infrastructure all yours to build. **No route is risk-free for your account**, so the practical question is which one you can run at a pace your account tolerates. ## The Python options for scraping LinkedIn, compared | Route | What it is | What LinkedIn's servers see | What it costs you | Best for | | --- | --- | --- | --- | --- | | **Authenticated account API (Linked API)** | Hosted browser per account, structured responses | A browser you signed into yourself, acting at human pace under configured limits | Subscription; account-paced | Production pipelines that must keep running | | **`linkedin-scraper`** | Playwright library for profiles, companies, jobs | An automated browser driving authenticated state you exported | Free; you follow its releases and API changes | Getting something working today | | **`linkedin-api`** | Client for LinkedIn's internal Voyager endpoints | Direct requests to private, undocumented endpoints | Free; no release since November 2024 | Reading the approach, not shipping on it | | **DIY Playwright / Selenium** | Your own browser automation | Same as above, with every detail your responsibility | Free; ongoing selector and infrastructure work | Full control, unusual requirements | | **Scrapy** | HTTP crawling framework, no browser | Plain HTTP requests; signed-out unless you build session handling | Free; auth and blocking are yours to solve | Crawling at scale beyond LinkedIn | | **Proxy / unblocker products** | Request infrastructure for public pages | Signed-out public-page requests spread across many addresses | Metered; a different dataset | Bulk signed-out collection | ## What LinkedIn's servers see from each route This is the part most comparisons skip, and it is the one that decides whether a scraper survives contact with production. What follows describes what each route *provides* architecturally. Nobody outside LinkedIn can tell you what its systems flag, and this guide does not try. ### Authenticated account API (Linked API) Each connected account runs in its own dedicated cloud browser on LinkedIn's real pages. You sign in yourself inside that browser, and Linked API never sees or stores your password. The browser keeps a stable residential IP from your region and a consistent device profile, and actions run at human pace – a simple visit-and-like takes around 20 seconds by design. You configure per-action daily, weekly and monthly limits, and the platform enforces them: an action that would cross one returns a `limitExceeded` error instead of executing. The [safety model](/safety) covers the architecture, and the [limits guide](/guides/understanding-linkedin-limits) covers sensible values. ### linkedin-scraper, and any DIY Playwright or Selenium script What this route gives you is **reusable authenticated state**. The library's own quick start calls `browser.load_session("session.json")`, which means a credential-equivalent artifact lives in your filesystem – and, if you are not careful, in your repository. What it does not provide is an account-bound stable device and network identity, or enforced pacing. Beyond that, the footprint depends entirely on your runtime: a script running on your own machine may well use the same region and network the account normally signs in from, and the session itself can be created by logging in manually inside that browser. ### linkedin-api This route is wire-level. In the library's own words, "To retrieve structured data, the LinkedIn Website uses a service they call Voyager. Voyager endpoints give us access to pretty much everything we could want from LinkedIn: profiles, companies, connections, messages, etc." Those endpoints are private and undocumented, which means there is no public compatibility contract behind them. If LinkedIn changes an endpoint, a client that has not adapted can keep sending an outdated request shape, and that shape is potentially distinctive on the server side. Read that alongside the release history below: nothing has shipped since November 2024. ### Scrapy and plain HTTP No browser is involved at all. Authenticated pages require session handling you build and maintain yourself, and without a session you receive only what LinkedIn shows signed-out, which is a much thinner page than the one you see logged in. ### Proxy and unblocker products These distribute signed-out public-page requests across many IP addresses. That is a different architecture solving a different problem, so it is not a like-for-like comparison with account-based throughput – there is no account in the picture at all. ### No route is risk-free Every route on this page, including ours, can end in a restricted account. Architecture changes the shape of the footprint your account leaves, not whether a footprint exists. Our guide to [scraping LinkedIn](/guides/how-to-scrape-linkedin) covers how restrictions actually happen, and the guide to a [restricted LinkedIn account](/guides/linkedin-account-restricted) covers what to do if you hit one. ## The open-source libraries: current release and repository status Both libraries below are recommended constantly in tutorials and forum threads. Their current state is worth checking before you build on either, and it is easy to check from primary sources. ### linkedin-scraper Actively maintained. The current release on [PyPI](https://pypi.org/project/linkedin-scraper/) is **3.1.2**, published 10 April 2026, with several releases earlier that year. Its GitHub repository has around 4,369 stars and 964 forks and was created in 2017. It describes itself as an "Async LinkedIn scraper built with Playwright for extracting profile, company, and job data from LinkedIn." The cost you inherit is API churn, and the project states it plainly in its own README: "**Version 3.0.0 introduces breaking changes and is NOT backwards compatible with previous versions.**" That release replaced Selenium with Playwright in a complete rewrite, moved everything to async/await, restructured the package, and switched to Pydantic data models. Code written against 2.x does not run on 3.x; the migration path offered is to pin the old version with `pip install linkedin-scraper==2.11.2`. That is healthy maintenance, not a defect – a scraper that never changes is a scraper that has stopped tracking its target. But it is work that lands on you rather than on a vendor. One detail to check before embedding it in a commercial product: the `LICENSE` file in the repository is the **GNU General Public License v3**, while the PyPI metadata and the README badge both say Apache 2.0. Those licenses have very different implications for closed-source distribution, so confirm which applies to your use before shipping. ### linkedin-api The current release on [PyPI](https://pypi.org/project/linkedin-api/) is **2.3.1**, published 7 November 2024, and nothing has shipped since. It requires Python 3.10 or newer, and its PyPI metadata lists MIT. Its linked sources are, at the time of writing, unreachable. Under PyPI's "Unverified details" the project lists a GitHub URL as both Homepage and Repository, and a ReadTheDocs URL as Documentation. Both return HTTP 404. The tomquirk GitHub account itself resolves normally, so this is specific to the repository rather than the account, and a control request to another LinkedIn scraping repository returned normally, so it is not a rate-limiting artifact. We report that as observed state and draw no conclusion about why. What it means practically is straightforward: the package is still installable, its source and documentation are not currently reachable at the addresses it publishes, and the wire-level approach described above has had no shipped adaptation for well over a year. ## Rolling your own with Playwright Here is the task both of the next two blocks solve, so you can compare them line for line: **given one LinkedIn profile URL, return the person's name, headline, and location.** The DIY version launches a browser, loads a session you saved earlier, opens the page, and reads three fields: ```python import asyncio from playwright.async_api import async_playwright async def scrape_profile(url: str) -> dict: async with async_playwright() as p: browser = await p.chromium.launch() # storage_state carries the logged-in session you exported earlier context = await browser.new_context(storage_state="session.json") page = await context.new_page() await page.goto(url, wait_until="domcontentloaded") name = await page.locator("main h1").first.inner_text() headline = await page.locator("main div.text-body-medium").first.inner_text() location = await page.locator("main span.text-body-small").first.inner_text() await browser.close() return { "name": name.strip(), "headline": headline.strip(), "location": location.strip(), } print(asyncio.run(scrape_profile("https://www.linkedin.com/in/john-doe"))) ``` Three lines in that snippet are the whole maintenance story. The selectors are illustrative and will not match forever – LinkedIn's class names are generated and its profile layout varies by account type, by whether sections are present, and by which experiment a given page is in. `storage_state="session.json"` is a file holding your authenticated session. And nothing in the script paces anything: it runs as fast as the event loop allows. Scaling this means solving, in order: selector drift, layout variants, pagination, session expiry, retry logic, and eventually request infrastructure. That work never finishes, which is the honest argument for and against the route – it is entirely yours to control, and entirely yours to maintain. ## The same extraction through an authenticated account API The same task, through a connected account. Install the SDK and initialize it with your tokens: ```bash pip install linkedapi ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, FetchPersonParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) workflow = linkedapi.fetch_person.execute( FetchPersonParams(person_url="https://www.linkedin.com/in/john-doe") ) # result() waits for the workflow to finish result = linkedapi.fetch_person.result(workflow.workflow_id) for error in result.errors or []: print("workflow error:", error.type, error.message) if result.data: print({ "name": result.data.name, "headline": result.data.headline, "location": result.data.location, }) ``` No selectors, no session file, no browser to launch. `fetch_person` takes flags for the heavier parts of a profile – experience, education, skills, languages, recent posts – and returns them as typed fields rather than text you parse; the [SDK reference](/sdks/fetch-person) has the full parameter list, and [people search](/sdks/search-people) covers finding profiles rather than reading a known one. Requests are not instant, and that is deliberate: the workflow runs in a real browser at human pace, so a simple read takes seconds and a heavy one can take minutes. What you get in exchange is that the pacing, the limits and the browser are handled rather than left to your code. **Two ways to use it, both first-class.** You can build it in – the REST API, the [Node and Python SDKs](/sdks/installation), or the [shell CLI](/cli/getting-started) – and embed extraction directly in your own backend. Or you can skip the wiring: an AI agent can run the same workflows through the [MCP server](/mcp/overview), the agent-friendly CLI in Claude Code, Cursor or Codex, or ready-made [skills](/skills) installed with `npx @linkedapi/skills` – building the extraction from a plain-language description instead of code. **On capacity.** Activity capacity is account-bound: exceeding what one account can do means connecting additional accounts, on any route. Each account you connect to Linked API gets its own isolated cloud browser, its own identification token, and its own configurable limits. For entity-specific extraction, the deeper guides carry working code: [profiles](/guides/linkedin-profile-scraper), [companies](/guides/linkedin-company-scraper), [jobs](/guides/linkedin-jobs-scraper), [posts](/guides/linkedin-post-scraper), and [Sales Navigator](/guides/linkedin-sales-navigator-scraper). If you want the API-product comparison rather than the language one, see [LinkedIn scraper API](/guides/linkedin-scraper-api). ## When the DIY route is still the right call Plenty of cases, and they are not consolation prizes: - **You want full control and no vendor dependency.** Your scraper, your infrastructure, your roadmap. Nothing about it can be repriced or deprecated by someone else. - **You are collecting from sources beyond LinkedIn.** If LinkedIn is one target among many, one scraping stack across all of them is simpler than one integration per source. - **You need raw speed on a single account.** Linked API enforces sequential, human-paced execution, so a simple action takes around 20 seconds by design. A Playwright script, and especially a Voyager client issuing direct HTTP requests, leaves pace and concurrency entirely to your code, so its wall-clock rate on one account can be considerably higher. That speed is unmanaged, and the safety burden moves to you – but the raw number is real, and it is a different question from what volume is sustainable, which nobody here can quantify for you. - **You are collecting signed-out public pages in bulk.** At high volume that genuinely bursts harder than any account-based route. It is a thinner dataset, and the legal picture around it is its own subject – our [scraping guide](/guides/how-to-scrape-linkedin) covers it. - **You need a one-off dataset**, once, with no subscription attached. - **You are learning.** Writing the Playwright version teaches you more about how LinkedIn is built than any API call will. ## Frequently Asked Questions (FAQ) #### Can you scrape LinkedIn with Python? Yes, by three practical routes: an open-source library such as linkedin-scraper, your own browser automation with Playwright or Selenium, or an authenticated account API that returns structured data directly. They differ in how much code you maintain and in what runs your LinkedIn account while they execute. None of them is risk-free for the account doing the work. #### Which Python library is best for scraping LinkedIn? It depends on the route you want, and the current state of each is worth checking before you commit. The linkedin-scraper library is actively maintained on Playwright, at version 3.1.2 as of April 2026, with a major API rewrite in 3.0.0 that broke compatibility with 2.x. The linkedin-api library takes the wire-level approach against LinkedIn's internal Voyager endpoints and has not shipped a release since November 2024, with its published repository and documentation URLs currently returning 404. #### Does scraping LinkedIn with Python get your account banned? Account restriction is possible on every route, and nothing described here removes that risk. What triggers it, how LinkedIn's restrictions work, and what recovery looks like are covered in our [guide to scraping LinkedIn](/guides/how-to-scrape-linkedin) and our guide to a [restricted LinkedIn account](/guides/linkedin-account-restricted). #### Can Python scrape emails or phone numbers from LinkedIn? Sometimes, but not reliably, and it is the wrong tool for the job. Members can publish an email or phone number in the [Contact info section](https://www.linkedin.com/help/linkedin/answer/a565128) of their profile, and a logged-in route can read it when the visibility setting makes it visible to that account. But it is optional and visibility-controlled: per LinkedIn Help, the registered primary email is by default visible only to [1st-degree connections](https://www.linkedin.com/help/linkedin/answer/a523134), with member-chosen settings ranging from "only visible to me" to anyone on LinkedIn. That makes LinkedIn an unreliable contact source, and consented enrichment providers are the right lane for it. Linked API result types carry no email or phone field. #### Do I need proxies to scrape LinkedIn in Python? At low volume from an ordinary connection, no. Proxies commonly become necessary infrastructure once you are collecting public pages at scale, which is what proxy and unblocker products are built for. Two things worth separating: that is signed-out collection, and it is not a way to make a single logged-in account go faster. An authenticated account API does not require you to run a proxy pool at all. #### Is there an official LinkedIn API for Python? There is an official LinkedIn API, but not one that covers this use case. Its open access is Sign In with LinkedIn via OpenID Connect, which returns the profile of the member who just authenticated and consented – their own data, not other people's. Broader profile and community access sits behind restricted or vetted partner programs, as Microsoft's own [Getting Access to LinkedIn APIs](https://learn.microsoft.com/en-us/linkedin/shared/authentication/getting-access) documentation sets out, and there is no general people-search or profile-lookup product available by self-service. That gap is why the routes in this guide exist. --- If you are deciding between building and buying, the honest split is this: build when control matters more than upkeep, and buy when the extraction has to keep working while you do something else. Our [account safety model](/safety) covers how Linked API runs actions, and [pricing](/pricing) is flat per seat. *Library versions, release dates, and repository status verified 2026-07-28.* ## LinkedIn Account Restricted: Why It Happens and How to Get Back In A LinkedIn account restricted without warning is disorienting in a specific way: the notice names a policy area, and in most reports it does not name the action that crossed a line – while the timer on screen may not appear to move. Most advice you will find fills that gap with numbers nobody published – a "safe" weekly invitation figure, an appeal success rate, a universal timeline. This guide does the opposite. Every claim below is either quoted from LinkedIn's own Help pages and User Agreement, or explicitly labelled as an observation rather than a documented rule. That distinction matters most in the first hour, because the route back is different for each restriction type – and one of them is not a restriction at all, but a phishing message designed to look like one. > **The short version.** LinkedIn documents four restriction categories – content, profile, identity and automated tools – plus a separate invitation restriction and a separate route for accounts someone else got into. Which one you have decides your way back. Invitation restrictions lift on their own, most within a week, and Support cannot remove one if you ask. Automated-activity suspensions re-enable themselves at the time given in your notice, once you disable the tool. Policy restrictions may offer a 48-hour wait-then-agree path, and you can appeal a decision only once. Identity restrictions need ID verification through Persona; a compromised account goes through a different form entirely. Whether other people can still see your profile depends on the type – on the 48-hour path they can, under a full restriction they cannot. ## Which LinkedIn restriction do you have? **First, check that the restriction is real.** Fake "your account has been restricted" messages are an active phishing pattern, and LinkedIn publishes a sample of one: an email subjected "Account Suspended" telling the reader that "a temporary limitations has been placed on your Linkedin account" with a link to continue. The clearest rule is on [LinkedIn's phishing page](https://www.linkedin.com/help/linkedin/answer/a1339266): "LinkedIn will never ask you for your password or ask you to download any programs." Sender address is a weaker signal than people assume – LinkedIn states it "has several email domains, which are determined by our email service provider", and vouches for three specific addresses (`linkedin_support@cs.linkedin.com`, `linkedin@e.linkedin.com`, `linkedin@el.linkedin.com`) without presenting that as an exhaustive list. So a message from some other address is not automatically fake, and one that merely looks plausible is not automatically real. Never follow an appeal link that arrives in a comment, DM or email – open LinkedIn directly and see whether the notice is there. LinkedIn also warns to "be cautious of third-party sites offering assistance with our products." Suspicious email goes to `phishing@linkedin.com`; a suspicious message is reported in-app through the More icon → Report/Block → "It's spam or a scam". Once you have confirmed the notice on LinkedIn itself, match it to a type: | What you're seeing | Restriction type | What's blocked | Official route back | Duration LinkedIn publishes | | --- | --- | --- | --- | --- | | You can still sign in, but you can't send invitations | **Invitation restriction** | Sending new invitations | None needed – [it lifts on its own](https://www.linkedin.com/help/linkedin/answer/a551012) | **Most removed within one week** | | A suspension notice naming automated activity | **Automated-activity suspension** | Your account, for the stated period | [Disable the software or extension](https://www.linkedin.com/help/linkedin/answer/a1340567); it re-enables itself | **The time stated on your suspension notice** | | A policy notice offering another chance after a wait | **Content or policy violation** | Your account – but others can still view and message you | [Wait, then agree to the Professional Community Policies](https://www.linkedin.com/help/linkedin/answer/a1507211) | **48 hours** | | Parts of your profile removed, or access lost after repeated profile edits | **Profile violation** | Varies – from a removed photo to account access | [Sign in and follow the on-screen prompts](https://www.linkedin.com/help/linkedin/answer/a1340522) | Not published | | You're asked to verify your identity | **Identity violation** | Your account | [Verify your identity through Persona](https://www.linkedin.com/help/linkedin/answer/a1342692) | Not published – temporary *or* indefinite | | You can't sign in, or you see changes you didn't make | **Suspected compromise** | Your account | [Report Unauthorized Account Access or Changes](https://www.linkedin.com/help/linkedin/answer/a1340402) | Not published | Three of these six have no published duration. Any page that gives you one for them is guessing. ## Why LinkedIn restricts accounts – what's documented and what isn't [LinkedIn's Account restrictions page](https://www.linkedin.com/help/linkedin/answer/a1340522) lists four categories, and its invitation page lists three more specific triggers. Everything else circulating on this topic is inference. The table separates the two: | Possible trigger | Evidence status | What LinkedIn actually says | | --- | --- | --- | | Content that breaches the Professional Community Policies | **Documented** | "Some violations of our Professional Community Policies may result in permanent account restriction after a single violation." | | Profile content – Name, Profile Photo, Background Photo, Experience or Education | **Documented** | "If you repeatedly add violating content to your profile, we may restrict access to your account." | | A profile that is "intentionally fraudulent or do[es] not reflect your true identity" | **Documented** | Access "may be restricted either temporarily or indefinitely." | | Third-party software or browser extensions | **Documented** | "we don't allow the use of third-party software or browser extensions that scrape, modify the appearance of, or automate activity on LinkedIn's website." | | Invitation volume in a short window | **Documented** | "You've sent many invitations within a short amount of time." | | Invitation quality | **Documented** | "Many of your invitations have been ignored, left pending, or marked as spam by the recipients." | | Excessive invitations plus suspected tooling | **Documented** | "If you send an excessive number of invitations and we suspect the use of an automation tool, we may suspend or restrict your account." | | Size of your pending-invitation backlog | *Community-observed* | No numeric cap is published. See [our limits guide](/guides/understanding-linkedin-limits). | | Acceptance rate | *Community-observed* | No threshold is published. See [the connection limit guide](/guides/linkedin-connection-limit-2026). | | Profile-view velocity | *Community-observed* | No threshold is published. | | Pace of sending within a single day | *Community-observed* | No threshold is published. | Two numbers you will meet repeatedly are worth naming as unpublished: the "safe range is 100 to 200 connection requests per week" figure, and any stated appeal success rate. LinkedIn publishes neither. Its own position on limits is that "All LinkedIn members (Basic and Premium) are subject to invitation limits and restrictions" – with no member-facing number attached. On automation specifically, the [prohibited software page](https://www.linkedin.com/help/linkedin/answer/56347) points at [User Agreement](https://www.linkedin.com/legal/user-agreement) section 8.2, which bars members from: > "Develop, support or use software, devices, scripts, robots or any other means or processes (such as crawlers, browser plugins and add-ons or any other technology) to scrape or copy the Services, including profiles and other data from the Services" and from: > "Use bots or other unauthorized automated methods to access the Services, add or download contacts, send or redirect messages, create, comment on, like, share, or re-share posts, or otherwise drive inauthentic engagement" That is the rule as written, and it applies regardless of which tool you use. We cover what it means in practice, including the *hiQ* case law, in [our guide to LinkedIn scraping](/guides/how-to-scrape-linkedin). ## Why the notice doesn't tell you what you did For invitation restrictions, the silence is deliberate and documented. LinkedIn's [invitation restrictions page](https://www.linkedin.com/help/linkedin/answer/a551012) states plainly: "For your privacy, LinkedIn Support cannot disclose any additional information regarding the type or reason for the invitation restriction on your account." Contacting Support will not produce the missing detail, because withholding it is the policy. Outside that specific case, LinkedIn has not published a rule about disclosure. What readers actually encounter – and this is an observation about the notices, not a documented LinkedIn position – is that a restriction notice names the policy area it falls under rather than the action that crossed a line. So a member sees "Professional Community Policies" or "automated activity" and is left to work backwards. This is why "restricted for no reason" is such a common search: there usually *was* a reason, it simply is not printed on the screen. The practical response is to work from the type in the table above and the documented triggers in the previous section, rather than from a support reply that will not come. ## How to get your account back ### If it's an invitation restriction Do nothing, and do not open a support case. Per LinkedIn: "Most restrictions will automatically be removed within one week. LinkedIn won't be able to remove invitation restrictions upon request." Use the time on something LinkedIn does name: invitations "ignored, left pending, or marked as spam by the recipients" are one of its listed triggers, so withdrawing invitations that have gone stale addresses a documented signal directly. Note the cost before you start: "After you withdraw an invitation, you won't be able to resend an invite to the same recipient for up to three weeks." Our guide to [pending invitations](/guides/linkedin-pending-invitations) covers where both queues live and how withdrawal actually behaves. ### If it's an automated-activity suspension [LinkedIn's instruction](https://www.linkedin.com/help/linkedin/answer/a1340567) is one step: "Review and disable the software or extension that automates activities. Your account will then automatically be re-enabled at the time specified on the suspension notification." Two things follow from that. First, the clock is already set – there is no faster route. Second, repetition escalates: "Repeated suspensions may result in permanent restriction of your LinkedIn account." LinkedIn also offers a second-look contact form for members who want to explain their use, but be aware of its status in LinkedIn's own words: "This feature is being gradually rolled out and is currently available only to some LinkedIn members." If you do not see it, it is not available to you yet. ### If it's a content or policy restriction Where LinkedIn offers [another chance to regain access](https://www.linkedin.com/help/linkedin/answer/a1507211), the path is a wait followed by an agreement: "you'll need to wait 48 hours and then agree to comply with our Professional Community Policies." Three details save a lot of confusion here: 1. **The clock starts when you log in.** "The restriction period begins when you log in, even if the amount of time shown on the screen doesn't change." A frozen-looking counter is not a bug. 2. **Your profile stays visible.** "Your account will still be active to others during this restriction period, meaning other members will be able to view your account and message you." 3. **One appeal per decision.** If you believe the content complies, you can appeal – but "You may appeal each decision only once." Spend it on your strongest case. ### If you're asked to verify your identity LinkedIn's verification partner for account recovery is **Persona** – not CLEAR, which belongs to a separate verified-badge feature and is widely misreported as part of this flow. Per the [identity verification page](https://www.linkedin.com/help/linkedin/answer/a1342692), you supply a photo of a valid government-issued ID card, driver's licence or passport, and may be asked for a selfie to match it. Library and school ID cards are not accepted. Only on your consent does Persona pass LinkedIn your verification result, full name, year of birth, city/state/province/country, ID type and issuer, and a redacted copy of the ID "with only the full name and face portrait being visible". LinkedIn states it does not receive your biometric data, or the numbers, expiry dates or issue dates on the ID. The submitted data is "generally permanently deleted within 14 days of submission". On desktop you scan a QR code to continue on your phone. If you would rather not submit an ID or a photo of yourself and you are "located in Canada, the EU or the UK", LinkedIn offers an alternative: print the Affidavit of Identity, sign it before a Notary Public, then scan and attach it to your support case. ### If someone else got into your account This is a different route from an identity violation, with its own form. Per [LinkedIn's page on unauthorized account access](https://www.linkedin.com/help/linkedin/answer/a1340402), submit the **Report Unauthorized Account Access or Changes** form immediately, and include your profile URL if you have it. If you can still log in, LinkedIn asks you to do all of the following straight away: change your password to one not used anywhere else, turn on two-factor authentication, review your active sessions and sign out of any you do not recognise (individually or everywhere at once), check that the email addresses and phone numbers on the account are current, and secure the personal email accounts tied to it. ## How long does a LinkedIn restriction last? LinkedIn publishes a duration for three situations and no others: | Situation | Published duration | | --- | --- | | Invitation restriction | "Most restrictions will automatically be removed within one week" | | Automated-activity suspension | "the time specified on the suspension notification" | | The policy second-chance path | 48 hours | For profile violations, identity violations and account reviews, **no duration is published**, and for invitation restrictions Support will not discuss the case. If a page gives you "7 to 30 days" or "24 to 48 hours" for those, it is not quoting LinkedIn. Two corrections to the common mental model: **A permanent restriction does not always arrive last.** It is easy to assume permanence is the end of an escalation ladder, but LinkedIn documents three separate paths to it. Some content violations "may result in permanent account restriction after a single violation", and for egregious categories – LinkedIn names child sexual abuse material, terrorism, extremely violent content and egregious sexual harassment – a single violation may end the account. Identity restrictions may be "either temporarily or indefinitely" from the outset. And repeated automated-activity suspensions "may result in permanent restriction". Only the third is an escalation. **Whether people can still see you depends on the type.** On the [48-hour policy path](https://www.linkedin.com/help/linkedin/answer/a1507211), "other members will be able to view your account and message you". Under a [full restriction](https://www.linkedin.com/help/linkedin/answer/a1447823), the opposite is documented: "Other people will no longer be able to find your LinkedIn profile or message you." If your profile has gone from search entirely, you are not on the 48-hour path. ## How to resume LinkedIn activity without repeating this This section assumes you already have your access back and are deciding how to work from here. **Ramp back rather than resuming where you stopped.** LinkedIn publishes no ramp schedule, so treat this as a precaution rather than a documented rule: return well below your previous volume and rebuild gradually. What *is* documented is what to prioritise – two of LinkedIn's listed invitation triggers are things you control directly: sending "many invitations within a short amount of time", and invitations that end up "ignored, left pending, or marked as spam by the recipients". So send fewer in any short window, be more selective about who you send to, and clear the stale pending queue before adding to it. Our [connection request guide](/guides/how-to-automate-linkedin-connection-requests) covers the mechanics, and [the limits guide](/guides/understanding-linkedin-limits) collects the working numbers. **Be clear about where automation stands.** Nothing in this section changes section 8.2 of the User Agreement, quoted above. LinkedIn's position is that third-party software which scrapes or automates activity is not permitted, and that "Automated inauthentic activity violates the LinkedIn User Agreement and can result in temporary or permanent restriction of your account." No product choice makes automation an approved activity, and no configuration removes that risk. What a tool can change is one class of trigger: how much you do and how fast. That is the narrow claim [Linked API](/) makes. Each account runs in its own dedicated cloud browser on LinkedIn's real pages, at human pace with natural pauses – you sign in yourself, and Linked API never sees or stores your password. You then [set per-action limits](/docs/admin-limits) once, by category and by day, week or month, and the platform enforces them: an action that would cross a limit returns a `limitExceeded` error instead of executing, so a script cannot run past a cap you set. You can drive it from your own backend through the REST API, the [Node and Python SDKs](/sdks/installation), or the shell CLI – or skip the wiring entirely and let an AI agent run it through the [MCP server](/mcp/overview), the [agent-friendly CLI](/cli/getting-started) in Claude Code, Cursor or Codex, or a ready-made [skill](/skills). **The honest scope of that.** Enforced caps govern only the volume and pace you configured. They do nothing about content, profile, identity, compromise or report-driven restrictions, which is most of the table in the first section. Linked API still carries restriction risk. Treat configurable limits as one control over one trigger class, not as protection. ## Frequently Asked Questions (FAQ) #### How do I appeal a LinkedIn account restriction? There is no single universal appeal form. The route is the on-screen prompt on your own notice: LinkedIn asks you to "login and follow the onscreen prompts to ask us to revisit our decision." You may appeal each decision only once, so make the strongest version of your case the first time. The separate second-look contact form for automated-activity suspensions exists, but LinkedIn describes it as gradually rolling out and currently available only to some members. #### Can a permanently restricted LinkedIn account be recovered? LinkedIn does not document a recovery path for a permanent restriction, and no honest answer promises one. What it does document is how permanence arises: a single violation in an egregious content category, an identity restriction imposed indefinitely, or repeated automated-activity suspensions. If you believe the decision was an error, the on-screen appeal is the only route LinkedIn publishes, and it can be used once. #### Will people still see my profile while my account is restricted? It depends on the type. On the 48-hour policy path, LinkedIn states that "other members will be able to view your account and message you" even though you cannot get in. Under a full account restriction, LinkedIn states the opposite: "Other people will no longer be able to find your LinkedIn profile or message you." So a profile that has vanished from search indicates a full restriction rather than the temporary policy path. #### Can LinkedIn Support lift an invitation restriction if I ask? No. LinkedIn states that "LinkedIn won't be able to remove invitation restrictions upon request", and that most lift automatically within a week. It also will not tell you why: "For your privacy, LinkedIn Support cannot disclose any additional information regarding the type or reason for the invitation restriction on your account." #### Does LinkedIn restrict accounts for using automation tools? Yes, and it says so directly: "we don't allow the use of third-party software or browser extensions that scrape, modify the appearance of, or automate activity on LinkedIn's website," and such activity "can result in temporary or permanent restriction of your account." Section 8.2 of the User Agreement bars both scraping tooling and bots used to send messages, add contacts or drive engagement. This applies to every tool in the category, without exception. #### What should I do if my account was restricted after someone else accessed it? Use the compromise route, not the identity-verification one. Submit the Report Unauthorized Account Access or Changes form with your profile URL. If you can still log in, change your password to one you use nowhere else, turn on two-factor authentication, review your active sessions and sign out of anything unfamiliar, confirm the email addresses and phone numbers on the account, and secure the personal email accounts linked to it. --- If you are rebuilding an outreach or data workflow after a restriction, our [account safety model](/safety) sets out how Linked API runs actions, and [pricing](/pricing) is flat per seat. *Facts verified 2026-07-27 against LinkedIn Help and the LinkedIn User Agreement.* ## LinkedIn InMail: What It Is, How Credits Work, and When to Use It LinkedIn InMail is the feature everyone has received and few people fully understand: the paid message that lands in your inbox from someone you never connected with. The mechanics behind it – who can send it, what a credit actually costs, when you get one back – are documented by LinkedIn across half a dozen help pages that almost nobody reads in full. This guide pulls the whole picture together, verified against those pages, and adds the part LinkedIn doesn't cover: when spending an InMail credit is actually the right move. > **The short version.** InMail is LinkedIn's paid direct message to people you are **not** connected to – free accounts can't send it. Each paid plan gets monthly credits: Premium Career **5**, Premium Business **15**, Sales Navigator Core **50**, Recruiter Lite **30**, accumulating up to plan-specific caps (15 / 45 / 150 / 120). A credit comes **back** if the recipient accepts, declines, or replies within 90 days – as a rule, only a message that stays unanswered costs you. Subjects run up to 200 characters, the body up to 2,000. There is no unlimited-InMail plan, and outside Recruiter there is no way to buy extra credits – but anyone, including free members, can message **Open Profile** Premium members at no cost. And before spending a credit: a connection request costs nothing and is usually the better first move. ## What is InMail on LinkedIn? Per LinkedIn's [InMail Messages help page](https://www.linkedin.com/help/linkedin/answer/a543895), "InMail messages is a premium feature, and it allows you to directly message another LinkedIn member that you're not connected to." That one sentence carries the two defining constraints: it is **paid**, and it is for **strangers** – people outside your first-degree network whom you could otherwise only reach after they accept a connection request. The free-account boundary is explicit on the same page: "If you have a Basic (free) account, then you can only directly message LinkedIn members that you're connected to." Free members can still *reply* to any InMail they receive, and can message [Open Profile members](#free-inmail-the-open-profile-exception) without paying anything. Two more facts from the same page that most guides skip: - **Credit pools are siloed.** "Premium InMail message credits can't be used to send InMail messages on Sales Navigator or LinkedIn Recruiter" – each product has its own bucket. - **Some people are unreachable by design.** Members who have turned off InMail in their message preferences can't be InMailed at all, at any price. ## How many InMail credits do you get per plan? Credits are the currency of InMail. The Premium and Sales Navigator allowances are documented on LinkedIn's [credits and renewal page](https://www.linkedin.com/help/linkedin/answer/a543695), and the Recruiter figures on its Recruiter help pages: | Plan | Credits per month | Max accumulation | Can you buy extra? | | --- | --- | --- | --- | | Premium Career | 5 | 15 | No | | Premium Business | 15 | 45 | No | | Sales Navigator (all tiers) | 50 | 150 | No | | Recruiter Lite | 30 | 120 | [Packs of 10, max 70 per seat](https://www.linkedin.com/help/recruiter/answer/a414231) | | Recruiter | [150 per license](https://www.linkedin.com/help/recruiter/answer/a745199) | – | Yes | Unused credits roll forward until you hit your plan's accumulation cap – note the caps are plan-specific, not one universal multiplier. New credits arrive "every month, on the first day of your billing cycle." For Sales Navigator there is one wrinkle worth knowing: LinkedIn's own pages currently describe the renewal day differently – the [Sales Navigator credits page](https://www.linkedin.com/help/sales-navigator/answer/a101030) says credits arrive "the first day of each month (in UTC time)," while the general credits page says the last day of the month. Treat the Sales Navigator page as the authority for Sales Navigator. To see your balance: **Me** icon → **Premium features** → **See all features** → expand the **InMail** section – "Credits available" is shown there, per the [view-credits help page](https://www.linkedin.com/help/linkedin/answer/a543685). ## The credit-back rule (and the mechanics that surprise people) The most misunderstood part of InMail is that sending one doesn't necessarily spend a credit for good. LinkedIn's rule, quoted from the [credits page](https://www.linkedin.com/help/linkedin/answer/a543695): "Every InMail message that is accepted/declined or responded to directly within 90 days of it being sent is credited back." Quick Replies count as a response, and on Sales Navigator even "Not interested" and auto-replies qualify. Meanwhile, "a pending InMail message isn't counted as either accepted or declined" – it sits in limbo. So as a rule, only a message that stays unanswered for 90 days costs you – with one documented exception: on Sales Navigator, [deleting an InMail that never got a response does not refund the credit](https://www.linkedin.com/help/sales-navigator/answer/a101030). Well-targeted InMail is close to self-funding; ignored InMail quietly drains the balance. ![Diagram: the life of an InMail credit over the 90 days after sending, per LinkedIn's help pages. Sending an InMail spends one credit from the monthly allowance, then one of three things happens. If the recipient responds within 90 days – accepting, declining, or replying, with Quick Replies counting too – the credit is returned. While the message is still pending it is counted as neither accepted nor declined and the 90-day clock keeps running. If there is silence for 90 days, the credit is gone. A footer notes that on Sales Navigator deleting an unanswered InMail does not refund the credit, and unused credits accumulate up to plan caps of 15, 45, 150, or 120.](/images/guides/linkedin-inmail-credit-lifecycle.webp) #### Can you send a second InMail before they reply? This is where the products genuinely differ, and flattening them into one rule (as most guides do) gets it wrong: - **Premium:** no – per the [credits page](https://www.linkedin.com/help/linkedin/answer/a543695), you can't send another InMail to the same person until they respond to the first. - **Sales Navigator:** yes – a second InMail before any response is allowed, and [it costs another credit](https://www.linkedin.com/help/sales-navigator/answer/a101030). - **Recruiter:** a built-in cooldown – [24 hours before re-messaging the same candidate](https://www.linkedin.com/help/recruiter/answer/a745199), unless they reply sooner. Three more mechanics worth knowing before you budget around credits: Sales Navigator rollover credits "must be used within 90 days"; credits are forfeited if you cancel or switch plans ("your InMail credits won't be transferred"); and Recruiter Lite's purchase option has a skimming gotcha – the balance is hard-capped at 120 per seat at billing renewal, so per LinkedIn's [own example](https://www.linkedin.com/help/recruiter/answer/a414231), a recruiter holding 115 credits receives only 5 of their 30 monthly credits, and the remaining 25 are "skipped and be non-recoverable." ## How much does InMail cost? There is **no standalone per-InMail price** – InMail only comes bundled with a paid plan. For Premium, LinkedIn states "it's not possible to purchase additional InMail message credits outside of the monthly allotment" ([send-InMail page](https://www.linkedin.com/help/linkedin/answer/a546814)), and for Sales Navigator, "at this time, you can't purchase additional InMail credits" ([credits page](https://www.linkedin.com/help/sales-navigator/answer/a101030)). Only the Recruiter products sell extra credits, in packs of 10 for Recruiter Lite, with the price shown only at checkout. So the real question is which plan's monthly grant you are paying for – 5 on Premium Career, 15 on Premium Business, 50 on Sales Navigator, 30 on Recruiter Lite. LinkedIn's plan prices vary by billing period, region, and signup cohort (a consumer price change has been rolling out through 2026), and are displayed at LinkedIn's own checkout rather than on a public price list – so check there. Two things soften the arithmetic regardless of the sticker: credits roll over within your cap, and answered InMail comes back free. ## InMail character limits (and how to send one) Per the current [Send an InMail help page](https://www.linkedin.com/help/linkedin/answer/a546814): the subject line takes "up to 200 characters" and the body "up to 2000 characters." (Many guides still cite a 1,900-character body – the current help page says 2,000.) The subject is optional. Sending one is unglamorous: open the person's profile → **More** → **Message [Name]** → add a subject if you want one → write → **Send**. On mobile, the **Message** button on their profile does the same. If the compose box opens without any credit warning, either you are connected (a regular free message), or the recipient has Open Profile enabled – see below. ## InMail vs connection request vs email: when to spend a credit InMail's pitch is response rates. LinkedIn's own sales-solutions page [claims](https://business.linkedin.com/sales-solutions/b2b-sales-strategy-guides/improve-inmail-response-rates-on-linkedin) InMail gets "a 10-25% hit rate when it comes to soliciting a response from potential clients -- 300% higher than emails with the exact same content" (LinkedIn's number, from an undated page live as of 2026). Its [Talent Blog study](https://www.linkedin.com/business/talent/blog/talent-strategy/these-inmails-get-best-response-rates) of tens of millions of recruiter InMails (May 2022) is more actionable: | Finding | Effect on response rate | | --- | --- | | Keep it under 400 characters | +22% vs the average InMail | | Personalize – don't bulk-send | +15% vs bulk InMails | | Don't send on Friday or Saturday | −4% / −8% vs other days | The honest decision framework, though, starts before the credit. A **connection request** is free, and once accepted, messaging is free forever – for most steady prospecting it is the better first move, and [automating that flow](/guides/how-to-automate-linkedin-connection-requests) scales it. **Email** wins when you already have the address: no credit, no character ceiling, your own tooling. **InMail** earns its credit in two situations: you can't wait for an acceptance (a closing candidate, a time-boxed deal), or connecting is unrealistic (a far-out-of-network executive). What to actually write – openers, follow-ups, cadence – is craft, not mechanics: the [cold outreach guide](/guides/linkedin-cold-outreach-strategies-2026) covers it. ## Free InMail: the Open Profile exception One carve-out makes "InMail is paid-only" slightly false in practice. Premium members can enable **Open Profile**, which per [LinkedIn's help page](https://www.linkedin.com/help/linkedin/answer/a545663) allows "anyone on LinkedIn to contact a Premium member for free, even if they're not in the sender's network" – no credit consumed, and the sender doesn't even need a paid plan. If a Premium member's profile shows the **Message** button open to you without a credit prompt, that is usually why. The volume is not unlimited: LinkedIn [limits the number of Open Profile messages](https://www.linkedin.com/help/linkedin/answer/a544787) you can send in a given period, without publishing a number. For outbound prospecting, Open Profile members are the cheapest people on LinkedIn to reach – worth identifying deliberately. ## Sending InMail from your own system (in brief) Everything above is manual. If InMail is part of an outreach flow you run in code – a recruiting pipeline, a sales sequence in your own CRM – [Linked API](/), a flexible LinkedIn automation API you can embed in your own product, backend, or workflow, sends InMail programmatically on your own account through a human-paced cloud browser. One scope note, stated precisely: Linked API automates **Sales Navigator** InMail only, via the [`nvSendMessage`](/docs/action-nv-send-message) action, and it requires a Sales Navigator seat on the connected account – it does not send Premium or Recruiter InMail. The API contract is also stricter than the LinkedIn compose box: subject up to **80** characters and text up to **1,900** via the API, versus 200 and 2,000 in the UI – budget copy for the tighter limits when the same message runs both ways. The full flow – the connect-or-InMail branching loop, reading replies, monitoring the inbox, with working TypeScript and Python – lives in [How to Automate LinkedIn Messages](/guides/how-to-automate-linkedin-messages), and the [`nvSendMessage` reference](/sdks/nv-send-message) has the exact contract. The action is available across all Linked API surfaces, enumerated below. ## Frequently Asked Questions (FAQ) #### What is the difference between InMail and a regular message on LinkedIn? A regular message is free but only works between 1st-degree connections. InMail is a paid-plan feature that reaches members you are not connected to, spends a credit per send (refunded if they respond within 90 days), and supports a subject line. If you are already connected to someone, LinkedIn simply sends a regular message – no credit involved. Which of the two applies depends on the [degree of connection](/guides/linkedin-connection-degrees) between you. #### How many InMail credits do you get per month? Premium Career gets 5, Premium Business 15, Sales Navigator 50, and Recruiter Lite 30 per month, per LinkedIn's help pages. Unused credits accumulate up to plan-specific caps: 15, 45, 150, and 120 respectively. There is no plan with unlimited InMail. #### Do InMail credits roll over – and do they expire? They roll over up to your plan's accumulation cap (15/45/150/120). On Sales Navigator, rolled-over credits must be used within 90 days. Credits are also forfeited if you cancel or change your subscription type – LinkedIn states they won't be transferred. #### How do I get an InMail credit back? The recipient has to act on your message within 90 days: accepting, declining, or replying all return the credit – Quick Replies count, and on Sales Navigator even "Not interested" qualifies. A message that stays pending returns nothing, and on Sales Navigator deleting an unanswered InMail forfeits the credit. #### Can free LinkedIn members send InMail? No – sending InMail requires a paid plan. Free members can, however, reply to any InMail they receive, and can message Premium members who have Open Profile enabled at no cost, since Open Profile lets anyone on LinkedIn contact that member for free. #### Is there a daily limit on InMail? On the Recruiter products, yes: up to 1,000 InMails per day per seat, with a 200-InMail cap in a new seat's first week, per [LinkedIn's Recruiter help](https://www.linkedin.com/help/recruiter/answer/a745199). Premium and Sales Navigator have no published daily InMail cap – the monthly credit allowance is the effective limit. #### Is InMail spam? The channel itself is not – credits, opt-outs, and the credit-back-on-response mechanic are all designed to make irrelevant InMail expensive and relevant InMail sustainable. Any individual InMail can still be unwanted, which is exactly why recipients can decline it or switch off InMail entirely. Judge the sender and the message, not the mechanism. --- Running outreach on your own account, at any scale? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually – with Sales Navigator InMail and every other action across the full surface set: the [REST API and Node/Python SDKs](/sdks/installation), the shell [CLI](/cli/getting-started), the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`) – on your own account and at a human pace. ## LinkedIn Pending Invitations: How to See, Withdraw, and Manage Them Every LinkedIn account quietly accumulates two queues: connection requests you sent that nobody answered, and invitations other people sent you. LinkedIn does not surface either one prominently, so most people never learn how to see pending connections on LinkedIn at all – and the stale pile grows. This guide covers where both queues live, how to withdraw what has gone stale, and the facts LinkedIn actually documents about what happens next: who gets notified, when invitations expire, and how soon you can try again. > **The short version.** Your pending **sent** invitations live under My Network → Manage invitations → the **Sent** tab (on mobile: My Network → Invitations → Sent); **received** invitations are reached through the Notifications icon – and also from My Network. From Sent, click Withdraw and confirm – and per LinkedIn Help: the recipient is **not notified**, reminder emails stop, the withdrawal can't be undone, and you **can't re-invite the same person for up to three weeks**. Unanswered invitations **expire after six months** (LinkedIn sends up to two reminders first, and once expired you can send a new one), and there is **no native bulk withdraw** – it's one at a time. ## What counts as a pending invitation on LinkedIn? A pending invitation is any invitation that has not been acted on yet, and it exists on both sides: requests **you sent** that the other person has not accepted or ignored, and invitations **you received** and have not answered. Connection requests are the most common kind, but LinkedIn's invitation list also carries other categories – its filters cover [People, Events, Pages, and Newsletters](https://www.linkedin.com/help/linkedin/answer/a540852), so an invitation can also be someone inviting you to an event or to follow a page or newsletter. One side-effect worth knowing, in its exact form: on profiles where **Follow** is the primary button, choosing Connect from the overflow menu means [you automatically follow the person](https://www.linkedin.com/help/linkedin/answer/a702683) while your invitation is pending – and if they decline, you keep following them unless you manually unfollow. ## How to see the connection requests you've sent On desktop, per LinkedIn's [withdraw help page](https://www.linkedin.com/help/linkedin/answer/a568295): 1. Click the **My Network** icon at the top of your LinkedIn homepage. 2. In the Invitations section, click **Show all** – or, if nothing is pending on the received side, click **Manage** next to "No pending invitations". 3. Click the **Sent** tab under **Manage invitations**. On mobile: tap the **My Network** tab → **Invitations** → the **Sent** tab. The Sent tab is the full list of everything still outstanding. The direct address most people end up bookmarking is `linkedin.com/mynetwork/invitation-manager/sent/` – LinkedIn's help pages describe the menu route above, but the URL takes you straight there. ## How to see invitations you've received LinkedIn's current [help page for received invitations](https://www.linkedin.com/help/linkedin/answer/a540852) routes through notifications: on desktop, click the **Notifications** icon at the top of your homepage; on iOS and Android, tap the **Notifications** tab in the navigation bar. Invitations also appear in the **My Network** section on every platform, so either entry works. For each invitation you have two buttons, in LinkedIn's words: "Click Accept to add the person as one of your 1st-degree connections," or "Click Ignore to remove the invitation without accepting it." An invitation waiting here is why a profile can still show "2nd" while the right move is to accept rather than invite – see [degrees of connection](/guides/linkedin-connection-degrees) for how that state differs from the badge. Two details people always ask about: - **Ignoring is silent.** Per the same help page: "The sender won't be notified that you've ignored their invitation, so they may try to connect with you again." - **"I don't know this person"** – the option LinkedIn offers after you ignore – goes further: it prevents that member from sending you further invitations. ## How to withdraw a LinkedIn invitation (and what happens next) From the **Sent** tab (previous section), click **Withdraw** next to the invitation, then confirm in the pop-up. That is the whole mechanism – and LinkedIn's [help page](https://www.linkedin.com/help/linkedin/answer/a568295) documents exactly what it triggers: - **The other person is not told.** "The recipient will not be notified when you withdraw the invitation." - **Reminders stop.** Withdrawing an invitation stops LinkedIn's reminder emails to the recipient. - **It's permanent.** "Withdrawn invitations can't be restored." - **A cooldown starts.** "You won't be able to send a new invitation to the same member for up to three weeks." If you followed the person as part of inviting them (the Follow-primary case from the first section), check your follow state after withdrawing – following is managed separately from the invitation itself. One edge case: if the Withdraw button is missing, the invitation was most likely already accepted – there is nothing to withdraw anymore, but you can still remove them as a connection. ## Do LinkedIn invitations expire? Yes. Per LinkedIn's [connections and invitations FAQ](https://www.linkedin.com/help/linkedin/answer/a554142): "Invitations expire after six months. Before they expire, LinkedIn sends up to two reminders to the recipient." So an ignored request does not sit in your Sent tab forever – LinkedIn retires it on its own schedule. What happens after expiry is the part the FAQ answers directly: "Expired invitations no longer appear as pending, and you can send a new one." Note the asymmetry with withdrawal – the up-to-three-weeks block applies only to invitations **you withdrew**; an invitation that expired on its own can be re-sent right away. ![Diagram: the life of a LinkedIn invitation. A sent invitation ends in one of four states. Accepted – you become 1st-degree connections. Ignored – the sender is not notified and the invitation quietly disappears. Withdrawn – the recipient is not notified, reminder emails stop, and you cannot re-invite the same person for up to three weeks. Expired – after six months and up to two reminders, it no longer appears as pending and you can send a new one. A footer notes sent invitations are managed under My Network, Manage invitations, Sent tab.](/images/guides/linkedin-invitation-lifecycle.webp) ## Why stale pending invitations are worth cleaning up A long Sent list is not just clutter – it is a signal LinkedIn reads. Its [page on invitation restrictions](https://www.linkedin.com/help/linkedin/answer/a551012) lists, among the triggers: "Many of your invitations have been ignored, left pending, or marked as spam by the recipients." Accounts that trip these signals get temporarily blocked from sending invitations; the same page notes that having too many pending invitations out can mean waiting up to a month before sending another, and that most restrictions lift within a week. If that has already happened to you, our guide to a [restricted LinkedIn account](/guides/linkedin-account-restricted) covers how an invitation restriction differs from the other documented types and what the route back is for each. The practical habit: open the Sent tab monthly and withdraw anything older than three or four weeks. If someone has not accepted in a month, the request is functionally dead – withdrawing it keeps your outstanding queue small and your acceptance rate honest. As for how many pending invitations is "too many": LinkedIn does not publicly state a numeric pending-invitation maximum – how invitation restrictions and sending limits work in practice is covered in the [connection limit guide](/guides/linkedin-connection-limit-2026). ## When there are hundreds: managing invitations programmatically Everything above is one invitation at a time – and LinkedIn's help page is explicit that this is the only native way: "You can't bulk withdraw invitations." If you prospect at real volume, the queue becomes a job for code rather than clicks. [Linked API](/) – a flexible LinkedIn automation API you can embed in your own product, backend, CRM, or workflow, running on your own account through a human-paced cloud browser – exposes the whole queue as composable primitives (its withdraw action also unfollows by default, with an opt-out): | Job | API action | CLI | | --- | --- | --- | | List pending sent requests | [`retrievePendingRequests`](/docs/action-st-retrieve-pending-requests) | `linkedin connection pending --json` | | Withdraw a stale request | [`withdrawConnectionRequest`](/docs/action-st-withdraw-connection-request) | `linkedin connection withdraw ` | | List received invitations | [`retrieveInvitations`](/docs/action-st-retrieve-invitations) | `linkedin connection invitations --json` | | Accept or ignore by rule | [`acceptInvitation`](/docs/working-with-invitations) / [`ignoreInvitation`](/docs/working-with-invitations) | `linkedin connection accept ` / `linkedin connection ignore ` | Hosted tools automate this queue too, at the layer of pre-built campaign agents; the primitives above are for wiring it into your own system. The full lifecycle with working code – sending, status checks, pruning stale invites on a schedule, handling incoming – lives in [How to Automate LinkedIn Connection Requests](/guides/how-to-automate-linkedin-connection-requests) and the [working-with-invitations docs](/docs/working-with-invitations), and the same actions are available to AI agents through the [MCP server](/mcp/overview), the AI-agent-friendly [CLI](/cli/getting-started), and ready-made [skills](/skills). ## Frequently Asked Questions (FAQ) #### Can someone see if you withdraw a LinkedIn request? No. LinkedIn's help page states the recipient "will not be notified when you withdraw the invitation" – the request simply disappears from their list, and its reminder emails stop. Keep in mind the withdrawal itself is permanent, and you won't be able to re-invite that person for up to three weeks. #### How long do LinkedIn invitations stay pending? Until someone acts on them – or for six months, whichever comes first. Per LinkedIn's connections and invitations FAQ, invitations expire after six months, with up to two reminders sent to the recipient before that happens. #### Why can't I withdraw a LinkedIn invitation? The usual reason is that it was already accepted – once someone accepts, there is no pending invitation left to withdraw, and your option becomes removing the connection instead. Already-withdrawn invitations also can't be touched again: LinkedIn states withdrawn invitations can't be restored. #### Can you bulk-withdraw LinkedIn invitations? Not natively – LinkedIn's help page says plainly, "You can't bulk withdraw invitations," so the built-in flow is one at a time from the Sent tab. At real volume, the programmatic route in this guide's last section does the same job through an API on your own account. #### What happens if someone ignores my invitation? Nothing visible to you. You are not notified when someone ignores your request – the invitation quietly leaves their list. Ignoring is not a block: LinkedIn notes the sender "may try to connect with you again." #### How many pending invitations can you have on LinkedIn? LinkedIn does not publicly state a numeric pending-invitation maximum. What it does document is the consequence: many invitations left ignored or pending is a listed trigger for invitation restrictions, and too many outstanding invitations can mean waiting up to a month before sending more. How invitation restrictions and sending limits work in practice is covered in the connection limit guide. --- Keeping the queue clean on your own account, at any scale? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually – with every invitation primitive across the full surface set: the [REST API and Node/Python SDKs](/sdks/installation), the shell [CLI](/cli/getting-started), the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`) – on your own account and at a human pace. ## How to Automate LinkedIn Posts in 2026: Native Scheduler, Tools, and API Publishing consistently on LinkedIn is one of those jobs that rewards a system and punishes willpower. Writing a good post is real work; pasting it into the composer at the right time on the right account is not – that part is exactly what you should automate. This guide covers how to automate LinkedIn posts at every level: what LinkedIn's built-in scheduler actually does, when a scheduling tool earns its subscription, and how to publish to your profile or company page from your own pipeline in code. > **The short version.** You can automate LinkedIn posts three ways: LinkedIn's **native scheduler** (free, up to 3 months ahead, but manual and one post at a time), a **scheduling tool** (content calendar, approvals, multi-network publishing), or an **API that posts on your own account**. With Linked API's `createPost` you publish to your personal profile or a company page from your own pipeline – a CMS, an RSS feed, a backend job, or an AI agent – with images, video, or a document attached. The rest of this guide is the honest decision path, plus working code. ## The three ways to automate LinkedIn posts | Route | Cost | Where it runs | Personal / Page | Media | Best for | | --- | --- | --- | --- | --- | --- | | Native scheduler | Free | LinkedIn's composer | Both | What the composer supports (minus the excluded types below) | One person queueing posts ahead | | Scheduling tool | Subscription | The vendor's app | Both | Vendor-dependent | Teams: calendars, approvals, multi-network | | Your own pipeline via [Linked API](/) | Flat [per-seat plan](/pricing) | Your code, on your own account | Both (`companyUrl` for Pages) | 9 images, or 1 video, or 1 PDF | Posting as a feature of your product, backend, or agent | There is also a fourth, narrower route – building an approved developer app on LinkedIn's official Posts API – covered [further down](#the-official-linkedin-posts-api-when-its-the-right-route), because for most people it is an integration project, not an automation setup. ## Can you schedule posts on LinkedIn natively? Yes. LinkedIn's built-in scheduler is free and covers more than most people assume, so start here before adding any tool. For **personal posts**, per LinkedIn's [Schedule posts](https://www.linkedin.com/help/linkedin/answer/a1347212) help page, you can schedule from the clock icon in the composer for any time "within 10 minutes to 3 months from the current time", on both desktop and the mobile app. Scheduled posts are not fire-and-forget drafts: from **View all scheduled posts** you can preview, edit the content, reschedule the time, or delete. Three post types are excluded – LinkedIn's list is "Events, Jobs, Services" – and scheduled times are standardized in UTC based on your device's time-zone settings, which is the usual explanation when a personal post publishes at an unexpected hour. For **company pages**, the [Schedule a LinkedIn Page post](https://www.linkedin.com/help/linkedin/answer/a1419179) help page allows scheduling "anywhere between an hour from the current time to three months in advance" – note the 1-hour minimum lead time, versus 10 minutes for personal posts. The exclusion list is longer: "You can't schedule events, multiple photos, reshares, polls, jobs, and service posts for your Page." Scheduled Page posts are managed from the Page admin view, and one operational gotcha: a scheduled Page post cannot be promoted in Campaign Manager until it has actually published. Where the native scheduler stops is structural, not cosmetic: there is no recurring queue, the horizon is capped at 3 months, every post goes through the composer one at a time, and there are no approval workflows or multi-network calendars. If your bottleneck is any of those, you have outgrown it. ## How to automate LinkedIn posts with an API The programmatic route treats posting as what it is in a real content system: one step in a pipeline you already own. [Linked API](/) is a LinkedIn automation API that runs actions on your own account through a human-paced cloud browser, and its [`createPost`](/docs/action-st-create-post) action is the publishing step: - **Post as yourself or as a company page** – pass `companyUrl` to publish on behalf of a Page you have content-admin access to. - **Text up to 3,000 characters**, matching the composer. - **Attachments:** up to 9 images (JPEG, PNG, GIF, WebP, max 8 MB each), or 1 video (MP4, MOV, WebM, max 200 MB), or 1 PDF document (max 100 MB). Attachment types cannot be mixed in one post. - **Returns the created post's URL**, so your system can store, track, or announce it. One honest mechanic before the code: Linked API has **no scheduled-post queue**. `createPost` publishes when you call it – your cron job, queue worker, or workflow engine owns the timing. That is by design: the point of this route is that scheduling logic lives in *your* system, next to your content. Install the SDK (`npm install -S @linkedapi/node` or `pip install linkedapi`) and publish through `customWorkflow` – posting is a raw-definition action, so there is no dedicated SDK method: ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const workflow = await linkedapi.customWorkflow.execute({ actionType: 'st.createPost', text: 'We just shipped scheduled exports – here is what changed and why it matters.', attachments: [{ url: 'https://example.com/release-chart.png', type: 'image' }], }); const { data: completion } = await linkedapi.customWorkflow.result(workflow.workflowId); console.log('Published:', completion.data.postUrl); ``` ```python import os from linkedapi import LinkedApi linkedapi = LinkedApi( linked_api_token=os.environ["LINKED_API_TOKEN"], identification_token=os.environ["IDENTIFICATION_TOKEN"], ) workflow = linkedapi.custom_workflow.execute( { "actionType": "st.createPost", "text": "We just shipped scheduled exports – here is what changed and why it matters.", "attachments": [{"url": "https://example.com/release-chart.png", "type": "image"}], } ) completion = linkedapi.custom_workflow.result(workflow.workflow_id).data print("Published:", completion["data"]["postUrl"]) ``` To publish as your company page instead, add `companyUrl: "https://www.linkedin.com/company/your-company"` to the same definition. The same action is one line in the shell [CLI](/cli/getting-started): ```bash linkedin post create "We just shipped scheduled exports – here is what changed" \ --attachments "https://example.com/release-chart.png:image" # Post as your company page linkedin post create "Release 2.4 is live" \ --company-url https://www.linkedin.com/company/your-company ``` ## Build the pipeline end to end: an approved-posts queue A publish call is not yet automation. The smallest pipeline that behaves like a real content system has four parts: a **queue** of posts, an optional **drafting** step, a human **approval gate**, and a **cron tick** that publishes whatever is due. The queue is just data – a JSON file or a database table where each item carries `text`, optional `attachments`, optional `companyUrl`, a `publishAt` time, and an `approved` flag. Drafts can come from anywhere: your team, a blog-to-post script, or an AI agent writing *into the queue* through the [MCP server](/mcp/overview), the AI-agent-friendly [CLI](/cli/getting-started), or the ready-made [skills](/skills) (`npx @linkedapi/skills`) – the agent drafts, a human flips `approved`, and nothing publishes unreviewed. Then a cron job (every 15 minutes is plenty) publishes what is due: ```typescript import LinkedApi from '@linkedapi/node'; import { readFileSync, writeFileSync } from 'fs'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const queue = JSON.parse(readFileSync('queue.json', 'utf8')); const due = queue.filter( (post) => post.approved && !post.postUrl && new Date(post.publishAt) <= new Date(), ); for (const post of due) { const workflow = await linkedapi.customWorkflow.execute({ actionType: 'st.createPost', text: post.text, ...(post.attachments && { attachments: post.attachments }), ...(post.companyUrl && { companyUrl: post.companyUrl }), }); const { data: completion } = await linkedapi.customWorkflow.result(workflow.workflowId); if (completion.success) { post.postUrl = completion.data.postUrl; } else { console.error(`Failed "${post.text.slice(0, 40)}…": ${completion.error.type}`); } } writeFileSync('queue.json', JSON.stringify(queue, null, 2)); ``` ```python import json import os from datetime import datetime, timezone from linkedapi import LinkedApi linkedapi = LinkedApi( linked_api_token=os.environ["LINKED_API_TOKEN"], identification_token=os.environ["IDENTIFICATION_TOKEN"], ) queue = json.load(open("queue.json")) now = datetime.now(timezone.utc) due = [ post for post in queue if post["approved"] and not post.get("postUrl") and datetime.fromisoformat(post["publishAt"]) <= now ] for post in due: definition = {"actionType": "st.createPost", "text": post["text"]} if post.get("attachments"): definition["attachments"] = post["attachments"] if post.get("companyUrl"): definition["companyUrl"] = post["companyUrl"] workflow = linkedapi.custom_workflow.execute(definition) completion = linkedapi.custom_workflow.result(workflow.workflow_id).data if completion["success"]: post["postUrl"] = completion["data"]["postUrl"] else: print(f"Failed {post['text'][:40]!r}: {completion['error']['type']}") json.dump(queue, open("queue.json", "w"), indent=2) ``` Published items keep their `postUrl` (your audit trail), failures stay in the queue with the error surfaced for retry, and the review gate means automation never outruns editorial judgment. Reading posts back – tracking who reacted and commented on what you published, or watching anyone else's feed – is the reverse direction, covered in the [LinkedIn post scraper guide](/guides/linkedin-post-scraper). ![Diagram: a LinkedIn post automation pipeline built on an approved-posts queue. Content sources on the left – an RSS feed, a CMS webhook, and an AI agent drafting via MCP, CLI, or skills – feed post drafts into an approval queue where a human reviews and approves each item. A cron tick on your schedule picks up due approved posts and calls the createPost action, which publishes to a personal profile or a company page and returns a postUrl that is stored back on the queue item. A footer notes the pipeline runs on your own account, at a human pace.](/images/guides/linkedin-post-automation-pipeline.webp) Three variations on the same skeleton: - **Blog/RSS → personal profile.** A script watches your feed; each new article becomes a queued summary post with the article link and cover image. - **CMS webhook → company page.** Your CMS fires a webhook on release-notes publish; the handler queues a Page post with `companyUrl`, and the cron picks it up in the next tick. - **n8n / Make.** In [n8n](/integrations/n8n/overview), use the Linked API node's **Execute custom workflow** action with the same raw `st.createPost` definition; a plain HTTP Request to the REST API works as a fallback, and Make wires up the same way. (n8n also ships its own built-in LinkedIn node – that one posts through LinkedIn's official API with OAuth, a separate route with its own constraints.) ## The official LinkedIn Posts API: when it's the right route LinkedIn does have an official publishing API, and for some projects it is the right choice. The current route is the versioned [Posts API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api) (`POST https://api.linkedin.com/rest/posts`, with required `Linkedin-Version` headers), which replaced the legacy ugcPosts/Share endpoints. Access comes in two distinct paths, and it is worth being precise about them. Posting **as a member** (`w_member_social`) is available through the self-serve [Share on LinkedIn](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin) product on a developer app – no review gate for that scope. Posting **as an organization** (`w_organization_social`) and broader content management run through the Community Management API, whose Standard-tier access requires an application form, a screen recording of your app, and test credentials, per [Microsoft's quick-start](https://learn.microsoft.com/en-us/linkedin/marketing/quick-start) (docs as of May 2026). Notably, the Posts API exposes no scheduling parameter – posts are created as published (or draft), so timing logic lives in the caller there too. Build on the official API when you are shipping an approved integration – a social suite, a marketing platform, a product feature for LinkedIn's own ecosystem. For automating your own account's publishing inside your own stack, the app-review path is usually more machinery than the job needs – which is exactly the gap the own-account API route above fills. ## When native scheduling or a scheduler tool is all you need Honest routing, in both directions. If you are one person queueing next week's posts, the **native scheduler** already does it: free, official, editable, three months out. Nothing in this guide beats it for that job. If a team runs your content, a **scheduler tool** earns its keep with the things native lacks: a shared calendar, approval flows, cross-network publishing, and analytics. Buffer's free plan, for example, covers 3 connected channels with 10 scheduled posts per channel, with paid plans from around $5 per channel per month, billed annually (per Buffer's pricing, 2026). That is a fair price for coordination features you would otherwise build yourself. The API route earns its keep at a specific threshold: when posting must live **inside your product, backend, or agent** – triggered by your data, gated by your review process, publishing to profiles and Pages your system manages. Below that threshold, use the simpler tool; past it, the tool becomes the bottleneck. ## Keeping automated posting safe (and worth reading) Automated posting is the calmest kind of LinkedIn automation – you are publishing to your own feed, not messaging strangers. Outbound is a different discipline with its own pacing rules: [automating messages](/guides/how-to-automate-linkedin-messages) and [automating connection requests](/guides/how-to-automate-linkedin-connection-requests) each have their own guide. For posting, two habits keep it calm. First, run it like a human: Linked API executes every action on **your own account** through a cloud browser at a human pace, and a steady weekly rhythm serves you better than bursts of machine-gun publishing. Quality compounds; volume alone does not. Second, automate the *publishing*, not the *engagement*: auto-posting your content is a workflow, while automating fake engagement on it – pods, mass reactions, comment rings – is the pattern LinkedIn's [Professional Community Policies](https://www.linkedin.com/legal/professional-community-policies) actually target. For pacing questions across all your LinkedIn activity, the [limits guide](/guides/understanding-linkedin-limits) has the current picture. ## Frequently Asked Questions (FAQ) #### Can you automate posts on LinkedIn? Yes, at three levels: LinkedIn's native scheduler queues individual posts up to 3 months ahead for free; scheduling tools add calendars, approvals, and multi-network publishing; and an API like Linked API's `createPost` publishes to your profile or company page directly from your own pipeline – CMS, RSS, backend job, or AI agent. #### Can you schedule posts on LinkedIn without a third-party tool? Yes. The built-in scheduler covers personal posts from 10 minutes to 3 months ahead (desktop and mobile) and company-page posts from 1 hour to 3 months ahead, per LinkedIn's help pages. Scheduled posts can be previewed, edited, rescheduled, or deleted before they publish. #### How many posts can you schedule on LinkedIn? LinkedIn's help pages document no numeric cap on how many posts you can schedule. The practical constraints are different: the 3-month scheduling horizon, and the excluded post types – events, jobs, and services for personal posts, with polls, reshares, and multi-photo posts additionally excluded for Pages. #### Why is LinkedIn not publishing my scheduled post? Check the post type first: scheduling unsupported types (events, jobs, services – and for Pages also polls, reshares, multi-photo posts) fails with an error. For personal posts publishing at the wrong hour, scheduled times are standardized in UTC based on your device's time-zone settings. If a third-party tool did the scheduling, the failure may be caused by an expired account connection or a revoked Page permission – reconnect and check admin access. #### Can you post to a LinkedIn company page via API? Yes, two ways. With Linked API, pass `companyUrl` to `createPost` and the post publishes on behalf of a Page you have content-admin access to – same action, same pipeline as personal posts. On LinkedIn's official Posts API, organization posting uses the `w_organization_social` scope, which sits behind Community Management API access review. #### Is automating LinkedIn posts allowed? Automation on LinkedIn always deserves care, but publishing your own content to your own feed is its low-risk end. Keep it on your own account at a human pace – which is how Linked API runs every action – keep editorial review in the loop, and put your automation effort into content quality rather than engagement tricks like pods or mass reactions. --- Ready to make posting a feature of your own system? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually – and get `createPost` with every other action across the full surface set: the [REST API and Node/Python SDKs](/sdks/installation), the shell [CLI](/cli/getting-started), the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`) – on your own account and at a human pace. ## LinkedIn Social Selling Index (SSI): What It Is, How to Check It, and Whether It Still Matters The Social Selling Index is the one score LinkedIn keeps about how you use LinkedIn – and most people have never looked at it, while the people who have tend to overrate what it means. Both are mistakes worth fixing: SSI is free to check, genuinely informative about your activity, and – per LinkedIn's own current messaging – not the sales KPI it was once marketed as. This guide covers what the SSI measures, how to check it, what a good score actually is, and how to track it over time instead of glancing at it once. > **The short version.** The Social Selling Index is LinkedIn's 0–100 score of your social-selling activity, built from four components worth 25 points each: your professional brand, finding the right people, engaging with insights, and building relationships. Check it free at [linkedin.com/sales/ssi](https://www.linkedin.com/sales/ssi) (you must be logged in); it updates daily. There is no current official target: LinkedIn's dated guidance from 2017 called anything above 70 high and below 30 weak, but today's dashboard labels no threshold – your industry and network percentile ranks, and your trend, are the more useful signal. LinkedIn itself now downplays SSI's link to sales outcomes, so treat it as a directional activity metric: worth tracking as a trend (which you can automate), not worshipping as a KPI. ## What is the LinkedIn Social Selling Index? The Social Selling Index (SSI) is a 0–100 score that LinkedIn computes daily from your activity, as a measure of how well you practice what it calls social selling. Per LinkedIn's official [tip sheet](https://business.linkedin.com/sales-solutions/learning-center/resources/tip-sheets/ts022), the score has four components: 1. **Create a Professional Brand** – how complete and content-rich your presence is. 2. **Find the right people** – how deliberately you search for and view relevant prospects. 3. **Engage with Insights** – how much you share, react, comment, and join conversations. 4. **Build Strong Relationships** – how consistently you connect, especially with decision-makers. The scoring is officially documented on LinkedIn's [social selling page](https://business.linkedin.com/sales-solutions/social-selling): "Each of the above elements is assigned a value between one and 25, and the sum of all four elements results in a total SSI score between 0–100." The same tip sheet notes the index "measures the four social selling activities as they take place on Sales Navigator and LinkedIn.com" – so Sales Navigator actions like advanced searches and saved leads feed the score alongside ordinary LinkedIn activity. ![Diagram: the four components of the LinkedIn Social Selling Index, each worth up to 25 points. Create a Professional Brand covers profile completeness and published content; Find the right people covers deliberate searches and viewing relevant prospects; Engage with Insights covers reactions, comments, shares, and joining conversations; Build Strong Relationships covers connecting steadily, especially with decision-makers. Added together they make the 0-100 SSI, updated daily and free to check while logged in at linkedin.com/sales/ssi.](/images/guides/linkedin-ssi-four-components.webp) SSI started as a Sales Navigator metric; LinkedIn [opened it to every member for free](https://www.linkedin.com/business/sales/blog/modern-selling/get-your-score-linkedin-makes-the-social-selling-index-available-for-everyone) on August 3, 2015, and its learning-center materials describe [daily SSI updates](https://business.linkedin.com/sales-solutions/learning-center/resources/guides/g001/en-en/ts007), which matches the dashboard's own wording. ## How to check your SSI score Go to **[linkedin.com/sales/ssi](https://www.linkedin.com/sales/ssi)** while logged in to LinkedIn – the page is free for every member but shows a login wall if you are signed out, which is why the URL sometimes looks "broken" when shared. The dashboard shows more than the headline number, and the extras are the interesting part: - Your **total SSI score** (0–100) and the **four component sub-scores** (up to 25 each), so you can see which pillar is dragging. - Your **Industry SSI rank** – the percentile you occupy among people in your industry. - Your **Network SSI rank** – the same percentile within your own network. - Your movement over the last week. The percentile ranks matter more than the raw score, because they answer the question a raw number cannot: high or low *compared to whom*? ## What is a good SSI score? The honest answer has two layers, and most guides get one of them wrong. **LinkedIn's dated official guidance did set thresholds.** In a January 18, 2017 [Sales Blog post](https://www.linkedin.com/business/sales/blog/management/3-leading-indicators-of-social-selling-success), LinkedIn wrote that "a high SSI is typically anything above 70" and that "a less-than-stellar SSI would be anything below 30." Its official [Achieving Social Selling Success infographic](https://www.linkedin.com/business/sales/blog/b2b-sales/achieving-social-selling-success) draws the same lines: SSI leaders above 70, laggards below 30. This is where the "70+" benchmark you see everywhere comes from – it is genuinely LinkedIn's number, just an old one. **Today, there is no current official target.** The modern SSI dashboard labels no threshold, and LinkedIn's current pages set none. Third-party guides embellish beyond the official numbers – higher "thought leader" bars, claimed platform-wide averages – but none of that comes from LinkedIn. So the useful present-day yardstick is **relative and temporal** rather than absolute: where you sit in your industry and network percentile ranks, and whether your score is trending up as you invest in LinkedIn. A 55 in the top 5% of your industry is a better signal than a 72 in the bottom half of it. ## Does SSI still matter in 2026? This is the question the classic SSI guides have not caught up with, because the primary source changed underneath them. LinkedIn's main SSI page is now titled ["From Social Selling Index (SSI) to AI"](https://business.linkedin.com/sell/resources/SSI), and it argues for moving past the score – "less scoring, more selling" – conceding outright that "a high SSI score doesn't always represent the efficacy of a sales person or correlate with measurable sales outcomes." That is LinkedIn, on its own flagship SSI page, telling you not to treat SSI as a performance metric. At the same time, LinkedIn still publishes the classic numbers on its [social selling page](https://business.linkedin.com/sales-solutions/social-selling), from internal research dating back to roughly 2014–2016: "Social selling leaders create 45% more opportunities than peers with lower SSI," "social selling leaders are 51% more likely to reach quota," and "78% of social sellers outsell peers who don't use social media." Read those for what they are – LinkedIn's own marketing research about correlation between activity and outcomes, not proof that raising the score raises revenue. The fair verdict: SSI is a **directional activity metric**. It tells you whether you (or a team) are consistently present on LinkedIn – completing profiles, searching deliberately, engaging, connecting. That makes it useful as a trend line and a team-hygiene signal, and useless as a quota predictor – by LinkedIn's own admission. If consistent LinkedIn presence is part of how you sell, SSI is a cheap proxy for whether the presence is actually happening. ## How to improve your LinkedIn SSI score Each component responds to a specific kind of activity. The table covers what feeds each pillar and the highest-leverage moves – and where a pillar is automatable, the linked guide goes deeper: | Component (official name) | What feeds it | What moves it | | --- | --- | --- | | Create a Professional Brand | Profile completeness, content you publish | Complete every profile section; post consistently – [automating your posting](/guides/how-to-automate-linkedin-posts) keeps the cadence without the willpower | | Find the right people | Deliberate searches and prospect views | Search with intent instead of browsing – [Boolean search](/guides/linkedin-boolean-search) makes each search count; Sales Navigator searches feed this too | | Engage with Insights | Reactions, comments, shares, group activity | Comment substantively on prospects' posts; share with a point of view; [messaging that gets replies](/guides/how-to-automate-linkedin-messages) beats broadcasting | | Build Strong Relationships | Connections made, especially decision-makers | Connect steadily with relevant people, not in bursts – the [connection-request guide](/guides/how-to-automate-linkedin-connection-requests) covers doing it at a human pace | Two honest notes. First, because Sales Navigator-only actions (advanced searches, saved leads, prospecting views) officially count toward the score, Navigator users tend to score higher by construction – LinkedIn publishes no boost percentage, and any specific figure you see circulating is a third-party estimate. Second, the fastest way to a higher SSI is simply doing the four activities regularly for a few weeks; the score updates daily, so effort shows up quickly. ## Who can see your SSI score? By default, only you. Your SSI is not displayed on your profile, not visible to your connections, and LinkedIn documents no direct effect of your SSI on how your profile or posts rank in feeds or search. There is one documented exception: Sales Navigator team contracts. LinkedIn's [data-privacy help page for Sales Navigator](https://www.linkedin.com/help/linkedin/answer/a103026) states: "Your Social Selling Index (SSI) score will be made available to your admin and other members of your team." If your company runs Sales Navigator with team seats, your admin and teammates can see your score – worth knowing before you assume it is fully private. ## Track your SSI over time (the part worth automating) A score you glance at once is trivia. The signal is the **trend** – is your score climbing while you invest in LinkedIn, and are your industry and network percentiles moving? The ranking guides reviewed in July 2026 do not show how to do that programmatically, and it is the one part that genuinely benefits from automation. [Linked API](/) – a flexible LinkedIn automation API you can embed in your own product, backend, CRM, or workflow, running every action on your own account through a human-paced cloud browser – has two dedicated actions for exactly this, documented under [retrieving SSI and performance](/docs/retrieving-ssi-and-performance). [`retrieveSSI`](/docs/action-st-retrieve-ssi) returns your score **plus both percentile ranks** (`ssi`, `industryTop`, `networkTop`), and [`retrievePerformance`](/docs/action-st-retrieve-performance) adds your dashboard analytics: `followersCount`, `postViewsLast7Days`, `profileViewsLast90Days`, `searchAppearancesPreviousWeek`. A weekly cron that appends one CSV row gives you a chart LinkedIn itself never shows – your SSI and reach over months, next to your actual outreach activity: ```typescript import LinkedApi from '@linkedapi/node'; import { appendFileSync } from 'fs'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const ssiWorkflow = await linkedapi.retrieveSSI.execute(); const { data: ssi } = await linkedapi.retrieveSSI.result(ssiWorkflow.workflowId); const perfWorkflow = await linkedapi.retrievePerformance.execute(); const { data: perf } = await linkedapi.retrievePerformance.result(perfWorkflow.workflowId); appendFileSync( 'ssi-history.csv', [ new Date().toISOString().slice(0, 10), ssi.ssi, ssi.industryTop, ssi.networkTop, perf.followersCount, perf.postViewsLast7Days, perf.profileViewsLast90Days, perf.searchAppearancesPreviousWeek, ].join(',') + '\n', ); ``` ```python import os from datetime import date from linkedapi import LinkedApi linkedapi = LinkedApi( linked_api_token=os.environ["LINKED_API_TOKEN"], identification_token=os.environ["IDENTIFICATION_TOKEN"], ) ssi_workflow = linkedapi.retrieve_ssi.execute() ssi = linkedapi.retrieve_ssi.result(ssi_workflow.workflow_id).data perf_workflow = linkedapi.retrieve_performance.execute() perf = linkedapi.retrieve_performance.result(perf_workflow.workflow_id).data row = [ date.today().isoformat(), ssi.ssi, ssi.industry_top, ssi.network_top, perf.followers_count, perf.post_views_last_7_days, perf.profile_views_last_90_days, perf.search_appearances_previous_week, ] with open("ssi-history.csv", "a") as f: f.write(",".join(map(str, row)) + "\n") ``` The same numbers are one command in the shell [CLI](/cli/getting-started) – easy to pipe into whatever you already use: ```bash linkedin stats ssi --json linkedin stats performance --json ``` One constraint to be clear about: both actions return data **for the connected account only** – there is no org-wide endpoint that reads a whole team's scores. A team SSI dashboard means each member's own LinkedIn account is connected, one seat per connected account on the flat [per-seat plan](/pricing), with the tracker running the same pull per account. And for AI-first workflows the whole loop works out of the box: an agent wired up through the [MCP server](/mcp/overview), the AI-agent-friendly [CLI](/cli/getting-started), or the ready-made [skills](/skills) can check the weekly numbers and report the movement in plain language – nothing to build or maintain. ## Frequently Asked Questions (FAQ) #### What is a good SSI score on LinkedIn? LinkedIn's dated guidance from 2017 called anything above 70 a high SSI and anything below 30 weak, and that is where the common "70+" benchmark comes from. Today's dashboard labels no threshold, and LinkedIn's current pages set no target – so use the relative signals instead: your industry and network percentile ranks, and whether your score is trending up. #### How often does the SSI score update? Daily. LinkedIn's learning-center materials reference daily SSI updates, and the dashboard carries the same wording – so changes in your activity show up within a day or so, which is what makes the score practical to track as a trend. #### Is my SSI score visible to others? By default, no – it is not on your profile and your connections cannot see it. The documented exception is Sales Navigator team contracts: LinkedIn's data-privacy help page states your SSI score "will be made available to your admin and other members of your team." #### Does Sales Navigator increase your SSI? Sales Navigator activity officially counts: LinkedIn's tip sheet says the SSI measures the four activities "as they take place on Sales Navigator and LinkedIn.com," including Navigator-only signals like advanced searches and saved leads. LinkedIn publishes no boost percentage, though – any specific figure circulating in third-party guides is an estimate, not LinkedIn's number. #### Is checking your SSI score free? Yes. SSI has been free for every LinkedIn member since August 3, 2015, when LinkedIn opened what was previously a Sales Navigator metric to everyone. You just need to be logged in when you open linkedin.com/sales/ssi. #### Does a high SSI make LinkedIn show my content to more people? LinkedIn documents no direct ranking effect from SSI on your feed reach or search visibility. The overlap is indirect: the activities that raise SSI – posting, engaging, connecting – are the same ones that grow reach on their own. Treat SSI as a reflection of that activity, not a lever on distribution. --- Want the trend, not a one-off glance? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually – and pull `retrieveSSI` and `retrievePerformance` on a schedule through the full surface set: the [REST API and Node/Python SDKs](/sdks/installation), the shell [CLI](/cli/getting-started), the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`) – on your own account and at a human pace. ## How to Automate LinkedIn Connection Requests in 2026: Send, Track, and Manage Invitations Sending LinkedIn connection requests by hand is exactly the work a computer should do: open a profile, click Connect, sometimes add a note, then keep track of who you already invited, who accepted, and which requests went stale enough to withdraw. At a handful a day it is a chore; at the scale of real prospecting it becomes a queue you can never keep clean. So the practical question is how to automate LinkedIn connection requests – both sending them and managing the pending and incoming queue – without babysitting a browser tab. > **The short version.** You can automate LinkedIn connection requests three ways: a **Chrome extension** that clicks inside your logged-in browser, a **hosted campaign tool** you configure in a UI, or an **API that runs on your own account**. The API approach composes the whole lifecycle from primitives – send a request, check the connection status, list and withdraw stale pending invites, accept or ignore incoming invitations, and detect when someone accepts so you can send a welcome. With Linked API each step runs on your own account through a human-paced cloud browser, and you can embed it in your own product, backend, or CRM. ## Three ways to automate LinkedIn connection requests LinkedIn does not offer a public connect API you can call directly. Its official invitation endpoints are [restricted to approved partners](https://learn.microsoft.com/en-us/linkedin/shared/integrations/communications/invitations) and are not generally available, so every automation option acts on your own logged-in account instead. Three shapes are practical: | Approach | Runs on | Where the logic lives | Lifecycle coverage | Embeddable in your stack | Best for | |---|---|---|---|---|---| | Chrome extension | Your browser, while it is open | Inside the extension | Sending, with some tracking | No | One person, light volume, zero setup | | Hosted campaign tool | The vendor's cloud | A campaign UI you configure | Full, inside the vendor's UI | Limited (some expose an API over pre-built agents) | Marketers who want a packaged UI | | API on your own account (Linked API) | A human-paced cloud browser on your account | Your own code | Full, as composable primitives | Yes – REST API, Node/Python SDKs, shell CLI, MCP server, AI-agent CLI, and skills | Teams embedding it in a product, backend, or CRM | Hosted tools are genuinely capable. PhantomBuster's Outreach Flow tracks a request from sent to accepted to follow-up, while its separate Auto Invitation Withdrawer prunes pending requests, and Dripify supports automatic withdrawal and acceptance-based branches (per each vendor's own docs, as of 2026). Some even expose an API – PhantomBuster's, for example, launches, configures, and chains its pre-built "Phantoms." The real difference is the **layer you program at**: those APIs orchestrate the vendor's packaged agents inside a campaign model, while Linked API exposes the underlying LinkedIn actions themselves as primitives you compose and embed. If you just want a campaign to run, a hosted tool is fine. If you want the connection lifecycle as building blocks inside your own system, read on. ## The connection-request lifecycle, as API primitives Automating connection requests well is not one call – it is a small lifecycle. Linked API exposes each step as its own action so you assemble exactly the flow you need: - [`sendConnectionRequest`](/sdks/send-connection-request) – send a request, optionally with a note. - [`checkConnectionStatus`](/sdks/check-connection-status) – returns `connected`, `notConnected`, `pending`, or `incoming`, so you never invite the same person twice. - [`retrievePendingRequests`](/sdks/retrieve-pending-requests) – lists the outgoing requests you are still waiting on. - [`withdrawConnectionRequest`](/sdks/withdraw-connection-request) – cancels a request that has gone stale. - [`retrieveInvitations`](/sdks/retrieve-invitations) plus [`acceptInvitation`](/sdks/accept-invitation) / [`ignoreInvitation`](/sdks/ignore-invitation) – triage the invitations coming *in*. - [`syncNetwork`](/sdks/sync-network) and [`pollNetwork`](/sdks/poll-network) – retrieve acceptance events after Linked API observes them (and other network changes) without diffing lists yourself. ![Diagram: the LinkedIn connection-request lifecycle as Linked API primitives. An outbound lane runs sendConnectionRequest, then checkConnectionStatus returns pending, then retrievePendingRequests lists the queue, then withdrawConnectionRequest prunes stale invites. A parallel inbound lane runs retrieveInvitations, then acceptInvitation or ignoreInvitation. A monitoring lane runs syncNetwork once, then pollNetwork surfaces a connectionAccepted event with a detectedAt timestamp, which hands off to sendMessage for a welcome. A footer notes it runs on your own account, at a human pace.](/images/guides/linkedin-connection-lifecycle.webp) The rest of this guide walks each lane, in code. All of it runs on the [Node and Python SDKs](/sdks) and the [shell CLI](/cli/connections); the same actions are available over the [REST API](/sdks), the [MCP server](/mcp), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and the ready-made [agent skills](/skills). If you want an agent to run this rather than writing the code yourself, start with [how to give an AI agent access to LinkedIn](/guides/linkedin-ai-agent). ## How to automate sending connection requests (in code) The one rule that separates a clean automation from a messy one is the **duplicate gate**: check where you stand before you send. `checkConnectionStatus` tells you the [connection status](/docs/checking-connection-status) – whether you are already `connected`, already have a `pending` request out, have an `incoming` request to accept, or are genuinely `notConnected` and clear to invite. Three of those four wear the same profile badge, which is why the [degree of connection](/guides/linkedin-connection-degrees) you can see is not the state you need. Send only in the last case, and handle the one common edge – some people can only be added with a verified email, which `sendConnectionRequest` reports as `emailRequired`. ```typescript import { LinkedApi } from "@linkedapi/node"; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); async function connectIfNew( personUrl: string, note: string, knownEmail?: string, ): Promise { const check = await linkedapi.checkConnectionStatus.execute({ personUrl }); const { data } = await linkedapi.checkConnectionStatus.result(check.workflowId); if (data?.connectionStatus !== "notConnected") { return data?.connectionStatus ?? "unknown"; // connected, pending, or incoming – skip } const req = await linkedapi.sendConnectionRequest.execute({ personUrl, note }); const { errors } = await linkedapi.sendConnectionRequest.result(req.workflowId); // Edge: this person requires a verified email. Retry with one if you have it. if (errors?.some((e) => e.type === "emailRequired") && knownEmail) { const retry = await linkedapi.sendConnectionRequest.execute({ personUrl, note, email: knownEmail, }); await linkedapi.sendConnectionRequest.result(retry.workflowId); return "sent"; } return errors?.length ? errors[0].type : "sent"; } ``` ```python import os from linkedapi import ( LinkedApi, LinkedApiConfig, CheckConnectionStatusParams, SendConnectionRequestParams, WithdrawConnectionRequestParams, AcceptInvitationParams, IgnoreInvitationParams, NetworkPollRequest, ) linkedapi = LinkedApi( LinkedApiConfig( linked_api_token=os.environ["LINKED_API_TOKEN"], identification_token=os.environ["IDENTIFICATION_TOKEN"], ) ) def connect_if_new(person_url: str, note: str, known_email: str | None = None) -> str: check = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url=person_url) ) data = linkedapi.check_connection_status.result(check.workflow_id).data if data.connection_status != "notConnected": return data.connection_status # connected, pending, or incoming – skip req = linkedapi.send_connection_request.execute( SendConnectionRequestParams(person_url=person_url, note=note) ) errors = linkedapi.send_connection_request.result(req.workflow_id).errors # Edge: this person requires a verified email. Retry with one if you have it. if any(e.type == "emailRequired" for e in errors) and known_email: retry = linkedapi.send_connection_request.execute( SendConnectionRequestParams( person_url=person_url, note=note, email=known_email ) ) linkedapi.send_connection_request.result(retry.workflow_id) return "sent" return errors[0].type if errors else "sent" ``` From the shell, the same two steps are one command each: ```bash linkedin connection status https://www.linkedin.com/in/john-doe linkedin connection send https://www.linkedin.com/in/john-doe --note "Great to connect." ``` The note is a single field, not a sequence; what it should *say* is outreach copy, covered in the [cold outreach guide](/guides/linkedin-cold-outreach-strategies-2026). If the note is too long or your free account has used up its personalized invitations, `sendConnectionRequest` returns `noteTooLong` or `noteLimitExceeded` so you can drop the note and send anyway. > **This runs on your own account.** Every request goes through a human-paced cloud browser on your own LinkedIn account – no cookie handoff to a third-party cloud, and no blast queue. Linked API surfaces LinkedIn's own signals, such as `requestNotAllowed` when you are pushing too hard, so your code can back off automatically. For how many requests are actually safe per day and per week, see the [LinkedIn connection limit guide](/guides/linkedin-connection-limit-2026). ## Keep your pending queue clean: list and withdraw Requests that sit unanswered for weeks are dead weight: they count against the pending invitations LinkedIn lets you hold, and a pile of ignored invites is a signal you do not want to send – LinkedIn names invitations "ignored, left pending, or marked as spam" among its documented triggers for a [restricted LinkedIn account](/guides/linkedin-account-restricted). Withdrawing the stale ones keeps the queue healthy, and [managing your connections and requests](/docs/managing-existing-connections) is a first-class part of the API. `retrievePendingRequests` returns everything still outstanding – each with a `name`, `publicUrl`, `headline`, and a relative `sentTime` like `"1 month ago"` – and `withdrawConnectionRequest` cancels one. ```typescript const wf = await linkedapi.retrievePendingRequests.execute(); const { data } = await linkedapi.retrievePendingRequests.result(wf.workflowId); for (const request of data ?? []) { console.log(`${request.name} – sent ${request.sentTime}`); // Withdraw the ones that have been out too long (unfollow defaults to true). if (isStale(request.sentTime)) { await linkedapi.withdrawConnectionRequest.execute({ personUrl: request.publicUrl }); } } ``` ```python wf = linkedapi.retrieve_pending_requests.execute() data = linkedapi.retrieve_pending_requests.result(wf.workflow_id).data for request in data or []: print(f"{request.name} – sent {request.sent_time}") # Withdraw the ones that have been out too long (unfollow defaults to true). if is_stale(request.sent_time): linkedapi.withdraw_connection_request.execute( WithdrawConnectionRequestParams(person_url=request.public_url) ) ``` ```bash linkedin connection pending --json linkedin connection withdraw https://www.linkedin.com/in/john-doe ``` By default, withdrawing also unfollows the person; pass `unfollow: false` (or `--no-unfollow` on the CLI) to keep following them. The manual side of this queue – where the Sent tab lives, what the recipient sees on withdrawal, expiry and cooldown rules – is covered in the [pending invitations guide](/guides/linkedin-pending-invitations). How stale is "too stale," and how many pending invites are safe to hold, are pacing questions – the [connection limit guide](/guides/linkedin-connection-limit-2026) has the current numbers. ## Handle incoming invitations automatically The inbound side is its own queue – the docs cover it under [working with invitations](/docs/working-with-invitations). `retrieveInvitations` lists every incoming invitation, and each carries an `invitationType` of `connect`, `companyFollow`, or `newsletterSubscribe`. For `connect` invitations you also get the sender's `headline` and `note` (both nullable), which is enough to auto-accept the ones that fit your ICP and ignore the rest with `acceptInvitation` / `ignoreInvitation`. ```typescript const wf = await linkedapi.retrieveInvitations.execute(); const { data } = await linkedapi.retrieveInvitations.result(wf.workflowId); for (const invite of data ?? []) { if (invite.invitationType !== "connect") continue; const relevant = /founder|head of|vp|director/i.test(invite.headline ?? ""); if (relevant) { await linkedapi.acceptInvitation.execute({ invitationType: "connect", personUrl: invite.publicUrl, }); } else { await linkedapi.ignoreInvitation.execute({ invitationType: "connect", personUrl: invite.publicUrl, }); } } ``` ```python wf = linkedapi.retrieve_invitations.execute() data = linkedapi.retrieve_invitations.result(wf.workflow_id).data for invite in data or []: if invite.invitation_type != "connect": continue headline = invite.headline or "" if any(k in headline.lower() for k in ("founder", "head of", "vp", "director")): linkedapi.accept_invitation.execute( AcceptInvitationParams(invitation_type="connect", person_url=invite.public_url) ) else: linkedapi.ignore_invitation.execute( IgnoreInvitationParams(invitation_type="connect", person_url=invite.public_url) ) ``` ```bash linkedin connection invitations --json linkedin connection accept connect https://www.linkedin.com/in/john-doe linkedin connection ignore connect https://www.linkedin.com/in/someone-else ``` Company-follow and newsletter invitations come through the same method with their own `companyUrl` / `newsletterUrl` fields, so you can route those too. ## Detect when someone accepts (and welcome them) The payoff of automating requests is acting on each acceptance without watching for it by hand. Rather than re-list your connections and diff them, enable [network monitoring](/docs/monitoring-network) once with `syncNetwork`, then read connection events with `pollNetwork`. Each event carries a `type` (`connectionRequestReceived`, `connectionAccepted`, or `connectionAdded`), the other person's `personUrl`, and a `detectedAt` timestamp for when Linked API observed it. The events come back newest-first, so filter for `connectionAccepted`, hand each new connection to your own welcome step, and advance your cursor to the newest event you saw. ```typescript // Enable network monitoring once per account. const sync = await linkedapi.syncNetwork.execute(); await linkedapi.syncNetwork.result(sync.workflowId); // Later, poll for acceptances. `events` comes back newest-first. const { data } = await linkedapi.pollNetwork({ type: "connectionAccepted", since: lastSeen }); const events = data?.events ?? []; for (const event of events) { await sendWelcome(event.personUrl); // your own follow-up } if (events.length) { lastSeen = events[0].detectedAt; // advance the cursor to the newest event } ``` ```python # Enable network monitoring once per account. sync = linkedapi.sync_network.execute() linkedapi.sync_network.result(sync.workflow_id) # Later, poll for acceptances. `events` comes back newest-first. result = linkedapi.poll_network( NetworkPollRequest(type="connectionAccepted", since=last_seen) ) events = result.data.events if result.data else [] for event in events: send_welcome(event.person_url) # your own follow-up if events: last_seen = events[0].detected_at # advance the cursor to the newest event ``` ```bash linkedin network sync linkedin network events --type connectionAccepted --json ``` `pollNetwork` is a polling call, so `detectedAt` is when Linked API observed the acceptance, not a live push; events are retained for 90 days, so poll often enough to consume them. If you prefer push, subscribe to the `network.connectionAccepted` [webhook](/docs/webhooks) and skip polling entirely. What the welcome should say, and when to follow up, is outreach rather than mechanics: see [how to automate LinkedIn messages](/guides/how-to-automate-linkedin-messages) for the send-and-read flow and the [cold outreach guide](/guides/linkedin-cold-outreach-strategies-2026) for copy and cadence. ## When an extension or hosted tool is the better choice An API is not always the right tool. If you are one person sending a handful of invites a week, a Chrome extension is the fastest path – nothing to code, nothing to host. If you want a packaged campaign UI with built-in dashboards and templates and you do not need the flow embedded anywhere, a hosted tool will run the lifecycle for you. Linked API earns its place when you are building connection automation into your own product, backend, or CRM, want the individual LinkedIn actions as programmable primitives, and want it running on your account at a human pace instead of in a browser tab you keep open. Finding *who* to connect with is a separate job – for that, see the [Boolean search guide](/guides/linkedin-boolean-search) and the [Sales Navigator scraper guide](/guides/linkedin-sales-navigator-scraper) – and so is what your new connections see in their feed: automating your own publishing is covered in [How to Automate LinkedIn Posts](/guides/how-to-automate-linkedin-posts). ## Frequently Asked Questions (FAQ) #### How do I avoid sending a duplicate connection request? Call `checkConnectionStatus` before you send and skip anyone who is already `connected` or `pending`. As a backstop, `sendConnectionRequest` also reports `alreadyPending` or `alreadyConnected` if a request or connection already exists, so a duplicate is a caught error rather than a second invite. #### What happens if a profile requires an email to connect? Some people only accept connection requests that include a verified email. When that is the case, `sendConnectionRequest` returns `emailRequired`. Pass the person's `email` as the optional input and retry. Email is an input for that case only – Linked API never returns email addresses as data. #### Can I see and cancel the connection requests I already sent? Yes. `retrievePendingRequests` lists every outgoing request you are still waiting on, with the person's name, URL, headline, and a relative sent time. `withdrawConnectionRequest` cancels one; withdrawing unfollows the person by default, which you can turn off with `unfollow: false`. #### Can I automatically accept or ignore incoming invitations? Yes. `retrieveInvitations` returns your incoming invitations, and `acceptInvitation` / `ignoreInvitation` act on each by type and URL. For `connect` invitations you get the sender's headline and note, so you can auto-accept the ones that match your criteria and ignore the rest. #### How do I know when someone accepts my request? Enable monitoring once with `syncNetwork`, then poll `pollNetwork` for `connectionAccepted` events – each carries the person's URL and a `detectedAt` timestamp. If you would rather not poll, subscribe to the `network.connectionAccepted` webhook and Linked API pushes each event to your endpoint. #### Do I need Sales Navigator to automate connection requests? No. `sendConnectionRequest` and the rest of the connection lifecycle work on a standard LinkedIn account. Sales Navigator only matters for its larger search, covered in the [Sales Navigator API guide](/guides/linkedin-sales-navigator-api), and for [InMail to non-connections](/guides/linkedin-inmail). #### Does this run on my own LinkedIn account? Yes. Requests run through a human-paced cloud browser on your own account, with no cookie handoff to a third-party cloud. Linked API surfaces LinkedIn's own limit signals, such as `requestNotAllowed`, so your code can slow down. For safe pacing, see the [connection limit guide](/guides/linkedin-connection-limit-2026). #### How much does it cost? Linked API starts at $49/mo on the Core plan, billed annually, flat per seat – there is no per-request or per-lead metering. See [pricing](/pricing) for the current plans. ## Build it on your own account You can build the whole connection lifecycle today. Linked API gives you sending, status checks, pending-queue management, incoming-invitation handling, and acceptance detection as composable primitives across the full surface set: the [REST API and Node/Python SDKs](/sdks), the shell [CLI](/cli), the [MCP server](/mcp), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`) – all running on your own account at a human pace. Start on the [Core plan at $49/mo, billed annually](/pricing) and wire it into your product, backend, or CRM. ## LinkedIn Company Scraper: How to Extract Company Profiles, Employees, and Decision-Makers (2026) A LinkedIn company page holds a small database: the firmographics, the people who work there, the decision-makers, and everything the company posts. Turning that into structured data you can actually use – at scale, and while managing the risk to your account – is what a company scraper is for. > **The short version.** A LinkedIn company scraper turns a company's LinkedIn URL into structured data. You have three methods: a **cookieless dataset** (runs via proxies with no login, but typically returns company fields, and sells people or posts as separate products), a **session-borrowing browser tool** (you hand your logged-in LinkedIn cookie to a third-party cloud, which is the real ban vector), or an **API on your own account**. Linked API's `fetchCompany` returns the company profile *plus* its employees, posts, and decision-makers in one workflow, and `searchCompanies` finds companies by size, industry, and location – on your own account at a human pace, returning company and people data, **not emails**. ## What you can pull from a LinkedIn company One `fetchCompany` workflow returns the firmographic profile, and you can opt in to the employees, decision-makers, and posts alongside it. | Data | What you get | How | | --- | --- | --- | | Company profile | name, industry, size (`employeesCount`), HQ location, specialties, website, year founded, whether it has venture financing, open-jobs count, logo | `fetchCompany` | | Employees | up to **500** people (name, headline, location, profile URL), filterable by role, location, school, or past company | `retrieveEmployees` | | Decision-makers | up to **20** key people (name, headline, location, country code (`countryCode`), profile URL) | `retrieveDMs` | | Company posts | up to **20** recent posts (text, media, reaction and comment counts) | `retrievePosts` | | **Not returned** | emails, phone numbers, follower counts, funding-round or investor detail (only a yes/no `ventureFinancing` flag) | – | That last row is the honest boundary: Linked API returns company and people data from LinkedIn, not contact details or a firmographic database's funding history. ## Three ways to scrape LinkedIn company data The methods differ mostly in *where the browser runs* and *whose account it uses* – which is also what decides your ban risk. | Method | How it runs | What you get | Account-safety trade-off | | --- | --- | --- | --- | | Cookieless dataset / scraper | On a vendor's cloud via rotating proxies, no login | Company profile fields; some are pre-collected datasets, others are live URL scrapers, and people or posts tend to be separate products (per Bright Data's collection options, 2026) | Doesn't touch your account, but you consume a product rather than composing your own authenticated query | | Session-borrowing browser tool | On a vendor's cloud, driven by *your* logged-in LinkedIn cookie | Company data, often with a chained employee export | You hand your session to a third party – the real ban vector, and often day-capped (around 80 companies/day, per PhantomBuster's page, 2026) | | API on your own account | On your own authenticated account through a human-paced cloud browser | Profile + employees + posts + decision-makers in one workflow | Runs as you, at a human pace, with no cookie handoff; account-paced rather than a bulk overnight dump | A common question here: **does LinkedIn have an official way to do this?** LinkedIn's own APIs (Marketing, Talent) are partner-gated and need [program approval](https://learn.microsoft.com/en-us/linkedin/shared/authentication/getting-access) – they are not a general-purpose way to pull an arbitrary company from its URL. So in practice you either scrape the public page or use an API that acts on your own account. The rest of this guide covers the API route, where [Linked API](/) retrieves [structured company records](/docs/retrieving-company-data) on your behalf. ## How to scrape one company from its URL Install the SDK (`npm install -S @linkedapi/node` or `pip install linkedapi`), initialise the client with your tokens, and call `fetchCompany` with the company URL. Toggle `retrieveEmployees`, `retrieveDMs`, and `retrievePosts` to pull those alongside the profile; each has a config for its limit (and the employees list takes a filter). ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const company = await linkedapi.fetchCompany.execute({ companyUrl: 'https://www.linkedin.com/company/microsoft', retrieveEmployees: true, retrieveDMs: true, retrievePosts: true, employeesRetrievalConfig: { limit: 100, // up to 500 filter: { position: 'engineer', locations: ['United States'] }, }, postsRetrievalConfig: { limit: 20 }, // up to 20 dmsRetrievalConfig: { limit: 20 }, // up to 20 }); const { data } = await linkedapi.fetchCompany.result(company.workflowId); if (data) { console.log(data.name, data.industry, data.employeesCount); console.log(data.employees?.length, 'employees,', data.dms?.length, 'decision-makers'); } ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, FetchCompanyParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) company = linkedapi.fetch_company.execute( FetchCompanyParams( company_url="https://www.linkedin.com/company/microsoft", retrieve_employees=True, retrieve_dms=True, retrieve_posts=True, employees_retrieval_config={ "limit": 100, # up to 500 "filter": {"position": "engineer", "locations": ["United States"]}, }, posts_retrieval_config={"limit": 20}, # up to 20 dms_retrieval_config={"limit": 20}, # up to 20 ) ) data = linkedapi.fetch_company.result(company.workflow_id).data if data: print(data.name, data.industry, data.employees_count) print(len(data.employees or []), "employees,", len(data.dms or []), "decision-makers") ``` On the shell, the [company CLI](/cli/company) does the same in one line, and the AI-agent surfaces – the [MCP server](/mcp/overview) and ready-made [skills](/skills) – expose it to Claude Code, Cursor, and Codex. ```bash linkedin company fetch https://www.linkedin.com/company/microsoft --employees --dms --json ``` The employees filter is what turns "a company" into "the right people at a company": pass a `position`, `locations`, `schools`, `currentCompanies`, or `previousCompanies` and you get a targeted slice instead of the whole roster. Full field reference is in the [`fetchCompany` docs](/sdks/fetch-company) and the [company data guide](/docs/retrieving-company-data). ![Diagram: one fetchCompany workflow returns four kinds of data. A company URL feeds fetchCompany, with retrieveEmployees, retrievePosts, and retrieveDMs toggles, fanning out to four output cards: Company profile (industry, size, HQ, specialties, website, founded); Employees array (name, headline, location – up to 500); Posts array (up to 20); and Decision-makers array (name, headline, location, countryCode – up to 20), highlighted as the differentiator. A footer notes it runs on your own account, at a human pace, returning company and people data, not emails.](/images/guides/linkedin-company-scraper-bundle.webp) ## Find the companies to scrape If you do not already have the URLs, `searchCompanies` builds the list. Filter by `sizes`, `locations`, and `industries`, or hand it a LinkedIn search results URL as `customSearchUrl`, then loop each result's `publicUrl` into `fetchCompany`. ```typescript const search = await linkedapi.searchCompanies.execute({ term: 'fintech', limit: 50, // up to 1000 filter: { sizes: ['51-200', '201-500'], locations: ['United States'], industries: ['Financial Services'], }, }); const { data: companies } = await linkedapi.searchCompanies.result(search.workflowId); for (const c of companies ?? []) { console.log(c.name, c.industry, c.publicUrl); // feed publicUrl back into fetchCompany } ``` ```python from linkedapi import SearchCompaniesParams search = linkedapi.search_companies.execute( SearchCompaniesParams( term="fintech", limit=50, # up to 1000 filter={ "sizes": ["51-200", "201-500"], "locations": ["United States"], "industries": ["Financial Services"], }, ) ) companies = linkedapi.search_companies.result(search.workflow_id).data for c in companies or []: print(c.name, c.industry, c.public_url) # feed public_url back into fetch_company ``` ```bash linkedin company search --term "fintech" --sizes "51-200,201-500" --industries "Financial Services" --json ``` Company sizes use LinkedIn's own bands (`1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+`). Each result returns `name`, `publicUrl`, `industry`, `location`, and `logoUrl`; pass the `publicUrl` straight into `fetchCompany` to go deep. If you work in Sales Navigator, the [`nvSearchCompanies`](/sdks/nv-search-companies) and [`nvFetchCompany`](/sdks/nv-fetch-company) variants add filters like revenue (they need a Sales Navigator seat); see the [Sales Navigator API guide](/guides/linkedin-sales-navigator-api). See also the [search reference](/sdks/search-companies) and [how to find companies with filters](/docs/searching-for-companies). ## Staying safe: your own account, human pace, no cookie handoff The account risk in company scraping is not the data – public company pages are meant to be read. It is *how* a tool gets it. Browser tools that run on a vendor's cloud ask for your logged-in LinkedIn cookie, and handing your session to a third party is what actually gets accounts flagged – the tools' own pages flag it too. Linked API takes a different path: every workflow runs on **your own account through a human-paced cloud browser**, so you never export a cookie to anyone. It also applies its own caps by design – one `fetchCompany` returns up to 500 employees, 20 posts, and 20 decision-makers, and `searchCompanies` up to 1,000 results, with the workflow pacing itself between actions. Separately, it surfaces LinkedIn's own limit signals: if LinkedIn restricts a retrieval you get a clear `retrievingNotAllowed` or `searchingNotAllowed` to back off on, rather than a silent overrun. Our [account limits guide](/guides/understanding-linkedin-limits) and [safety model](/safety) go deeper on pacing your volumes. ## When a cookieless scraper or data provider fits better Linked API is not always the right tool. If you need a one-off dump of tens of thousands of company records and you do not have (or do not want to use) a LinkedIn account, a **cookieless dataset provider** is the better fit – it trades composability for raw scale. And if your job is really about **emails, direct dials, or funding-round histories**, that is a firmographic or enrichment database, not a LinkedIn scraper. Linked API is the right fit when you want the profile, the people, and the decision-makers *together*, on your own account, wired into your own product or workflow. For the broader picture, see the [LinkedIn scraping pillar](/guides/how-to-scrape-linkedin) and the [scraper API overview](/guides/linkedin-scraper-api); for other entities, the [profile scraper](/guides/linkedin-profile-scraper), [jobs scraper](/guides/linkedin-jobs-scraper), and [post scraper](/guides/linkedin-post-scraper). ## Frequently Asked Questions (FAQ) #### Can you scrape LinkedIn company data? Yes. A company scraper turns a LinkedIn company URL into structured JSON you can store or feed into a workflow. The available fields and account-safety trade-offs depend on the method you pick, covered above. #### How do I get a list of all employees at a company on LinkedIn? Call `fetchCompany` with `retrieveEmployees` on. You get up to 500 employees per company (name, headline, location, profile URL), and you can filter the list by role, location, school, or current/previous company to pull just the people you care about. #### Can you scrape company size, industry, and headcount from LinkedIn? Yes. `fetchCompany` returns the firmographic profile – industry, exact headcount (`employeesCount`), HQ location, specialties, website, year founded, and open-jobs count – from the company URL, with no extra configuration. #### How do I scrape LinkedIn without getting my account blocked? The main ban vector is handing your logged-in cookie to a third-party cloud tool. Avoid that: use a method that runs on your own account at a human pace and stays within LinkedIn's limits. Linked API does this by design and surfaces LinkedIn's own limit signals so you can slow down before hitting a wall. #### Can you scrape a LinkedIn company page without logging in? Cookieless dataset providers scrape public pages via proxies with no login, but they typically return company fields only. To also get the employee roster, posts, and decision-makers, you use an authenticated method – Linked API runs on your own account rather than borrowing a session. #### Can you get a company's decision-makers? Yes. Turn on `retrieveDMs` and `fetchCompany` returns up to 20 decision-makers (name, headline, location, country code (`countryCode`), profile URL) in the same result as the profile and employees – so you know who to contact, not just where. #### How many employees can you pull per company? Up to 500 per company with `fetchCompany`, controlled by `employeesRetrievalConfig.limit`. To go wider than one company, use `searchCompanies` (up to 1,000 companies) and fetch each in turn. #### What is the best LinkedIn company scraper? It depends on the job. For a raw bulk dump with no account, a cookieless dataset provider wins. For the profile, employees, posts, and decision-makers together, embedded in your own stack on your own account, an API like Linked API is the better fit. --- Want the company profile, its people, and its decision-makers in one workflow, on your own account? [Start with Linked API](/pricing) – pull company data through the [API and SDKs](/sdks/installation), [CLI](/cli/getting-started), [MCP server](/mcp/overview), and [skills](/skills), and wire it straight into your own stack. ## How to Automate LinkedIn Messages (2026): The LinkedIn Messaging API, What's Safe, and the Flow in Code If you send the same LinkedIn connection requests and follow-up messages by hand every week, you have already hit the ceiling: it does not scale, and copy-pasting into the message box is exactly the work a computer should do. This guide covers what you can automate on LinkedIn, the safe ways to do it, and – for teams who want it wired into their own product or CRM – the exact connect-to-reply flow in code. > **The short version.** Standard LinkedIn messaging has no native way to schedule or sequence outbound DMs (LinkedIn documents scheduled outreach only for its paid recruiting tools – Recruiter and Recruiter Lite can schedule initial InMail, and Recruiter alone can schedule one automated follow-up; Premium's away message is just an auto-reply), so automating everyday outreach means a tool or an API acting on your own account. The real automation is a branching loop: if you are already connected, send a message; if not, either send a connection request → wait until it is accepted → message, or use Sales Navigator InMail to reach a non-connection directly. Then read the reply, or monitor your whole inbox for incoming messages at scale. Keep it on your own account, at a human pace, within LinkedIn's limits. ## Can you automate LinkedIn messages? Yes – but for regular direct messages, not with a native LinkedIn button. LinkedIn's own [Message content automation](https://www.linkedin.com/help/linkedin/answer/a1336512) page is about *inbound* processing – how it renders and suggests replies to messages you receive – not about scheduling messages you send. The one place LinkedIn documents scheduled outbound is its paid recruiting product: per LinkedIn's [help center](https://www.linkedin.com/help/recruiter/answer/a1457303), you can schedule initial outreach InMail in **Recruiter and Recruiter Lite**, and schedule one automated follow-up InMail in **Recruiter** only. Everyday Messaging has no equivalent, and the Premium [away message](https://www.linkedin.com/help/linkedin/answer/a550614) is an automatic *reply* shown to people who message you – not a way to send scheduled outreach. So for standard outreach, "automating LinkedIn messages" means a tool or an API that performs the clicks and keystrokes on your own account. The rest of this guide is how to do that well. ## Is there a LinkedIn messaging API? Yes, but it is not a general developer product. LinkedIn documents two official messaging surfaces, both gated behind partner approval, and neither one lets you schedule or sequence outbound DMs. | Official surface | What it does | Who can use it | | --- | --- | --- | | [Messages API](https://learn.microsoft.com/en-us/linkedin/shared/integrations/communications/messages) | Creates a message to one or more first-degree connections, or replies in an existing thread | Approved partners only, under an API agreement | | [Pages messaging integration](https://www.linkedin.com/help/linkedin/answer/a6246714) | Sends and receives messages in a **Company Page's** inbox – not your personal DMs | Page admins, through six approved platforms: Hootsuite, Sprinklr, Oktopost, Brandwatch, Zoho Recruit, Bird CRM | The Messages API documentation opens on the access rule: *"Usage of this API is restricted to approved partners, subject to limitations via API agreement."* Approval on its own would still not buy you automation, because the same page rules it out in the requirements: *"A message must be associated with a specific member action. Member actions do not include an automated or scheduled event."* LinkedIn further requires that the member can edit any pre-prepared draft, takes an affirmative action to send it, and that the message posts at or around the time of that action. That leaves the account-level route for everyone else, and it is what the rest of this guide covers: Linked API's messaging actions – [`sendMessage`](/docs/action-st-send-message) and [`nvSendMessage`](/docs/action-nv-send-message), with [conversation sync](/docs/working-with-conversations) and [inbox monitoring](/docs/monitoring-inbox) for reading replies – run on your own account through a human-paced cloud browser rather than under a partner agreement. ## The three ways to automate LinkedIn messages There are three approaches, and the right one depends on how much control you need and whether you write code. | Approach | How it runs | Best for | Honest trade-off | | --- | --- | --- | --- | | Browser (Chrome) extension | On your own computer, in your logged-in browser | Quick, occasional personal use | Only works while your machine and browser are on; more exposed than a cloud setup | | Cloud no-code tool | On the vendor's cloud, on a schedule, through a visual campaign builder | Non-technical users running standard campaigns | A closed UI – you get the vendor's features, not your own logic; you live inside their product | | API / code | You call an API from your own backend, on your own account | Teams embedding messaging in a product, CRM, or agent | You write a little code – in exchange you own the whole flow | The first two are covered well elsewhere (see our [roundup of LinkedIn automation tools](/blog/top-10-linkedin-automation-tools) for the "which tool" question). This guide focuses on the third, because it is the one no listicle actually shows you: the real flow, in code, on your own account. ## What you can automate: messages, InMail, and connection notes Before the code, get the routing right – *who* you can message depends on your relationship to them. | Message type | Who it reaches | Method | Notes | | --- | --- | --- | --- | | Standard message | People you are already connected to (1st-degree) | `sendMessage` | Text up to 1,900 characters | | Sales Navigator InMail | People you are **not** connected to | `nvSendMessage` | Needs a Sales Navigator seat and a subject line – credits and consumer mechanics in the [InMail guide](/guides/linkedin-inmail) | | Connection request + note | Someone you want to connect with | `sendConnectionRequest` | A single note field, not a sequence – automating the request itself is its own [connection-requests guide](/guides/how-to-automate-linkedin-connection-requests) | Two honest boundaries that pages blurring "reach anyone instantly" skip: a standard message only works once someone is a 1st-degree connection, and reaching a non-connection means either connecting first or spending a Sales Navigator InMail. There is no button that DMs a stranger for free. If the 1st/2nd/3rd labels themselves are the part you want pinned down, [degrees of connection](/guides/linkedin-connection-degrees) covers what each one allows – and why the badge alone does not tell you what to do next. ## How to automate the full flow (in code) Here is the branching loop with [Linked API](/), the [LinkedIn automation API](/) that runs these actions on your own account. Install the SDK (`npm install -S @linkedapi/node` or `pip install linkedapi`) and initialise the client with your tokens. The core decision is the **acceptance gate** – the step generic messaging relays skip. You check where you stand, and only message a first-degree connection; if you are not connected, you get connected first and message *after* the request is accepted. Automating the connection request itself – sending it, handling the `emailRequired` edge, withdrawing stale invites, and detecting acceptance – is its own lifecycle, covered in [How to Automate LinkedIn Connection Requests](/guides/how-to-automate-linkedin-connection-requests); here it is only the gate before a message. ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const NOTE = 'Hi! I work on LinkedIn tooling and would love to connect.'; async function reach(personUrl: string, text: string): Promise { // 1. Where do we stand? -> connected | pending | notConnected const check = await linkedapi.checkConnectionStatus.execute({ personUrl }); const { data } = await linkedapi.checkConnectionStatus.result(check.workflowId); // 2. Already connected -> message them directly. if (data?.connectionStatus === 'connected') { const sent = await linkedapi.sendMessage.execute({ personUrl, text }); await linkedapi.sendMessage.result(sent.workflowId); return; } // 3. Not connected -> get connected first, then message once accepted. // Sending the request, the emailRequired edge, and withdrawing stale invites // are their own lifecycle: /guides/how-to-automate-linkedin-connection-requests if (data?.connectionStatus === 'notConnected') { await linkedapi.sendConnectionRequest.execute({ personUrl, note: NOTE }); // Re-run checkConnectionStatus on a schedule (cron or webhook); // when it returns 'connected', call sendMessage. Do not busy-wait. } } ``` ```python from linkedapi import ( LinkedApi, LinkedApiConfig, CheckConnectionStatusParams, SendMessageParams, SendConnectionRequestParams, NvSendMessageParams, SyncConversationParams, NvSyncConversationParams, ConversationPollRequest, InboxPollRequest, ) linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) NOTE = "Hi! I work on LinkedIn tooling and would love to connect." def reach(person_url: str, text: str) -> None: # 1. Where do we stand? -> connected | pending | notConnected check = linkedapi.check_connection_status.execute( CheckConnectionStatusParams(person_url=person_url) ) data = linkedapi.check_connection_status.result(check.workflow_id).data # 2. Already connected -> message them directly. if data and data.connection_status == "connected": sent = linkedapi.send_message.execute( SendMessageParams(person_url=person_url, text=text) ) linkedapi.send_message.result(sent.workflow_id) return # 3. Not connected -> get connected first, then message once accepted. # Sending the request, the emailRequired edge, and withdrawing stale invites # are their own lifecycle: /guides/how-to-automate-linkedin-connection-requests if data and data.connection_status == "notConnected": linkedapi.send_connection_request.execute( SendConnectionRequestParams(person_url=person_url, note=NOTE) ) # Re-run check_connection_status on a schedule (cron or webhook); # when it returns "connected", call send_message. Do not busy-wait. ``` To reach someone you are **not** connected to without waiting, send a Sales Navigator InMail instead. This route has no acceptance gate, but it needs a Sales Navigator seat and a subject line – how InMail credits, refunds, and limits work is covered in the [LinkedIn InMail guide](/guides/linkedin-inmail). ```typescript const inmail = await linkedapi.nvSendMessage.execute({ personUrl, subject: 'Quick question about your team', text: 'Hi Dana, saw your team is hiring SDRs. Mind if I share how we help?', }); await linkedapi.nvSendMessage.result(inmail.workflowId); ``` ```python inmail = linkedapi.nv_send_message.execute( NvSendMessageParams( person_url=person_url, subject="Quick question about your team", text="Hi Dana, saw your team is hiring SDRs. Mind if I share how we help?", ) ) linkedapi.nv_send_message.result(inmail.workflow_id) ``` Finally, read the reply. `syncConversation` turns on message capture once, then `pollConversations` returns the messages, each tagged with a `sender` of `us` or `them`, so you can detect a real response. A standard message and a Sales Navigator InMail live in different threads, so match the sync method and the `type` to the route: `syncConversation` with `type: 'st'` for a connection, `nvSyncConversation` with `type: 'nv'` for an InMail. ```typescript // Standard reply (connection route): sync once, then poll the 'st' thread. const s1 = await linkedapi.syncConversation.execute({ personUrl }); await linkedapi.syncConversation.result(s1.workflowId); const { data } = await linkedapi.pollConversations([{ personUrl, type: 'st' }]); const theirReplies = data?.[0]?.messages.filter((m) => m.sender === 'them'); console.log(theirReplies?.at(-1)?.text); // InMail reply (Sales Navigator route): use nvSyncConversation and type 'nv'. const s2 = await linkedapi.nvSyncConversation.execute({ personUrl }); await linkedapi.nvSyncConversation.result(s2.workflowId); const inmail = await linkedapi.pollConversations([{ personUrl, type: 'nv' }]); ``` ```python # Standard reply (connection route): sync once, then poll the "st" thread. s1 = linkedapi.sync_conversation.execute(SyncConversationParams(person_url=person_url)) linkedapi.sync_conversation.result(s1.workflow_id) result = linkedapi.poll_conversations( [ConversationPollRequest(person_url=person_url, type="st")] ) messages = result.data[0].messages if result.data else [] their_replies = [m for m in messages if m.sender == "them"] if their_replies: print(their_replies[-1].text) # InMail reply (Sales Navigator route): use nv_sync_conversation and type "nv". s2 = linkedapi.nv_sync_conversation.execute(NvSyncConversationParams(person_url=person_url)) linkedapi.nv_sync_conversation.result(s2.workflow_id) inmail = linkedapi.poll_conversations( [ConversationPollRequest(person_url=person_url, type="nv")] ) ``` Prefer the shell? The [CLI](/cli/getting-started) runs the same flow, and the AI-agent surfaces – the [MCP server](/mcp/overview) and ready-made [skills](/skills) – expose it to Claude Code, Cursor, and Codex. If you are setting an agent up for the first time, [how to give an AI agent access to LinkedIn](/guides/linkedin-ai-agent) covers the connection choices and the first-run sequence. ```bash linkedin connection status https://www.linkedin.com/in/john-doe # connected | pending | notConnected linkedin connection send https://www.linkedin.com/in/john-doe --note "Hi! Would love to connect." linkedin message send https://www.linkedin.com/in/john-doe "Thanks for connecting - quick question..." linkedin navigator message send https://www.linkedin.com/in/john-doe "Hi..." --subject "Quick question" linkedin message get https://www.linkedin.com/in/john-doe --json # wraps sync + poll to read replies ``` Where this flow ends is deliberate: it gets a message sent and the reply back into your system. *What* the follow-up should say, and *when* to send it, is outreach strategy – see our guide to [LinkedIn cold outreach](/guides/linkedin-cold-outreach-strategies-2026) for the copy, timing, and sequence design. For method-by-method reference, see the docs for [sending messages](/docs/sending-message), [connection requests and invitations](/docs/working-with-invitations), [checking connection status](/docs/checking-connection-status), and [working with conversations](/docs/working-with-conversations), or the SDK pages for [`sendMessage`](/sdks/send-message), [`nvSendMessage`](/sdks/nv-send-message), [`sendConnectionRequest`](/sdks/send-connection-request), [`checkConnectionStatus`](/sdks/check-connection-status), [`syncConversation`](/sdks/sync-conversation), [`nvSyncConversation`](/sdks/nv-sync-conversation), and [`pollConversations`](/sdks/poll-conversations). ![Diagram: the branching LinkedIn message automation flow. Entry asks who you are messaging. If already connected, send a message with sendMessage. If not connected, either send a connection request with sendConnectionRequest, poll checkConnectionStatus until it returns connected, then sendMessage – or send a Sales Navigator InMail with nvSendMessage, which has no acceptance gate. Both paths converge on reading the reply: for a single thread, syncConversation (or nvSyncConversation for a Sales Navigator thread) then pollConversations; for the whole inbox, syncInbox then pollInbox. Follow-up copy and cadence are handed off to the cold-outreach guide. A footer notes it runs on your own account, at a human pace, returning profile data, not emails.](/images/guides/linkedin-message-automation-flow.webp) ## Automate the whole inbox (monitor every reply) The loop above reads a reply when you already know the person. To automate messaging at the **inbox** level – catch every incoming message across all your conversations, then route, tag, or reply to it – switch on whole-inbox monitoring instead of syncing people one by one. Enable it once per account with [`syncInbox`](/sdks/sync-inbox) (or [`nvSyncInbox`](/sdks/nv-sync-inbox) for the Sales Navigator inbox), then [`pollInbox`](/sdks/poll-inbox) returns every new message across every thread, each with the `personUrl` to reply to and a `sender` of `us` or `them`. ```typescript // Enable whole-inbox monitoring once per account. const sync = await linkedapi.syncInbox.execute(); await linkedapi.syncInbox.result(sync.workflowId); // Poll only what arrived since your last run, using a cursor you persist. // Without `since`, pollInbox returns everything captured - so you would // reprocess (and re-reply to) old messages on every run. const since = await loadCursor(); // your storage; undefined on first run const { data } = await linkedapi.pollInbox({ type: 'st', since }); for (const m of data?.messages ?? []) { if (m.sender !== 'them') continue; // inbound only const reply = await linkedapi.sendMessage.execute({ personUrl: m.personUrl, text: 'Thanks for the reply!' }); await linkedapi.sendMessage.result(reply.workflowId); } // Advance the cursor to the newest message (pollInbox returns newest-first). if (data?.messages.length) await saveCursor(data.messages[0].time); ``` ```python # Enable whole-inbox monitoring once per account. sync = linkedapi.sync_inbox.execute() linkedapi.sync_inbox.result(sync.workflow_id) # Poll only what arrived since your last run, using a cursor you persist. # Without `since`, poll_inbox returns everything captured - so you would # reprocess (and re-reply to) old messages on every run. since = load_cursor() # your storage; None on first run result = linkedapi.poll_inbox(InboxPollRequest(type="st", since=since)) messages = result.data.messages if result.data else [] for m in messages: if m.sender != "them": # inbound only continue reply = linkedapi.send_message.execute( SendMessageParams(person_url=m.person_url, text="Thanks for the reply!") ) linkedapi.send_message.result(reply.workflow_id) # Advance the cursor to the newest message (poll_inbox returns newest-first). if messages: save_cursor(messages[0].time) ``` On the shell it is `linkedin inbox sync` once (add `--nv` for the Sales Navigator inbox), then `linkedin inbox get --since `. Poll with an advancing `since`, or skip polling entirely and subscribe to [inbox webhook events](/sdks/webhooks) for push delivery. This stays the mechanics layer – detecting and retrieving the message; what an auto-reply should actually say still belongs to your [outreach strategy](/guides/linkedin-cold-outreach-strategies-2026). ## LinkedIn message and connection limits (staying safe) LinkedIn does not publish exact message or connection-request limits, so treat the numbers you see online as guardrails, not gospel, and expect them to move with your account's age and standing. Third-party trackers land in a similar ballpark. For **connection requests**, ZELIQ's 2026 automation guide suggests roughly 20-40 a day (100-200 a week), while PhantomBuster, as of its July 2026 update, puts free and Premium accounts nearer 100 a week. For **direct messages**, ZELIQ's 2026 guide suggests about 30-60 a day, and PhantomBuster, as of July 2026, cites roughly 100 a week on a free account and 150 on Premium – all unofficial observations, not published caps. Our own breakdowns of the [connection request limit](/guides/linkedin-connection-limit-2026) and [LinkedIn's limits](/guides/understanding-linkedin-limits) go deeper. The safer way to automate is to stay well inside those bounds on your own account. Linked API is built for exactly that: every action runs on **your own account through a human-paced cloud browser**, not a blast queue, and it surfaces LinkedIn's own limit signals instead of steamrolling them. Your code receives `messagingNotAllowed` when you are not connected or messaging is restricted, `noteLimitExceeded` when a free account runs out of personalized invitation notes, and `requestNotAllowed` when you approach LinkedIn's request limits – so you can back off automatically. It works within [our safety model](/safety), and it returns profile, company, and post data – **not** emails or phone numbers. ## When a no-code tool is the better choice Code is not always the answer. If you want a point-and-click campaign builder, a visual sequence editor, and a shared team inbox – and you never want to touch an SDK – a no-code tool (the Dripify / HeyReach / Waalaxy class) will get you live faster. Those products are a good fit when the standard "connect, then follow up on a schedule" campaign is all you need and a closed UI is acceptable. Linked API is the better choice when you want messaging as a primitive inside your own stack: triggered by events in your CRM, branched on your own logic, wrapped in your product, or driven by an AI agent. You trade a visual builder for full control and embeddability. Start from the [installation guide](/sdks/installation) and the [CLI quickstart](/cli/getting-started). And messaging is only one lane of automation – publishing content on a schedule is its own, covered in [How to Automate LinkedIn Posts](/guides/how-to-automate-linkedin-posts). ## Frequently Asked Questions (FAQ) #### Can you automate LinkedIn messages? Yes, for standard direct messages – but not with a native LinkedIn feature. LinkedIn only offers scheduled outbound in its paid Recruiter products; everyday Messaging has none. Automating standard outreach means using a tool or an API, like [Linked API](/), that performs the actions on your own account. #### Does LinkedIn have a messaging API? Yes, but not one most developers can reach. LinkedIn's official [Messages API](https://learn.microsoft.com/en-us/linkedin/shared/integrations/communications/messages) creates messages to first-degree connections or replies in an existing thread, and its documentation restricts usage "to approved partners, subject to limitations via API agreement". Approval alone would not buy automation either: LinkedIn requires each message to follow "a specific member action" and states that those "do not include an automated or scheduled event". Pages messaging is a separate integration again, covering a Company Page's inbox rather than your personal DMs. Sending and reading messages programmatically on your own account means an account-level API such as [Linked API](/). #### Is automating LinkedIn messages safe – will I get my account restricted? Any automation carries some risk, and how you use it decides how much. Aggressive volume and speed are what push accounts toward restriction, so the safer approach is to stay well within LinkedIn's limits and keep a human pace. Linked API runs each action on your own account through a human-paced cloud browser and surfaces LinkedIn's own limit signals – `messagingNotAllowed`, `noteLimitExceeded`, `requestNotAllowed` – so your code can back off before it hits a wall. #### Can you schedule LinkedIn messages? Not for standard direct messages – LinkedIn has no native scheduler for them. LinkedIn Recruiter and Recruiter Lite can schedule initial InMail, and Recruiter can schedule one automated follow-up, but that is a paid recruiting feature. For everything else you schedule the send in your own code or in a third-party tool. #### How do I send bulk or mass messages on LinkedIn? There is no safe "blast to thousands" button, and trying to fake one is how accounts get restricted. The scalable-but-safe approach is to loop over your list programmatically and send each message on your own account at a human pace, respecting LinkedIn's limits – which is exactly what the [flow above](/guides/how-to-automate-linkedin-messages) does. #### Can you message someone you're not connected to? Yes, two ways. Send a connection request and message them once they accept (a standard 1st-degree message), or send a **Sales Navigator InMail** with `nvSendMessage`, which reaches a non-connection directly but needs a Sales Navigator seat and a subject line. #### How many messages and connection requests can you send per day? LinkedIn publishes no exact cap. Third-party trackers put safe connection requests around 20-40 a day, and direct messages around 30-60 a day, per ZELIQ's 2026 guide; PhantomBuster, as of July 2026, cites nearer 100 messages a week on a free account. They are unofficial and move with your account standing, so treat them as guardrails and stay comfortably under them. See our [connection limit guide](/guides/linkedin-connection-limit-2026) for detail. #### Can you automate LinkedIn messages for free? You can send messages and connection requests on a free LinkedIn account, but free accounts hit tighter limits – for example, a capped number of personalized invitation notes (Linked API returns `noteLimitExceeded` when you reach it). Automation tools and APIs are paid products; Linked API starts at $49/mo (Core, billed annually), flat per seat. #### Can you automate your LinkedIn inbox and replies? Yes. Turn on whole-inbox monitoring once with `syncInbox` (or `nvSyncInbox` for the Sales Navigator inbox), then `pollInbox` returns every new message across all your conversations, each tagged with the `personUrl` and a `sender` of `us` or `them` – so you can route, tag, or reply to incoming messages in code. Prefer push over polling? Subscribe to inbox webhook events. On the CLI it is `linkedin inbox sync` once, then `linkedin inbox get`. #### Chrome extension vs cloud-based automation – which is safer? A cloud setup is generally the safer of the two: it runs independently of your own machine and browser session, at a controlled pace. A Chrome extension only runs while your computer and browser are on, and ties the automation directly to your everyday browsing session. An API on a cloud browser, like Linked API, gives you the cloud's consistency plus full control over pacing. --- Want to run the connect-to-reply flow on your own account, inside your own stack? [Start with Linked API](/pricing) – the same actions across the [API and SDKs](/sdks/installation), [CLI](/cli/getting-started), [MCP server](/mcp/overview), and ready-made [skills](/skills), on your own account and at a human pace. ## LinkedIn Boolean Search: Operators, Examples, and How to Run Searches at Scale (2026) LinkedIn boolean search combines keywords with a handful of operators – quotation marks for exact phrases, and **AND, OR, and NOT (typed in uppercase)** plus parentheses for grouping – to build precise searches for people and companies. It works in the standard search bar, in LinkedIn Recruiter, and in Sales Navigator, though each puts boolean in slightly different fields. This guide covers the operators with copy-paste examples, the differences between the three surfaces, the mistakes and limits that trip people up, and the part most boolean guides skip: how to run the same search programmatically at scale. > **The short version.** Five operators do the work: `"exact phrase"`, and **AND / OR / NOT in uppercase** plus **parentheses** to group. They go inside the field you're searching – the standard search box, or the Keywords/Title/Company fields in Recruiter and Sales Navigator – not as `title:`-style commands (those don't work). To run a search at scale, you build it in LinkedIn's interface, copy the results URL, and hand that URL to a search API. It runs the search you built – it is not a raw boolean engine, and it returns profile data, not emails. ## The LinkedIn boolean operators There are five, and they are the same across LinkedIn's search surfaces. (Reference: LinkedIn's own [Use Boolean search on LinkedIn](https://www.linkedin.com/help/linkedin/answer/a524335).) | Operator | What it does | Example | Rule | | --- | --- | --- | --- | | Quotation marks | Matches an exact phrase | `"product manager"` | LinkedIn ignores common words (and, or, the, by, in, with); quote a phrase to force them in | | AND | Requires all terms (narrows) | `finance AND CPA` | Type it in UPPERCASE | | OR | Matches any term (broadens) | `sales OR marketing` | Type it in UPPERCASE | | NOT | Excludes a term (narrows) | `developer NOT manager` | UPPERCASE, immediately before the term | | Parentheses | Groups logic, evaluated first | `VP NOT (assistant OR SVP)` | The only grouping symbol LinkedIn recognizes | Two rules save most of the headaches. First, **AND, OR, and NOT must be uppercase** – written in lowercase, LinkedIn reads them as ordinary keywords. Second, when you mix operators, LinkedIn evaluates them in a fixed order: **quotes, then parentheses, then NOT, then AND, then OR**. When in doubt, add parentheses to make your intent explicit. LinkedIn does **not** support wildcards (`*`), curly, square, or angle brackets, or the `+`/`-` operators – use AND and NOT instead. ## Where boolean search works: standard search, Recruiter, and Sales Navigator Boolean behaves the same, but *where* you type it differs by product. | Surface | Where boolean goes | Structured filters | | --- | --- | --- | | Standard search | The single search box at the top | Minimal; most advanced filters are gated behind Premium | | Recruiter / Recruiter Lite | The Keywords field, plus filter fields like Job titles, Location, Companies, Schools, Skills, Industries, and Spoken languages | 20+ filters | | Sales Navigator | The Keywords, Title, and Company fields | 30+ Lead and Account filters | One correction, because a lot of older guides get it wrong: **colon field-commands like `title:`, `company:`, or `school:` are not documented by LinkedIn and do not work** in standard search. Boolean today goes *inside* the named field you want to search – the Keywords box, or the dedicated Title, Company, and School fields in [Recruiter](https://www.linkedin.com/help/recruiter/answer/a415295) and Sales Navigator – not as a `field:value` prefix. LinkedIn's [Sales Navigator help](https://www.linkedin.com/help/linkedin/answer/a168061) confirms boolean in its Company, Title, and Keyword fields (that page is a few years old, so treat the finer field details as a guide rather than gospel). The practical takeaway: in the standard search bar you lean on boolean; in Recruiter and Sales Navigator you keep the boolean simple and let the structured filters do the heavy lifting. ## Boolean search examples you can copy Here are six strings that follow the rules above. Adapt the roles to your target; keep the syntax. - **Backend engineers, excluding recruiters** (tech recruiting): `"software engineer" AND (Java OR Kotlin OR Scala) AND (backend OR "back end") NOT recruiter` - **Sales VPs at software companies, minus junior roles** (SaaS prospecting): `("VP of Sales" OR "Vice President of Sales" OR "VP Sales") AND (SaaS OR software) NOT (assistant OR intern)` - **Individual-contributor developers only** (IC sourcing): `developer NOT (manager OR director OR contractor)` - **Advanced-degree data/ML people who use Python or R** (DS/ML recruiting): `("data scientist" OR "machine learning engineer" OR "ML engineer") AND (Python OR R) AND (PhD OR "Master's")` - **Marketing leaders in fintech, excluding freelancers** (exec prospecting): `(CMO OR "Chief Marketing Officer" OR "VP Marketing") AND (fintech OR "financial services") NOT freelance` - **ICU nurses, excluding students and retirees** (healthcare recruiting): `("registered nurse" OR RN) AND (ICU OR "intensive care") NOT (student OR retired)` Notice the pattern: quotes lock multi-word titles together, parentheses hold a group of synonyms joined by OR, and NOT trims out the roles you never want to see. ## Common boolean mistakes (and LinkedIn's limits) - **Lowercase operators.** `java and python` searches for the word "and." Capitalize AND, OR, NOT. - **Forgetting quotes on phrases.** `product manager` can match "product" and "manager" separately, and LinkedIn drops the stop words in between; `"product manager"` keeps it exact. - **Expecting field-commands to work.** `title:CMO` does nothing useful in standard search. Put the term in the Keywords box, or use the Title field in Recruiter or Sales Navigator. Colon commands like `intitle:` and `inurl:` belong to Google, not LinkedIn – searching LinkedIn profiles through Google is a separate technique, covered in the [X-ray search guide](/guides/linkedin-xray-search). - **Hitting the Commercial Use Limit.** On a free account, LinkedIn caps how much you can search each month once your activity looks like hiring or prospecting. Per LinkedIn's [help page](https://www.linkedin.com/help/linkedin/answer/a564226), it does not display the exact number of searches you have left, and the limit resets at the start of each calendar month. Recruiter and Sales Navigator raise or remove that ceiling. - **Expecting boolean in every filter.** Boolean only works in the fields that accept free text (Keywords, Title, Company). The dropdown and checkbox filters use their own logic. ## From boolean to scale: run your search via API A boolean search is a one-off in the browser. When you want to run it repeatedly, feed the results into a workflow, or search on behalf of a product, you move it to an API – and the clean way to do that is to **build the search in LinkedIn, then hand over its URL**. LinkedIn encodes your whole query (keywords, boolean, and filters) into the search results URL; [Linked API](/guides/linkedin-scraper-api) takes that URL as `customSearchUrl` and runs the same search on your own authenticated account. ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); // Build the boolean search in LinkedIn, copy the results URL, and run it at scale. // customSearchUrl runs exactly that search (it overrides term and filter). const search = await linkedapi.searchPeople.execute({ customSearchUrl: 'https://www.linkedin.com/search/results/people/?keywords=%22software%20engineer%22%20AND%20(Java%20OR%20Kotlin)', limit: 25, }); const { data: people } = await linkedapi.searchPeople.result(search.workflowId); for (const person of people ?? []) { console.log(person.name, person.headline, person.publicUrl); } ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, SearchPeopleParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) search = linkedapi.search_people.execute( SearchPeopleParams( custom_search_url="https://www.linkedin.com/search/results/people/?keywords=%22software%20engineer%22%20AND%20(Java%20OR%20Kotlin)", limit=25, ) ) people = linkedapi.search_people.result(search.workflow_id).data for person in people or []: print(person.name, person.headline, person.public_url) ``` If you would rather describe the search in code than build it in the UI, use structured filters instead. One rule to know: a request needs **either a `term` or a `customSearchUrl`** – a `filter`-only call is rejected – so pass a keyword `term` alongside your `filter`: ```typescript const search = await linkedapi.searchPeople.execute({ term: 'engineer', filter: { position: 'Software Engineer', locations: ['United States'], industries: ['Software Development'], }, limit: 25, }); ``` ```python search = linkedapi.search_people.execute( SearchPeopleParams( term="engineer", filter={ "position": "Software Engineer", "locations": ["United States"], "industries": ["Software Development"], }, limit=25, ) ) ``` The [people search](/docs/searching-for-people) and [company search](/docs/searching-for-companies) endpoints work the same way, and the [Sales Navigator variants](/guides/linkedin-sales-navigator-api) (`nvSearchPeople`, `nvSearchCompanies`) add filters like years of experience and company revenue. Two honest limits: `term` is plain keyword text, not a boolean parser – for real boolean, use `customSearchUrl`; and results come back at your account's safe pace with profile URL, name, headline, and location, **not emails**. From a returned profile you can go deeper with the [profile scraper](/guides/linkedin-profile-scraper) or turn matches into [outreach](/guides/linkedin-cold-outreach-strategies-2026), always [within your account's limits](/safety). ![Diagram: build a boolean or filtered search in LinkedIn's standard search, Recruiter, or Sales Navigator, then copy the search results URL, pass it to the API as customSearchUrl, and receive structured JSON results (name, profile URL, headline, location) at your account's pace. A note reads: the API runs the search you built, not a raw boolean-string engine, and returns profiles, not emails.](/images/guides/linkedin-boolean-search-to-api.webp) ## Frequently Asked Questions (FAQ) #### What is boolean search on LinkedIn? It is a way to combine keywords with operators – quotation marks for exact phrases, and AND, OR, NOT, and parentheses – to build a precise search for people or companies instead of a single loose keyword. #### What boolean operators does LinkedIn support? Quotation marks for exact phrases, and AND, OR, and NOT (typed in uppercase) plus parentheses for grouping. LinkedIn does not support wildcards (`*`), brackets of any kind, or the `+`/`-` operators. #### Do field commands like `title:` still work on LinkedIn? No. LinkedIn does not document colon field-commands, and they do not work in standard search. Type your boolean inside the named field instead – the Keywords box in standard search, or the dedicated Title, Company, and School fields in Recruiter and Sales Navigator. Commands like `intitle:` work in Google, where they are used for [X-ray search](/guides/linkedin-xray-search) against public profiles. #### Does boolean search work in Sales Navigator and Recruiter? Yes. Recruiter accepts boolean in its Keywords field and several filter fields; Sales Navigator accepts boolean in its Keywords, Title, and Company fields, layered on top of its structured filters. In both, keep the boolean simple and let the filters narrow the rest. #### Why did my LinkedIn search stop showing results? Free accounts hit a monthly Commercial Use Limit once your searching looks like hiring or prospecting. LinkedIn does not state the exact number and it resets at the start of each calendar month. Recruiter and Sales Navigator raise or remove that limit. #### Can I run a LinkedIn boolean search programmatically? Yes. Build the search in LinkedIn's interface and pass its results URL to a search API – Linked API's `searchPeople` and `searchCompanies` accept it as `customSearchUrl` – or describe the search with structured filters plus a keyword term. It runs the search you built on your own account and returns profile data, not emails, and it is not a raw boolean-string engine. #### Is boolean search free on LinkedIn? In the standard search bar, yes, subject to the monthly commercial use limit. Recruiter and Sales Navigator add more boolean-capable fields and structured filters on their paid plans. --- Built the perfect boolean search and want to run it every day, at scale, from your own account? [Start with Linked API](/pricing) – paste your LinkedIn search URL, get clean JSON for every match, and wire it into your stack through the [API and SDKs](/sdks/installation), [CLI](/cli/getting-started), [MCP](/mcp/overview), and [skills](/skills). ## LinkedIn Post Scraper: How to Extract Posts, Reactions, and Comments via API (2026) A LinkedIn post scraper pulls the content and engagement of a post – its text, media, and reaction, comment, and repost counts – and, most usefully, the list of people who reacted and commented, each with their name, headline, and profile URL. There is no official LinkedIn API to read arbitrary post data, so every option is a scraper, and they differ a lot in reliability and account risk. This guide shows exactly what fields you can get, a real sample response, how to scrape one post and a person's or company's recent posts in code (Node and Python), and the part most guides skip: turning post engagers into a warm-lead list. > **The short version.** A post's content and its engager list – who reacted and who commented, with name, headline, and profile URL – are both extractable. Personal emails are not. Linked API reads known post URLs and a person's or company's recent posts (bounded by a limit and an optional `since` date), not an arbitrary keyword search of LinkedIn, and on big posts it samples engagement rather than dumping it whole. Chrome extensions and scripts run on *your own* account at machine speed and get it restricted; an API paces like a human and returns clean JSON. ## What data is in a LinkedIn post A post is really three records: the post itself, the reactions on it, and the comments on it. Here is what you get from each. | Group | Fields | | --- | --- | | Post | `url`, `time`, `type` (`original` / `repost`), `repostText`, `text` | | Media | `images`, `hasVideo`, `hasPoll` | | Counts | `reactionsCount`, `commentsCount`, `repostsCount` | | Reaction (per engager) | `engagerUrl`, `engagerName`, `engagerHeadline`, `engagerType` (`person` / `company`), `type` (`like` / `celebrate` / `support` / `love` / `insightful` / `funny`) | | Comment (per commenter) | `commenterUrl`, `commenterName`, `commenterHeadline`, `text`, `time`, `isReply`, `reactionsCount`, `repliesCount` | The reaction and comment records are the valuable part: each one carries the engager's **profile URL** – a durable key you can enrich or act on later. Note what is *not* here: no email, no phone. Post data is public-facing profile data, not contact data. ## A real scraped post The reactions and comments sections are opt-in – you ask for them, and you get typed JSON back instead of HTML to parse. ```json { "url": "https://www.linkedin.com/posts/jane-doe_growth-activity-7440011668937568257", "time": "2026-07-08T14:20:00Z", "type": "original", "repostText": null, "text": "We cut our onboarding drop-off by 40% this quarter. Three things that worked...", "images": ["https://media.linkedin.com/…/chart.png"], "hasVideo": false, "hasPoll": false, "reactionsCount": 214, "commentsCount": 37, "repostsCount": 9, "reactions": [ { "engagerUrl": "https://www.linkedin.com/in/mark-lee", "engagerName": "Mark Lee", "engagerHeadline": "VP Sales at Northwind", "engagerType": "person", "type": "insightful" }, { "engagerUrl": "https://www.linkedin.com/in/sara-kim", "engagerName": "Sara Kim", "engagerHeadline": "Head of RevOps at Acme", "engagerType": "person", "type": "celebrate" } ], "comments": [ { "commenterUrl": "https://www.linkedin.com/in/david-ross", "commenterName": "David Ross", "commenterHeadline": "Growth Lead at Beacon", "text": "Which onboarding step moved the needle most?", "time": "2026-07-08T15:02:00Z", "isReply": false, "reactionsCount": 4, "repliesCount": 1 } ] } ``` ## Four ways to scrape LinkedIn posts, compared There is no official post API, so the real choice is between four methods that trade off scale, output, and the risk to your account. | Method | Scale | Output | Maintenance | Account-ban risk | | --- | --- | --- | --- | --- | | Manual copy-paste | A few a day | You retype it | None | None | | Chrome extension | Tens/day | A spreadsheet | Breaks on layout changes | High – acts on your account at machine speed | | DIY Python scraper | High, until blocked | Raw HTML you parse | You own selectors and proxies | High – headless on your account | | API on your own account | Steady, paced | Structured JSON (post + engagers) | Handled for you | Lower – paced like a human | One scope note before the code: this account-based approach reads **known posts** – a specific post URL, or a person's or company's own timeline. It does not do an arbitrary **post keyword search** ("every post mentioning X"). For that job, a dedicated post-search scraper (Apify, Bright Data, as of 2026) is the better fit, with the usual scraping-risk and stale-versus-live tradeoffs. For the account-based model across all entities, see the [LinkedIn scraper API guide](/guides/linkedin-scraper-api); for vacancies rather than content posts, the [LinkedIn jobs scraper](/guides/linkedin-jobs-scraper). What follows is the live, account-based path for posts you can point to. ## Scrape a single post and its engagers Install the SDK: ```bash # Node.js npm install -S @linkedapi/node # Python pip install linkedapi ``` Then [fetch a post](/docs/retrieving-post-data) by URL, asking for its reactions and comments: ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const workflow = await linkedapi.fetchPost.execute({ postUrl: 'https://www.linkedin.com/posts/jane-doe_growth-activity-7440011668937568257', retrieveReactions: true, retrieveComments: true, reactionsRetrievalConfig: { limit: 50 }, commentsRetrievalConfig: { limit: 50, sort: 'mostRecent' }, }); const { data } = await linkedapi.fetchPost.result(workflow.workflowId); console.log(data?.text, data?.reactionsCount, data?.reactions?.length); // each reaction: { engagerUrl, engagerName, engagerHeadline, engagerType, type } ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, FetchPostParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) workflow = linkedapi.fetch_post.execute( FetchPostParams( post_url="https://www.linkedin.com/posts/jane-doe_growth-activity-7440011668937568257", retrieve_reactions=True, retrieve_comments=True, reactions_retrieval_config={"limit": 50}, comments_retrieval_config={"limit": 50, "sort": "mostRecent"}, ) ) data = linkedapi.fetch_post.result(workflow.workflow_id).data print(data.text, data.reactions_count, len(data.reactions or [])) ``` The `limit` in each config is what keeps this honest on a viral post: you pull a bounded, recent slice of the engagement, not a guaranteed copy of every one of thousands of reactions. ## Scrape a person's or company's recent posts To watch what someone publishes, [fetch the person](/docs/retrieving-person-data) (or [company](/docs/retrieving-company-data)) with `retrievePosts` and a `postsRetrievalConfig`. The `since` date is the key to monitoring – pass your last run's timestamp and you get only what is new: ```typescript const workflow = await linkedapi.fetchPerson.execute({ personUrl: 'https://www.linkedin.com/in/jane-doe', retrievePosts: true, postsRetrievalConfig: { limit: 20, since: '2026-06-01T00:00:00Z' }, }); const { data } = await linkedapi.fetchPerson.result(workflow.workflowId); for (const post of data?.posts ?? []) { console.log(post.time, post.reactionsCount, post.url); } // swap fetchPerson for fetchCompany with a companyUrl to track a company's posts ``` ```python from linkedapi import FetchPersonParams workflow = linkedapi.fetch_person.execute( FetchPersonParams( person_url="https://www.linkedin.com/in/jane-doe", retrieve_posts=True, posts_retrieval_config={"limit": 20, "since": "2026-06-01T00:00:00Z"}, ) ) data = linkedapi.fetch_person.result(workflow.workflow_id).data for post in data.posts or []: print(post.time, post.reactions_count, post.url) ``` This returns a **recent, bounded set** of posts, not a full archive. Run it on a schedule (a nightly cron with `since` set to yesterday) and you have a monitor for a person's or company's new activity – the foundation for the use case below. ## Turn post engagers into warm leads Here is the part worth building for. A person who reacted to or commented on a relevant post has raised their hand. You already have their profile URL from the post – so the pipeline is short: read the post's engagers, optionally enrich each live, and act. ```typescript // You already fetched the post with retrieveReactions + retrieveComments above. const leads = [ ...(data?.reactions ?? []).map((r) => ({ url: r.engagerUrl, name: r.engagerName, headline: r.engagerHeadline, signal: r.type, })), ...(data?.comments ?? []).map((c) => ({ url: c.commenterUrl, name: c.commenterName, headline: c.commenterHeadline, signal: 'commented', })), ]; // From here: enrich one live with fetchPerson(url), or act with // sendConnectionRequest / sendMessage - all on your own account. ``` ![Pipeline diagram: a monitored LinkedIn post feeds into pulling its reactions and comments, which give each engager's profile URL, name, and headline; those flow into optional live enrichment via fetchPerson and then into outreach with a connection request or message. A note marks that you get profile URLs, not emails.](/images/guides/linkedin-post-engagers-pipeline.webp) Two honest limits keep this grounded. You get profile URLs and public headlines, **not emails** – so this builds a targeted list and a warm-intent signal, not a contact database. And it runs on your own account at a human pace, so it is built for steady, relevant lists, not for scraping every reactor on a 10,000-like post. For what to do with the list next, see our [cold outreach guide](/guides/linkedin-cold-outreach-strategies-2026); if your source list lives in Sales Navigator, see the [Sales Navigator guide](/guides/linkedin-sales-navigator-scraper). ## Can you get an engager's email? No, and it is the same story as any [profile scraper](/guides/linkedin-profile-scraper): emails and phone numbers are not shown on posts or profiles, so a scraper reading LinkedIn cannot return them. What you get is the engager's profile URL and headline. To attach a verified email, match that person against a separate, consented B2B enrichment provider – a different product from post scraping, and the data does not come off LinkedIn. ## Is scraping LinkedIn posts legal, and will my account get banned? Post content and public engagement are largely visible data, and courts have treated scraping genuinely public data more leniently than accessing private data – but public is not a free pass. Personal data still falls under GDPR and CCPA, and LinkedIn enforces against extraction operations directly. We break down the full legal picture in our [guide to scraping LinkedIn](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers). The ban risk is highest exactly where most people start: a Chrome extension or headless script driving your own account too fast. Linked API takes the account-based path – it runs on your own authenticated account through a human-paced cloud browser, within built-in workflow limits – and returns maintained, structured JSON, so you are not rewriting selectors every time the feed changes. Pace like a human, stop on any warning, and keep contact data to consented sources. Read our [safety model](/safety) before you scale. ## Frequently Asked Questions (FAQ) #### What data can you scrape from a LinkedIn post? The post's content (text, media flags), its reaction, comment, and repost counts, and the list of people who reacted and commented – each with name, headline, and profile URL, plus the reaction type or comment text. Personal emails and phone numbers are not available. #### Can I see who liked or commented on a LinkedIn post? Yes. Requesting reactions returns each reactor's profile URL, name, headline, and reaction type (`like`, `celebrate`, `support`, `love`, `insightful`, `funny`); requesting comments returns each commenter's URL, name, headline, and comment text. This is what makes a post a lead source. #### Can I get the email of someone who engaged with a post? No. LinkedIn does not expose personal emails on posts or profiles, so you get profile URLs, not contacts. Match those profiles against a consented enrichment provider if you need verified emails. #### Can I scrape a person's recent LinkedIn posts? Yes – fetch the person with `retrievePosts` and a `postsRetrievalConfig` (`limit`, optional `since`). It returns a bounded recent set of their posts, not a guaranteed full archive; the same works for a company via `fetchCompany`. #### Can I monitor a person's or company's posts for new activity? Yes. Pass a `since` date in `postsRetrievalConfig` to pull only posts published after your last run, and schedule the call (for example, a nightly cron job). That turns the fetch into a monitor for new posts. The publishing direction – scheduling and automating your own posts – is covered in [How to Automate LinkedIn Posts](/guides/how-to-automate-linkedin-posts). #### Can Linked API search LinkedIn posts by keyword? No. It reads known post URLs and a person's or company's recent timeline; there is no arbitrary post keyword search. For "find every post mentioning X," a dedicated post-search scraper is the better fit. #### Is there an official LinkedIn API for reading post data? No. LinkedIn's official APIs cover your own content and approved partner use cases; there is no public API to read arbitrary posts and their engagement. Every "post scraper" you see is a scraping or account-based product. #### Is scraping LinkedIn posts legal? Reading public post data is treated more leniently than accessing private data, but personal data still falls under GDPR and CCPA. See our [legal breakdown](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers). #### How many posts can I scrape per day? There is no universal number – it depends on your account's age and history and on staying within a safe daily pace. An account-based API paces the work like a human rather than bursting; treat volume as bounded, not a firehose. --- Need LinkedIn post data and the people behind the engagement, without running an extension or a fragile script? [Start with Linked API](/pricing) – read posts, reactions, and comments from your own account and get structured JSON back, through the [API and SDKs](/sdks/installation), [CLI](/cli/getting-started), [MCP](/mcp/overview), and [skills](/skills). ## How to Export Your LinkedIn Connections in 2026 (Manual and via API) Exporting your LinkedIn connections gets your network out of LinkedIn and into a spreadsheet, a CRM, or your own product. There are two honest ways to do it, and they answer different needs. The manual way – LinkedIn's own data export – hands you a one-time CSV in a few clicks, and it is the right answer if you just want the file. If you need your connections kept up to date in a CRM or product, there is no one-click tool for that; you pull them as JSON through an API on your own schedule. This guide covers both: the exact current steps to download the file, what fields you actually get (email is included only if the connection shared it), and how to build a continuous sync when a one-time export is not enough. > **The short version.** The manual way is LinkedIn's **Data Export**: Me → Settings & Privacy → Data privacy → Get a copy of your data → "Download larger data archive, including connections…" → Request archive, then download the `Connections.csv` from the email. You get a one-time snapshot of your 1st-degree connections, with an email address only for those who chose to share it. That is the right answer for a one-time download. If you need your connections **kept in sync** – upserted into a CRM or product on a schedule – there is no built-in export for that; you pull them as JSON through an account-based API like [Linked API](/pricing) and update your system yourself. ## How to export your LinkedIn connections (step by step) Here is the route [LinkedIn currently documents](https://www.linkedin.com/help/linkedin/answer/a566336/export-connections-from-linkedin): ![The manual LinkedIn connections export flow: Settings and Privacy, then Data privacy, then Get a copy of your data, then the larger data archive option, then Request archive, then download the CSV from the email](/images/guides/export-linkedin-connections-steps.webp) 1. Click **Me** (your photo, top right) → **Settings & Privacy**. 2. Open **Data privacy**. 3. Click **Get a copy of your data**. 4. Choose **"Download larger data archive, including connections, verifications, contacts, account history, and information we infer about you based on your profile and activity."** 5. Click **Request archive** and confirm your password. 6. LinkedIn emails you when the archive is ready. Download it and open **`Connections.csv`**. Two things to know before you start. You can only export your **1st-degree connections** – [LinkedIn notes](https://www.linkedin.com/help/linkedin/answer/a566336/export-connections-from-linkedin) you "currently can't export a list of your contacts that aren't 1st-degree connections." And if your account still shows a granular "Connections" option, that works too; the larger-archive option above is the route LinkedIn currently documents. ## What you get in the export (and what you don't) The `Connections.csv` gives you one row per 1st-degree connection, typically with these columns: - **First name** and **last name** - **LinkedIn profile URL** - **Company** – as of the time you export - **Position** – job title - **Connected on** – the date you connected, in MM/DD/YYYY format - **Email address** – but [only for connections who allowed it](https://www.linkedin.com/help/linkedin/answer/a566336/export-connections-from-linkedin), so many rows have none What you will not get: anyone beyond your 1st degree, a personal email for every connection, or any ongoing updates. The file is a snapshot as of the moment you export it, and the only way to refresh it manually is to export again. If you need it to stay current, use the API below. ## How to export your connections programmatically (build a continuous sync) The manual export is a snapshot. If you want your connections to stay current in a CRM or product, run the export in code on a schedule instead. Linked API's `retrieveConnections` returns your connections as structured JSON through your own account, and its `since` parameter returns only connections you made on or after a timestamp – so each scheduled run picks up the **new connections** since the last one. Reach it through the REST API, the [Node and Python SDKs](/sdks/retrieve-connections), or the shell [CLI](/cli); the same engine is available to an AI agent through the [MCP server](/mcp), the AI-agent-friendly CLI, or [ready-made skills](/skills). ![A continuous connection-sync pipeline: a cron scheduler triggers retrieveConnections with a since timestamp through the API, SDK, or CLI; Linked API runs it on your own account and returns new connections as JSON, which you upsert into your CRM or product](/images/guides/export-linkedin-connections-pipeline.webp) ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); // Run this on a cron; persist lastRun (ISO date) between runs const lastRun = process.env.LAST_RUN_ISO; const workflow = await linkedapi.retrieveConnections.execute({ since: lastRun, limit: 500 }); const { data: connections } = await linkedapi.retrieveConnections.result(workflow.workflowId); for (const c of connections ?? []) { // Upsert each new connection into your CRM console.log(c.name, '-', c.publicUrl, '-', c.connectedAt); } ``` ```python from linkedapi import LinkedApi, LinkedApiConfig from linkedapi.types import RetrieveConnectionsParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) # Run this on a cron; persist last_run (ISO date) between runs last_run = "2026-06-01T00:00:00Z" workflow = linkedapi.retrieve_connections.execute( RetrieveConnectionsParams(since=last_run, limit=500) ) connections = linkedapi.retrieve_connections.result(workflow.workflow_id).data or [] for c in connections: # Upsert each new connection into your CRM print(c.name, "-", c.public_url, "-", c.connected_at) ``` Run that on a schedule, persist the last run time, and upsert each new connection into your CRM. Two honest limits: `since` returns newly made connections, not a full change-feed – it will not report profile edits or connections that were removed, so treat it as an append-and-update sync – and, like the manual export, the API returns no personal email addresses. It runs on your own account, bounded by your account's [daily limits](/docs/core-concepts). Grab your tokens from the [installation guide](/sdks/installation), and see [retrieving connections](/docs/action-st-retrieve-connections) and [managing existing connections](/docs/managing-existing-connections) for the full contract. Today you poll for the workflow result, as shown above. **Webhook delivery is rolling out shortly** – a `workflow.completed` event sent to your own endpoint, so you can receive the result without polling. We will update this guide when it lands; until then, see [executing workflows](/docs/executing-workflows) for the result model. ## Manual export vs API sync: which do you need? | | Manual Data Export | Account-based API (`retrieveConnections`) | | --- | --- | --- | | Output | One-time CSV | Structured JSON on demand | | Freshness | Snapshot when you export | On demand / scheduled polling; `since` returns connections made on or after a timestamp | | Keep a CRM updated | Re-download by hand | Scheduled sync you build | | Emails | Only if the connection shared it | No email field | | Setup | A few clicks | API tokens + code | | Best for | A one-time download | Continuous sync into a product or CRM | If you need the file once, export it. If you need your network to stay current inside a system, sync it with the API. ## Frequently Asked Questions (FAQ) #### How do I export my LinkedIn connections? Use LinkedIn's Data Export: Me → Settings & Privacy → Data privacy → Get a copy of your data → "Download larger data archive, including connections…" → Request archive, then download `Connections.csv` from the email LinkedIn sends. It is a one-time snapshot of your 1st-degree connections. #### Can I export my LinkedIn connections with email addresses? Only partly. LinkedIn includes an email address only for connections who allowed their connections to see or download it, so many rows have none. The API route returns no personal emails at all. If email coverage matters, do not count on having one for your whole list. #### Where is the option to download my connections? It lives under Settings & Privacy → Data privacy → Get a copy of your data, via the "Download larger data archive, including connections…" option. That is LinkedIn's currently documented route; if a granular "Connections" checkbox appears in your account, that works too. #### Can I export contacts that aren't 1st-degree connections? No. LinkedIn states you currently can't export a list of contacts that aren't 1st-degree connections. The export covers your direct connections only. #### How do I export LinkedIn connections to a CSV or Excel? The Data Export archive already contains a `Connections.csv` – open it directly in Excel, Google Sheets, or any tool that reads CSV. There is no separate "export to Excel" step. #### Can I export LinkedIn connections to my CRM automatically? Yes, with an API. Schedule `retrieveConnections({ since })` on a cron via the API, SDKs, or CLI, and upsert new connections into your CRM. You set the cadence, because there is no "new connection" push event – LinkedIn does not notify third parties when your network changes. Webhook delivery of the result is rolling out shortly, and we will update this guide when it is available. #### Is there a LinkedIn connections API? LinkedIn does not offer a self-serve, general-access API for exporting your connections. A [restricted Connections API](https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/connections-api) exists for LinkedIn-approved developers (with permissions like `r_1st_connections`), but most teams cannot use it. For product or CRM sync, an account-based API like Linked API retrieves your connections as JSON through your own account – see the [retrieve-connections docs](/docs/action-st-retrieve-connections). --- Need your connections to stay current in your product, not just a CSV you re-download? [Start with Linked API](/pricing) to pull them as JSON on your own schedule, or see how the account-based model works across LinkedIn data in our [LinkedIn scraper API guide](/guides/linkedin-scraper-api). ## LinkedIn Sales Navigator Scraper: How to Export Leads to CSV (2026) You have built the search, saved the list, and now you want the rows in a spreadsheet. That is the job a LinkedIn Sales Navigator scraper exists to do. Which one fits that job, what does it actually export, and where does each route stop? This guide answers all three, starting with what Sales Navigator itself will and will not give you. > **The short version.** Sales Navigator does not export leads or accounts to CSV or XLS natively – LinkedIn says so in its own help centre – which is why an entire tool category exists. Your realistic options are a browser extension for a quick list from the search you are looking at, a dedicated export tool like Evaboot or Scrupp that runs your saved search through your own session and returns a cleaned CSV, a cloud actor or your own script for custom bulk work you will maintain, or a no-account dataset like Bright Data when you need public data at a scale no single seat should touch. None of them acts on the data, and they refresh differently: an extension or export tool gives you a file you rerun by hand, an actor runs on a schedule you maintain, and a dataset refreshes on its vendor's cycle. If the export is not the end of the job, you want an API instead. ## Can Sales Navigator export leads natively? No. LinkedIn states it plainly in its [Sales Navigator help centre](https://www.linkedin.com/help/sales-navigator/answer/a102031): > "LinkedIn currently doesn't offer the option to export account and lead information from Sales Navigator into a CSV or XLS file." Two things get mistaken for a native export, and neither one is: - **CRM Sync on Advanced Plus.** The same help page notes that Advanced Plus users can sync lead and account information between Sales Navigator and Salesforce. That is a CRM synchronisation, not a file you download, and it requires the top tier plus a Salesforce instance. - **The usage report export.** Sales Navigator admins can export a usage report covering seat activity. That is licence administration data, not your leads. So when a tool advertises "Sales Navigator export", it is not unlocking a hidden feature. It is reading the search on your behalf and building the file itself. ## What a Sales Navigator export actually produces Whatever route you take, the ceiling is what Sales Navigator itself renders. A row typically carries the lead's name, headline or position, company, and location, plus the profile URL that identifies them. Two things a Sales Navigator export does **not** carry, regardless of tool: - **Verified email addresses and phone numbers are not Sales Navigator fields.** Export tools that advertise emails run a separate enrichment step against other data sources after the export, and coverage and accuracy vary by provider. - **Anything behind a filter you did not apply.** The export inherits your search, so the quality of the file is decided before you export anything. Search scope also caps the job: a single Sales Navigator search surfaces a bounded result set, so very large pulls mean splitting the search rather than exporting harder. Our [Boolean search guide](/guides/linkedin-boolean-search) covers building searches that segment cleanly. ## The four extraction routes, compared Capabilities below are as described on each named vendor's live product and help pages. | Route | How it works | Owned seat required | Refresh model | Whose session runs it | Best for | | --- | --- | --- | --- | --- | --- | | **Browser extension** | Reads the Sales Navigator page open in your logged-in browser | Yes | Manual, you rerun it | Yours | A quick list from a search you are already looking at | | **Export tool** (Evaboot, Scrupp) | Runs your saved search through your own account, cleans the rows, returns a file, and offers email enrichment as a separate step | Yes | Manual, one export per run | Yours | A tidy bulk CSV of a saved search | | **Cloud actor or DIY script** (Apify, GitHub projects) | Runs an actor or your own code; cookie-based actors use your session, some run cookie-free against public pages | Only if cookie-based | Scheduled or on demand, maintained by you | Yours if cookie-based, the provider's if not | Custom bulk work you are willing to maintain | | **No-account dataset** (Bright Data) | Serves pre-collected public data at scale | No | Vendor-refreshed, so records can lag | Neither, it never touches your seat | Volume with no owned Sales Navigator seat | The distinction the tool pages blur is **whose session does the work**. A browser extension and an export tool both run on your own logged-in Sales Navigator session, so the activity sits on your account. A cloud actor may use your session cookies, in which case the same applies, or run cookie-free against public pages, in which case it never sees your Sales Navigator searches at all. A dataset never touches your account, and equally cannot see anything your seat can see. Pace matters on every route that uses your own session: a bulk extension pulling a large search as fast as it can render is a very different activity profile from a paced export. We compare the export-tool options in detail in our [Evaboot alternatives guide](/blog/evaboot-alternatives). ## When you need an API instead An export tool is the right answer when the file is the deliverable. It stops being the right answer when the export is step one. If you need to search, pull a field, branch on it, message the lead, and pick the reply back up as a repeatable workflow inside your own product, you want programmatic access to your seat rather than a CSV. That is a different question with a different answer, including what LinkedIn's own partner-gated API does and does not cover. See our [Sales Navigator API guide](/guides/linkedin-sales-navigator-api). ## Frequently Asked Questions (FAQ) #### Can you export leads from Sales Navigator to CSV? Not natively. LinkedIn's help centre states it does not offer export of account and lead information to CSV or XLS. Every CSV you have seen from Sales Navigator was produced by a browser extension, an export tool, or a script reading the results on your behalf. #### What is the best Sales Navigator export tool? It depends on what ends the job. For a clean CSV of a saved search with optional email enrichment, dedicated export tools like Evaboot and Scrupp are built for exactly that. For a quick grab from a search you are viewing, an extension is faster. For volume with no owned seat, a dataset provider fits better. For a repeatable workflow rather than a file, none of them is the answer. #### Do I need a Sales Navigator subscription to export leads? For any route that runs through your own session, yes. Extensions, export tools, and cookie-based actors all read what your seat can see, so the seat is the prerequisite. No-account datasets are the exception, because they never use your account and correspondingly cannot see your searches. What that seat costs, and which tier it needs to be, is covered in our [Sales Navigator cost guide](/guides/linkedin-sales-navigator-cost). #### Does a Sales Navigator export include email addresses? Not from Sales Navigator itself. Email is not a field the product exposes in a lead export. Tools advertising emails run a separate enrichment step against other sources afterwards, and coverage varies by provider. #### How many leads can I export at once? A single Sales Navigator search returns a bounded result set, so the practical limit is the search, not the tool. Large pulls mean splitting one broad search into several narrower ones, which usually improves the list anyway. #### What is the difference between an export tool and an account-based API? An export tool produces a snapshot and the job ends when the file downloads. An account-based API exposes Sales Navigator actions as composable primitives on your own seat, so you can build a workflow that reads and acts repeatedly from inside your own code. ## Get the file, or build the workflow If a CSV is what you need, pick the route above that matches how the job ends. If the export is only the first step and you want search, fetch, and outreach running as one repeatable workflow on your own Sales Navigator seat, that is what [Linked API](/pricing) does – see the [Sales Navigator API guide](/guides/linkedin-sales-navigator-api) for how programmatic access actually works. *Facts verified 29 July 2026 – native export availability checked against LinkedIn's Sales Navigator help centre, and tool capabilities checked against their live pages, on that date.* ## LinkedIn Jobs Scraper: How to Collect LinkedIn Job Data via API (2026) There is no official LinkedIn API for searching or reading job postings. LinkedIn's only jobs API is a partner-gated tool for *publishing* jobs, so every "LinkedIn jobs API" you find is really a scraper or a dataset. The upside: job postings are largely public, which makes them one of the more accessible things to pull from LinkedIn. This guide covers what job data you can actually get, a real Python example against LinkedIn's public endpoints (and where it breaks), the search-then-enrich pattern that returns full detail as clean JSON, and how to monitor new postings for hiring signals. > **The short version.** LinkedIn job postings are public, so jobs are easier to collect than profiles – but there is no official jobs API, hand-rolled scrapers break at scale, and it is still automated access under LinkedIn's terms. The reliable pattern: search jobs, open each one for the full record, get structured JSON. ## Does LinkedIn have a jobs API? Not the kind you want. LinkedIn's official [Job Posting API](https://learn.microsoft.com/en-us/linkedin/talent/job-postings/api/overview) exists only for approved partners to *post* jobs through applicant-tracking and job-distribution integrations, and LinkedIn is not accepting new partners for it. There is no public, self-serve API to search or read job listings. So when a tool advertises a "LinkedIn Jobs API," it is a scraping or dataset product wrapping LinkedIn's public pages, not an official endpoint. That is fine – it is how the entire category works – but it changes how you should think about reliability and compliance: you are reading public pages, not calling a sanctioned API. ## What data you can get from a LinkedIn job A single posting carries a fairly complete record. Grouped by what it tells you: ![Anatomy of a scraped LinkedIn job: identity, company, role, compensation, and demand-signal fields](/images/guides/linkedin-job-data-fields.webp) The full per-job object looks like this: ```json { "jobId": "4416248954", "jobUrl": "https://www.linkedin.com/jobs/view/4416248954/", "title": "Senior Product Manager", "companyName": "Example Company", "companyUrl": "https://www.linkedin.com/company/example-company", "location": "San Francisco, CA", "workplaceType": "remote", "employmentType": "Full-time", "postedDate": "1w", "applicantsCount": 84, "salary": { "currency": "usd", "minAmount": 140000, "maxAmount": 180000, "period": "yearly" }, "description": "Example job description text.", "applyUrl": "https://www.linkedin.com/jobs/view/4416248954/apply/", "easyApply": true } ``` Two honest caveats: `salary` only appears when the posting shows pay (driven by pay-transparency laws), and `applicantsCount` is approximate and varies by posting and region. Everything else is reliably present. ## Method 1: DIY with Python LinkedIn serves logged-out job data through public "guest" endpoints, which is why a basic Python scraper works with no account and no cookies: - **Search/list:** `https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=...&start=0` (paginate in steps of 25) - **Per-job detail:** `https://www.linkedin.com/jobs-guest/jobs/api/jobPosting/{jobId}` A minimal scraper over the search endpoint: ```python import requests from bs4 import BeautifulSoup # LinkedIn's public, logged-out jobs endpoint - there is no official jobs API url = "https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search" params = {"keywords": "product manager", "location": "San Francisco", "start": 0} res = requests.get(url, params=params, headers={"User-Agent": "Mozilla/5.0"}) soup = BeautifulSoup(res.text, "html.parser") for card in soup.select("li"): title = card.select_one("h3.base-search-card__title") company = card.select_one("h4.base-search-card__subtitle") if title and company: print(title.get_text(strip=True), "|", company.get_text(strip=True)) # Paginate by increasing "start" in steps of 25. ``` **Where this breaks.** The guest endpoint is generous until it isn't. You will hit `429 Too Many Requests` after roughly ten pages from one IP, so any real volume needs rotating proxies. Fields silently drop out (salary and applicant counts are inconsistent), the HTML changes and your selectors rot, and the most popular open-source LinkedIn job libraries now note that their anonymous mode is no longer maintained. It is great for a one-off pull, painful as a pipeline. We cover why hand-rolled scrapers decay in the [scraping guide](/guides/how-to-scrape-linkedin#why-diy-headless-scraping-breaks). ## Method 2: Scraping APIs and datasets If you would rather not maintain a scraper, two options handle the proxies for you: **real-time scraping APIs** (Apify job actors, Bright Data) that take a search or a job URL and return JSON, and **job datasets** (Coresignal, TheirStack) for bulk historical postings. The trade-off is the usual one – datasets are cheap at scale but stale, real-time is fresh but priced per request. We compare these models in the [LinkedIn scraper API guide](/guides/linkedin-scraper-api); to read a person's or company's posts and who engaged with them, see the [LinkedIn post scraper guide](/guides/linkedin-post-scraper). ## Method 3: Account-based API (search, then enrich) The pattern most teams actually want is **search-then-enrich**: a search returns a list of thin job cards, and opening each job returns the full posting. With [Linked API](/) you run this through your own account in a cloud browser and get structured JSON back, with no proxies or CAPTCHAs to manage. ![The search-then-enrich pipeline: st.searchJobs to st.doForJobs to st.openJob to structured JSON](/images/guides/linkedin-jobs-scraper-pipeline.webp) As a workflow, you [search](/docs/searching-for-jobs), iterate over each result, and [open every job](/docs/retrieving-job-data) for its details: ```json { "actionType": "st.searchJobs", "term": "product manager", "limit": 25, "filter": { "location": "San Francisco, California, United States", "datePosted": "pastWeek" }, "then": { "actionType": "st.doForJobs", "then": { "actionType": "st.openJob", "basicInfo": true } } } ``` The same workflow through the SDK, via [`customWorkflow`](/sdks/custom-workflow): ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const workflow = await linkedapi.customWorkflow.execute({ actionType: 'st.searchJobs', term: 'product manager', limit: 25, filter: { location: 'San Francisco, California, United States', datePosted: 'pastWeek' }, then: { actionType: 'st.doForJobs', then: { actionType: 'st.openJob', basicInfo: true }, }, }); const { data } = await linkedapi.customWorkflow.result(workflow.workflowId); console.log(data); ``` ```python from linkedapi import LinkedApi, LinkedApiConfig linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) workflow = linkedapi.custom_workflow.execute( { "actionType": "st.searchJobs", "term": "product manager", "limit": 25, "filter": {"location": "San Francisco, California, United States", "datePosted": "pastWeek"}, "then": { "actionType": "st.doForJobs", "then": {"actionType": "st.openJob", "basicInfo": True}, }, } ) result = linkedapi.custom_workflow.result(workflow.workflow_id) print(result.data) ``` `st.searchJobs` supports the same filters as the LinkedIn jobs UI – `datePosted`, `experienceLevels`, `employmentTypes`, `workplaceTypes`, `companies`, `industries`, `easyApply`, and more (see [`st.searchJobs`](/docs/action-st-search-jobs)). The honest caveat: this runs on your own account, so throughput is bounded by your account's daily limits and it is automated access under LinkedIn's terms. Human-like pacing keeps the account healthy, but it is not a bulk firehose – for millions of historical postings, a dataset is the better tool. ## How to monitor new job postings The highest-value jobs use case is freshness, and it is the one almost no guide covers. LinkedIn's native job alerts are a delayed daily digest; for real-time signals you poll. Filter the search by recency and dedupe by `jobId`: ```json { "actionType": "st.searchJobs", "term": "account executive", "limit": 50, "filter": { "location": "United States", "datePosted": "past24Hours", "companies": ["Example Company"] } } ``` Run that search on a schedule, keep a set of `jobId`s you have already seen, and treat anything new as a fresh posting. A new opening for a role you sell into is a buying signal; a competitor's new hire is a market signal. This is the pattern behind most "hiring intent" data products – and you can run it yourself. ## What you can build - **Recruiting and sourcing** – pull matching roles with company and apply URL, refreshed daily. - **Sales hiring-signals** – new postings mean teams are growing and budgets are moving; route them to your CRM. - **Market and compensation research** – aggregate salary ranges and titles across a sector or competitor set. - **Job-board aggregation** – keep a fresh, deduped feed of relevant roles. - **Monitoring** – alert the moment a target company opens a role. ## Is it legal to scrape LinkedIn jobs? Job postings are largely public – the guest endpoints serve logged-out data – and in the US, the Ninth Circuit affirmed a preliminary injunction in [hiQ Labs v. LinkedIn (9th Cir. 2022)](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/18/17-16783.pdf), finding hiQ had raised serious questions that scraping publicly available data likely does not violate the Computer Fraud and Abuse Act. But public is not a free pass. LinkedIn enforces against extraction operations with blocks and lawsuits, and any personal data you touch falls under GDPR and CCPA. Jobs are lower-risk than personal profiles because the data is corporate and public, but the same rules apply. We break down the full model – computer-access law, privacy, and enforcement – in our [guide to scraping LinkedIn](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers). This is not legal advice. ## Limits and staying unblocked A DIY scraper against the guest API gets rate-limited fast, often by around the tenth page from a single IP, which is why proxies become mandatory at any volume. An account-based run is instead bounded by your own account's [daily limits](/docs/core-concepts). Either way, the rule is the same: pace like a human, and stop the moment you see a CAPTCHA or a restriction notice rather than pushing through. ## Frequently Asked Questions (FAQ) #### Does LinkedIn have a jobs API? Only a partner-gated Job Posting API for *publishing* jobs, and LinkedIn is not accepting new partners for it. There is no public, self-serve API to search or read job listings, so every third-party "LinkedIn jobs API" is a scraper or dataset wrapping LinkedIn's public pages. #### Is it legal to scrape LinkedIn jobs? Job postings are public, and in *hiQ v. LinkedIn* the Ninth Circuit affirmed a preliminary injunction finding that scraping public data likely does not violate the CFAA. But LinkedIn's terms still prohibit automated access, and personal data is subject to GDPR and CCPA. Jobs are lower-risk than profiles, not risk-free. See our [legal breakdown](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers). #### How do I scrape LinkedIn jobs with Python? The simplest path is LinkedIn's public guest endpoints (`/jobs-guest/jobs/api/seeMoreJobPostings/search` and `/jobPosting/{id}`) with `requests` and BeautifulSoup, paginating in steps of 25. It works for small pulls but gets rate-limited quickly; for a reliable pipeline, use a scraping API or an account-based API instead. #### Do I need a LinkedIn account or cookies to scrape jobs? Not for the public guest endpoints – they serve logged-out data. You only need an account for higher-volume, structured access through an account-based API, where the requests come from your own authenticated session. #### Can I get salary data from LinkedIn job postings? Yes, when the posting includes it. Pay-transparency laws mean many postings now show a salary range, which comes through as a structured `salary` object. Postings without published pay return no salary. #### How many LinkedIn jobs can I scrape before getting blocked? There is no fixed number. A single IP hitting the guest API is usually throttled after about ten pages, so volume needs rotating proxies. An account-based approach is limited instead by your account's safe daily pace. Behavior matters more than raw count. #### How do I monitor new LinkedIn job postings? Poll a search filtered by `datePosted` (for example `past24Hours`), dedupe results by `jobId`, and treat new IDs as fresh postings. That gives you near-real-time hiring signals, which LinkedIn's once-a-day native alerts do not. #### Is there a free LinkedIn jobs scraper? Open-source Python libraries and the public guest endpoints are free for small, occasional pulls. Most managed tools and APIs offer a small free tier and then charge by volume. #### Can I scrape LinkedIn jobs without Python? Yes. The job data is just JSON, so any language works over plain HTTP. With Linked API you can use the Node.js SDK, the Python SDK, or raw HTTP requests – all hit the same workflow API. --- Need fresh LinkedIn job data without running and unblocking your own scraper? [Start with Linked API](/pricing) – search jobs, open each one for the full record, and get clean JSON straight from your own account. ## LinkedIn Profile Scraper: Extract Profile Data by URL or in Bulk (2026) A LinkedIn profile scraper pulls structured data from a person's profile – name, headline, experience, education, skills, activity – from its URL, whether you need one profile or a list of thousands. There is no official LinkedIn API for reading profile data, so every option is a scraper, and they differ a lot in reliability and account risk. This guide shows exactly what fields you can get, a real sample JSON, how to fetch one profile and a whole list in code, an honest take on Chrome extensions versus an API, and what you cannot get (spoiler: emails). > **The short version.** A person's headline, experience, education, skills, and activity are all extractable from their profile. Personal emails and phone numbers are not on the profile and need consented enrichment. Chrome extensions and homemade scripts run on *your own* account at machine speed and get it restricted; an API paces like a human and returns clean JSON. ## What data is on a LinkedIn profile A single profile is a rich record. Grouped by what it tells you: | Group | Fields | | --- | --- | | Identity | `name`, `headline`, `publicUrl`, `hashedUrl` | | Role and company | `position`, `companyName`, `location`, `countryCode` | | About | `about`, `followersCount` | | Experience | `experiences[]` – role, company, dates, duration, employment and location type | | Education | `education[]` – school and details | | Skills and languages | `skills[]` (`{ name }`), `languages[]` (`{ name, proficiency }`) | | Activity | `posts`, `comments`, `reactions` – [scrape posts and engagers](/guides/linkedin-post-scraper) | One detail worth building around: use `hashedUrl` as your stable key, not `publicUrl`. The vanity URL (`/in/jane-doe`) changes when someone renames their profile, but the hashed member URL is durable – it is the difference between a deduped database and silent duplicates. ## A real scraped profile Each section is opt-in: you request the parts you need, and you get back typed JSON instead of HTML to parse. ```json { "name": "Jane Doe", "publicUrl": "https://www.linkedin.com/in/jane-doe", "hashedUrl": "https://www.linkedin.com/in/ACoAAB1a2b3c4d", "headline": "Head of Growth at Acme", "location": "San Francisco, California, United States", "countryCode": "US", "position": "Head of Growth", "companyName": "Acme", "about": "Demand generation and lifecycle marketing leader.", "followersCount": 4820, "experiences": [ { "position": "Head of Growth", "companyName": "Acme", "employmentType": "fullTime", "locationType": "hybrid", "duration": 26, "startTime": "2024-01-01T00:00:00Z", "endTime": null, "location": "San Francisco, CA" } ], "education": [ { "schoolName": "Stanford University", "details": "MBA, Marketing" } ], "skills": [ { "name": "Demand Generation" }, { "name": "SEO" } ], "languages": [ { "name": "English", "proficiency": "nativeOrBilingual" }, { "name": "Spanish", "proficiency": "professionalWorking" } ] } ``` ## Four ways to scrape a profile, compared There is no official profile API, so the real choice is between four methods that trade off scale, output, and the risk to your account. | Method | Scale | Output | Maintenance | Account-ban risk | | --- | --- | --- | --- | --- | | Manual copy-paste | A few a day | You retype it | None | None | | Chrome extension | 50–100/day | A spreadsheet | Breaks on layout changes | High – acts on your account at machine speed | | DIY Python | High, until blocked | Raw HTML you parse | You own selectors and proxies | High – headless on your account | | API on your own account | Hundreds to thousands/day | Structured JSON | Handled for you | Lower – paced like a human | The honest distinction is account risk. A Chrome extension or a headless Python script acts through *your own logged-in account* at machine speed, which is exactly the pattern LinkedIn flags – accounts get restricted within days. An account-based API runs the same account but paces actions like a real user and returns maintained, structured JSON, so you are not re-writing selectors every time the page changes. We rank all the methods by risk in the [guide to scraping LinkedIn](/guides/how-to-scrape-linkedin#the-5-ways-to-scrape-linkedin-ranked-by-risk), and compare the API models in the [LinkedIn scraper API guide](/guides/linkedin-scraper-api). ## Scrape one profile by URL Install the SDK: ```bash # Node.js npm install -S @linkedapi/node # Python pip install linkedapi ``` Then [fetch a profile](/docs/retrieving-person-data) by URL, choosing which sections to pull: ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const workflow = await linkedapi.fetchPerson.execute({ personUrl: 'https://www.linkedin.com/in/jane-doe', retrieveExperience: true, retrieveEducation: true, retrieveSkills: true, retrieveLanguages: true, }); const { data } = await linkedapi.fetchPerson.result(workflow.workflowId); console.log(data?.name, data?.position, data?.hashedUrl); ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, FetchPersonParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) workflow = linkedapi.fetch_person.execute( FetchPersonParams( person_url="https://www.linkedin.com/in/jane-doe", retrieve_experience=True, retrieve_education=True, retrieve_skills=True, retrieve_languages=True, ) ) data = linkedapi.fetch_person.result(workflow.workflow_id).data print(data.name, data.position, data.hashed_url) ``` If you only have a name rather than a URL, [search for people](/docs/searching-for-people) first and feed the resulting URLs into the fetch. ## Scrape a list of profiles in bulk The common job is enrichment: you have a list of profile URLs – from a CSV, a search export, or your CRM – and you want a structured row for each. Loop over the URLs and collect the results. Linked API runs workflows sequentially on your account, so this paces itself automatically: ```typescript const profileUrls = [ 'https://www.linkedin.com/in/jane-doe', 'https://www.linkedin.com/in/john-smith', // one row per profile ]; const rows = []; for (const personUrl of profileUrls) { const workflow = await linkedapi.fetchPerson.execute({ personUrl, retrieveExperience: true }); const { data, errors } = await linkedapi.fetchPerson.result(workflow.workflowId); if (data) rows.push(data); else console.warn('Skipped', personUrl, errors); } ``` ```python profile_urls = [ "https://www.linkedin.com/in/jane-doe", "https://www.linkedin.com/in/john-smith", # one row per profile ] rows = [] for person_url in profile_urls: workflow = linkedapi.fetch_person.execute( FetchPersonParams(person_url=person_url, retrieve_experience=True) ) result = linkedapi.fetch_person.result(workflow.workflow_id) if result.data: rows.append(result.data) else: print("Skipped", person_url, result.errors) ``` Two realities to plan for: a share of URLs in any list are stale (people rename or delete profiles), and some profiles are private, so expect to skip a percentage rather than get a clean 100%. Handle the error case, as the loop above does, instead of assuming every fetch succeeds. Bulk volume is also bounded by your account's [daily limits](/docs/core-concepts) – this is your real account, not a proxy pool. ## Can you get an email or phone from a profile? Mostly no, and this is where a lot of tools oversell. Personal emails and phone numbers are almost never shown on a LinkedIn profile, so a scraper reading the profile cannot reliably return them. Tools that advertise "verified emails" are matching the person against a separate, ideally consented, B2B enrichment database – that is a different product from profile scraping, and the data does not come off the profile itself. The practical split: scrape the profile for role, company, experience, and activity; get contact details from a consented enrichment provider. We cover why direct contact-harvesting is the riskiest move in the [scraping guide](/guides/how-to-scrape-linkedin). ## Is it legal, and will my account get banned? Profile data is largely public, and in [hiQ Labs v. LinkedIn (9th Cir. 2022)](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/18/17-16783.pdf) the Ninth Circuit affirmed a preliminary injunction, finding hiQ had raised serious questions that scraping publicly available data likely does not violate the Computer Fraud and Abuse Act. But public is not a free pass: LinkedIn enforces against extraction operations with account restrictions and lawsuits – the data API Proxycurl was [sued into shutting down](/guides/proxycurl-alternatives) – and personal data falls under GDPR and CCPA. The ban risk is highest exactly where most people start: Chrome extensions and headless scripts driving your own account too fast. Pace like a human, stop on a CAPTCHA or restriction notice, and keep contact data to consented sources. The full legal model is in our [guide to scraping LinkedIn](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers). This is not legal advice. ## Frequently Asked Questions (FAQ) #### What data can you extract from a LinkedIn profile? Name, headline, location, current role and company, the full experience and education history, skills, languages, follower count, and recent activity (posts, comments, reactions). Personal email and phone are generally not on the profile. #### Does LinkedIn have an official profile API? No. LinkedIn's official APIs are limited to your own data and approved partner use cases; there is no public API to read other people's profile data. Every "profile API" you see is a scraping or enrichment product. #### Can I scrape LinkedIn profiles in bulk? Yes – pass a list of profile URLs and fetch each one in a loop, collecting structured rows. Plan for a percentage of stale or private URLs, and remember that volume on an account-based approach is bounded by your account's safe daily pace. #### Can I get someone's email from their LinkedIn profile? Not from the profile itself in most cases. Emails and phone numbers are rarely published on profiles, so reliable contact data comes from a separate consented enrichment provider, not from scraping the profile. #### Chrome extension vs API – which is safer? An API on your own account, by a wide margin. Extensions act through your logged-in account at machine speed and tend to get it restricted quickly; an API paces actions like a human, returns structured JSON, and is maintained against LinkedIn UI changes. #### How do I scrape a LinkedIn profile with Python? Install the SDK (`pip install linkedapi`), then call `fetch_person.execute(...)` with the profile URL and the sections you want, and read the result with `fetch_person.result(...)`. See the code above for a full example, including the bulk loop. #### Can I scrape a profile without an account? Only the limited public version LinkedIn shows logged-out, which omits much of the profile. Full, structured profile data comes through an authenticated session – your own account, driven by an API. #### How many profiles can I scrape per day? There is no universal number; it depends on your account's age and history. A new account should stay conservative and ramp up, while an established one handles more. Behavior matters more than raw count – pace like a human and stop on warnings. --- Need clean LinkedIn profile data by URL or in bulk, without running an extension or a fragile script? [Start with Linked API](/pricing) – fetch a profile or a whole list from your own account and get structured JSON back. ## How to Scrape LinkedIn in 2026 Without Getting Banned (and Is It Legal?) Yes, you can extract data from LinkedIn, and scraping *public* LinkedIn data is not a federal crime in the US. But that one fact hides two more: aggressive extraction can get your account permanently restricted, and how you store or sell the data can expose you – and your buyers – to privacy law. This guide gives you the real methods ranked by risk, how bans actually happen, and the legal picture, in plain terms. > **The short version.** Public profile and activity data is extractable. Personal contact details (emails, phone numbers) are not, and chasing them is where most trouble starts. The durable approach is to operate on *your own* authenticated account with human-like pacing, or use consented data – not to point a headless bot at LinkedIn and hope. ## What you can actually extract from LinkedIn Before picking a method, be clear about what is realistically available. Most of LinkedIn's value sits in data the platform shows publicly. The data it hides – private contact details – is exactly the data that creates legal risk. | Data | Public? | Realistic to extract | Notes | | --- | --- | --- | --- | | Name, headline, role, location | Yes | Yes | Visible on public profiles, even logged out | | Work history, education, skills | Mostly | Yes | Privacy-dependent – [scrape a full profile](/guides/linkedin-profile-scraper) | | Posts, reactions, comments | Yes | Yes | Engagement data for social selling – [scrape posts and engagers](/guides/linkedin-post-scraper) | | Company info, employees | Yes | Yes | Headcount, industry, decision-makers – [scrape a company](/guides/linkedin-company-scraper) | | Job postings | Yes | Yes | Public listings, salary when shown – [scrape jobs](/guides/linkedin-jobs-scraper) | | Your own connections | Private to you | Yes | You can export your own network | | Personal email, phone | No | No (not directly) | LinkedIn does not expose these; harvesting them is the highest-risk move | The line that matters: profile and activity data is fair to extract; personal contact data is not. Any tool that promises to "scrape verified emails straight from LinkedIn" is either guessing or pulling from a separate database – and that distinction matters legally, as we will see. ## The 5 ways to scrape LinkedIn, ranked by risk There is no single "LinkedIn scraper." There are five common approaches, and they trade off scale against the risk to your account. ![Five LinkedIn scraping methods plotted by scale and account-ban risk](/images/guides/linkedin-scraping-methods-quadrant.webp) | Method | Typical scale | Account-ban risk | Legal exposure | Best for | | --- | --- | --- | --- | --- | | Manual copy-paste | A few/day | None | Low | One-off lookups | | Browser extension | 50–100/day | High | Medium | Small lists, non-technical users | | No-code tools | Hundreds/day | High | Medium | Sales ops without engineers | | Custom code (headless + proxies) | High, until it breaks | Very high | High | Engineers who want full control | | API on your own account | Hundreds to thousands/day | Low–moderate | Medium | Products and repeatable pipelines | **Manual copy-paste.** Slow, free, zero risk. Fine for a handful of profiles. Useless at scale. **Browser extensions.** A Chrome extension reads the page you are looking at and dumps it to a sheet. Easy to start, but it acts on your logged-in account at machine speed, which is easy for LinkedIn to flag. Good for tens of profiles, not thousands. **No-code tools.** PhantomBuster, Evaboot, and similar SaaS run cloud automations on your account. More throughput than an extension, same core risk: it is still your account doing the actions, so account safety depends entirely on how conservatively the tool paces itself. **Custom code.** Python with Selenium, Playwright, or a headless browser plus rotating proxies. Maximum control, maximum maintenance, and the highest ban risk – which brings us to why this path is harder than it looks. **API on your own account.** A managed service drives a real browser session for *your* account with human-like timing and built-in limits, and returns structured JSON. This is the sweet spot for anything ongoing or product-facing, and it is where [Linked API](/) sits – see our [LinkedIn scraper API guide](/guides/linkedin-scraper-api) for how that model works. ### Why DIY headless scraping breaks Most engineers try the custom-code route first, then abandon it. Here is what they run into: - **The login wall.** LinkedIn shows only a few public profiles before forcing authentication. Once you log in to get past it, every request is tied to a real account that can be restricted. - **Fingerprinting.** LinkedIn profiles your TLS handshake, headers, timing, and browser fingerprint. Datacenter proxies and vanilla headless browsers are trivially detectable, so you end up paying for residential proxies and stealth patches. - **Fragile selectors.** The DOM changes constantly. Your parser breaks, silently returns empty fields, and you maintain it forever. - **Permanent bans.** Account restrictions on LinkedIn escalate fast and are rarely reversed. A burned account is gone, along with its connections and history. None of this means scraping is dead. It means brute force is dead. The methods that survive are the ones that behave like a human. ## How LinkedIn detects scraping (and how accounts get banned) Bans are not random. LinkedIn watches a handful of signals, and you can stay clear of all of them. - **Request velocity.** Hundreds of profile views in an hour is not human. Real users pause, scroll, and get distracted. - **Behavioral patterns.** Identical timing between actions, no mouse movement, perfectly sequential browsing – all classic automation tells. - **Fingerprint and IP reputation.** Headless browsers, datacenter IPs, and mismatched geolocation raise the score. - **The "log in to continue" wall.** Hitting it repeatedly while logged out is a strong scraping signal. When the score crosses a threshold, accounts move through a ladder: a soft warning, then a temporary restriction (search and profile views blocked for a stretch), then a permanent ban. New accounts get the least slack, so warm them up over weeks rather than firing on day one. ### Safe volume is a range, not a number You will see "500 profiles a day is safe" thrown around. Ignore single numbers. Safe volume depends on account age, your Social Selling Index, whether you have Sales Navigator, and how human-like your pacing is. | Account state | Rough safe ceiling | Approach | | --- | --- | --- | | New or cold | 20–50 actions/day | Ramp slowly over 3–4 weeks | | Established, human-like pacing | A few hundred actions/day | Spread across the day, with pauses | | Aggressive headless + proxies | You will likely be flagged | Not recommended | For the real, account-specific picture, see our guides on the [LinkedIn connection limit](/guides/linkedin-connection-limit-2026) and [understanding LinkedIn limits](/guides/understanding-linkedin-limits). **Stop signals.** If you see a CAPTCHA, a forced re-login, an "unusual activity" notice, or a sudden drop in results, stop immediately and let the account rest. Pushing through is how a temporary restriction becomes a permanent one. ## Is scraping LinkedIn legal? The three layers This is where almost every other guide gets it wrong. "Scraping public data is legal, *hiQ* said so" is half a sentence. Legality is not one test – it is three separate layers, and clearing one does not clear the next. **1. US computer-access law (the CFAA).** In [hiQ Labs v. LinkedIn (9th Cir. 2022)](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/18/17-16783.pdf), the Ninth Circuit affirmed a preliminary injunction, finding hiQ had raised serious questions that scraping publicly available data likely does not violate the Computer Fraud and Abuse Act. The arc matters: an injunction in 2017, affirmed in 2019, vacated by the Supreme Court in 2021 after [Van Buren v. United States](https://www.law.cornell.edu/supremecourt/text/19-783), then reaffirmed in 2022. So scraping public data is not a federal hacking crime. That is the layer most articles stop at. **2. Privacy law (GDPR / CCPA).** "Public" does not mean "free to use." If your dataset includes personal data of EU or California residents, privacy law applies regardless of where you found it. You need a lawful basis – for B2B prospecting that is usually legitimate interest – and data subjects keep rights over their data. Personal emails and phone numbers are the hot zone; collecting them without consent is the fastest way into trouble. **3. Enforcement reality.** LinkedIn does not just win cases, it makes examples. It has permanently banned and sued operations that used fake profiles, pushed data brokers like Apollo and Seamless to remove company pages, and in 2025 sued [Proxycurl](/guides/proxycurl-alternatives) (Nubela) – whose LinkedIn data API then shut down entirely. For a 2026 reader, that suit, not *hiQ*, is the live warning shot. > This is not legal advice. It is the mental model to bring to your own counsel: pass all three, or you are carrying the risk. The honest takeaway: public data is collectible, but how you *use* it is what gets people sued. Stay on public, business-relevant data; keep a lawful basis; and never harvest personal contact details from LinkedIn directly. ## The durable approach: automate your own account like a human Strip away the noise and the safe path is consistent across all three legal layers and the detection signals: 1. **Use your own authenticated account.** No fake profiles, no bought accounts. Fake-profile operations are exactly who LinkedIn bans and sues. 2. **Behave like a human.** Pace actions, add real delays, run sequentially, and stay inside your account's limits. 3. **Stick to public, business data.** Profiles, companies, posts, your own connections. Get personal contact data from consented enrichment sources, not by scraping it off LinkedIn. This is precisely what Linked API is built to do. It runs your account in a cloud browser that emulates a real LinkedIn user, so requests are not instant – a simple action takes seconds, a heavy one minutes – and workflows run [sequentially, never in parallel](/docs/core-concepts), mirroring how a person actually browses. When an action would exceed a configured limit, it returns a `limitExceeded` error instead of pushing your account over the edge. To be clear about what that does and does not buy you: it is not a guarantee against restriction. What it buys you is account safety and clean, structured data without maintaining a scraper. In practice that means two calls rather than a scraper: a [people search](/docs/searching-for-people) that returns matching profiles as structured data, and a [profile fetch](/docs/retrieving-person-data) that returns experience, skills and recent posts in one request. Neither involves HTML parsing, selectors, or a session file you have to keep alive. Working code for both calls lives in our guide to building a [LinkedIn scraper in Python](/guides/linkedin-scraper-python), which also compares this route against the open-source libraries and a DIY Playwright script line for line. The [SDK reference](/sdks/installation) has the TypeScript equivalents. ## Which method should you use? - **A few profiles, occasionally.** Copy-paste or a browser extension. No need for anything heavier. - **A non-technical sales team.** A no-code tool, paced conservatively. Compare options in our [best LinkedIn automation tools](/blog/best-linkedin-automation-tools-2026) roundup. - **A product or a repeatable data pipeline.** An API on your own account. Predictable, structured, and account-safe at scale. - **Personal emails and phone numbers.** Not from LinkedIn. Use a consented B2B enrichment provider and keep a lawful basis. ## Frequently Asked Questions (FAQ) #### Is LinkedIn scraping legal in 2026? Scraping publicly available data likely does not violate the CFAA in the US — in *hiQ v. LinkedIn* the Ninth Circuit affirmed a preliminary injunction on that basis. That settles the criminal-law question only: GDPR and CCPA still apply if the data touches personal information, and LinkedIn enforces against extraction operations directly, as the Proxycurl suit showed. Public does not mean unrestricted. #### How many LinkedIn profiles can I scrape per day without getting banned? There is no universal number. A new account should stay around 20–50 actions a day and ramp up over weeks; an established account with human-like pacing can handle a few hundred. Limits depend on account age, SSI, and subscription. See our [limits guide](/guides/understanding-linkedin-limits). #### Can you get banned for scraping LinkedIn? Yes. LinkedIn escalates from warnings to temporary restrictions to permanent bans, and permanent bans are rarely reversed. Human-like pacing on your own account is the main defense. #### Can I scrape LinkedIn without an account? Only the limited public pages LinkedIn shows before the login wall. No account is involved, so nothing can be restricted, but it is the hardest route technically and it still falls under privacy law. #### Does LinkedIn detect API scraping? LinkedIn detects automation through behavioral patterns, request velocity, and fingerprinting regardless of how it is driven. The mitigation is not hiding – it is behaving like a real user on a real account, which is how a managed API approach works. #### What is the difference between scraping and data enrichment? Scraping pulls data from LinkedIn's interface. Enrichment matches a name or domain against a separate, ideally consented, database. Verified emails come from enrichment, not from LinkedIn directly. #### Is it legal to scrape Sales Navigator? The same layers apply. Treat it as higher risk, not lower, and pace accordingly. For the ways to extract it, see our [Sales Navigator export guide](/guides/linkedin-sales-navigator-scraper); for programmatic access, see the [Sales Navigator API guide](/guides/linkedin-sales-navigator-api). #### Can I put scraped LinkedIn data in my CRM? For public, business-relevant data with a lawful basis (legitimate interest for B2B), generally yes. For personal contact data gathered without consent, you are exposed under GDPR and CCPA. Keep records of your basis and honor deletion requests. #### What is the safest way to scrape LinkedIn? Operate your own authenticated account with human-like pacing and conservative limits, stick to public business data, and source contact details from consented providers. That clears the detection signals and keeps you on the right side of all three legal layers. --- Want structured LinkedIn data without running a scraper or risking your account? [Start with Linked API](/pricing) – it drives your own account safely and returns clean JSON for people, companies, posts, and connections. ## LinkedIn Scraper API: Extract LinkedIn Data Without Browser Scripts (2026) A LinkedIn scraper API lets you pull structured LinkedIn data – profiles, companies, posts, search results – from a single HTTP call, instead of building and babysitting your own headless browser. But "LinkedIn scraper API" is really three different products, and they are not interchangeable. This guide breaks down the three models, shows real requests and the JSON they return, and helps you pick the right one – including, honestly, where an account-based API like ours fits and where a dataset or a real-time scraper is the better tool. > **The short version.** If you need millions of cold profiles in bulk, use a dataset. If you need fresh public pages on demand, use a real-time scraper. If you need data you can actually see in your own account (your network, searches, Sales Navigator, inbox) and want to *act* on it, use an account-based API. Most "scraper API" pages pretend their model is the only one. It isn't. ## The three kinds of "LinkedIn scraper API" The label hides three very different products. They differ on how fresh the data is, what you can reach, whether you can act, and who carries the risk. | Model | How it works | Freshness | Data you reach | Can it act? | Risk sits with | Typical price | Best for | | --- | --- | --- | --- | --- | --- | --- | --- | | Dataset / database | Query a pre-scraped database | Months to years old | Bulk public profiles | No | The data holder | ~$2.50/1K (bulk) to ~$0.20/record | Bulk enrichment | | Real-time scraper | Pass a URL, scraped live via proxies | Live | Public pages | No | The provider | ~$1.50–$4 per 1K profiles | On-demand public data | | Account-based API | Runs your own authenticated account | Live | Your network, search, inbox, public data | Yes | Your account | Flat per connected account | Integrate and act safely | **Dataset providers** (Coresignal, Bright Data datasets) sell scale. You get tens of millions of records cheaply, but those records can be months or even years out of date – people change jobs and titles constantly – and you cannot act on the data or reach anything outside their index. **Real-time scrapers** (Bright Data, Scrapingdog, Apify) take a URL and return fresh JSON, handling proxies and CAPTCHAs for you. Great for pulling public pages on demand; they cannot see your network or send a message, and the legal exposure of live scraping sits with whoever runs it. **Account-based APIs** (Linked API, Unipile) work differently: they drive *your own* authenticated LinkedIn account in a cloud browser, like a careful human. That unlocks data only a logged-in user sees and lets you act – connect, message, react – in the same integration. The trade-off is honest: it is bounded by your account's daily limits, so it is not a firehose for a million cold profiles. Because it runs your own session, it also reaches data a scraper cannot – for example, [exporting your own connections](/guides/export-linkedin-connections) as JSON. ## What data can you actually get Across the real-time and account-based models, the extractable surface is similar: - **Profiles** – name, headline, location, current role, company, about text, follower count, full experience, education, skills, languages. See the [LinkedIn profile scraper guide](/guides/linkedin-profile-scraper). - **Companies** – description, industry, headquarters, employee count, year founded, recent posts, employees, and decision-makers. See the [LinkedIn company scraper guide](/guides/linkedin-company-scraper). - **Posts** – text, author, hashtags, media, plus reactions and comments with the engager's name and headline. See the [LinkedIn post scraper guide](/guides/linkedin-post-scraper). - **Search** – people and company search with filters (role, location, industry, current and past companies, schools), including Sales Navigator. See [LinkedIn boolean search](/guides/linkedin-boolean-search) for building the query. What you cannot get from LinkedIn directly is personal contact data – emails and phone numbers are not exposed, and harvesting them is the fastest way into legal trouble (see [how to scrape LinkedIn](/guides/how-to-scrape-linkedin) for the legal detail). A good scraper API returns typed JSON, so you skip HTML parsing entirely: ```json { "name": "Jane Doe", "publicUrl": "https://www.linkedin.com/in/jane-doe", "headline": "Head of Growth at Acme", "location": "San Francisco, California, United States", "position": "Head of Growth", "companyName": "Acme", "followersCount": 4820, "experiences": [ { "position": "Head of Growth", "companyName": "Acme", "employmentType": "fullTime", "locationType": "hybrid", "duration": 26, "startTime": "2024-01-01T00:00:00Z", "endTime": null } ], "skills": [ { "name": "Demand Generation" }, { "name": "SEO" }, { "name": "Lifecycle Marketing" } ] } ``` ## How an account-based scraper API works This is the model worth understanding, because it is the least obvious and the one we build. ![How an account-based LinkedIn scraper API works: one call runs your own account in a cloud browser and returns structured JSON](/images/guides/linkedin-scraper-api-architecture.webp) You make one call. Linked API runs the action through your own authenticated account in a cloud browser that emulates a real user, so requests are not instant – a simple fetch takes seconds, a heavy one minutes – and actions run [sequentially, never in parallel](/docs/core-concepts), the way a person actually browses. You poll for the result and get structured JSON back. No proxies to rotate, no CAPTCHAs to solve, no scraped database to keep fresh. Two honest boundaries. First, because it is your account doing the work, throughput is bounded by your account's natural daily limits (think hundreds of actions a day on an established account, not hundreds of thousands). Second, this model buys you account safety and reachable data, not legal immunity. For bulk cold data at massive scale, a dataset is the right tool, not this. ## Make your first call Install the SDK: ```bash # Node.js npm install -S @linkedapi/node # Python pip install linkedapi ``` Initialize the client and run a [people search](/docs/searching-for-people): ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const search = await linkedapi.searchPeople.execute({ term: 'head of growth', limit: 25, filter: { locations: ['United States'], industries: ['Software Development'] }, }); const { data } = await linkedapi.searchPeople.result(search.workflowId); data?.forEach((person) => console.log(person.name, '-', person.publicUrl)); ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, SearchPeopleParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) search = linkedapi.search_people.execute( SearchPeopleParams( term="head of growth", limit=25, filter={"locations": ["United States"], "industries": ["Software Development"]}, ) ) result = linkedapi.search_people.result(search.workflow_id) for person in result.data or []: print(person.name, "-", person.public_url) ``` Then pull a full [profile](/docs/retrieving-person-data) – or a [company](/docs/retrieving-company-data) with its employees and decision-makers – in one call: ```typescript const profile = await linkedapi.fetchPerson.execute({ personUrl: 'https://www.linkedin.com/in/jane-doe', retrieveExperience: true, retrieveSkills: true, retrievePosts: true, postsRetrievalConfig: { limit: 10 }, }); const { data } = await linkedapi.fetchPerson.result(profile.workflowId); console.log(data?.name, data?.position, data?.followersCount); ``` ```python from linkedapi import FetchPersonParams profile = linkedapi.fetch_person.execute( FetchPersonParams( person_url="https://www.linkedin.com/in/jane-doe", retrieve_experience=True, retrieve_skills=True, retrieve_posts=True, posts_retrieval_config={"limit": 10}, ) ) data = linkedapi.fetch_person.result(profile.workflow_id).data print(data.name, data.position, data.followers_count) ``` The same `execute` then `result` pattern covers [company data](/docs/retrieving-company-data) and [post engagement](/docs/retrieving-post-data). Grab your tokens from the [installation guide](/sdks/installation) to start. ## Scraper API vs building your own scraper The real question for most engineers is "should I just build this myself?" At the decision level, it comes down to maintenance and risk, not difficulty: | Factor | Build it yourself | Scraper API | | --- | --- | --- | | Time to first data | Days to weeks | Minutes | | Proxies and CAPTCHAs | You manage them | Handled | | Selectors break | You fix them, forever | Handled | | Account-ban risk | High, hard to control | Lower, paced for you | | Cost at low volume | "Free" but your time | Free tier to low monthly | | Cost at massive volume | Cheaper per record | More per record | | Best when | You have niche needs and time | You want data and to ship | If you genuinely want to build your own, our [guide to scraping LinkedIn](/guides/how-to-scrape-linkedin) walks through the methods, detection signals, and why headless scraping breaks. Working in Python specifically? Our [LinkedIn scraper in Python](/guides/linkedin-scraper-python) guide puts the same extraction side by side – a Playwright script against an account API call – and checks the current state of the open-source libraries. For most teams, an API removes a maintenance surface that never stops moving. ## Is a LinkedIn scraper API legal? Will my account get banned? Short answer: in [hiQ Labs v. LinkedIn (9th Cir. 2022)](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/18/17-16783.pdf) the Ninth Circuit affirmed a preliminary injunction, finding hiQ had raised serious questions that scraping *public* LinkedIn data likely does not violate the CFAA – but it still falls under LinkedIn's terms and under GDPR and CCPA when it touches personal data. The full legal model is in our [scraping guide](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers). The cautionary tale for this category is [Proxycurl](/guides/proxycurl-alternatives). It was one of the most popular LinkedIn data APIs until LinkedIn [sued its operator in 2025](https://dockets.justia.com/docket/california/candce/3:2025cv00828/443258) over fake accounts and scraping, and the service shut down entirely. The lesson is about *who carries the risk*: a dataset built on fake accounts is fragile, and so is a business that depends on it. An account-based API behaves more like normal usage – it runs your own real session, holds no scraped index, and paces itself – which is why it tends to keep accounts healthy. It is not immunity, and it will not let you pull a hundred thousand profiles a day. This is not legal advice; if your use case is sensitive, talk to counsel. ## How much does a LinkedIn scraper API cost? Pricing follows the model (rough market estimates as of 2026): - **Real-time scrapers** charge per record, roughly **$1.50 to $4 per 1,000 profiles**, plus higher rates for protected pages. - **Datasets** are cheapest in bulk, around **$2.50 per 1,000 records** wholesale, rising to ~$0.20 per record at retail tiers. - **Account-based APIs** charge a **flat rate per connected account**, decoupled from record count. The honest takeaway: account-based is not the cheapest per record at huge volume. It wins when you need your own reachable data plus the ability to act, with account safety, rather than a static pile of cold profiles. See [Linked API pricing](/pricing) for current plans. ## Frequently Asked Questions (FAQ) #### Does LinkedIn have an official API? Yes, but it is restricted. The open tier mainly returns your own profile and lets you share posts. Reading other people's profiles, search, or audience data requires [Partner Program approval](https://learn.microsoft.com/en-us/linkedin/shared/authentication/getting-access), which is selective and can take weeks. Most data use cases are not covered, which is why third-party scraper APIs exist. #### Is there a free LinkedIn API? LinkedIn's official API is free for its limited scope (your own data). Third-party scraper APIs typically offer a small free tier – a few hundred credits or records – then move to paid plans. #### How do I get a LinkedIn API key? For a scraper API, you sign up with the provider and get a token. For Linked API you get two tokens (an account token and a per-account identification token) from the dashboard; see the [installation guide](/sdks/installation). #### What is the difference between a scraper API and LinkedIn's official API? The official API exposes a narrow, approved slice of data through OAuth. A scraper API returns the data a user can actually see – profiles, companies, posts, search – without partner approval. The trade-off is that it operates outside LinkedIn's official program. #### Can I get LinkedIn data in real time? Yes, with a real-time scraper or an account-based API. Datasets are not real time – they serve pre-scraped records that can be months or even years out of date. #### How many profiles can I fetch per day? With an account-based API, throughput is bounded by your account's limits – usually hundreds of actions a day on an established account, fewer on a new one. Real-time scrapers and datasets scale higher because they do not use your account. #### Do I need Sales Navigator? Not for standard profile, company, and post data. Sales Navigator unlocks its own search and lead data, which account-based APIs can reach through your Sales Navigator seat. See our [Sales Navigator API guide](/guides/linkedin-sales-navigator-api) for how the nv actions work. #### Will my account get banned? Any automation carries some risk. It is lowest when the requests come from your own authenticated account at human-like pace, which is how an account-based API is designed to behave. Aggressive headless scraping on a logged-in account is what gets flagged. --- Want fresh LinkedIn data and the ability to act on it, without running a scraper or risking your account? [Start with Linked API](/pricing) – one call returns clean JSON for people, companies, posts, and search, straight from your own account. ## Proxycurl Is Gone: Best LinkedIn Data API Alternatives in 2026 (and How to Migrate) Proxycurl, for years one of the most popular LinkedIn data APIs, shut down in 2025 after LinkedIn sued the company behind it. If your pipeline broke overnight, this guide gives you the accurate story (the dates and the case, not the rumors), the real lesson for choosing your next vendor, an honest comparison of the live alternatives by model, and a concrete migration path. No gloating, no single-product hard sell – just what happened and what to do next. > **The short version.** For a like-for-like "profile URL in, JSON out" replacement, a real-time scraper (ScrapIn, Apify, Bright Data) is the closest drop-in. For bulk enrichment, a dataset provider (Coresignal, People Data Labs) fits. If you want live data plus the ability to act – and no shared scraped database that a court can order deleted – an account-based API (Linked API, Unipile) is the durable play. The thing that actually killed Proxycurl – fake accounts and a central scraped index – is the thing not to repeat. ## What happened to Proxycurl On January 24, 2025, LinkedIn sued the company behind Proxycurl (Nubela) in the U.S. District Court for the Northern District of California – *LinkedIn Corp. v. Nubela Pte. Ltd.*, No. 3:25-cv-00828 (N.D. Cal.), filed January 24, 2025; see [the docket](https://dockets.justia.com/docket/california/candce/3:2025cv00828/443258). The complaint brought six claims, including breach of contract, fraud and deceit, violation of the Computer Fraud and Abuse Act (CFAA), California's Unfair Competition Law, a Lanham Act claim, and misappropriation. The core allegation was not simply "they used LinkedIn data." It was that Proxycurl created hundreds of thousands of fake accounts to scrape millions of profiles, including non-public data, and resold that data through its API. The case settled in mid-2025; Proxycurl posted its goodbye in July 2025 and wound the service down, with customers notified and data slated for deletion. ![Why Proxycurl was shut down: fake accounts and a central scraped index led to a lawsuit, an injunction, and shutdown](/images/guides/proxycurl-why-shut-down.webp) To its credit, Proxycurl's founder was candid afterward. He wrote in his July 2025 goodbye post that the business was a roughly $10M revenue business, that about half of it came from scraping LinkedIn, and that there was "no winning in fighting this" against a company with an effectively unlimited legal budget. The team has since pivoted to a new product, NinjaPear, that explicitly does not scrape LinkedIn. ## The real lesson for picking your next API It is tempting to read this as "LinkedIn data is radioactive, stay away." That is the wrong takeaway. The fatal combination was specific: **fake accounts at scale, scraping non-public data, packaged as a central database and resold.** Each of those is a separate aggravating factor, and together they made the case easy to bring. The legal backdrop is more nuanced than the headlines. Scraping genuinely *public* data is not automatically illegal in the US – that is the takeaway from *hiQ Labs v. LinkedIn*. But the moment you spin up fake accounts or reach non-public data at scale, you are in CFAA and privacy-law territory. (We cover the full legal model in our [guide to scraping LinkedIn](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers).) For you as a buyer, the practical lesson is about continuity risk. A provider whose entire business is a central, scraped index of LinkedIn is one injunction away from gone – and that injunction can require deleting the very data you depend on, with little notice. Vendor selection now has to weigh "will this still exist, and will my data survive, in twelve months?" alongside price and coverage. ## The live alternatives, by model The LinkedIn data API landscape splits into three models. Most "Proxycurl alternative" listicles blur them together and then crown whichever product the author sells. Here they are honestly, with the trade-offs that matter. | Model | How it works | Closest Proxycurl match | Examples | Freshness | Risk sits with | Typical price | | --- | --- | --- | --- | --- | --- | --- | | Real-time scraper | Pass a URL, scraped live via proxies | Person/Company Profile endpoints | ScrapIn, Apify, Bright Data, Scrapingdog | Live | The provider | ~$1.50–$4 / 1K profiles | | Dataset / database | Query a pre-scraped database in bulk | Search and bulk enrichment | Coresignal, People Data Labs | Months to years | The data holder | ~$0.005–$0.20 / record | | Account-based API | Runs your own authenticated account | Profile reads, plus actions | Linked API, Unipile | Live | Your account | Flat per connected account | **Real-time scrapers** are the closest drop-in for Proxycurl's bread-and-butter "give a profile URL, get JSON." ScrapIn (now merged into Reverse Contact) and Apify's LinkedIn actors are the usual developer picks; Bright Data is the enterprise-scale option with the strongest legal track record. You hand over a URL, they manage proxies and return structured data – and they carry the scraping risk. **Dataset providers** like Coresignal and People Data Labs sell scale: hundreds of millions of records you can query or buy in bulk. They replace Proxycurl's *search and enrichment* use case, not real-time single lookups, and the trade-off is freshness – records are often months or years out of date between refreshes. **Account-based APIs** work differently: they drive *your own* authenticated LinkedIn account in a cloud browser. There is no shared scraped index for a court to seize, and you can act (connect, message, react), not just read. The honest caveat: the risk shifts to your account and LinkedIn's terms, and throughput is bounded by your account's daily limits – it is not a bulk firehose. This is the [account-based model](/guides/linkedin-scraper-api) we build at Linked API. ## Map your Proxycurl endpoints to a replacement Proxycurl exposed around 21 endpoints. Here is where each group goes now: | Proxycurl endpoint | What it did | Where to go now | | --- | --- | --- | | Person Profile / Person Lookup | Profile data from a URL | Real-time scraper (ScrapIn, Apify, Bright Data) or account-based `fetchPerson` | | Person Search / Role Lookup | Find people by criteria | Dataset search (Coresignal, PDL) or account-based [people search](/docs/searching-for-people) | | Company Profile / Employee Count | Company firmographics | Dataset (Coresignal) or account-based [company data](/docs/retrieving-company-data) | | Employee Listing / Search | Roster of a company's people | Dataset, or account-based company fetch with employees | | Work Email / Reverse Email / Personal Contact | Contact details | Consented enrichment (Reverse Contact, People Data Labs, Prospeo) | One honest note on contact data: the email and phone endpoints were always the legally hottest part of Proxycurl. Personal contact details are not something you should extract from LinkedIn directly – source them from a consented B2B enrichment provider instead. We explain why in the [scraping guide](/guides/how-to-scrape-linkedin). ## Migrating to an account-based API If you used Proxycurl for live profile reads and want control rather than a shared index, the account-based path maps cleanly. The Person Profile endpoint – a URL in, structured JSON out – becomes a single call against your own account. Install the SDK: ```bash # Node.js npm install -S @linkedapi/node # Python pip install linkedapi ``` Then fetch a profile by URL, the direct analog of Proxycurl's Person Profile endpoint: ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const profile = await linkedapi.fetchPerson.execute({ personUrl: 'https://www.linkedin.com/in/jane-doe', retrieveExperience: true, retrieveSkills: true, }); const { data } = await linkedapi.fetchPerson.result(profile.workflowId); console.log(data?.name, data?.position, data?.followersCount); ``` ```python from linkedapi import LinkedApi, LinkedApiConfig, FetchPersonParams linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) profile = linkedapi.fetch_person.execute( FetchPersonParams( person_url="https://www.linkedin.com/in/jane-doe", retrieve_experience=True, retrieve_skills=True, ) ) data = linkedapi.fetch_person.result(profile.workflow_id).data print(data.name, data.position, data.followers_count) ``` The same `execute` then `result` pattern covers [people search](/docs/searching-for-people) and [company data](/docs/retrieving-company-data). Grab your tokens from the [installation guide](/sdks/installation) to start. ## A one-day migration checklist 1. **Put a thin interface in front of your data source** so you can swap providers without touching the rest of the app. Proxycurl taught everyone why this matters. 2. **Pick a model by job:** live single-profile reads to a real-time scraper, bulk firmographics to a dataset, your-own-account reads plus actions to an account-based API. 3. **Map your fields.** Most providers return the same core profile shape; normalize names once at the boundary. 4. **Move contact lookups to consented enrichment** rather than direct extraction. 5. **Backfill** any records you lost when Proxycurl went dark. 6. **Handle limits.** Real-time and dataset APIs have rate limits; account-based APIs surface a limit error when you would exceed your account's safe pace. ## Frequently Asked Questions (FAQ) #### Why did Proxycurl shut down? LinkedIn sued the company behind Proxycurl in January 2025 over creating fake accounts to scrape profiles, including non-public data, and reselling it. The case settled in mid-2025, and Proxycurl wound the service down. Its founder said about half the revenue came from LinkedIn scraping, and that there was "no winning in fighting this." #### Is Proxycurl still working? No. The API was sunset in 2025 and new signups stopped. The team pivoted to a separate product, NinjaPear, that does not scrape LinkedIn. #### What is the best Proxycurl alternative? There is no single answer – it depends on the job. For "profile URL in, JSON out," a real-time scraper like ScrapIn or Apify is the closest drop-in. For bulk enrichment, Coresignal or People Data Labs. For live data plus actions on your own account, an account-based API like Linked API. #### Which alternative gives profile data by URL, like Proxycurl did? Real-time scrapers (ScrapIn, Apify, Bright Data, Scrapingdog) and account-based APIs both take a profile URL and return structured JSON. Dataset providers work by query and bulk export instead. #### Is scraping LinkedIn legal in 2026? Scraping genuinely public data is not automatically a crime in the US under *hiQ v. LinkedIn*, but it still falls under LinkedIn's terms and under GDPR and CCPA for personal data. Fake accounts and non-public data are what turn it into a lawsuit. See our [legal breakdown](/guides/how-to-scrape-linkedin#is-scraping-linkedin-legal-the-three-layers). #### Will my new alternative just get sued too? The risk depends on the model. A provider built on a fake-account farm and a central scraped index carries the same exposure Proxycurl did. A dataset or real-time provider that sources data carefully spreads that risk, and an account-based API moves it onto your own account and LinkedIn's terms rather than a shared index. None of these is risk-free; choose with that in mind. #### How much did Proxycurl cost, and how do alternatives compare? Proxycurl was credit-based, roughly $0.009–$0.02 per credit. As of 2026, real-time scrapers run about $1.50–$4 per 1,000 profiles, datasets from fractions of a cent to ~$0.20 per record, and account-based APIs charge a flat rate per connected account. #### How do I migrate from Proxycurl? Abstract your data source behind an interface, pick a replacement model by use case, map your fields, move contact lookups to consented enrichment, and backfill lost records. Most profile shapes are similar enough that the swap is a boundary-layer change, not a rewrite. --- Were you using Proxycurl for live profile reads and the ability to act? [Linked API](/pricing) runs your own authenticated account and returns clean JSON for people, companies, posts, and search – with no shared scraped database to disappear overnight. ## LinkedIn Cold Outreach That Actually Works: 7 Data-Driven Strategies The large majority of generic cold messages get ignored. Here's what the few that succeed do differently. You send 50 connection requests per week. Maybe 10 people accept. Two respond. Zero meetings booked. The problem isn't LinkedIn – it's your approach. Generic spray-and-pray outreach delivers weak response rates – often only a couple of percent. Sales teams track vanity metrics (messages sent) instead of what matters: meetings booked and deals closed. This article is different. Everything here is backed by real conversion data tested across 50,000+ LinkedIn outreach messages. You'll learn a data-driven framework that boosts response rates to 5-7% – a 3.5x improvement over generic templates. ## Why Most LinkedIn Cold Outreach Fails LinkedIn cold outreach has a failure rate problem. But the failure modes are predictable – and fixable. ### Failure Mode #1: Generic Messaging > 📈 Generic connection requests ("I'd like to connect") achieve 15-20% acceptance rates. Personalized requests referencing specific profile details hit 40-60%. That's a 3x difference from one small change. Yet most outreach still looks like this: *"Hi [Name], I help [Industry] companies with [Generic Value Prop]. Let's connect!"* Prospects see through templated messages instantly. They know you sent the same note to 50 other people. There's no reason to respond – you haven't shown genuine interest in their specific situation. ### Failure Mode #2: Single-Touch Approach > 📈 Most prospects need several touchpoints before they're ready to engage. Single-message outreach converts at 1-2%. Multi-touch sequences hit 5-7%. Most salespeople send one connection request, maybe one follow-up, then move on. They're giving up right when the prospect is starting to recognize their name. The psychology is simple: familiarity breeds trust. One cold message from a stranger triggers skepticism. Five strategic touchpoints over three weeks build recognition and credibility. ### Failure Mode #3: Poor Timing > 📈 Connection requests sent within 48 hours of prospect activity (posting content, changing jobs) convert 2.3x better than arbitrary outreach. Timing isn't just about day of week or time of day. It's about relevance windows. When someone just published a post about a challenge you solve, they're thinking about that problem right now. That's your window. Sending a connection request three weeks later? The moment has passed. The attention has moved on. ### Failure Mode #4: Vanity Metrics Focus Here's the real problem: most teams track vanity metrics like "connection requests sent," and far fewer track "meetings booked per 100 requests." You optimize what you measure. If you're tracking volume (requests sent, messages delivered, profile views), you'll optimize for volume. You'll send more generic messages to more people. If you track outcomes (acceptance rate, response rate, meeting rate), you'll optimize for quality. You'll personalize more, test more, and improve conversion at each stage. > 📈 Teams tracking outcome metrics achieve 3.1x better pipeline contribution than teams tracking vanity metrics. ### The Real Problem: Spray-and-Pray Thinking All four failure modes stem from the same root cause: treating LinkedIn outreach as a numbers game instead of a relationship-building process. Spray-and-pray assumes more volume = more results. Send 200 generic messages, get 2-3 meetings. It's mathematically tempting but strategically broken. Data-driven outreach flips the model: send 50 highly personalized messages, get 15-20 meetings. Less volume, 10x better results. The rest of this guide shows you how to make that shift. ## The Data-Driven Approach: What It Means Data-driven outreach isn't about having fancy analytics dashboards (though those help). It's about making decisions based on what actually converts, not what sounds good in a LinkedIn post. Here's what changes when you go data-driven: ### 1. You Track Full-Funnel Conversion Rates Instead of tracking "messages sent," you track the complete funnel: - **Connection requests sent** → **Accepted** (acceptance rate) - **Accepted** → **Responded** (response rate) - **Responded** → **Meeting booked** (meeting rate) - **Meeting** → **Deal created** (deal conversion) ![The LinkedIn outreach conversion funnel: connection requests sent narrow to accepted, responded, meeting booked, and deal created, with the rate to track at each stage](/images/guides/linkedin-cold-outreach-funnel.webp) Now you can see exactly where prospects drop off. If your acceptance rate is 50% but your response rate is 5%, you know the problem: your first message after connecting isn't compelling. Fix the bottleneck, measure the improvement, repeat. ### 2. You A/B Test Everything Anecdotal advice says "be conversational" or "lead with value." Data-driven teams test it. - Does a conversational tone or professional tone work better for C-level prospects in fintech? - Does asking a question or offering a resource get more responses? - Does a short connection note (50 characters) or detailed note (200 characters) perform better? You don't guess. You test with 50 prospects per variant, measure the results, and scale what wins. > 📈 Sales teams that A/B test messaging achieve 2.4x higher response rates than teams using static templates. ### 3. You Use Behavioral Data for Personalization Generic personalization: *"* > Hi {{Name}}, I see you work at {{Company}}... Data-driven personalization: > Hi Sarah, saw your recent post about AI adoption in sales ops – your point about data quality resonated with our experience at... The difference? You're using real behavioral data (recent posts, job changes, content engagement) to show genuine relevance, not just filling in template variables. > 📈 Messages personalized with recent activity convert 5.4x better than name-only personalization. ### 4. You Optimize Continuously Data-driven outreach isn't "set it and forget it." You run weekly reviews: - What's the current acceptance rate? (**Target: >40%**) - What's the response rate? (**Target: >25%**) - What's the meeting conversion? (**Target: >15%**) - Which message variants are winning? - Where are prospects dropping off? Based on the data, you adjust targeting, tweak messaging, or change timing. Each week gets incrementally better. **Why this beats spray-and-pray:** Spray-and-pray is static. You send the same template to everyone, hoping volume compensates for low conversion. Data-driven outreach improves every week. Your 40% acceptance rate becomes 50%, then 55%. Your 20% response rate becomes 30%. ## Strategy #1: Start with Deep Profile Research > 📈 Connection requests with personalized notes referencing specific profile details achieve 3.2x higher acceptance rates than generic "I'd like to connect" messages. ### Why Deep Research Works When you reference specific details from someone's profile – a recent post, a shared interest, a mutual connection – you trigger reciprocity (you invested time researching them) and relevance (you understand their world). Generic templates signal the opposite: "I sent this to 100 people." Prospects ignore that instantly. ### How to Implement You don't need 30 minutes per prospect. You need the right data points: recent posts, job changes, shared interests, mutual connections. Then reference specific insights, not just topics. **Weak:** *"Hi Sarah, I saw your recent post about sales automation."* **Strong:** *"Hi Sarah, saw your post about AI in sales ops – your point about data quality being the real bottleneck resonated."* The second example proves you actually read the post and understood the nuance. ### Scaling with Automation Use [Linked API's Fetch Person method](/sdks/fetch-person) to pull profile data automatically (recent posts, job changes, mutual connections), store it in your CRM with dynamic variables, and generate personalized connection notes: > Hi {{FirstName}}, saw your recent post about {{PostTopic}} – {{ContextualComment}}.I work with {{CompanyType}} companies on similar challenges. Would love to connect. **Expected results:** - Generic template: 15-20% acceptance rate - Data-enriched personalization: 45-60% acceptance rate Automation handles data collection. You control message quality and relevance. ## Strategy #2: Multi-Touch Warming Before Pitching > 📈 Prospects who received 3+ touchpoints before a connection request had 2.7x higher acceptance rate and 4.1x higher response rate than cold single-touch outreach. ### Why Multi-Touch Warming Works Single-touch outreach triggers skepticism: "Who is this person?" Multi-touch warming triggers recognition: "I've seen this name before. They engaged with my content." The psychology: [mere exposure effect](https://en.wikipedia.org/wiki/Mere-exposure_effect) – repeated exposure increases liking and trust over time. When someone sees your name three times before you ask to connect (profile visit, post like, comment), you're not a stranger anymore. Recognition lowers resistance. ### How to Implement A typical warming sequence: 4-5 low-stakes touchpoints over 1-2 weeks: 1. **Visit their profile** (Day 1) – generates notification, plants your name 2. **Like their recent post** (48h later) – reinforces your name 3. **Comment on their post** (optional, 48h later) – adds value, positions you as knowledgeable 4. **Send personalized connection request** (2-3 days later) – reference the post in your note 5. **Send value-add message after acceptance** (2-3 days later) – don't pitch immediately ### Automating with Linked API Build workflows using n8n, Make, or Zapier: 1. [Visit Profile](/docs/action-st-open-person-page) → delay 48h 2. [Like Post](/docs/action-st-react-to-post) → delay 48-72h 3. [Send Connection Request](/docs/action-st-send-connection-request) → wait for acceptance 4. [Send Message](/docs/action-st-send-message) after 2-3 days (value-add, not pitch) For the connection-request mechanics in code – send, check acceptance, and withdraw stale invites – see [How to Automate LinkedIn Connection Requests](/guides/how-to-automate-linkedin-connection-requests); for the message-and-read-reply flow, see [How to Automate LinkedIn Messages](/guides/how-to-automate-linkedin-messages). This guide owns the strategy – what to say and when; those own the mechanics. **Expected results:** - Single-touch cold request: 15-20% acceptance, 10% response - Multi-touch warming: 40-50% acceptance, 30-40% response ## Strategy #3: Personalize Your Offer Based on Company-Specific Needs > 📈 Messages with company-specific value propositions tailored to actual business context and workflows convert 5.4x better than generic pitches. Real personalization isn't about mentioning someone's LinkedIn post. It's about demonstrating you understand what your product solves for their specific company based on their actual situation, workflows, and friction points. ### Why Offer Personalization Works Generic pitch: > Our tool helps sales teams automate LinkedIn outreach. Personalized offer (example for DevOps SaaS): > I noticed your team uses Happy CRM for inbound lead management. We help teams like yours generate automated lead files from LinkedIn outreach and push them directly to S3 buckets for automatic Happy CRM import – cutting manual CSV uploads from 2 hours/week to zero. The difference? The second example shows you understand their specific workflow (Happy CRM → S3 import), identify a real friction point (manual CSV uploads), and explain exactly how your product fits their situation. This approach works regardless of industry – the key is demonstrating you understand their specific challenges. ### How to Implement Offer Personalization at Scale **Step 1: Identify data points relevant to YOUR product and industry** What data you need depends entirely on what you sell and who you sell to. There's no universal "tech stack" checklist – it's specific to your value proposition. **Examples:** - DevOps SaaS tool → CRM used, automation tools, integration stack, current data flows - Agricultural equipment → hectares owned, crop types, geographic region, existing machinery - Real estate services → property portfolio size, geographic focus, recent transactions - HR software → company size, hiring velocity, ATS used, remote/hybrid policies The pattern: identify data that reveals **specific friction points your product solves**. **Step 2: Map your product's value to their specific situation** Create personalization templates based on the data points you identified in Step 1: **Example: If prospect uses Happy CRM:** > I noticed your team uses Happy CRM for inbound leads. We help teams automate LinkedIn outreach lead export → S3 bucket integration → automatic Happy CRM import. Eliminates the manual CSV upload step entirely. Relevant for your workflow? **Example: If prospect uses Salesforce + Zapier:** > Saw you're running Salesforce with Zapier. We plug into that exact stack – LinkedIn leads flow directly to Salesforce via Zapier webhook, maintaining field mapping and deduplication. Would this close a gap in your current outreach process? **Step 3: Use** [**Linked API's Fetch Company method**](/sdks/fetch-company) **to automate research** Pull company-level data relevant to your product. Examples vary by industry: - Tech/SaaS → technologies mentioned, recent hiring signals, job postings revealing needs - Agriculture → company size, geographic mentions, equipment/technology discussed - Real estate → property mentions, market focus, transaction activity - General → company size, growth signals, recent news/announcements **Step 4: Generate tailored value propositions** Instead of generic benefits, explain the specific solution to their situation: > Hi {{FirstName}},Noticed {{CompanyName}} {{SpecificSituation}}.Quick question: are you currently {{CurrentProcess}},or do you have {{DesiredState}}?We help teams {{SpecificSolution}} – {{QuantifiedBenefit}}.Relevant for your {{Context}}? Example variables for a DevOps SaaS tool: - `{{SpecificSituation}}` = "uses Happy CRM for lead management" - `{{CurrentProcess}}` = "exporting LinkedIn leads manually" - `{{DesiredState}}` = "an automated pipeline into Happy CRM" - `{{SpecificSolution}}` = "push LinkedIn leads directly to S3 for automatic Happy CRM import" - `{{QuantifiedBenefit}}` = "cuts manual CSV uploads from 2 hours/week to zero" - `{{Context}}` = "workflow" ### Practical Example: Happy CRM + S3 Integration Workflow **Scenario:** You sell a LinkedIn automation tool. Your prospect uses Happy CRM, which imports leads from S3 buckets automatically. **Your personalized offer:** > "Hi Michael,Saw that your team at {{Company}} uses Happy CRM for inbound lead management. Quick question: when your SDRs generate leads from LinkedIn outreach, are they manually exporting CSVs and uploading to S3, or do you have that automated?We integrate directly with Happy CRM's S3 import workflow – LinkedIn leads flow automatically to your bucket in Happy CRM's required format (JSON schema with contact fields + interaction history). Eliminates the manual export/upload step.Would this close a workflow gap for your team?" **Why this works:** - Shows you researched their specific situation (Happy CRM in this case) - Identifies a real friction point (manual CSV → S3 uploads) - Explains exact solution specific to their setup (S3 bucket, JSON schema) - Positions your product as a tailored solution, not a generic tool **Expected results:** - Generic pitch: 15-20% response rate - Context-personalized offer: 45-55% response rate ### The Key: Sell Specific Solutions, Not Generic Features Don't say "We do LinkedIn automation." Say "We integrate LinkedIn outreach data directly into your Happy CRM → S3 pipeline" (or whatever is relevant to THEIR specific context). Specificity proves relevance. ## Strategy #4: Timing Matters – When to Reach Out > 📈 Connection requests sent Tuesday-Thursday between 8-10 AM achieve 1.8x higher acceptance rates than weekend or evening requests. Requests sent within 48 hours of profile activity convert 2.3x better than arbitrary timing. ### Why Timing Works **Professional hours:** Tuesday-Thursday, 8-10 AM is when prospects are in work mode – checking LinkedIn, reviewing notifications, engaging with content. Weekends and late evenings? Personal time. Professional outreach feels intrusive. **Activity-based timing:** When someone just published a post about a challenge you solve, they're thinking about that problem right now. That's your relevance window. The psychology: attention availability + recency. Send a request three weeks later? The moment has passed. ### How to Implement Activity-Based Timing Trigger outreach based on prospect behavior: 1. **Published a post** (+2.3x acceptance) – send within 24-48 hours, reference the post 2. **Changed jobs** (+2.8x acceptance) – send within first 30 days, congratulate them 3. **Shared/commented on content** (+1.9x acceptance) – engage with same content, send request 24-48h later 4. **Company news** (funding, launch, expansion) (+1.5x acceptance) – reference the news ### Automating Activity Monitoring Use [Linked API's Fetch Person](/sdks/fetch-person) to monitor activity daily: - IF recent post exists → [Like Post](/sdks/react-to-post) → delay 24h → [Send Connection Request](/sdks/send-connection-request) - IF job change detected → delay 7 days → send congratulations + request **Expected results:** - Arbitrary timing: 25-30% acceptance - Activity-triggered: 50-60% acceptance ## Strategy #5: Track Real Metrics > 📈 Most teams track "requests sent" (vanity metric) while far fewer track "meetings booked per 100 requests" (outcome metric). Teams tracking outcome metrics achieve 3.1x better pipeline contribution. ### Why Outcome Metrics Work **Vanity metrics** (requests sent, profile views) measure activity, not revenue. You can send 500 requests and book zero meetings. **Outcome metrics** (acceptance rate, response rate, meeting rate) measure results that correlate with pipeline and revenue. When you track outcome metrics, you optimize for what actually matters: turning cold prospects into qualified conversations into closed deals. ## Strategy #6: A/B Test Your Messaging and Approach > 📈 Sales teams that A/B test messaging achieve 2.4x higher response rates and 1.9x more meetings booked than teams using static templates. ### Why A/B Testing Works Static templates assume one message fits all audiences. A/B testing reveals what actually resonates with your specific audience based on real behavior, not assumptions. ### Testing Rules 1. **Test one variable at a time** – message angle OR personalization depth OR CTA type (not all at once) 2. **Use statistical significance** – minimum 50 prospects per variant, ideally 100+ 3. **Segment by ICP** – what works for enterprise buyers might not work for SMB buyers 4. **Give tests time** – minimum 2 weeks before calling a winner ### What to Test **High-impact variables:** - **Message angle** – problem-solution vs question-based vs value-first (2-3x response lift) - **Personalization depth** – basic (name/company) vs contextual (recent activity) vs advanced (content engagement) (2-3x acceptance lift) - **CTA type** – direct meeting ask vs value offer vs question (1.5-2x response lift) **Example:** Split 100 prospects into two groups (50 each). Test problem-solution opening vs question-based opening. Keep everything else constant. After 2-3 weeks, scale the winner, retire the loser. Each test reveals 10-20% improvement. After 6 tests, you've compounded small improvements into massive overall gains. That's how teams go from 20% to 55% acceptance rates in 6 months. ## Strategy #7: Follow Up Strategically > 📈 Most sales require several touchpoints, yet many reps give up after one follow-up. Prospects who received 3+ follow-ups had 4.6x higher meeting conversion than single-message outreach. Strategic follow-ups aren't optional. They're where most of your results come from. ### Why Follow-Ups Work When a prospect doesn't respond to your first message, it usually doesn't mean "no." It means: "I'm busy," "I didn't see it," "I need to think," or "Timing isn't right this week." Strategic follow-ups increase your chances of catching them when they have bandwidth to engage. ### Follow-Up Rules 1. **Space follow-ups 3-7 days apart** – daily follow-ups = spam; weekly follow-ups = strategic persistence 2. **Add value in every follow-up** – share new resource, reference new data, offer different help (don't just bump the thread) 3. **Know when to stop** – after 4-5 follow-ups with zero response, move on ### 4-Touch Follow-Up Framework **Follow-up #1 (Day 3):** Add new value (share insight, article, case study). Expected response: 5-10% **Follow-up #2 (Day 7):** Share different resource (tool, template). Acknowledge they're busy. Expected response: 3-7% **Follow-up #3 (Day 14):** Pattern interrupt. Acknowledge timing might be off. Create exit ramp. Expected response: 2-5% **Follow-up #4 (Day 21) – Breakup:** Give permission to decline. One final value offer ("before I close this..."). Expected response: 3-8% (highest due to scarcity psychology) ### Why Breakup Messages Work The final "I'll stop bothering you" message often gets the highest response rate. Psychology: scarcity ("this is your last chance"), permission (giving them an out makes them more likely to engage), and contrast effect (after multiple value-add messages, you've built goodwill). ### Automating Follow-Ups Use [Linked API](/) to automate the sequence: - Day 3: Check for response → if none, send Follow-up #1 - Day 7: Check for response → if none, send Follow-up #2 - Day 14: Check for response → if none, send Follow-up #3 - Day 21: Check for response → if none, send Follow-up #4 (breakup) → remove from sequence **Expected results:** - Single message: 10-15% response rate - 4-touch sequence: 40-50% cumulative response rate ## Best Practices & Common Mistakes ### 👍 DO: - **Start with small test batches** – Validate message quality with 20-30 prospects before scaling - **Personalize using real data** – Reference recent posts, job changes, tech stack using [Linked API's Fetch Person](/sdks/fetch-person) - **Build multi-touch warming** – Visit profile → engage with content → wait 48h → send connection request - **Track outcome metrics** – Acceptance rate (>40%), response rate (>25%), meeting rate (>15%) - **Follow up strategically** – 4-touch sequence over 3-4 weeks, add value in every message - **Use automation for scale** – [Linked API](/) automates data collection and scheduling while maintaining personalization ### 👎 DON'T: - **Scale volume before testing** – Scaling a broken process burns through your prospect list faster - **Use generic templates** – One-size-fits-all messaging looks automated and gets ignored - **Pitch immediately after connecting** – Wait 2-3 days, send value-add message first - **Track only vanity metrics** – Volume ≠ results; track acceptance rate, response rate, meeting rate - **Ignore follow-ups** – most of your value comes from follow-ups #3-5 - **Automate without personalization** – Mass sending identical messages triggers LinkedIn spam detection ## Frequently Asked Questions (FAQ) #### What is the average LinkedIn cold outreach acceptance rate? Generic LinkedIn cold outreach achieves 20-30% acceptance rates. Data-driven personalized outreach achieves 50-60% – a 2-3x improvement. Key factors: deep profile research, timing based on activity signals (send within 48 hours of their post), multi-touch warming, and contextual personalization. If your acceptance rate is below 30%, your outreach is too generic. #### How many touchpoints does successful LinkedIn outreach require? 5-7 touchpoints spread over 2-4 weeks. Most prospects don't respond to single-message outreach. Typical sequence: profile visit → like/comment on content (48h later) → connection request (48-72h later) → first message after acceptance (2-3 days later) → 3-4 follow-ups. Single-touch outreach converts at 1-2%. Multi-touch sequences convert at 5-7% – a 4-6x improvement. #### Can I automate LinkedIn cold outreach without getting banned? Yes, if you use workflow-based automation that simulates human behavior. Safety rules: respect connection limits (50-100 requests/week max), use realistic delays (2-5 minutes between actions), personalize with real data, avoid bulk patterns, and monitor account health. [Linked API](/) provides cloud browsers with unique digital identities, built-in human behavior simulation, and automatic rate limiting. For detailed limits, see our [LinkedIn Limits Guide](/guides/understanding-linkedin-limits). #### What metrics should I track for LinkedIn outreach campaigns? Track outcome metrics, not vanity metrics. Focus on: Acceptance Rate (target >40%), Response Rate (target >25%), Meeting Rate (target >15%). Don't track: requests sent, profile views, messages delivered. Use [Linked API's Stats Method](/sdks/get-api-usage) to pull data automatically and calculate weekly. #### How do I personalize LinkedIn outreach at scale? Use [Linked API's Fetch Person method](/sdks/fetch-person) to extract profile data automatically (recent posts, job changes, mutual connections). Integrate with AI (ChatGPT, Claude) to generate contextual messages using extracted data. Send via Linked API with realistic delays. Data-enriched personalization achieves 50-60% acceptance rates vs 15-20% for generic templates. #### What's the best time to send LinkedIn connection requests Tuesday-Thursday, 8-10 AM in prospect's timezone converts 1.8x better than evenings/weekends. Activity-based timing is even better: send within 24-48 hours of their recent post (+2.3x acceptance) or job change (+2.8x acceptance). Use [Linked API](/) to monitor prospect activity and trigger outreach automatically when they're most receptive. #### How many follow-up messages should I send before giving up? 3-4 follow-up messages over 3-4 weeks. Most meetings come from follow-ups #3-5. Sequence: Day 3 (add new value, 5-10% response), Day 7 (share resource, 3-7% response), Day 14 (pattern interrupt, 2-5% response), Day 21 (breakup message, 3-8% response). Space messages 3-7 days apart, add value in every message, stop after 4-5 touches with no response. ## LinkedIn Connection Limit 2026: Complete Guide (Weekly, Daily, and Account-Specific) You've sent 201 connection requests this week, and suddenly LinkedIn blocks you. The message is vague, the timeline unclear, and panic sets in. This happens because LinkedIn's limits aren't what most guides claim. There's no universal "200 per week" rule – your actual capacity depends on your account reputation, not a fixed number. This guide decodes how LinkedIn's limit system really works through a myth-busting approach, so you can work strategically within your constraints. For technical deep-dives and other LinkedIn limits, see our [detailed limits guide](/guides/understanding-linkedin-limits). ## Myth #1: Everyone Has the Same 200/Week Connection Limit ### ✅ Truth: Limits Are Dynamic, Based on Account Reputation LinkedIn doesn't use fixed categories – your reputation exists on a **continuous gradient**. Your actual weekly capacity depends on account age, activity consistency, acceptance rate, and engagement quality – not a single fixed number. For clarity, we can think of this gradient in **three broad ranges**: | Reputation | Daily Limit | Weekly Limit | Key Factors | | --- | --- | --- | --- | | **New** | ~10-15 | ~50-75 | Recently created, minimal activity history | | **Established** | ~20-25 | ~100 | Active 3-12 months, regular posting/engagement | | **Trusted** | ~30-40 | Up to 200 | Long history, high acceptance rate (>40%), strong SSI | > ✍️ These ranges aren't rigid categories – your limit fluctuates continuously based on recent behavior and engagement patterns. ### The 500 Pending Request Rule Beyond weekly sending limits, LinkedIn watches how many of your invitations sit unanswered – its [help pages](https://www.linkedin.com/help/linkedin/answer/a551012) list "many of your invitations have been ignored, left pending" among restriction triggers, without publishing a numeric cap. As a community-observed guardrail, keep outgoing (pending) connection requests under **500** (restrictions are commonly reported around 700). Exceeding 500 pending requests signals poor targeting and low acceptance rate. This hurts your reputation faster than hitting weekly send limits. > ✍️ **Best practice:** withdraw unaccepted connection requests after 2-3 weeks to maintain account health. You can [automate the whole send-track-withdraw lifecycle](/guides/how-to-automate-linkedin-connection-requests). ## Myth #2: Buying LinkedIn Premium Increases Your Connection Limits ### ✅ Truth: Premium Doesn't Affect Connection Request Limits **Premium, Sales Navigator, and Recruiter Lite do NOT increase your weekly connection request capacity.** Your limits remain reputation-based regardless of subscription tier. **Why the confusion?** Premium users appear to send more requests because they have better targeting tools (InMail, advanced search filters), not higher connection limits. If your only goal is increasing connection capacity, Premium won't help. Focus on building account reputation instead. ## Myth #3: Weekly Limits Reset Every Monday ### ✅ Truth: LinkedIn Uses Rolling 7-Day Windows LinkedIn's limits reset **7 days from when you send each request**, not on calendar weeks (Monday-Sunday). This creates a rolling window that tracks your last 7 days of activity at any given moment. **Example scenario:** - **Monday:** Send 30 requests - **Tuesday:** Send 40 requests (rolling 7-day total: 70) - **Wednesday:** Send 50 requests (rolling 7-day total: 120) - **Following Monday:** Only the 30 requests from previous Monday "expire" from your count, not all 120 ![LinkedIn's rolling 7-day connection-request window: 30 sent Monday, 40 Tuesday, 50 Wednesday reach a rolling total of 120, and the next Monday only the first 30 expire, dropping the count to 90](/images/guides/linkedin-connection-limit-rolling-window.webp) **Why this matters:** Bulk-sending 100+ requests on Monday morning burns through your weekly quota immediately and triggers spam detection. Spread requests evenly throughout the week (20-30 per day) for consistent capacity and natural behavior patterns. ## Myth #4: Once You Hit the Limit, You're Blocked for Exactly 7 Days ### ✅ Truth: Restriction Types and Timelines Vary Hitting the invitation limit does not put you on a fixed seven-day clock. LinkedIn treats invitation restrictions separately from policy restrictions, and per its [invitation restrictions page](https://www.linkedin.com/help/linkedin/answer/a551012), "Most restrictions will automatically be removed within one week" – and "LinkedIn won't be able to remove invitation restrictions upon request", so contacting Support will not speed it up. For the full picture – which restriction type you have, the documented route back for each, and which durations LinkedIn actually publishes – see our guide to a [restricted LinkedIn account](/guides/linkedin-account-restricted). ## Myth #5: It's Impossible to Track Your Limits and Reputation Signals ### ✅ Truth: Automated Monitoring Is Straightforward with Linked API Manually tracking connection limits requires logging into LinkedIn daily, counting pending requests, calculating acceptance rates, and checking your SSI score separately – tedious, time-consuming, and error-prone. **Linked API provides dedicated actions** to automate this monitoring: - [**Retrieve SSI**](/sdks/retrieve-ssi) – Automatically pulls your current Social Selling Index (SSI) score*.* - **Use case:** Track SSI weekly to monitor engagement quality. Drops in SSI often signal declining reputation or approaching spam territory. - [**Retrieve Performance**](/sdks/retrieve-performance) – Gets LinkedIn dashboard analytics (profile views, search appearances, post engagement)*.* - **Use case:** Monitor overall account health. Declining performance metrics (fewer profile views, lower engagement) often precede connection limit restrictions. - [**Get Actions Statistics**](/sdks/get-api-usage) – Returns requests sent, acceptance rate, and pending request count*.* - **Use case:** Calculate real-time acceptance rate and pending ratio to maintain reputation. Essential for catching problems early. **Workflow Example (n8n/Make/Zapier):** ``` Weekly Account Health Check: 1. Schedule trigger: Every Monday 9 AM 2. Retrieve SSI → Log to Google Sheets 3. Get Actions Statistics → Calculate: - Acceptance rate (last 30 days) - Pending requests count - Requests sent (rolling 7 days) 4. Conditional alerts: - IF acceptance rate <30% → Slack: "🚨 Slow down outreach" - IF pending requests >400 → Slack: "⚠️ Withdraw old requests" - IF rolling 7-day total >150 → Slack: "⏸️ Approaching limit" ``` **Why proactive monitoring prevents blocks:** Users who track these metrics proactively rarely experience hard blocks. You catch declining reputation signals before LinkedIn restricts you, adjust strategy in real-time based on data (not guesswork), and maintain long-term account health rather than reactive damage control. ## Myth #6: You Need 'Hacks' to Scale Beyond Limits ### ✅ Truth: Quality Over Quantity Wins Every Time **Case study comparison:** - **Approach A:** 50 personalized requests/week with 60% acceptance rate = 30 new connections - **Approach B:** 200 generic requests/week with 15% acceptance rate = 30 new connections Same outcome, but Approach A maintains account reputation and avoids restrictions. Approach B burns through your limit, tanks acceptance rate, and risks hard blocks. ### Smart Strategies Within Limits **1. Multi-touch engagement before connecting** Warm up prospects before sending requests. Sequence: Visit profile → Like/comment on recent post → Wait 2-3 days → Send personalized connection request mentioning shared interest or recent content. > ✍️ Higher acceptance rates from warm prospects improve your reputation, which gradually increases your limits over time. **2. Tight ICP targeting** Define your Ideal Customer Profile precisely. Use LinkedIn's advanced search filters (job title, company size, industry, seniority) to target only high-fit prospects. > ✍️ Better targeting = higher acceptance rate = better reputation = higher limits. This creates a positive feedback loop. **3. Consistent pacing (not bulk sending)** Send 20-30 requests per day consistently instead of 150 on Monday followed by nothing. LinkedIn's algorithm rewards natural, consistent behavior patterns. > ✍️ Bulk activity triggers spam detection regardless of your limits. **Linked API workflow example:** ``` Smart Outreach (30-40 requests/week, 50-70% acceptance): 1. Search People (Linked API) → Filter by ICP criteria 2. For each prospect: - Fetch Person → Get recent activity, posts, shared interests - Visit profile (simulated engagement) - IF they posted in last 7 days: - Extract post content - Generate contextual comment (ChatGPT integration) - Comment on Post (Linked API) - Wait 2-3 days (natural pacing) - Generate personalized connection note mentioning shared interest - Send Connection Request (Linked API) Result: 50-70% acceptance rate vs. 15-20% from generic templates ``` > ✍️ Automation doesn't mean sending more requests. It means personalizing at scale while respecting your limits and maintaining account health. For the connect → check-acceptance → message flow in code, see [How to Automate LinkedIn Messages](/guides/how-to-automate-linkedin-messages); to automate sending, tracking, and withdrawing the requests themselves, see [How to Automate LinkedIn Connection Requests](/guides/how-to-automate-linkedin-connection-requests). ## Best Practices & Common Mistakes ### 👍 DO: - **Know your reputation level** – Set realistic weekly targets: New accounts (~50-75/week), Established (~100/week), Trusted (up to 200/week) - **Target your ICP tightly** – Higher acceptance rate improves reputation and gradually increases your limits over time - **Warm up prospects first** – Multi-touch engagement (profile visit, post comment) before connecting boosts acceptance rate - **Personalize every request** – Custom notes dramatically increase acceptance (even Free accounts get a limited number of personalized invitations per month, per [LinkedIn Help](https://www.linkedin.com/help/linkedin/answer/a563153)) - **Monitor metrics weekly** – Track SSI, acceptance rate, and pending count using [Linked API](/) - **Maintain >40% acceptance rate** – LinkedIn rewards high acceptance rates with better reputation and potentially higher limits - **Withdraw old pending requests** – Keep pending count under 400-500; withdraw requests older than 2-3 weeks - **Spread requests evenly** – Send 20-30 per day consistently instead of 150 on Monday ### 👎 DON'T: - **Bulk send 100+ on Monday morning** – Unnatural behavior pattern triggers spam detection algorithms - **Ignore pending request ratio** – Exceeding 500 pending requests signals poor targeting and hurts reputation - **Use generic templates** – Low acceptance rates from "I'd like to connect" messages limit future capacity - **Skip metric monitoring** – You'll miss early warning signs before LinkedIn restricts your account - **Connect with everyone** – Shotgun approach tanks acceptance rate and wastes limited request slots - **Buy Premium for higher limits** – Premium doesn't increase connection limits (it only adds InMail, custom notes, advanced search) ## Frequently Asked Questions (FAQ) #### What is the LinkedIn connection limit in 2026? There's no single universal limit. LinkedIn uses a **reputation-based gradient** – your capacity ranges from ~50-75 requests/week for new accounts, ~100/week for established accounts, up to 200/week for highly trusted accounts. Your actual limit depends on account age, activity consistency, and acceptance rate – not fixed categories. #### Does LinkedIn Premium increase connection request limits? No. Premium subscriptions don't affect connection request limits at all. Premium adds [InMail credits](/guides/linkedin-inmail) (direct messaging without connecting), unlimited custom notes on requests, advanced search filters, and Open Profile messaging – but your weekly connection request cap remains reputation-based regardless of subscription tier. #### How do I know what my actual connection limit is? LinkedIn doesn't publicly display your exact limit. Best approach: monitor key metrics (SSI score, acceptance rate, pending requests) using Linked API monitoring actions and test conservatively. Start with 15-20 requests per day and adjust based on whether you encounter restrictions. #### What happens if I exceed my connection limit? You lose the ability to send new invitations while everything else on the account keeps working. LinkedIn publishes one figure for this: "Most restrictions will automatically be removed within one week", and Support cannot lift one on request or tell you the reason. Longer or account-wide consequences belong to other restriction types – our guide to a [restricted LinkedIn account](/guides/linkedin-account-restricted) covers which type is which and the documented route back for each. #### Can I track my SSI score and acceptance rate automatically? Yes, using Linked API's dedicated actions: [Retrieve SSI](/sdks/retrieve-ssi) pulls your Social Selling Index score, [Retrieve Performance](/sdks/retrieve-performance) gets dashboard analytics, and [Get Actions Statistics](/sdks/get-api-usage) calculates acceptance rate and pending count. Automate these in workflows (n8n, Make, Zapier) to monitor proactively. #### What is a good acceptance rate for LinkedIn connection requests? Aim for **>40% acceptance rate**. LinkedIn rewards high acceptance rates with better account reputation and potentially higher limits over time. Below 30% signals poor targeting and may trigger restrictions. Track acceptance rate weekly to catch declining trends early. #### How many pending connection requests is too many? Keep pending requests under **500** – a community-observed guardrail, since LinkedIn publishes no numeric cap; restrictions are commonly reported around 700. Exceeding 500 pending requests signals poor targeting and low acceptance rate, which hurts your reputation. Withdraw requests older than 2-3 weeks regularly to maintain account health and stay below this threshold. #### Can I bypass LinkedIn connection limits safely? No, there's no safe "bypass." The smart approach: work strategically within your limits by focusing on quality over quantity. Use multi-touch engagement (warm up prospects before connecting), tight ICP targeting (only connect with high-fit prospects), and personalization to maximize results with fewer requests. [Linked API](/) automates this smarter approach while respecting limits. ## Understanding LinkedIn Limits: Comprehensive Overview When using [Linked API](/docs), it's necessary to understand the limitations of your LinkedIn account to avoid potential account restrictions or blocks. While LinkedIn doesn't publish official limits, this guide combines community knowledge and our hands-on expertise. If a restriction has already been applied to your account, start instead with our guide to a [restricted LinkedIn account](/guides/linkedin-account-restricted), which covers the documented types and the route back for each. > Account limits primarily depend on two factors: **subscription type** (Premium, Sales Navigator, Recruiter etc.) and **reputation level**. Throughout the article, we divide reputation into 3 virtual levels: | Reputation Level | Account Characteristics | | --- | --- | | New | Recently created with minimal activity and engagement. | | Established | Active for 3–12 months with growing engagement and network size. | | Trusted | Long-established with a strong presence and proven activity patterns. | While we use these division for simplicity, think of reputation as a continuous gradient rather than fixed categories. Consider [Social Selling Index (SSI)](/guides/linkedin-social-selling-index) as one of the indirect metrics to assess your account's reputation. ## Connection request limits Connection requests are limited on a rolling weekly basis: | Reputation Level | Daily Limit | Weekly Limit | Notes | | --- | --- | --- | --- | | New | ~10-15 | ~50-75 | Start with 5-10/day and gradually increase. | | Established | ~20-25 | ~100 | Standard LinkedIn weekly cap. | | Trusted | ~30-40 | Up to 200 | Requires high acceptance rate. | **Good to know:** - Keep outgoing (pending) connection requests under 500 (a community-observed guardrail – LinkedIn publishes no numeric cap, and restrictions are commonly reported around 700) by [withdrawing unaccepted requests](/docs/working-with-invitations). - Personal notes don't increase limits. Free accounts are limited to ~10 noted invites monthly, while any paid plan removes this restriction. - Monitor acceptance rates – higher acceptance rates may increase your limits. ## Direct message limits There is no strict limitation, but high volumes trigger spam flags. Here are our recommended limits: | Reputation Level | Daily Limit | Notes | | --- | --- | --- | | New | ~50 | Monitor closely for spam flags. | | Established | ~100 | Moderate and steady pace recommended. | | Trusted | ~100-150 | Requires high engagement rates. | **Good to know:** - Personalize messages to maintain higher allowed volumes. - Monitor response rates – ignored messages can lower limits. - Consistent engagement is better than irregular bulk sending. To automate messages and connection requests while staying inside these limits, see [How to Automate LinkedIn Messages](/guides/how-to-automate-linkedin-messages). ## InMail and Open Profile message limits InMail works differently from the behavioral limits on this page: allowances are plan-defined monthly credits (5–50 depending on subscription, more on Recruiter products), not reputation-based caps, and a credit is returned whenever the recipient responds within 90 days. The full breakdown – credits and accumulation caps per plan, the credit-back rule, purchase rules, character limits, and the Open Profile exception that lets anyone message opted-in Premium members free – lives in the [LinkedIn InMail guide](/guides/linkedin-inmail). Pacing still applies here as everywhere: spread InMails across days even when credits are available, and keep Open Profile outreach at a steady, human volume – LinkedIn limits it without publishing a number. ## Company page view limits LinkedIn sets different limits for viewing company pages based on subscription type: | Subscription Type | Daily Views | Weekly Views | | --- | --- | --- | | Free | ~100 | ~700 | | Premium (all plans) | ~200 | ~1400 | | Sales Navigator (all plans), Recruiter (all plans) | ~200 / 1000 * | ~1400 / 7000* | **Good to know:** - Sales Navigator and Recruiter higher limits (1000/day) apply only when using their respective interfaces. - While there's no direct correlation with account reputation, your actual limits may slightly increase or decrease depending on the reputation level. ## Person profile view limits LinkedIn sets different limits for viewing person profiles based on subscription type: | Subscription Type | Daily Views | Weekly Views | | --- | --- | --- | | Free | ~80 | ~560 | | Premium (all plans) | ~150 | ~1050 | | Sales Navigator (all plans), Recruiter (all plans) | ~150 / 1000* | ~1050 / 7000* | **Good to know:** - Sales Navigator and Recruiter higher limits (1000/day) apply only when using their respective interfaces. - While there's no direct correlation with account reputation, your actual limits may slightly increase or decrease depending on the reputation level. ## Search query limits LinkedIn officially defines **Commercial Use Limit (CUL)** for search operations, regardless of whether you're searching for people, companies, or other content. | Subscription Type | Monthly Limit | Daily Recommended | Notes | | --- | --- | --- | --- | | Free | 300 | ~10 | Resets on 1st day of calendar month. | | Premium Career | 300 | ~10 | Resets on 1st day of calendar month. | | Premium Business | Unlimited | - | No monthly cap. | | Sales Navigator (all plans), Recruiter (all plans) | Unlimited | - | No monthly cap. | **Good to know:** - When Commercial Use Limit (CUL) is reached, users receive a notice to wait until the next calendar month (resets on the 1st), per [LinkedIn Help](https://www.linkedin.com/help/linkedin/answer/a564226). - Only business-oriented subscriptions (Premium Business, Sales Navigator, Recruiter) remove the Commercial Use Limit. - Certain search types don't count towards CUL, such as viewing 1st-degree connections, job searches, or searching by specific names. ## Search results limits LinkedIn limits the number of search results displayed within one search session **based on the interface used, not subscription type**. Here's a comparison between LinkedIn interfaces: | Interface | People Search | Companies Search | | --- | --- | --- | | Standard LinkedIn | 1000 (100 pages × 10) | 1000 (100 pages × 10) | | Sales Navigator | 2500 (100 pages × 25) | 1000 (40 pages × 25) | | Recruiter | 1000 (100 pages × 10) | 1000 (100 pages × 10) | **Good to know:** - To bypass search results limit, break your search into narrower segments (e.g. by region or industry) so each query shows fewer than the limit maximum. - Viewing search results doesn't count towards profile/company view limits. These limits only restrict how many results are displayed per search query. ## Jobs browsing limits Unlike the surfaces above, no concrete jobs-specific limits have surfaced in the community yet, so automated job search and viewing are bounded mostly by behavioral rate limiting rather than fixed caps. | Operation | Limit | Notes | | --- | --- | --- | | Job search (Commercial Use Limit) | Exempt | Doesn't count towards the CUL, unlike people or company search, and isn't affected by subscription tier. | | Job search results per query | ~1000 (100 pages × 10) | Same display cap as people search; split into narrower searches to see more. | | Individual job views | No published limit | Opening a job posting isn't a profile or company page view and doesn't consume those quotas. | | Daily job applications (Easy Apply) | ~50 | Soft daily throttle for Free and Premium alike, plus a speed limit that briefly pauses Easy Apply on rapid submissions. | **Good to know:** - Pace matters more than counts: paging through hundreds of results or opening postings back-to-back still trips anti-automation systems despite the exemptions above. Keep a human-like pace of a few seconds between actions. - Reputation still governs safe volume – ramp up bulk job viewing gradually, just as you would profile views, especially on new accounts. ## Likes and comments limits LinkedIn monitors your likes and comments, focusing on frequency and quality of interactions rather than strict numerical limits. Here are approximate daily thresholds based on account reputation: | Reputation Level | Daily Likes | Daily Comments | | --- | --- | --- | | New | ~20-30 | ~10-15 | | Established | ~40-60 | ~20 | | Trusted | ~100 | ~30 | **Good to know:** - Regardless of your reputation level, start with lower volumes and gradually increase interactions over time to reach your limit organically. ## Post publishing limits While LinkedIn doesn't enforce strict posting limits, excessive posting may trigger spam filters or diminish overall reach. Here are recommended daily posting frequencies: | Reputation Level | Recommended Posts per Day | | --- | --- | | New | 1-2 | | Established | 3-5 | | Trusted | 5-7 | **Good to know:** - Focus on content quality over quantity: one engaging post typically generates better results than multiple average-quality posts. ## Other LinkedIn limits | Category | Limit | | --- | --- | | Total network connections | 30,000 | | Outgoing (pending) connection requests | ~700 (community-observed – no official cap published) | | Group memberships | 100 | | Connection request note length | 200 characters (Free), 300 (Premium) | | Standard message length | 8,000 characters | | InMail subject length (LinkedIn UI) | 200 characters | | InMail message length (LinkedIn UI) | 2,000 characters | ## Enforcing limits with Linked API Linked API allows you to configure action limits for each connected LinkedIn account directly from the [platform](https://app.linkedapi.io/). Limits are organized by action category (profile views, connection requests, messages, etc.) and time period (daily, weekly, monthly). When a workflow contains an action that would exceed a configured limit, the action will return a `limitExceeded` error instead of executing. This helps you stay within safe boundaries automatically, without needing to track usage manually. For more details on how errors are returned, see the [documentation](/docs/executing-workflows). > This guide reflects common LinkedIn limits based on community knowledge and hands-on experience. It should be used as a reference point rather than official guidance, as actual limits vary by account and may change over time. Experiment carefully with your own limits while monitoring account health and engagement metrics. --- # Comparisons ## Linked API vs Dripify (2026): Features, Pricing, and Safety Compared Linked API vs Dripify is a choice between two shapes of LinkedIn automation. Linked API is a programmable LinkedIn automation API: you compose workflows from primitives and run them from your own code or an AI agent, with the execution layer – a dedicated cloud browser per account, human pacing, enforced limits – built in. Dripify is a no-code tool for LinkedIn and email drip campaigns: you assemble a fixed sequence of steps in a visual builder and it runs in Dripify's cloud. > **The short version.** Both are cloud tools that automate your own account and let you set daily activity limits; they differ in how you connect and how the pacing is enforced. Linked API connects your account into its own dedicated cloud browser that behaves like a person – a real browser on LinkedIn's own pages, and Linked API never stores your password – and enforces the daily limits you set. Dripify instead takes your LinkedIn password and uses it to obtain your session cookies, then drives your account through those. Beyond safety, Linked API covers two jobs: embed LinkedIn automation into your own product through 55+ composable actions, or run it out of the box – a ready-made skill lets an AI agent build and run the workflows for you. Dripify fits sales, founder, and recruiting teams who want a ready-made drip-campaign builder with a built-in inbox and analytics, no code involved. Pick by the job, not the brand. ## Pick in 10 seconds :::cards **Choose Linked API if…** - You need automation that is not a fixed campaign: compose [55+ actions](/docs/actions-overview) – search, fetch profiles and companies, connect, message, react, post – into any logic, branching on real profile data or your CRM. - You want a real developer surface: a REST API, Node/Python SDKs, and a shell CLI to embed LinkedIn automation in your product – or a ready-made skill an AI agent runs out of the box. Dripify has no public API. - You want predictable cost: flat per-seat pricing with no usage metering, and per-action limits enforced in a dedicated cloud browser. **Choose Dripify if…** - You want a ready-made, no-code drip-campaign builder: assemble a sequence from 15+ actions and conditions in a visual editor, with nothing to code. - You need the campaign extras built in: sequence templates, a unified reply inbox, analytics dashboards, and team management, plus an email channel alongside LinkedIn. - A standard drip campaign is exactly the job – you are not trying to build automation into your own product or branch on custom logic. ::: ## Linked API vs Dripify at a glance | | Linked API | Dripify | |---|---|---| | What it is | Programmable LinkedIn automation API | No-code LinkedIn + email drip-campaign tool | | Model | Compose 55+ actions into any workflow – from your code or an AI agent | Assemble a fixed sequence of steps in a visual campaign builder | | Developer surface | REST API, Node/Python SDKs, shell CLI, MCP server, agent skills | No public API – Zapier, Make, and outbound webhooks only | | Channels | LinkedIn, including Sales Navigator | LinkedIn and email | | Account connection | Your own [personal cloud browser](/safety) – Linked API never stores your password | You give Dripify your LinkedIn password; it uses that to obtain your session cookies | | Entry price | From $49/mo per seat, billed annually ($69 month-to-month) | Basic $39/user/mo billed annually ($59 month-to-month), per Dripify's pricing page | | Pricing model | Flat per seat, no usage metering | Per user/seat, tiered (Basic / Pro / Advanced / Enterprise) | | Safety model | Dedicated cloud browser behaving like a person; per-action limits you configure and the platform enforces | Cloud execution with daily activity limits you set, optionally auto-adjusted on Advanced | | Free trial | 7 days | 7 days | | Best for | Building automation into a product or AI-agent workflow | Ready-made no-code drip campaigns | Shopping a full list rather than a head-to-head? See the [best Dripify alternatives](/blog/dripify-alternatives). ## Programmable API or a fixed campaign builder? This is the distinction that decides the choice. Linked API gives you primitives – search, fetch profiles and companies, connect, message, react, comment, post – and you compose them into whatever logic you want, branching on any data rather than a preset checklist. Filter on a real profile field, pull data from your CRM mid-flow, trigger from your app: ![A programmable API versus a fixed drip sequence: Linked API exposes primitives you compose into any logic inside your own product, while Dripify runs a fixed templated sequence in its cloud](/images/vs/dripify-sequence-vs-api.webp) Dripify is a different shape: a closed no-code UI where you arrange a fixed sequence of steps – its help center describes building campaigns "by selecting automated LinkedIn actions, delays, and conditions" (Dripify's help center) – that runs in Dripify's cloud. It does offer branching, but from a fixed set of four predefined conditions (connected, message viewed, email available, open profile), each "a one-time action that checks the status at a specific moment" (Dripify's docs). It reaches its edge the moment your logic needs a field those conditions do not cover, or you want the automation to live inside your own product. Concretely, the primitive model looks like this: ```typescript import LinkedApi from '@linkedapi/node'; const linkedapi = new LinkedApi({ linkedApiToken: process.env.LINKED_API_TOKEN, identificationToken: process.env.IDENTIFICATION_TOKEN, }); const search = await linkedapi.searchPeople.execute({ term: 'head of growth', filter: { locations: ['United States'] }, }); const { data: people } = await linkedapi.searchPeople.result(search.workflowId); for (const person of people ?? []) { // Branch on a real profile field - your rule, not a fixed template const profile = await linkedapi.fetchPerson.execute({ personUrl: person.publicUrl }); const { data } = await linkedapi.fetchPerson.result(profile.workflowId); if (!data || (data.followersCount ?? 0) < 1000) continue; const req = await linkedapi.sendConnectionRequest.execute({ personUrl: person.publicUrl, note: `Hi ${person.name.split(' ')[0]}, loved your growth work, let's connect.`, }); await linkedapi.sendConnectionRequest.result(req.workflowId); } ``` ```python from linkedapi import ( LinkedApi, LinkedApiConfig, SearchPeopleParams, FetchPersonParams, SendConnectionRequestParams, ) linkedapi = LinkedApi( LinkedApiConfig( linked_api_token="your-linked-api-token", identification_token="your-identification-token", ) ) search = linkedapi.search_people.execute( SearchPeopleParams(term="head of growth", filter={"locations": ["United States"]}) ) people = linkedapi.search_people.result(search.workflow_id).data or [] for person in people: # Branch on a real profile field - your rule, not a fixed template profile = linkedapi.fetch_person.execute(FetchPersonParams(person_url=person.public_url)) data = linkedapi.fetch_person.result(profile.workflow_id).data if not data or (data.followers_count or 0) < 1000: continue req = linkedapi.send_connection_request.execute( SendConnectionRequestParams( person_url=person.public_url, note=f"Hi {person.name.split(' ')[0]}, loved your growth work, let's connect.", ) ) linkedapi.send_connection_request.result(req.workflow_id) ``` And this is not something you can bolt onto Dripify. Its integration surface is Zapier, Make, and outbound webhooks – "native integrations and modern webhooks let you easily connect Dripify with all the software your team already uses" (Dripify's integrations page) – with no public REST API to call. Those webhooks are data-out only: Dripify's own docs state they "can transfer lead's data ONLY … not … message content and other data", fire on a fixed event set (invite sent, message sent, reply), allow "only one condition per webhook integration", and are available on the Pro tier and up. Useful for piping leads into a CRM; not a surface for building automation on. Use Linked API's primitives two ways, both first-class: build them into your product through the REST API, SDKs, and CLI, or hand a ready-made [agent skill](/skills) to an AI agent that composes and runs the same workflows from a plain-language ask. ## What does account safety look like on each? Both products automate your own LinkedIn account from the cloud, both let you set daily activity limits, and both are unofficial – neither is a LinkedIn product. Two differences matter: how each connects to your account, and how limits are enforced. **How each connects.** Linked API connects the account into its own [personal cloud browser](/safety) – a real browser loading LinkedIn's own pages – and never stores your LinkedIn password. Dripify works the other way around: its onboarding step is titled "Enter Your LinkedIn Account Credentials", and it stores that password encrypted (Dripify's help center) to obtain your LinkedIn session cookies and drive your account through them. The difference in one line: your account lives in a real browser that stays yours, versus your password and session cookies held on Dripify's side. **How limits are enforced.** Linked API runs each action in a dedicated cloud browser where it [behaves like a person](/safety) on the real pages – a simple visit-and-like takes around 20 seconds by design – and executes workflows sequentially at human pace. You [configure per-action limits](/docs/admin-limits) once, and any action that would exceed one returns a `limitExceeded` error instead of running; our [limits guide](/guides/understanding-linkedin-limits) covers sensible values. Dripify also lets you set daily limits per action type, runs "in the cloud" with "random delays to make sure your LinkedIn activity looks manual" (Dripify's safety page), and on its Advanced plan adds an optional "activity control" feature that auto-adjusts your connecting and messaging limits. Those are daily caps Dripify applies, rather than a per-action gate enforced in a dedicated browser as each action runs. Neither model is free of risk – automating a real account never is. The honest framing: Linked API runs your account in its own real [personal cloud browser](/safety) and enforces limits per action, while Dripify holds your password and the session cookies it derives from it, applying daily caps from its side, with optional auto-adjust on its top tier. ## How does Linked API pricing compare with Dripify? | Plan | Price | Notes | |---|---|---| | Linked API – Core | $49/mo per seat billed annually; $69 month-to-month | Unlimited workflow execution within the limits you configure | | Dripify – Basic | $39/user/mo billed annually; $59 month-to-month | One drip campaign; no webhooks | | Dripify – Pro | $59/user/mo billed annually; $79 month-to-month | Unlimited campaigns, dedicated inbox, CSV export, webhooks | | Dripify – Advanced | $79/user/mo billed annually; $99 month-to-month | Multi-team management, advanced protection, step analytics | Dripify prices are per user/seat as displayed on its pricing page; both tools bill per seat and offer a 7-day free trial. The seats hold different things. A Linked API seat is one LinkedIn account driven by a full API, SDKs, and CLI, with no metering on how much you run. A Dripify seat is a campaign builder for one user; its programmatic surface – webhooks – unlocks at Pro ($59/user annual) and even then pushes lead data out rather than letting you build. At entry, Dripify's Basic ($39/user annual) sits just below Linked API's Core ($49/seat annual), and Basic is capped at a single campaign with webhooks unavailable until Pro. The seat prices are close; what each seat carries – a developer surface or a no-code builder – is the substantive difference. ## Frequently Asked Questions (FAQ) #### Can Dripify's webhooks replace Linked API? No – they solve different problems. Dripify's webhooks are outbound notifications: they "can transfer lead's data ONLY" (not message content), fire on a fixed set of events with one condition per integration, and unlock on the Pro tier and up (Dripify's help center). They can push a lead into your CRM when a reply comes in, but they cannot make LinkedIn do anything or return rich data on demand. Linked API is the inverse – an inbound control surface: your code calls [send message, fetch profile, search, connect](/docs/actions-overview) and gets structured JSON back. If you need to trigger and compose LinkedIn actions from your own stack rather than just receive event data, a webhook is not a substitute for an API. #### Can I move my Dripify campaigns to Linked API? The lead data moves; the campaign is rebuilt as code or an agent workflow. Export your leads from Dripify (CSV export is available on Pro and up) and feed the LinkedIn URLs into Linked API [actions](/docs/actions-overview) as parameters. Each campaign step maps to an action – a connection request with a note, a follow-up message, a profile view – chained with `then` and iterated with `doFor`, so the same sequence becomes a workflow the platform runs for you, with the branching logic a fixed template could not express. The account runs in its own [personal cloud browser](/safety), and piloting one account while Dripify runs the rest is the low-risk path. #### Is Linked API safe to use compared with Dripify? Both automate your own account from the cloud with human-like pacing and configurable daily limits, and both are unofficial tools that carry the risk any LinkedIn automation does. Two differences: Linked API runs your account in its own [personal cloud browser](/safety) and enforces the [per-action limits you configure](/docs/admin-limits), while Dripify holds your LinkedIn password and the session cookies it derives from it, applying daily caps (auto-adjusted by activity control on its top tier). Accounts that behave like people are the ones that keep working – on either tool, conservative limits matter more than the brand. #### Do I need Sales Navigator for either tool? Not for standard LinkedIn automation. Linked API needs Sales Navigator only for its `nv-*` actions, not for standard people, company, or post data; Dripify states it is "100% compatible with Free LinkedIn, Premium, Sales Navigator and Recruiter Lite accounts" (Dripify's help center), documenting Sales Navigator setup through its browser extension. Either works on a free or Premium account for core outreach. --- Automating LinkedIn for your team, product, or AI agent? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually, with a 7-day free trial – and use it both ways: build it into your product with the [REST API and Node/Python SDKs](/sdks/installation) and the shell [CLI](/cli/getting-started), or get automations out of the box through the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`). *Facts verified July 24, 2026 – Dripify prices, quotes, and feature coverage checked against its live pricing page, features pages, and help center on that date.* ## Linked API vs Expandi (2026): Features, Pricing, and Safety Compared Linked API vs Expandi is a choice between two shapes of LinkedIn automation. Linked API is a programmable LinkedIn automation API with safety built into the execution layer: you compose workflows from primitives and run them from your own code or an AI agent, and every action runs in a dedicated cloud browser per account, at a human pace, within limits the platform enforces. Expandi is a no-code LinkedIn outreach tool that emphasizes managed safety features – an automatic warm-up ramp and vendor-tuned daily limits – with conditional campaign sequences you assemble in a visual builder. > **The short version.** Both automate your own account from the cloud, and both are built with account safety in mind – they take different approaches. Linked API builds safety into how every action runs: a dedicated cloud browser that behaves like a person, per-action limits the platform enforces, and never stores your LinkedIn password. Expandi layers managed safety features onto a credential-based model – you enter your LinkedIn password for Expandi to obtain your session, with an automatic warm-up that ramps a new account from 5 to 21 actions a day and daily limits from vendor-defined presets. Beyond safety, Linked API covers two jobs a no-code tool cannot – embed LinkedIn automation into your own product through 55+ composable actions, or run it out of the box via an AI agent. Expandi fits agencies and sales teams who want a no-code campaign builder. Pick by the job, not the brand. ## Pick in 10 seconds :::cards **Choose Linked API if…** - You need a real developer surface: a REST API, Node/Python SDKs, and a shell CLI to compose [55+ actions](/docs/actions-overview) – search, fetch, connect, message, react, post – into your own product. Expandi has no public API. - You want automation working out of the box without building it: hand a ready-made skill to an AI agent that runs the workflows for you. - You want safety built into the execution: a dedicated cloud browser that behaves like a person, per-action limits the platform enforces, and no LinkedIn password stored. **Choose Expandi if…** - You want safety handled hands-off with no setup: an automatic warm-up that ramps a new account for you and vendor-tuned daily limits, with nothing to configure. - You want a no-code campaign builder: behaviour-based sequences combining "10 different actions and 10 conditions", A/B testing, a smart inbox, and multichannel LinkedIn, InMail, and email. - You run an agency: centralized multi-account management, client reporting, roles, and a white-label option. ::: ## Linked API vs Expandi at a glance | | Linked API | Expandi | |---|---|---| | What it is | Programmable LinkedIn automation API with built-in safety | No-code LinkedIn outreach tool with managed safety features | | Model | Compose 55+ actions into any workflow – from your code or an AI agent | Assemble behaviour-based campaign sequences in a visual builder | | Developer surface | REST API, Node/Python SDKs, shell CLI, MCP server, agent skills | No public API – webhooks, Zapier, and native CRM integrations only | | Channels | LinkedIn, including Sales Navigator | LinkedIn, InMail, and email | | Account connection | Your own [personal cloud browser](/safety) – Linked API never stores your password | You enter your LinkedIn password; Expandi uses it to obtain your session | | Safety model | Dedicated cloud browser behaving like a person; per-action limits you configure and the platform enforces | Automatic warm-up ramp; daily limits from vendor-defined "Safe Presets" | | Entry price | From $49/mo per seat, billed annually ($69 month-to-month) | Business $79/account/mo billed annually ($99 month-to-month), per Expandi's pricing page | | Free trial | 7 days | 7 days | | Credit card to start trial | Not required | Required | | Best for | Building automation into a product or AI-agent workflow | No-code outreach and agency campaign management | ## A programmable API or a no-code outreach tool? This is the distinction that decides the choice. Linked API gives you primitives – search, fetch profiles and companies, connect, message, react, comment, post – and you compose them into whatever logic you want, in your own code or through an AI agent. You reach the engine two ways, both first-class: build it into your product through the REST API, SDKs, and CLI, or hand a ready-made [agent skill](/skills) to an AI agent that composes and runs the same workflows from a plain-language ask. Expandi is a no-code outreach tool. Its sequence builder combines "10 different actions and 10 conditions in one sequence based on your prospects' behavior" (Expandi's site), with A/B testing, a unified inbox, personalization, and multichannel LinkedIn, InMail, and email. What it is not is programmable from the outside. Expandi has no public REST API; its own documentation is candid about this – "reversed webhook actions are the closest to the publicly available API we have" – and that reversed webhook is narrow: "with the reversed webhook, you can either add people to the campaign or pause and resume them" (Expandi's help center). Combined with outbound webhooks and native CRM connectors, that is enough to sync campaign data with your stack, but there is no surface to build your own LinkedIn automation on. If you have outgrown a campaign UI and need to compose actions from your own code, a webhook is not a substitute for an API. ## How does each keep an account safe? Both products automate your own LinkedIn account from the cloud, both are unofficial, and both are built with account safety in mind – Linked API builds it into how every action runs, and Expandi markets it heavily ("account safety is why Expandi was built", Expandi's site). Neither claims zero ban risk, and neither should. They take different approaches. Linked API builds safety into the execution layer. Each seat is a dedicated cloud browser matched to your device, where LinkedIn's own pages load and every action [behaves like a person](/safety) – real clicks, human pauses, a simple visit-and-like taking around 20 seconds by design – and workflows execute sequentially at human pace. Linked API never stores your LinkedIn password, and you [configure per-action limits](/docs/admin-limits) that the platform enforces, returning a `limitExceeded` error before any action crosses the line; our [limits guide](/guides/understanding-linkedin-limits) covers sensible values, including ramping a new account conservatively. Expandi takes a managed-features approach layered on a credential-based model. To connect, you enter your LinkedIn password, which Expandi uses to obtain your session; from there it adds "random delays… so that your automation mimics human behavior" and runs an automatic warm-up that ramps a new account gradually – "a default limit that starts at 5 actions per day" that increases "by 3 actions" "every two days" to "a maximum of 21 actions per day over the course of the warm-up period" (Expandi's help center). Daily volumes stay inside randomized ranges from "Safe Presets… defined by our team" to keep activity at "low-risk levels". The automatic warm-up is a convenience Linked API does not package as a named feature – if you want the ramp handled for you, that is a point in Expandi's favor. The counterweight: Expandi's model asks for your LinkedIn password and lets its team define the safe numbers, where Linked API takes no password and lets you set and enforce your own. Neither model removes the underlying risk any real-account automation carries – on either tool, conservative limits and human-like pacing are what keep an account healthy. ## How does Linked API pricing compare with Expandi? | Plan | Price | Notes | |---|---|---| | Linked API – Core | $49/mo per seat billed annually; $69 month-to-month | Full API, SDKs, and CLI; unlimited workflow execution within the limits you configure | | Expandi – Business | $79/account/mo billed annually; $99 month-to-month | Cloud-based outreach, warm-up, unlimited campaigns | | Expandi – Agency | Custom (10+ seats) | Centralized management, client reporting, white-label | Linked API bills a flat per-seat subscription; Expandi bills per LinkedIn account/seat, with the prices above as displayed on its pricing page (annual billing gives two months free). Both offer a 7-day free trial, but Expandi requires a bank card to start it (per its help center), while Linked API does not – you only add payment details when you decide to subscribe. The units carry different things. A Linked API seat is one LinkedIn account driven by a full API, SDKs, and CLI, with no metering on how much you run. An Expandi seat is one account in a no-code campaign tool; at $79/account/mo annual it sits above Linked API's $49 Core, and some personalization – dynamic images and GIFs – runs through a separate paid Hyperise subscription rather than the base plan. The seat prices are in the same range; what each seat carries – a developer surface or a managed no-code campaign tool – is the substantive difference. ## Frequently Asked Questions (FAQ) #### Does Linked API include account warm-up like Expandi? Not as a named auto-warm-up feature, and that is a real difference to weigh. Linked API gives you [per-action limits you configure](/docs/admin-limits) and the platform enforces, so you ramp a freshly connected account by starting conservative and raising the limits over the first weeks yourself – our [limits guide](/guides/understanding-linkedin-limits) covers sensible starting values. Expandi instead automates that ramp with a built-in warm-up that starts small and climbs gradually on its own. If a hands-off warm-up matters most, that is a point for Expandi; if you would rather set the pace, Linked API's configurable limits reach the same gradual ramp manually. #### How does account safety compare? Both are built for safety and neither eliminates risk. Linked API's is structural: your account runs in a dedicated cloud browser that behaves like a person, Linked API never stores your LinkedIn password, and the platform enforces the [per-action limits you configure](/docs/admin-limits). Expandi layers managed features – an automatic warm-up and daily limits from vendor-defined "Safe Presets" – onto a model where you enter your LinkedIn password for it to obtain your session, and its team defines the numbers. Both can keep an account healthy; the difference is a transparent, password-free execution model where you hold the controls versus a managed one where more is delegated to the vendor. #### Can I move from Expandi to Linked API? Yes – the campaign is rebuilt as a Linked API workflow, and your lead data comes with it. Each campaign step maps to a Linked API [action](/docs/actions-overview) – a connection request with a note, a follow-up message – chained with `then` and iterated with `doFor`; export your leads from Expandi and feed the LinkedIn URLs in as parameters. The account connects into its own [personal cloud browser](/safety), and because Linked API lets you set conservative per-action limits, you can ramp a freshly connected account gradually rather than switching it to full volume at once. Piloting one account while Expandi runs the rest is the low-risk path. #### Do I need Sales Navigator for either tool? Not to start. Linked API needs Sales Navigator only for its `nv-*` actions, not for standard people, company, or post data. Expandi states "you do not need any premium subscription to begin your outreach", with Sales Navigator or Recruiter searches requiring those subscriptions (Expandi's FAQ). Either works on a free or Premium account for core outreach. --- Automating LinkedIn for your team, product, or AI agent? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually, with a 7-day free trial – and use it both ways: build it into your product with the [REST API and Node/Python SDKs](/sdks/installation) and the shell [CLI](/cli/getting-started), or get automations out of the box through the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`). *Facts verified July 24, 2026 – Expandi prices, quotes, and feature coverage checked against its live pricing page, product pages, and help center on that date.* ## Linked API vs PhantomBuster (2026): Features, Pricing, and Safety Compared Linked API vs PhantomBuster is a choice between two different shapes of LinkedIn automation. Linked API is a programmable LinkedIn automation API: you compose workflows from primitives, and the execution layer – a dedicated cloud browser per account, human pacing, enforced limits – is built in. PhantomBuster is a no-code catalog of ready-made cloud automations ("Phantoms") for LinkedIn and 15+ other platforms, which you configure, schedule, and meter by execution time. > **The short version.** The safety difference is structural: Linked API runs your account in its own dedicated cloud browser that behaves like a person, with pacing and per-action limits enforced by the platform; PhantomBuster runs Phantoms from its cloud on a session cookie you supply, with published limits that are recommendations you configure and divide across Phantoms yourself. Beyond safety, Linked API covers two jobs: embed LinkedIn automation into your own product through 55+ composable actions, or run it out of the box – a ready-made skill lets an AI agent build and run the workflows for you. PhantomBuster fits sales and growth teams who prefer configuring template automations in a UI, across many platforms. Pick by the job, not the brand. ## Pick in 10 seconds :::cards **Choose Linked API if…** - You are building LinkedIn automation into a product, backend, or agent workflow: [55+ composable actions](/docs/actions-overview) behind a REST API, Node/Python SDKs, a shell CLI, an MCP server, and packaged agent skills. - You want the safety layer run for you: a dedicated cloud browser per account, sequential human-paced execution, and per-action limits the platform enforces. - You want automation working out of the box without assembling it: install a ready-made skill (`npx @linkedapi/skills`) and ask your AI agent in plain language – nothing to configure, schedule, or chain by hand. **Choose PhantomBuster if…** - You want no-code, ready-made automations: 100+ Phantoms and workflows you configure in a UI – around 40 for LinkedIn and 13 for Sales Navigator – instead of writing code. - You need built-in email enrichment: finding professional email addresses from LinkedIn searches and profiles is a core, metered feature. - Your team prospects across many platforms at once – Google, Instagram, Facebook, X, YouTube and more – not just LinkedIn. ::: ## Linked API vs PhantomBuster at a glance | | Linked API | PhantomBuster | |---|---|---| | What it is | Dedicated LinkedIn automation API | No-code catalog of cloud automations for 15+ platforms | | Model | Workflows from 55+ actions – composed in your code or by your AI agent; the platform executes them | You configure and schedule ready-made Phantoms; chains come as pre-made Workflows | | Entry price | From $49/mo per seat, billed annually ($69 month-to-month) | Start plan €56/mo billed annually, €69 month-to-month (per PhantomBuster's pricing page, in EUR) | | Pricing model | Flat per seat, no usage metering | Metered: monthly quotas for execution hours and email/AI/URL-finder credits, plus a fixed cap on automation slots | | LinkedIn connection | Connect once; your account then runs in its own [personal cloud browser](/safety) – no cookie to handle | Your li_at session cookie + user agent, via browser extension or manual paste | | Safety model | Real browser session behaving like a person – requests come from LinkedIn's own pages; enforced per-action limits | Runs from PhantomBuster's cloud on your cookie; published limits are recommendations you configure per Phantom | | Public API | LinkedIn actions as API calls, returning structured JSON | Account control – launches and monitors Phantoms; no LinkedIn action endpoints | | Free trial | 7 days | 14 days | | Best for | Developers, products, AI agents going deep on LinkedIn | Sales and growth teams prospecting no-code across many platforms | Shopping a full list of alternatives instead of a head-to-head? See the [10 best PhantomBuster alternatives](/blog/phantombuster-alternatives). ## What are you actually buying: an API or a catalog? Linked API sells you primitives – and two first-class ways to use them. Automation is expressed as [workflows](/docs/core-concepts) – actions chained with `then`, iterated with `doFor` blocks – and the platform executes them sequentially in a dedicated cloud browser, returning structured JSON. The embedding route: if you can describe a LinkedIn workflow, you can build it into your product through the API and SDKs; there is no fixed menu. The out-of-the-box route: hand a ready-made [agent skill](/skills) to an AI agent, and it composes and runs those same workflows from a plain-language ask – nothing to wire up or maintain. PhantomBuster sells you finished units. "Phantoms are individual automations. Each Phantom performs one task" (PhantomBuster's pricing FAQ) – its store lists around 40 LinkedIn and 13 Sales Navigator Phantoms, from search export and profile scraping to auto-connect, message sending, and post engagement. Multi-step sequences come as Workflows, which are "ready-made chains of Phantoms" with "predefined launch patterns that are automatically optimized and can't be fully customized" (PhantomBuster's docs). Its public API is a different layer than it may sound: by its own description it "gives you control over your account" – launching Phantoms, monitoring runs, fetching output – and exposes no LinkedIn action endpoints; the hosted MCP server likewise wraps "a curated subset" of that account API. A custom-scripts SDK exists if you want to write your own Phantoms in JavaScript. Coverage differs in both directions: - **First-class in Linked API, not in PhantomBuster:** LinkedIn actions as direct API calls from your backend (send a message, fetch a profile, react, comment – each returning JSON), free-form workflow composition with conditional chains, and account analytics – no SSI or dashboard-performance automation appears in PhantomBuster's LinkedIn catalog. - **Native in PhantomBuster, not in Linked API:** group surfaces (member export and messaging), event guest export and inviting, poll-voter export, Recruiter profile scraping, skill endorsements, and AI copywriting Phantoms for messages and comments. - **Both offer:** MCP servers for AI assistants, Sales Navigator coverage, and no-code integrations – n8n and Make on both sides, with PhantomBuster adding Zapier and HubSpot-native sync. ## What does account safety look like on each? Both products automate real member accounts, and neither is an official LinkedIn product. The difference is what LinkedIn's servers see and where the safety engineering lives. ### What LinkedIn's servers see **Linked API – a dedicated browser per account:** - Each seat is a cloud browser dedicated to that account, matched to your device profile, where [every action fully emulates a real user](/safety) – real pages, real clicks, human pauses. - The session lives inside that browser; there is no cookie to extract, paste, or keep alive. **PhantomBuster – your cookie in their cloud:** - Phantoms authenticate with a LinkedIn session cookie and user agent you supply – via its browser extension, or "by copying your li_at session cookie and user agent from your browser's Developer Tools" (PhantomBuster's help center). - Its automations run from PhantomBuster's infrastructure, and its own docs note they "may operate from a different location than where you're actually based", which "can trigger login alerts"; built-in proxies exist in five fixed regions, though PhantomBuster does not recommend proxies for LinkedIn by default. - Your real browsing and its cloud runs act on the same session, and the accounting is yours: "If you run multiple Phantoms on the same LinkedIn account, LinkedIn sees the combined activity" (PhantomBuster's help center). ### Where the safety engineering lives **Linked API – in the platform:** - Workflows execute one at a time at realistic speed – a simple visit-and-like takes around 20 seconds by design. - You [configure per-action limits](/docs/admin-limits) once; an action that would exceed them returns a `limitExceeded` error instead of executing. Our [limits guide](/guides/understanding-linkedin-limits) covers sensible values. **PhantomBuster – mostly in your configuration:** - Its documentation is detailed – per-action daily limit tables by account type, warm-up advice, and recovery steps after a restriction – but the numbers are framed as "starting guidelines, not guarantees" (PhantomBuster's help center). - Applying them is your job: you set volumes in each Phantom's Behavior settings, schedule launch frequency "based on rate limits", and when several Phantoms share one account you "divide the recommended limit by the number of Phantoms" yourself. One exception: Auto Connect invitation limits are enforced dynamically by the product. - Its own FAQ acknowledges the residual risk plainly: restrictions can occur "below published thresholds" when Phantoms stack, manual and automated activity mix, or volume resumes after a quiet period. Neither model is free of risk – automating a real account never is. The honest framing: on Linked API the safety engineering is the platform's job end to end; on PhantomBuster it is shared – the platform documents limits in unusual detail and enforces them for Auto Connect, and you configure everything else. ## How does Linked API pricing compare with PhantomBuster? ![Linked API's flat per-seat fee compared with PhantomBuster's five meters – monthly quotas for execution time and email, AI, and URL-finder credits, plus a fixed cap on automation slots](/images/vs/phantombuster-meters-vs-flat.webp) | Plan | Price | What it includes | |---|---|---| | Linked API – Core | $49/mo per seat billed annually; $69 month-to-month | Unlimited workflow execution within the per-action limits you configure; one seat = one LinkedIn account | | PhantomBuster – Start | €56/mo billed annually; €69 month-to-month | 20 h execution time, 5 automation slots, 500 email + 10k AI + 1k URL-finder credits per month | | PhantomBuster – Grow | €128/mo billed annually; €159 month-to-month | 80 h execution, 15 slots, 2,500 email + 30k AI + 10k URL-finder credits | | PhantomBuster – Scale | €352/mo billed annually; €439 month-to-month | 300 h execution, 50 slots, 10,000 email + 90k AI + 20k URL-finder credits | PhantomBuster prices are shown in EUR; its pricing page localizes currency by region. The units mean different things. A Linked API seat is one LinkedIn account with its own dedicated cloud browser; cost tracks the number of accounts you automate, and each runs unmetered. A PhantomBuster plan is a pool of metered resources shared by a workspace – "execution time is the total monthly time your automations can run", and when it runs out "your automations will pause until your execution time resets or you upgrade your plan", with no automatic overage charges (PhantomBuster's pricing FAQ). Slots count every Phantom kept on the dashboard, "including ones that are inactive or have never been launched", a Workflow "can occupy up to 3 slots", and Workflows run background tasks that consume execution time outside working hours (PhantomBuster's help center). The models meter differently: a PhantomBuster workspace can connect many LinkedIn accounts, but they all draw from that one shared, capped pool of hours and credits, so more accounts means a thinner slice per account; a Linked API seat is one account with its own browser and no metering on how much it runs. Both let you start free – a 7-day trial on Linked API, a 14-day trial on PhantomBuster. ## Frequently Asked Questions (FAQ) #### Can I build a product on PhantomBuster's API the way I can on Linked API? They expose different layers. PhantomBuster's API, by its own description, "gives you control over your account" – it launches and stops Phantoms, monitors runs, and fetches their output; LinkedIn behavior itself stays inside the packaged Phantoms. Linked API exposes the LinkedIn actions themselves: your backend calls [send message, fetch profile, connect, react](/docs/actions-overview) and receives structured JSON, so the automation logic lives in your code rather than in a launched job. #### Do Linked API and PhantomBuster connect to my LinkedIn account the same way? No. PhantomBuster authenticates with your LinkedIn session cookie (li_at) and user agent, supplied through its browser extension or pasted from your browser's developer tools, and expired sessions stop runs until you reconnect. Linked API connects the account once and maintains the session inside its own [personal cloud browser](/safety), with no cookie handling on your side. #### Can I use Linked API and PhantomBuster together? On different LinkedIn accounts, yes – for example PhantomBuster for cross-platform scraping and enrichment on one account, Linked API automating outreach from another, or Linked API's [actions](/docs/actions-overview) consuming lead lists a Phantom exported. On the same account, be careful: PhantomBuster's own docs note that LinkedIn sees combined activity across everything driving a session, so two automation tools sharing one account undermine each other's pacing. #### What does moving from PhantomBuster to Linked API look like? Lead data moves easily; the automation logic is rebuilt. Export your existing lead lists from Phantoms (they output spreadsheets), and feed the LinkedIn URLs straight into Linked API [actions](/docs/actions-overview) as parameters. Each Phantom's job then maps to a workflow you compose once – a search-export Phantom becomes a search action with `doFor` chains, an auto-connect Phantom becomes `sendConnectionRequest` with a note, follow-ups become message actions triggered on acceptance. The account runs in its own [personal cloud browser](/safety) with no cookie to paste, and the practical path is piloting one account on Linked API while PhantomBuster keeps running the rest – just not both tools on the same account at once. #### Does PhantomBuster enforce LinkedIn limits for me? Mostly no – with one exception. Its published limit tables are explicitly "starting guidelines, not guarantees"; you configure volumes per Phantom and divide limits when several share an account. The exception is Auto Connect, whose invitation limits the product enforces dynamically. Linked API enforces limits across every action: you [set them once](/docs/admin-limits), the platform paces each workflow like a human, and any action that would exceed a limit returns `limitExceeded` instead of executing. --- Automating LinkedIn for your team, product, or AI agent? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually, with a 7-day free trial – and use it both ways: build it into your product with the [REST API and Node/Python SDKs](/sdks/installation) and the shell [CLI](/cli/getting-started), or get automations out of the box through the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`). *Facts verified July 24, 2026 – PhantomBuster prices, quotes, and feature coverage checked against its live pricing page, Phantom store, and Help Center on that date; prices as displayed in EUR.* ## Linked API vs Unipile (2026): Features, Pricing, and Safety Compared Linked API vs Unipile is really a choice between two layers of the same problem – letting your software act on LinkedIn. Linked API is a dedicated LinkedIn automation platform where the execution layer – a cloud browser per account, human pacing, enforced limits – is built in. Unipile is a unified multi-channel communication API – LinkedIn is one of its six-plus channels – and you build the automation logic on top. > **The short version.** The safety difference is structural: Linked API automates through a real browser session that behaves like a person, with pacing and per-action limits enforced by the platform; Unipile constructs reverse-engineered wire-level calls to LinkedIn's private interface and, on its GA API, leaves pacing to your application. Beyond safety, Linked API covers two jobs: embed LinkedIn automation into your own product through 55+ composable actions, or run it out of the box – a ready-made skill lets an AI agent build and run the workflows for you. Unipile fits SaaS products that need messaging across LinkedIn, WhatsApp, Instagram, Telegram, email, and calendars, with sequencing and campaign logic yours to build. Pick by the job, not the brand. ## Pick in 10 seconds :::cards **Choose Linked API if…** - LinkedIn automation is the actual job: search-to-outreach workflows (visit → connect → message → react → comment) composed from [55+ actions](/docs/actions-overview), not just messaging endpoints. - You want the safety layer run for you: every action executes in a dedicated cloud browser at a human pace, within per-action limits the platform enforces. - You want automation working out of the box: install a ready-made skill (`npx @linkedapi/skills`) and ask your AI agent in plain language – no flows to assemble, nothing to configure by hand. **Choose Unipile if…** - You need channels beyond LinkedIn behind one API – WhatsApp, Instagram, Telegram, Gmail/Outlook/IMAP email, and calendars – with a unified inbox in your product. - You connect many end-user accounts and per-account economics matter: €49 / $55 per month covers the first 10 accounts, then €3.00–5.00 per account as volume grows (per Unipile's pricing). - You need recruiter-side surfaces: Recruiter and Company Page inboxes, publishing job posts, retrieving applicants and their resumes. ::: ## Linked API vs Unipile at a glance | | Linked API | Unipile | |---|---|---| | What it is | Dedicated LinkedIn automation API | Unified multi-channel communication API | | Layer | Execution engine – workflows run in a cloud browser, pacing and limits enforced | Unified transport – REST endpoints over LinkedIn's private interface; sequencing and campaign logic are yours to build | | Channels | LinkedIn, including Sales Navigator | LinkedIn, WhatsApp, Instagram, Telegram, Gmail/Outlook/IMAP, Google/Outlook calendars | | Entry price | From $49/mo per seat, billed annually ($69 month-to-month); one seat = one LinkedIn account | €49 / $55 per month minimum, covering up to 10 connected accounts (per Unipile's pricing) | | Pricing model | Flat per seat, no usage metering | Per connected identity, sliding from €5.00 down to €3.00 per account at volume, post-paid on peak usage | | Account connection | Your account runs in its own [personal cloud browser](/safety) | White-label hosted auth wizard, or username/password or li_at cookie through your own UI | | Safety model | Real browser session behaving like a person – requests come from LinkedIn's own pages; enforced per-action limits | Direct calls to LinkedIn's private endpoints (reverse-engineered) via fixed proxies; on the GA v1 API pacing is implemented by you (the v2 beta adds enforced rate limits) | | Free trial | 7 days | 7 days | | Best for | LinkedIn automation depth – teams, products, AI agents | Multi-channel messaging inside a SaaS at end-user scale | Shopping a wider list than these two? See the [best LinkedIn automation tools roundup](/blog/best-linkedin-automation-tools-2026). ## What layer does each product sit at? Linked API sits at the execution layer. Automation is expressed as [workflows](/docs/core-concepts) – actions chained with `then`, iterated with `doFor` blocks – and the platform executes them sequentially in a dedicated cloud browser that matches your device, at the pace of a real user. You describe *what* ("search these people, visit each profile, connect with a note, message on accept"); the execution layer owns *how fast* and *how humanly*. And you reach that engine two ways, both first-class: embed it in your product through the REST API, SDKs, and CLI – or skip building entirely and hand a ready-made [agent skill](/skills) to an AI agent, which composes and runs the same workflows from a plain-language ask. Unipile sits one layer down, at transport. It describes its API as "designed for messaging use cases" (Unipile's AI-agent page) and aggregates channels into one schema of 100+ REST endpoints: connect an account, then send and sync messages, list chats, manage invitations, retrieve profiles, react, comment, post. For LinkedIn specifically it works "through reverse engineering" of LinkedIn's private interface (Unipile's pricing FAQ), routing each account's requests through a fixed proxy. What it deliberately does not include is automation semantics: there is no campaign or sequence object in its API – its own outreach guide shows you how to wire routes and webhooks into a sequencer you build. Feature coverage follows the layer split, in both directions: - **First-class in Linked API, not native in Unipile's GA API:** [SSI retrieval](/docs/action-st-retrieve-ssi), dashboard performance analytics, own-feed retrieval, and removing existing connections. Unipile's raw-data "magic route" can reach some of these if you identify LinkedIn's private endpoints yourself, and its v2 beta adds a native Delete Relation method – the practical difference is DIY endpoints versus documented, supported actions. The structural difference stands regardless: composed multi-step workflows with built-in pacing make a prospecting sequence one API call rather than an engine you host. - **Native in Unipile, not in Linked API:** editing and deleting sent messages within LinkedIn's short edit window, and voice notes. - **Both offer:** REST APIs, official Node.js and Python SDKs (Unipile's Python SDK targets its v2 beta), MCP servers, and n8n integrations. Linked API adds a shell [CLI](/cli/getting-started) and packaged [agent skills](/skills); Unipile adds a PHP wrapper. ## What does account safety look like on each? Both products automate real member accounts, and neither is an official LinkedIn product – Unipile states plainly that it "is not affiliated with, endorsed by, or sponsored by LinkedIn" (Unipile's LinkedIn API page), and the same is true of Linked API. Two differences matter for risk, taken in turn below. ### What LinkedIn's servers see **Linked API – a real browser session:** - No request construction at all: a dedicated cloud browser loads the real LinkedIn pages, and every backend call is made by LinkedIn's own web app – exactly as in a normal browsing session. - After a LinkedIn update, the browser simply runs whatever code LinkedIn shipped – there is no synthesized request shape that could go stale. - What LinkedIn's servers see is a logged-in browser session [behaving like a person](/safety): real pages, real clicks, human pauses, on a device profile matching yours. **Unipile – wire-level API calls:** - Its reverse-engineered integration (its own description, quoted above) constructs requests to LinkedIn's private backend interface and sends them through each account's fixed proxy. - That interface is private and undocumented, so LinkedIn can change its URL, parameters, or protocol without a public compatibility contract. - This creates a structural risk pacing cannot address: if LinkedIn changes a private endpoint before a reverse-engineered client adapts, the client can keep sending an outdated request shape that the current frontend no longer produces – a potentially distinctive server-side signal. The timing follows LinkedIn's release schedule, not a change in your integration. ### Where the safety engineering lives **Linked API – in the platform:** - Workflows execute one at a time at realistic speed – a simple visit-and-like takes around 20 seconds by design. - You [configure per-action limits](/docs/admin-limits) once; an action that would exceed them returns a `limitExceeded` error instead of executing. Our [limits guide](/guides/understanding-linkedin-limits) covers what values are sensible. **Unipile's GA v1 API – mostly in your codebase:** - Each account gets a fixed, geo-matched proxy (you can bring your own), but enforcement is explicitly absent: "We don't enforce any limits on our side, so you'll have the exact same limit in the LinkedIn UI" (Unipile's developer docs). - The documented daily numbers are labeled conservative recommendations for your application to implement, and its docs advise spacing calls out "with random values" across working hours. - Your application also handles surfaced checkpoints – 2FA, OTP, CAPTCHA, in-app validation – within a five-minute window, and its docs note that Recruiter accounts hit a session conflict that forces cookie-based reconnection, with a Chrome extension suggested as the workaround. **Unipile's v2 beta – part moves into the platform:** - It enforces rate limits at its Methods API level, rejecting over-limit requests "before they are sent to the provider", with limits configurable in its dashboard and a caching layer that serves repeat reads without hitting LinkedIn (per Unipile's v2 documentation). - Caveats run in both directions: v2 is a beta that "may introduce breaking changes" and requires a new account per its migration docs – and rate limiting is throttling, not execution. Rejected requests are your application's to reschedule; sequencing and humanization still live in your code, where on Linked API the platform executes the queue itself at a human pace. Neither model is free of risk – automating a real account never is. The honest framing: on Linked API the safety engineering is the platform's job end to end; on Unipile it is split between you and the platform (mostly you on v1, more of the platform in the v2 beta). ## How does Linked API pricing compare with Unipile? | Connected LinkedIn accounts | Linked API (annual billing) | Unipile (per its pricing) | |---|---|---| | 1 | $49/mo | €49 / $55/mo (minimum, covers up to 10) | | 10 | $490/mo | €49 / $55/mo | | 60 | $2,940/mo | ~€270/mo (Unipile's own worked example) | The unit of pricing differs in what it includes. A Linked API seat is a dedicated cloud browser running that account, with pacing, sequencing, and limit enforcement part of the price. Unipile's per-account fee covers transport ("no additional cost per request", per its pricing page), with sequencing, quota tracking, and checkpoint handling running in your application. Both sides offer a 7-day free trial, so a side-by-side pilot on a test account costs nothing but a week. One billing-model note: Unipile bills post-paid on the peak number of simultaneously connected accounts in each 30-day period, prices excluding VAT (per its pricing FAQ); Linked API is a flat prepaid subscription per seat – $49/mo billed annually, $69 month-to-month. ## Frequently Asked Questions (FAQ) #### Is Unipile an official LinkedIn API? No. Unipile's own FAQ states it "is not affiliated with, endorsed by, or sponsored by LinkedIn", and for LinkedIn it operates "through reverse engineering" (Unipile's site). Linked API is not affiliated with LinkedIn either – it automates your own account through a cloud browser. LinkedIn's official developer platform is a separate, partner-gated program that does not offer account-level automation of this kind. #### Can I use Linked API and Unipile together? Yes, split by channel: Linked API running the LinkedIn automation, and Unipile handling email, WhatsApp, or a unified inbox inside your product. What you should not do is point two automation tools at the same LinkedIn account at once – pacing only protects an account when a single tool controls all of its activity. #### What does migrating between Linked API and Unipile look like? It is a rewrite of the integration layer, not a search-and-replace, because the models differ: Linked API expects the logic expressed as a [workflow](/docs/building-workflows) – actions chained with `then` – that the platform executes for you; Unipile exposes per-action REST endpoints you orchestrate yourself. Accounts reconnect through each side's hosted connection flow (end users re-authenticate; sessions do not transfer). The practical path is piloting the target on one account while the incumbent keeps running the rest. #### Does Unipile enforce LinkedIn limits for me? On its GA v1 API, no – the documented numbers there are guidance your application implements. Its v2 beta changes this at the transport level: over-limit requests are rejected before they reach LinkedIn, with limits configurable in its dashboard (per Unipile's v2 documentation); scheduling and retries remain in your code. Linked API enforces limits at the execution level: you [set per-action limits once](/docs/admin-limits), the platform paces every workflow like a human, and any action that would exceed a limit returns `limitExceeded` instead of executing. --- Automating LinkedIn for your team, product, or AI agent? [Start with Linked API](/pricing) – flat per-seat pricing from $49/mo, billed annually, with a 7-day free trial – and use it both ways: build it into your product with the [REST API and Node/Python SDKs](/sdks/installation) and the shell [CLI](/cli/getting-started), or get automations out of the box through the [MCP server](/mcp/overview), the AI-agent-friendly CLI (Claude Code, Cursor, Codex), and ready-made [agent skills](/skills) (`npx @linkedapi/skills`). *Facts verified July 23, 2026 – Unipile prices, quotes, and feature coverage checked against Unipile's live pricing page and developer documentation (v1 and v2 beta) on that date.*