LeadShark API

Pro

Programmatic access to LinkedIn enrichment, automations, and more.

Last updated: May 7, 2026

Overview

The LeadShark API provides programmatic access to LinkedIn enrichment, automation management, bookmarking, and scheduled posting. Available on all paid plans (Pro, Pro+, Apex).

Base URLhttps://apex.leadshark.io

Authentication

Include your API key in the request header.

Headerx-api-key— Your API key

Getting Your API Key

  1. Log in to your LeadShark account (any paid plan)
  2. Go to Settings → API Access
  3. Generate your API key

Rate Limits

250

req/hr

Hourly

1,000

req/day

Daily

100

req/min

Burst

Exceeding limits returns 429 Too Many Requests. Use exponential backoff.

Enrichment API

Enrich LinkedIn profiles and companies with detailed data.

Recommended daily volume

Each call performs a real profile view from your connected LinkedIn account. We recommend capping enrichment at ~200–250 profiles per day, spread across the day.

Soft guideline — not enforced by the API.

Automations API

Create and manage LinkedIn post engagement automations.

How to automate a specific post

Creating an automation requires the LinkedIn post's URN (the field post_id) and its share URL (linkedin_post_url). Both come straight out of GET /api/v1/posts.

  1. Call GET /api/v1/posts (omit linkedin_id for your own posts) and pick the post you want to automate from the items array.
  2. Copy that item's post_id (a URN like urn:li:activity:7150…) and share_url.
  3. POST them to /api/automations as post_id and linkedin_post_url respectively.
# 1. Find the post you want to automate
curl 'https://apex.leadshark.io/api/v1/posts?limit=5' \
  -H "x-api-key: YOUR_API_KEY"
# → grab items[N].post_id  (e.g. "urn:li:activity:7150123456789012345")
# → grab items[N].share_url (e.g. "https://www.linkedin.com/posts/...")

# 2. Create the automation using those values
curl -X POST 'https://apex.leadshark.io/api/automations' \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "name": "Guide Giveaway",
    "post_id": "urn:li:activity:7150123456789012345",
    "linkedin_post_url": "https://www.linkedin.com/posts/...",
    "keywords": ["interested"],
    "dm_template": "Hi {{firstName}}! Here is the guide: https://example.com/guide"
  }'

Format matters: use the full URN exactly as returned by /api/v1/posts. The webhook matcher compares automations against the LinkedIn URN; passing only the numeric tail (e.g. 7150…) creates the row but engagement events won't bind to it.

How to paginate

  • Use response.data for the current page of automations; use response.pagination for metadata.
  • First page: GET /api/automations?page=1&limit=10 (or omit params for defaults).
  • Next page: same URL with page=2, then page=3, etc.
  • Stop when pagination.has_more is false, or when page > pagination.total_pages.
  • pagination.total is the total number of automations across all pages.
FieldDescription
totalTotal automations (all pages)
pageCurrent page number
limitItems per page requested
total_pagesTotal number of pages
has_moretrue if there is a next page

stats object (same as dashboard)

FieldTypeDescription
total_commentsnumberProcessed (GREATEST of webhooks, replies, chats with comment)
total_dms_sentnumberInitial DMs sent
total_connections_sentnumberConnection requests we sent (pending; outcome unknown)
total_connections_acceptednumberConnection requests we accepted (inbound)
total_comments_repliednumberFirst-degree comment replies sent
total_non_first_degree_repliesnumberNon-connected replies sent
total_follow_ups_sentnumberFollow-up DMs sent
total_follow_ups_skippednumberFollow-ups skipped (e.g. no reply)
total_auto_likesnumberComments auto-liked

Aliases: processed, dms_sent, sent, accepted are also set for backward compatibility.

Attaching a Page (lead magnet) to an automation

Pages don't attach directly to automations — they attach to the lead-magnet links the automation generates. Three steps:

  1. Create a Page via POST /api/v1/pages and save the page.id.
  2. Create the automation with links_enabled: true (see parameter above).
  3. Create one or more lead-magnet links via POST /api/v1/links passing the new automation_id and page_id. Every link the automation surfaces in DMs/comments will route through that page.

Doing it all in one shot? Scheduled posts with a pre_automation object accept page_id directly — see Pages + Pre-Automation (Full Flow) for the end-to-end recipe.

Webhook-only automation (minimal payload)

Pass only the three required fields and skip everything else. The automation performs no LeadShark-side actions — no DMs, no comment replies, no connection requests — but every comment on the post still streams to your configured webhooks in real time. Use this when you want to handle engagement entirely in your own backend.

curl -X POST 'https://apex.leadshark.io/api/automations' \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "name": "Webhook only — Q1 lead magnet",
    "post_id": "urn:li:activity:7150123456789012345",
    "linkedin_post_url": "https://www.linkedin.com/posts/..."
  }'

Defaults applied: keywords: [], dm_template: "", auto_connect: false, auto_like: false, status: "Running". This is the same shape the auto-detect cron uses internally for posts you publish with Auto-Automate turned on.

Template Variables

{{firstName}}John
{{fullName}}John Doe
{{linkedinUsername}}john-doe
{{firstNameMention}}@John

Leads API

List all your leads (from automations and post engagement). Each lead includes email & ICP score (when present). Paginated for large lists.

  • Use response.data for the current page; use response.pagination.has_more to know if there is a next page.
  • First page: GET /api/leads?page=1&limit=250 (omit params for defaults).
  • Only non-archived, individual leads (LinkedIn profile IDs starting with ACo) are returned.
  • Response uses a limited field set; user_id, icp_starred, icp_negative and related internal fields are not included.

Lead object — available fields

Each item in data includes these fields (when present):

FieldTypeDescription
idstring (UUID)Lead record ID
namestringFull name
titlestringLinkedIn headline / job title
linkedin_urlstringLinkedIn profile URL
sourcestringAutomation or source name
created_atstring (ISO 8601)When the lead was captured
updated_atstring (ISO 8601)Last update time
commenter_idstringLinkedIn member URN (e.g. ACoAAB...)
post_idstring | nullLinkedIn post ID if from a post
linkedin_usernamestring | nullLinkedIn username
first_namestring | nullFirst name (when available)
icp_scorenumber | nullICP match score (0–1)
icp_analysisobject | nullICP analysis details
lead_typestringautomation | post_engagement | connection_requests
engagementsarrayEngagement events (e.g. comments)
icp_fitstring | nullfit | maybe | not
archivedbooleanWhether the lead is archived
enriched_profileobject | nullApex auto-enrich: full profile JSON
enriched_atstring | nullApex auto-enrich: when enriched
emailstring | nullApex auto-enrich: email when found

Note: enriched_profile, enriched_at, and email are populated by Apex auto-enrich (Apex tier). They may be null for other plans or before enrichment runs.

Bookmarks API

Save and organize LinkedIn profiles with tags and notes.

Scheduled Posts API

Schedule LinkedIn posts with optional pre-configured automations that activate when the post goes live.

Scheduling Constraints

Minimum: 15 minutes from now
Maximum: 90 days in advance

Find your template_id

List every automation template on your account (including ones you created in the dashboard) so you can reuse an existing template in automation.template_id instead of defining a new one inline.

Here's the simplest call

No query params needed — returns the first 20 templates (default page size), newest first.

curl 'https://apex.leadshark.io/api/automation-templates' \
  -H "x-api-key: YOUR_API_KEY"

Reuse a template: copy any id from the response above and pass it as automation.template_id in POST /api/scheduled-posts. When template_id is provided, name and dm_template become optional — any other fields you send will patch the template in place.

Pre-Automation Object

When the scheduled post publishes, an automation is automatically created with these settings:

FieldTypeRequiredDescription
namestringrequired*Automation name
dm_templatestringrequired*Primary DM template (max 2000 chars)
keywordsstring[]optionalTrigger keywords (max 20)
dm_templatesstring[]optionalMultiple DM templates (rotated)
comment_reply_templatestring[]optionalReply templates for comments
non_first_degree_reply_templatestring[]optionalReply for non-connections
auto_connectbooleanoptionalSend connection requests (default: false)
auto_likebooleanoptionalAuto-like all comments — Pro+/Apex only (default: false)
auto_enrichbooleanoptionalAuto-enrich lead profiles with full LinkedIn data — Apex only (default: false)
icp_preset_idstring | nulloptionalICP preset UUID for follow-up gating. When set, follow-ups are only sent to leads matching this preset — Apex only
enable_follow_upbooleanoptionalEnable follow-up DMs (default: false)
follow_up_delay_minutesnumberoptionalDelay before follow-up (default: 60)
follow_up_only_if_no_responsebooleanoptionalOnly follow up if no reply (default: true)
follow_up_templatestringoptionalFollow-up message template
links_enabledbooleanoptionalEnable link tracking — wraps URLs in DM templates with leads.sh tracking links (default: false)
page_idstringoptionalAttach a Page to the automation. Pass the page UUID from POST /api/v1/pages. Requires links_enabled: true
quiz_enabledbooleanoptionalEnable quiz questions on the attached page (default: false)
template_idstringoptionalUse existing template (makes name/dm_template optional). Find IDs via GET /api/automation-templates

* Required unless template_id is provided

File Attachments

Use multipart/form-data to upload files with your post:

Images: JPG, PNG, GIF — 5MB each
PDF: 1 file only — 50MB
Video: MP4 only — 50MB

Note: PDF and video cannot be combined with other files. Only one PDF or video per post.

Error Codes

VALIDATION_ERRORInvalid content, time, or automation config
TIME_CONFLICTAnother post scheduled at this time (15-min window)
SCHEDULING_TOO_SOONCannot edit post within 15 minutes of publish time
POST_ALREADY_PUBLISHEDCannot modify published or failed posts

Guide: Pages + Pre-Automation (Full Flow)

Create a lead magnet page, then schedule a post with a pre-automation that has the page attached — all via the API. When the post publishes, everything activates automatically.

Step 1 — Create a Page

Use the Pages API to create and publish a page. Save the page.id from the response.

curl -X POST 'https://apex.leadshark.io/api/v1/pages' \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "LinkedIn Growth Playbook",
    "status": "published",
    "page_title": "Get Your Free Playbook",
    "page_description": "The exact strategies I used to grow to 100K followers",
    "questions": [{
      "question_text": "What is your biggest LinkedIn challenge?",
      "answers": [
        { "answer_text": "Getting more engagement", "mapped_outcome": "fit" },
        { "answer_text": "Growing my network", "mapped_outcome": "fit" },
        { "answer_text": "Just browsing", "mapped_outcome": "not" }
      ]
    }]
  }'

Response: { "page": { "id": "abc-123", ... } }

Step 2 — Schedule Post with Page Attached

Pass the page_id in the automation object with links_enabled: true.

curl -X POST 'https://apex.leadshark.io/api/scheduled-posts' \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Comment PLAYBOOK and I'\''ll DM it to you!",
    "scheduled_time": "2026-04-25T14:00:00Z",
    "automation": {
      "name": "Playbook Giveaway",
      "keywords": ["playbook", "interested", "yes"],
      "dm_template": "Hey {{firstName}}! Here is your playbook: https://example.com/playbook",
      "auto_connect": true,
      "links_enabled": true,
      "page_id": "abc-123",
      "quiz_enabled": true
    }
  }'

Step 3 — Automatic on Publish

When the scheduled time arrives, everything activates automatically:

  • Post publishes to LinkedIn
  • Automation starts — comments matching keywords trigger DMs and connection requests
  • URLs in DM templates are wrapped with leads.sh tracking links tied to your page
  • Visitors see your page (with quiz if enabled) before reaching the destination URL

You can also attach a page to an existing automation using the Links API with page_id.

Posts & Stats API

List recent posts (your own or anyone's) and pull aggregate engagement stats.

Tip: Each item's post_id is the LinkedIn post URN — pass it straight to /api/v1/posts/{post_id}/comments (and friends) for follow-up reads.

List recent posts

New
Deprecated

GET /api/post-stats (list mode) is now superseded by GET /api/v1/posts. Existing callers continue to work — the response carries a Deprecation: true header and a Link header pointing at the successor. Migrate at your convenience. Summary mode (?summary=true) is unaffected and remains the canonical home for aggregate post metrics — see below.

Aggregate totals (summary mode)

Apex

One call returns lifetime totals across your last N posts (impressions, comments, reactions, reposts) without you having to paginate and sum yourself. Great for dashboards and weekly reports. Apex-gated because each call may walk multiple upstream pages.

When truncated is true: you hit max_posts before reaching the end of your post history. Pass next_cursor back as cursor to continue scanning — but in practice 500 covers nearly everyone.

Signals APIApex

Access your ranked hot leads and raw engagement signal events via API. Requires an Apex subscription.

  • Use response.pagination.total_pages to know how many pages exist. Increment page until you reach it.
  • Both endpoints use total_pages pagination (not has_more). Check page < total_pages to determine if more data is available.
  • All results are scoped to the authenticated user's data only.

Signal object — available fields

Each item in signals includes these fields:

FieldTypeDescription
idstring (UUID)Hot lead record ID
actor_namestringLead's full name
actor_linkedin_idstringLinkedIn member identifier
actor_linkedin_urlstringLinkedIn profile URL
actor_profile_picture_urlstring | nullProfile picture URL
heat_scorenumberComposite engagement score (higher = hotter lead)
signal_countnumberTotal number of signal events from this lead
signal_breakdownobjectCounts per signal type, e.g. { "comment": 5, "reaction": 3 }
top_signalsarrayRecent signal events with type, date, and source_url
connection_statusstringLinkedIn relationship: FIRST_DEGREE, SECOND_DEGREE, etc.
first_seen_atstring (ISO 8601)When this lead first appeared in signals
computed_atstring (ISO 8601)When the heat score was last computed

Event object — available fields

Each item in events includes these fields:

FieldTypeDescription
idstring (UUID)Signal event record ID
actor_linkedin_idstringLinkedIn member identifier
actor_namestringLead's full name
actor_titlestring | nullLinkedIn headline / job title
actor_linkedin_urlstringLinkedIn profile URL
signal_typestringOne of the signal types below
signal_weightnumberWeight of this signal toward heat score
source_urlstring | nullLinkedIn post or page URL that triggered the signal
metaobjectAdditional metadata (varies by signal type — e.g. reaction_type for reactions, comment_text for comments)
signal_datestring (ISO 8601)When the engagement happened on LinkedIn. Day-truncated. For comments / DMs / clicks / connections this is the actual event date. For reactions and reposts, LinkedIn does not surface a per-actor timestamp, so this is the day we first detected the actor (or, for historical pre-Apr 2026 rows, the post's publish date). Filterable via since / until.
created_atstring (ISO 8601)When the row was first inserted into our database. Filterable via since_captured / until_captured. Use this when you want "what's new this week" for reactions/reposts.

Signal Types

TypeDescription
commentCommented on your post
reactionReacted to your post
repostReposted your content
profile_viewViewed your LinkedIn profile
lead_magnet_clickClicked your lead magnet link
dm_sentYou sent them a DM
comment_replyYou replied to their comment
connection_acceptedConnection request accepted
connection_sentConnection request sent
automation_engagementEngagement via automation

Picking the right date filter

Most engagement types come from sources that include a per-event timestamp (LinkedIn comments, your DMs, your link clicks, connection requests). For those, signal_date is the actual event date and since / until behave intuitively.

Reactions and reposts are different. LinkedIn does not expose when each individual person reacted or reposted — only the current list of who has done so. We diff that list daily and stamp newly-detected rows with the day we first saw them, which is the most truthful approximation we can give.

Practical guidance:

  • "Show me everyone who engaged with my recent posts" → use since (filters by post / engagement date).
  • "Show me what new engagement I've received this week" → use since_captured (filters by when our system detected it). This is what you almost always want for reactions and reposts, and it's safe for every other signal type too.
  • Historical reactions imported during your account's initial signals backfill share the same created_at day. They also have signal_date rooted near the original post's publish day.

Engagement APIPro & up

See exactly who's engaging with you on LinkedIn — the people who reacted to a specific post, left a comment, reshared it, or recently viewed your profile. Results are pulled live from your connected LinkedIn account, so what you see is what's on LinkedIn right now.

Where do I get a post_id?

Call /api/post-stats, pick any post, and use its social_id — that's the post_id for these endpoints.

"social_id": "urn:li:ugcPost:74549342603287*****"   →   use as post_id
When to use this vs. the Signals API: the Signals API is a deduped, scored roll-up across all your tracked posts (use it for ICP-fit triage and heat-score ranking). These endpoints are the per-engagement, per-post slice — use them when you have a specific post_id in mind, want to count anonymous profile viewers, or need reaction_type for sentiment segmentation.

Common envelope & pagination

All four endpoints return the same shape. To fetch the next page, pass ?cursor=<value>. When has_more is false you've reached the end.

{
  "items": [...],
  "pagination": {
    "cursor": "abc123" | null,
    "has_more": true | false
  }
}

Endpoint-specific error codes

CodeStatusWhen
invalid_post_id400post_id isn't a LinkedIn activity URN or numeric id
invalid_cursor400cursor exceeds 4096 characters (reactions / comments only — real cursors are typically <200 chars)
linkedin_not_connected400Workspace has no LinkedIn account connected
linkedin_post_not_found404LinkedIn returned 404 for this post id (deleted, unauthorized, or wrong urn form)
premium_required403/profile-viewers only — connected LinkedIn account isn't Premium (detected as zero results on first page)
  • Reposts pagination uses single-page-per-call (~10 items) deliberately. The underlying LinkedIn surface is sensitive to aggressive polling; spread your walks with a 1–2 second delay between cursor calls.
  • Profile viewers Premium gate hard-fails on the first page only. Subsequent calls with a valid cursor that return zero elements are returned as a normal empty page with has_more: false.
  • network_distance values: DISTANCE_1, DISTANCE_2, DISTANCE_3, OUT_OF_NETWORK, SELF, or null when LinkedIn doesn't expose it.

Pages APIPro+

Create and manage quiz pages, view responses, and export collected emails. Requires Pro+ or Apex subscription.

Field reference
  • email / name / phone — captured directly on the form.
  • commenter_id — LinkedIn vanity slug of the visitor (when the link was personalized). Build the profile URL as https://linkedin.com/in/{commenter_id} when linkedin_url is missing.
  • linkedin_url — joined from the lead profile when available; falls back to the click event's metadata.
  • country — IP-derived country at capture time. Useful for geo-disqualification before paid enrichment.
  • engagement_score — quiz response engagement score (0100) when the visitor completed a scored quiz on this page; null otherwise.
  • lead — joined LeadShark lead profile (name, title, linkedin_url) when we've seen this LinkedIn identity before; null for first-touch unknown profiles.
  • sourceemail_gate (entered email on a gate) or quiz_response (submitted via quiz flow).
When to use this endpoint vs. /links/:slug/events

Use /pages/:id/emails when you want a single deduped roll-up of every email captured for a lead magnet across all of its links — ideal for one-shot exports into a CRM. Use /links/:slug/events?event_type=email_capture when you want the raw event stream per link with timestamps and pagination — ideal for incremental ingestion. Both endpoints surface commenter_idfor joining captures back to LinkedIn identities. Construct the profile URL as https://linkedin.com/in/{commenter_id}when linkedin_url is null.

Webhooks APIPro

Programmatically create, update, list, delete, and test webhooks. Webhooks let LeadShark push real-time events (new comments, likes, profile visits, captured emails, etc.) to your own URL with an HMAC-SHA256 signature. Available on Pro and above; some event types are Apex-only.

Event types & tier requirements
  • new_commentPro— new comment on a tracked post
  • email_capturedPro— email captured via personalized link
  • lead_sentPro— lead manually sent from the Leads table
  • new_profile_visitApex— LinkedIn profile view
  • new_likeApex— new reaction on a tracked post

Pro / Pro+ accounts can create webhooks for the three Pro events. Including new_profile_visit or new_like in event_types from a non-Apex account returns 403 with apex_only_event_types.

Signature verification

Every event includes X-Webhook-Signature (HMAC-SHA256 of the raw body, hex encoded) andX-Webhook-Event (e.g. new.comment). Verify it using your webhook secret:

// Node.js example
const crypto = require('crypto');
const expected = crypto
  .createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(rawBody)
  .digest('hex');
const ok = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(req.headers['x-webhook-signature'])
);

The secret is only returned once — at creation time, or when you rotate it via PUT with rotate_secret: true. Store it securely.

Error Codes

400Bad RequestInvalid parameters or malformed body
401UnauthorizedMissing or invalid API key
404Not FoundResource does not exist
429Too Many RequestsRate limit exceeded
500Server ErrorUnexpected error, try again

Need Help?

Contact our team for API support.

Contact