Browser

Scavio Linkedin

Try it

Pull LinkedIn person and company profiles, their posts, job listings and post comments as structured JSON. 9 endpoints from 1 to 30 credits, four of them paginated, for prospecting, recruiting, and market research.

What it does

Pull LinkedIn person and company profiles, their posts, job listings and post comments as structured JSON. 9 endpoints from 1 to 30 credits, four of them paginated, for prospecting, recruiting, and market research.

The skill document

LinkedIn via Scavio

Look up LinkedIn people and companies, read their posts, search job listings, and read a single job or post with its comments. All endpoints return structured JSON.

When to trigger

Use this skill when the user asks to:

  • Look up a LinkedIn member's profile, about section, or recent posts
  • See what a member engages with rather than what they publish - the posts they commented on or reacted to
  • Look up a company profile or its recent posts
  • Search for job listings by keyword and location
  • Read a single job listing, or a single post with its comments
  • Build B2B prospecting, recruiting, or market-research pipelines

Note: LinkedIn upstream can be slow. Set a client timeout of at least 60 seconds and be ready to retry.

Setup

Get a free API key at scavio.dev (50 free credits to get started, no card required):

export SCAVIO_API_KEY=sk_live_your_key

Every request is a POST with a JSON body and:

Authorization: Bearer $SCAVIO_API_KEY

Endpoints

Base URL: https://api.scavio.dev. All paths are under /api/v1/linkedin. Cost is not uniform - check the table before planning a run.

EndpointCreditsWhat it returns
POST /api/v1/linkedin/person1Full profile: about, experience, education, links
POST /api/v1/linkedin/person/about1Just the narrative sections of a profile
POST /api/v1/linkedin/person/posts10 per pageA member's posts, or the posts they commented on or reacted to. 50 per page, paginated
POST /api/v1/linkedin/company1Company profile, locations, related companies
POST /api/v1/linkedin/company/posts10 per pageA company's posts, 50 per page, paginated
POST /api/v1/linkedin/search/jobs10 per pageJob listings by keyword and location, 25 per page, paginated
POST /api/v1/linkedin/job30Full detail for one job listing
POST /api/v1/linkedin/post1Full detail for one post
POST /api/v1/linkedin/post/comments10 per pageComments on a post, paginated by page number

/job is by far the most expensive call here. Job search already returns title, company, location, posted time, workplace type and salary for every hit, so only call /job for listings the user has actually picked out - never in a loop over search results.

Every reference param also accepts a full LinkedIn URL. {"username": "williamhgates"} and {"url": "https://www.linkedin.com/in/williamhgates/"} are equivalent, and the same holds for company, job_id and post_id.

Retired endpoints

The upstream data provider withdrew five datasets. These paths still exist but always return HTTP 410 with {"code": "endpoint_retired", "reason": ...} and are never billed. Do not retry them, and never describe the data as temporarily unavailable - it is gone.

RetiredClosest substitute
/person/contactNone. Public contact info is no longer available.
/company/people/company returns featured_employees, a sample of 4-6 staff.
/company/jobs/search/jobs with the company name as search.
/search/peopleNone.
/search/postsNone.

Workflow

  1. A person: call /linkedin/person with username (the vanity handle) or url. Use /linkedin/person/about when you only need the narrative sections, and /linkedin/person/posts for their feed - type selects whether that feed is their own posts, the posts they commented on, or the posts they reacted to.
  2. A company: call /linkedin/company with company (a slug like microsoft) or url. Use /linkedin/company/posts for its feed.
  3. Jobs: /linkedin/search/jobs with a search keyword and optional location, then /linkedin/job with a job_id from the results for full detail.
  4. Posts: /linkedin/post with a post_id (bare id or activity urn), and /linkedin/post/comments with the same id for its comment thread.

Pagination

Four endpoints paginate, in two different styles. Every page is a separate call and costs 10 credits, so decide up front how many pages the user actually needs. The other five (/person, /person/about, /company, /job, /post) return a single object and take no paging parameter.

Cursor style - /person/posts, /company/posts, /search/jobs. The response carries has_more and next_cursor. While has_more is true, send next_cursor back verbatim as cursor on the next call; stop when has_more is false, at which point next_cursor is null. The cursor is opaque - it is not an offset and must not be parsed, constructed or incremented - and it belongs to the endpoint and the query that produced it, so never move one between endpoints or reuse it after changing username, company, search or type. A cursor the API cannot read is not an error: it silently restarts at page one, so a mangled cursor shows up as a paging loop that never advances.

Page sizes are 50 posts for the two feeds and 25 listings for job search.

Page-number style - /post/comments. Pass a 1-based page. Two upstream quirks mean you cannot use page size to detect the end: the page size varies (10, 10, 8 and 9 comments were observed on a single thread), so a short page does not mean the last page, and total is only returned on page 1 - it is null on every later page, so there is no running count to compare against. The only reliable stop signal is an empty page. Keep incrementing until a page comes back with no comments; has_more says exactly that (this page had at least one comment) and next_page carries the number to ask for.

/search/jobs pages, but is not a snapshot. Upstream rotates its result set, so pages overlap slightly and two identical searches return different listings. Paginate it to widen coverage, dedupe by job id across pages, and never present the union as the complete set of matching jobs or diff two runs to infer new postings.

Parameters

Person (/person, /person/about)

ParameterTypeDefaultDescription
usernamestringone ofPublic identifier (vanity handle), e.g. williamhgates
urlstringone ofFull LinkedIn profile URL

Person posts (/person/posts)

ParameterTypeDefaultDescription
usernamestringone ofPublic identifier (vanity handle)
urlstringone ofFull LinkedIn profile URL
typestringpostsWhich feed: posts (the member's own), comments (posts they commented on) or reactions (posts they reacted to). Any other value is a 400.
cursorstring--Opaque cursor: the previous response's next_cursor. Omit for page 1.

Company (/company)

ParameterTypeDefaultDescription
companystringone ofCompany universal name (slug), e.g. microsoft
urlstringone ofFull LinkedIn company URL

Company posts (/company/posts)

ParameterTypeDefaultDescription
companystringone ofCompany universal name (slug)
urlstringone ofFull LinkedIn company URL
cursorstring--Opaque cursor: the previous response's next_cursor. Omit for page 1.

Search jobs (/search/jobs)

ParameterTypeDefaultDescription
searchstringrequiredKeyword, e.g. software engineer
locationstring--Geographic filter, e.g. United States. Omit to search everywhere.
cursorstring--Opaque cursor: the previous response's next_cursor. Omit for page 1.

Job (/job)

ParameterTypeDefaultDescription
job_idstringone ofJob listing id, e.g. 4415427228
urlstringone ofFull LinkedIn job URL

Post (/post) and post comments (/post/comments)

ParameterTypeDefaultDescription
post_idstringone ofPost id or activity urn, e.g. 7488618410256523265
urlstringone ofFull LinkedIn post URL
pagenumber1(comments only) 1-based page. Page size varies, so page until a page comes back empty.

Examples

import requests

BASE = "https://api.scavio.dev"
# Your key from https://scavio.dev. Load it from your environment or secret
# store in real code - keep it out of source control.
API_KEY = "sk_your_key_here"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# 1. A person's profile, with experience and education inline
person = requests.post(f"{BASE}/api/v1/linkedin/person", headers=HEADERS,
    json={"username": "williamhgates"}).json()

# 2. Their recent posts (a full URL works just as well as a handle)
posts = requests.post(f"{BASE}/api/v1/linkedin/person/posts", headers=HEADERS,
    json={"url": "https://www.linkedin.com/in/williamhgates/"}).json()

# 2b. The posts they reacted to instead of the ones they wrote
reactions = requests.post(f"{BASE}/api/v1/linkedin/person/posts", headers=HEADERS,
    json={"username": "williamhgates", "type": "reactions"}).json()

# 3. A company profile
company = requests.post(f"{BASE}/api/v1/linkedin/company", headers=HEADERS,
    json={"company": "microsoft"}).json()

# 4. Jobs at that company: search by name, then pull one listing in full
found = requests.post(f"{BASE}/api/v1/linkedin/search/jobs", headers=HEADERS,
    json={"search": "Microsoft software engineer", "location": "United States"}).json()

job = requests.post(f"{BASE}/api/v1/linkedin/job", headers=HEADERS,
    json={"job_id": found["data"]["data"][0]["id"]}).json()

# 5. A post and page 1 of its comments
comments = requests.post(f"{BASE}/api/v1/linkedin/post/comments", headers=HEADERS,
    json={"post_id": "7488618410256523265", "page": 1}).json()

Cursor paging, capped so it cannot run away with the user's credits. next_cursor is echoed back untouched; each page is 10 credits.

def paged(path, body, max_pages=3):
    """Walk a cursor-paginated LinkedIn endpoint. 10 credits per page."""
    cursor, out = None, []
    for _ in range(max_pages):
        payload = {**body, **({"cursor": cursor} if cursor else {})}
        data = requests.post(f"{BASE}/api/v1/linkedin/{path}",
                             headers=HEADERS, json=payload).json()["data"]
        out += data["data"]
        if not data["has_more"]:
            break
        cursor = data["next_cursor"]
    return out

feed = paged("person/posts", {"username": "williamhgates"})
jobs = paged("search/jobs", {"search": "software engineer", "location": "United States"})
# Job search rotates upstream, so pages overlap - dedupe by id.
jobs = list({j["id"]: j for j in jobs}.values())

Comment paging is by page number, and only an empty page means the end - page size varies and total is on page 1 only.

def all_comments(post_id, max_pages=5):
    out, total = [], None
    for page in range(1, max_pages + 1):
        data = requests.post(f"{BASE}/api/v1/linkedin/post/comments", headers=HEADERS,
                             json={"post_id": post_id, "page": page}).json()["data"]
        if page == 1:
            total = data["total"]          # null on every later page
        if not data["data"]:
            break                          # the only reliable stop signal
        out += data["data"]
    return out, total

Response shapes

Every response uses the envelope { data, response_time, credits_used, credits_remaining }. Key data fields per endpoint:

  • personid, public_identifier, first_name, last_name, full_name, headline, about, location, city, country_code, avatar, banner, follower_count, connection_count, current_company{name,company_id,title}, experiences[], educations[], education_summary, honors_and_awards[], bio_links[], people_also_viewed[], similar_profiles[], url.
  • person/aboutabout, headline, education_summary, experiences[], educations[], honors_and_awards[], bio_links[].
  • person/posts, company/postsdata[] (id, share_urn, text, url, created_at, posted_relative, is_reshare, num_comments, num_reposts, reactions{total,likes,appreciations,empathy,interests,praises,entertainments}, images[], article, author), count, has_more, next_cursor (null on the last page). 50 per page.
  • companyid, universal_name, name, description, about, website, industries[], specialties[], company_size, organization_type, employee_count, follower_count, headquarters, locations[], country_codes[], logo, cover, featured_employees[], similar_companies[], affiliated_companies[], recent_updates[], url.
  • search/jobsdata[] (id, title, url, company, company_url, company_logo, location, posted_at, workplace_type, salary), count, has_more, next_cursor (null on the last page). 25 per page.
  • jobid, title, url, description, location, employment_type, experience_level, job_functions[], industries[], benefits[], skills[], is_remote, is_closed, expires_at, posted_at, applicant_count, view_count, salary, company{id,name,url,employee_count,employee_range,follower_count,headquarters{...}}.
  • postid, post_type, title, headline, text, url, created_at, hashtags[], embedded_links[], images[], videos[], num_likes, num_comments, tagged_companies[], tagged_people[], top_comments[], author{public_identifier,full_name,avatar,url,follower_count,post_count}.
  • post/commentsdata[] (text, url, created_at, is_pinned, author{urn,full_name,headline,avatar,url}, replies[]), page, total (page 1 only, null afterwards), has_more (this page carried at least one comment), next_page (null when the page was empty).

An experience entry has two shapes. A single role carries company plus its own dates. Several roles at the same employer are grouped: title holds the company name and each role sits in positions[] with its own title and dates.

{
  "data": {
    "full_name": "Bill Gates",
    "headline": "Chair, Gates Foundation and Founder, Breakthrough Energy",
    "public_identifier": "williamhgates",
    "follower_count": 40550634,
    "current_company": { "name": "Gates Foundation", "title": "Co-chair" },
    "experiences": [
      {
        "title": "Co-chair",
        "company": "Gates Foundation",
        "start_date": "2000",
        "positions": []
      }
    ],
    "education_summary": "Harvard University"
  },
  "credits_used": 1,
  "credits_remaining": 999
}

Guardrails

  • Cost varies by endpoint: the four profile and single-item reads are 1 credit, the four paginated endpoints are 10 credits per page, and /job is 30. A ten-page job search is 100 credits, so cap any paging loop and tell the user the credit cost you intend to spend before you start.
  • /person already contains the about text, experience and education - do not call /person/about as well for the same member, it is the same upstream fetch billed twice.
  • Stop paging on the signal the endpoint actually gives you: has_more false for the three cursor endpoints, an empty data[] for /post/comments. Never stop on a short page of comments and never use total as a page budget - it is present on page 1 only.
  • Cursors are opaque and endpoint-specific. Echo next_cursor back unchanged; do not parse, build or increment one, and do not carry a cursor across endpoints or across a changed username, company, search or type. An unreadable cursor silently returns page one instead of erroring, which looks like a loop that never advances.
  • /search/jobs rotates its result set upstream, so pages overlap and a repeated search returns different listings. Dedupe by job id, do not present the pages you walked as an exhaustive list, and do not diff two runs to infer new postings.
  • A 410 is permanent. Report the data as unavailable and move on rather than retrying or substituting a guess.
  • Never fabricate names, titles, employers, job listings, post text, or counts. Only return API data.
  • This is public profile data - treat it accordingly and do not infer private details.

Failure handling

  • 400 means an invalid or missing parameter (e.g. neither username nor url, or a type outside posts / comments / reactions) - fix and retry. A bad cursor is the exception: it does not 400, it quietly returns page one.
  • 401 means the API key is invalid or missing. Check SCAVIO_API_KEY.
  • 404 on /job means the listing has no detail record upstream. Not billed. Roughly one job id in five returned by /search/jobs answers this way - expired and delisted postings linger in search after their detail page is gone. Skip that id and move to the next; retrying will not produce a record.
  • 410 means the endpoint was retired upstream. Permanent - do not retry. Read reason in the body.
  • 429 means rate or usage limit exceeded. Wait before retrying. See rate limits.
  • 502 / 503 mean upstream is temporarily unavailable - wait a few seconds and retry, up to a few times.
  • If a search returns no results, relax filters or try different keywords.
  • If SCAVIO_API_KEY is not set, prompt the user to export it before continuing.

Python SDK

langchain-scavio has no LinkedIn tool - use the Scavio SDK directly (it handles the auth header, and next_cursor is passed straight back as cursor):

pip install scavio==0.15.0
from scavio import ScavioClient

client = ScavioClient()  # reads SCAVIO_API_KEY

profile = client.linkedin.person(username="williamhgates")
feed = client.linkedin.person_posts(username="williamhgates", type="reactions")
more = client.linkedin.person_posts(username="williamhgates", cursor=feed["data"]["next_cursor"])
jobs = client.linkedin.search_jobs("software engineer", location="United States")

Related skills

Looks up LinkedIn company, product, and showcase pages by ID via the Crawlora API, returning clean JSON. Use when the user wants a company's LinkedIn profile info, a product page, or a showcase page — instead of scraping LinkedIn directly. Covers company/product/showcase pages only, not personal LinkedIn profiles.

2 installs

Search ads running across LinkedIn by keyword and/or advertiser company id, returning each ad's advertiser, ad copy, format, promoted label, thumbnail and a detail link, then open one ad in full with its media, headline, who paid for it and the advertiser's company URL. 2 endpoints, 6 credits each, structured JSON.

Enrich a list of prospects (profile URLs, slugs, or names) with LinkedIn role and experience data via Veezee. Use when the user wants current titles, companies, or work history for known people, or asks to "enrich" leads/prospects/candidates.

1 installs