LeadShark API
ProProgrammatic 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).
https://apex.leadshark.ioAuthentication
Include your API key in the request header.
x-api-key— Your API keyGetting Your API Key
- Log in to your LeadShark account (any paid plan)
- Go to Settings → API Access
- 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.
- Call
GET /api/v1/posts(omitlinkedin_idfor your own posts) and pick the post you want to automate from theitemsarray. - Copy that item's
post_id(a URN likeurn:li:activity:7150…) andshare_url. - POST them to
/api/automationsaspost_idandlinkedin_post_urlrespectively.
# 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.datafor the current page of automations; useresponse.paginationfor metadata. - First page:
GET /api/automations?page=1&limit=10(or omit params for defaults). - Next page: same URL with
page=2, thenpage=3, etc. - Stop when
pagination.has_moreisfalse, or whenpage > pagination.total_pages. pagination.totalis the total number of automations across all pages.
| Field | Description |
|---|---|
total | Total automations (all pages) |
page | Current page number |
limit | Items per page requested |
total_pages | Total number of pages |
has_more | true if there is a next page |
stats object (same as dashboard)
| Field | Type | Description |
|---|---|---|
total_comments | number | Processed (GREATEST of webhooks, replies, chats with comment) |
total_dms_sent | number | Initial DMs sent |
total_connections_sent | number | Connection requests we sent (pending; outcome unknown) |
total_connections_accepted | number | Connection requests we accepted (inbound) |
total_comments_replied | number | First-degree comment replies sent |
total_non_first_degree_replies | number | Non-connected replies sent |
total_follow_ups_sent | number | Follow-up DMs sent |
total_follow_ups_skipped | number | Follow-ups skipped (e.g. no reply) |
total_auto_likes | number | Comments 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:
- Create a Page via
POST /api/v1/pagesand save thepage.id. - Create the automation with
links_enabled: true(see parameter above). - Create one or more lead-magnet links via
POST /api/v1/linkspassing the newautomation_idandpage_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}}@JohnLeads API
List all your leads (from automations and post engagement). Each lead includes email & ICP score (when present). Paginated for large lists.
- Use
response.datafor the current page; useresponse.pagination.has_moreto 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_negativeand related internal fields are not included.
Lead object — available fields
Each item in data includes these fields (when present):
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Lead record ID |
name | string | Full name |
title | string | LinkedIn headline / job title |
linkedin_url | string | LinkedIn profile URL |
source | string | Automation or source name |
created_at | string (ISO 8601) | When the lead was captured |
updated_at | string (ISO 8601) | Last update time |
commenter_id | string | LinkedIn member URN (e.g. ACoAAB...) |
post_id | string | null | LinkedIn post ID if from a post |
linkedin_username | string | null | LinkedIn username |
first_name | string | null | First name (when available) |
icp_score | number | null | ICP match score (0–1) |
icp_analysis | object | null | ICP analysis details |
lead_type | string | automation | post_engagement | connection_requests |
engagements | array | Engagement events (e.g. comments) |
icp_fit | string | null | fit | maybe | not |
archived | boolean | Whether the lead is archived |
enriched_profile | object | null | Apex auto-enrich: full profile JSON |
enriched_at | string | null | Apex auto-enrich: when enriched |
email | string | null | Apex 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
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:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | required* | Automation name |
dm_template | string | required* | Primary DM template (max 2000 chars) |
keywords | string[] | optional | Trigger keywords (max 20) |
dm_templates | string[] | optional | Multiple DM templates (rotated) |
comment_reply_template | string[] | optional | Reply templates for comments |
non_first_degree_reply_template | string[] | optional | Reply for non-connections |
auto_connect | boolean | optional | Send connection requests (default: false) |
auto_like | boolean | optional | Auto-like all comments — Pro+/Apex only (default: false) |
auto_enrich | boolean | optional | Auto-enrich lead profiles with full LinkedIn data — Apex only (default: false) |
icp_preset_id | string | null | optional | ICP preset UUID for follow-up gating. When set, follow-ups are only sent to leads matching this preset — Apex only |
enable_follow_up | boolean | optional | Enable follow-up DMs (default: false) |
follow_up_delay_minutes | number | optional | Delay before follow-up (default: 60) |
follow_up_only_if_no_response | boolean | optional | Only follow up if no reply (default: true) |
follow_up_template | string | optional | Follow-up message template |
links_enabled | boolean | optional | Enable link tracking — wraps URLs in DM templates with leads.sh tracking links (default: false) |
page_id | string | optional | Attach a Page to the automation. Pass the page UUID from POST /api/v1/pages. Requires links_enabled: true |
quiz_enabled | boolean | optional | Enable quiz questions on the attached page (default: false) |
template_id | string | optional | Use 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:
Note: PDF and video cannot be combined with other files. Only one PDF or video per post.
Error Codes
VALIDATION_ERROR — Invalid content, time, or automation configTIME_CONFLICT — Another post scheduled at this time (15-min window)SCHEDULING_TOO_SOON — Cannot edit post within 15 minutes of publish timePOST_ALREADY_PUBLISHED — Cannot modify published or failed postsGuide: 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.shtracking 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.
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
NewGET /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)
ApexOne 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.
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_pagesto know how many pages exist. Incrementpageuntil you reach it. - Both endpoints use
total_pagespagination (nothas_more). Checkpage < total_pagesto 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:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Hot lead record ID |
actor_name | string | Lead's full name |
actor_linkedin_id | string | LinkedIn member identifier |
actor_linkedin_url | string | LinkedIn profile URL |
actor_profile_picture_url | string | null | Profile picture URL |
heat_score | number | Composite engagement score (higher = hotter lead) |
signal_count | number | Total number of signal events from this lead |
signal_breakdown | object | Counts per signal type, e.g. { "comment": 5, "reaction": 3 } |
top_signals | array | Recent signal events with type, date, and source_url |
connection_status | string | LinkedIn relationship: FIRST_DEGREE, SECOND_DEGREE, etc. |
first_seen_at | string (ISO 8601) | When this lead first appeared in signals |
computed_at | string (ISO 8601) | When the heat score was last computed |
Event object — available fields
Each item in events includes these fields:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Signal event record ID |
actor_linkedin_id | string | LinkedIn member identifier |
actor_name | string | Lead's full name |
actor_title | string | null | LinkedIn headline / job title |
actor_linkedin_url | string | LinkedIn profile URL |
signal_type | string | One of the signal types below |
signal_weight | number | Weight of this signal toward heat score |
source_url | string | null | LinkedIn post or page URL that triggered the signal |
meta | object | Additional metadata (varies by signal type — e.g. reaction_type for reactions, comment_text for comments) |
signal_date | string (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_at | string (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
| Type | Description |
|---|---|
comment | Commented on your post |
reaction | Reacted to your post |
repost | Reposted your content |
profile_view | Viewed your LinkedIn profile |
lead_magnet_click | Clicked your lead magnet link |
dm_sent | You sent them a DM |
comment_reply | You replied to their comment |
connection_accepted | Connection request accepted |
connection_sent | Connection request sent |
automation_engagement | Engagement 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_atday. They also havesignal_daterooted 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.
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
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
| Code | Status | When |
|---|---|---|
invalid_post_id | 400 | post_id isn't a LinkedIn activity URN or numeric id |
invalid_cursor | 400 | cursor exceeds 4096 characters (reactions / comments only — real cursors are typically <200 chars) |
linkedin_not_connected | 400 | Workspace has no LinkedIn account connected |
linkedin_post_not_found | 404 | LinkedIn returned 404 for this post id (deleted, unauthorized, or wrong urn form) |
premium_required | 403 | /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_distancevalues:DISTANCE_1,DISTANCE_2,DISTANCE_3,OUT_OF_NETWORK,SELF, ornullwhen LinkedIn doesn't expose it.
Links APIPro+
Create and manage lead magnet links with analytics. Requires Pro+ or Apex subscription.
For event_type=click, email/name/phone are always null (clicks don't capture contact info). For event_type=email_capture, both email and commenter_id are populated together. Construct the LinkedIn profile URL as https://linkedin.com/in/{commenter_id}. When commenter_id is "anonymous", the visitor came through a generic share rather than a personalized redirect — there is no LinkedIn identity to join.
Pages APIPro+
Create and manage quiz pages, view responses, and export collected emails. Requires Pro+ or Apex subscription.
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 ashttps://linkedin.com/in/{commenter_id}whenlinkedin_urlis 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 (0–100) when the visitor completed a scored quiz on this page;nullotherwise.lead— joined LeadShark lead profile (name,title,linkedin_url) when we've seen this LinkedIn identity before;nullfor first-touch unknown profiles.source—email_gate(entered email on a gate) orquiz_response(submitted via quiz flow).
/links/:slug/eventsUse /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.
new_commentPro— new comment on a tracked postemail_capturedPro— email captured via personalized linklead_sentPro— lead manually sent from the Leads tablenew_profile_visitApex— LinkedIn profile viewnew_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.
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 body401UnauthorizedMissing or invalid API key404Not FoundResource does not exist429Too Many RequestsRate limit exceeded500Server ErrorUnexpected error, try againNeed Help?
Contact our team for API support.
