LeadPanther API Workflows#
LeadPanther API v1 is available to accounts with active billing. API keys are bearer tokens. Keep them server-side and never expose them in browser code.
Base URL:
https://app.leadpanther.ai/api/v1
Use only documented paths under this base URL. https://api.leadpanther.ai/v1 is not the app API base URL.
All examples use anonymized IDs, keys, and personal data. Replace placeholder values such as lp_live_REDACTED, 00000000-0000-4000-8000-000000000001, 00000000-0000-4000-8000-000000000003, and 00000000-0000-4000-8000-000000000002 with values from your account.
Direct and Agency Patterns#
Direct-user work uses the direct account routes:
/posts
/lead-magnets
/leads
/activity
Agency-client work uses nested client routes:
/clients/{clientId}/posts
/clients/{clientId}/lead-magnets
/clients/{clientId}/leads
/clients/{clientId}/activity
For agency access, use the nested client routes. A valid agency API key alone is not enough; the agency must also have an active grant for the target client account. If a call returns 403, check both the API key scope and the agency-client grant.
Workspace social actions use /workspaces/{workspaceId}/social-actions. Those routes are workspace-scoped, not agency-client-scoped, and do not use clientId.
All list endpoints use limit and offset pagination unless otherwise documented. The default limit is generally 50 and the maximum is generally 100. For full exports, start at offset=0, keep filters stable, advance by the returned pagination.limit while pagination.has_more is true, and retry the same offset if a page fails. Use the response request_id when contacting LeadPanther support about an API call.
Scope Checklist#
| Workflow | Required scope |
|---|---|
| Verify identity | valid API key |
| List clients | clients:read |
| Create hosted lead magnet | lead_magnets:write |
| Create media upload URL | media:write |
| Create or schedule post | posts:write |
| Get post engagement | analytics:read |
| Get lead magnet analytics | analytics:read |
| List lead magnet follow-up actions | lead_magnets:read |
| Cancel lead magnet follow-up action | lead_magnets:write |
| List leads | leads:read |
| List activity | activity:read |
| Read workspace social actions | social_actions:read |
| Schedule workspace social action | social_actions:write |
| Read pod boost plans | pods:read |
| Create/stop pod boosts and takedowns | pods:write |
| List pod company actors | pods:read |
| Update pod company actors / refresh LinkedIn organizations | pods:write |
The * scope satisfies any required scope.
Posting a JPEG, PNG, or static WebP? Start with the Clean Image Post Workflow. It explains the private upload, finalization, post scheduling, and polling sequence end to end.
Fast Response Workflow#
Fast Response polls only one selected, lead-magnet-linked post for 30 or 60 minutes. A connected account may have only one overlapping scheduled or active Fast Response session. The normal broad comment scan yields while the session is active and resumes automatically when it ends. Because the user explicitly starts or schedules this temporary mode, its single-post polling continues outside the account's ordinary work hours. Disabling automations or comment capture, pausing the account, or disconnecting LinkedIn still blocks Fast Response.
Fast Response API writes are released per direct-user account. The global API switch alone does not grant access: the resolved account must also have an enabled rollout grant. During the initial canary, agency-client Fast Response writes are unavailable. A create/update payload containing fast_response, or a start/stop action, returns 404 not_found when the feature is unavailable for the API subject. Post reads may still include the additive fast_response status summary.
To reserve Fast Response while scheduling a post, add this to the normal post create or update payload:
"fast_response": {
"enabled": true,
"interval_minutes": 10,
"duration_minutes": 60
}
Intervals are 2, 5, or 10 minutes and durations are 30 or 60 minutes. Omitted enabled values default to 10 and 60. On PATCH, omitting fast_response leaves the reservation unchanged; { "enabled": false } cancels a scheduled reservation.
Start a provider-confirmed published post manually with a required replay key:
curl -sS "https://app.leadpanther.ai/api/v1/posts/00000000-0000-4000-8000-000000000002/fast-response/start" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Idempotency-Key: fr_public_safe_example_001" \
-H "Content-Type: application/json" \
--data '{"interval_minutes":2,"duration_minutes":60}'
Stop the exact session with its public fast_response.id:
curl -sS "https://app.leadpanther.ai/api/v1/posts/00000000-0000-4000-8000-000000000002/fast-response/stop" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
--data '{"boost_id":"00000000-0000-4000-8000-000000000020"}'
Agency calls use the same payloads under /clients/{clientId}/posts/{id}/fast-response/.... Do not send a provider account ID or provider post ID; LeadPanther resolves them from the authorized post and connected account. A 409 with error.details.reason = "boost_window_conflict" identifies a safe same-account collision; change the schedule or disable Fast Response rather than replacing or queueing the existing session.
Verify Identity#
Use GET /me to verify the API key, actor type, granted scopes, and subject account.
Required scope: valid API key.
curl -sS "https://app.leadpanther.ai/api/v1/me" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Example response:
{
"data": {
"owner": {
"user_id": "00000000-0000-4000-8000-000000000010",
"full_name": "Example Agency Owner",
"business_name": "Example Agency",
"account_status": "active"
},
"actor_type": "agency",
"scopes": ["clients:read", "lead_magnets:write", "posts:write"],
"subject": {
"user_id": "00000000-0000-4000-8000-000000000010"
}
},
"request_id": "req_000000000000000000000001"
}
List Clients#
Use GET /clients to list accounts available to the API key.
Required scope: clients:read.
curl -sS "https://app.leadpanther.ai/api/v1/clients" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Example response:
{
"data": [
{
"user_id": "00000000-0000-4000-8000-000000000001",
"full_name": "Client A",
"business_name": "Client A Company",
"account_status": "active",
"grant": {
"id": "00000000-0000-4000-8000-000000000006",
"label": "Client A",
"status": "active",
"permissions": ["posts:read", "posts:write", "lead_magnets:write"],
"created_at": "2026-05-25T00:00:00.000Z",
"updated_at": "2026-05-25T00:00:00.000Z"
}
}
],
"pagination": {
"limit": 1,
"offset": 0,
"has_more": false
},
"request_id": "req_000000000000000000000002"
}
For agency workflows, use the returned user_id as {clientId} in nested routes.
Create Hosted Lead Magnet#
Use POST /lead-magnets for direct-user work. Use POST /clients/{clientId}/lead-magnets for agency-client work.
Required scope: lead_magnets:write.
Hosted lead magnets use delivery_mode: "hosted_content" and require a hosted_page. If the hosted page is published, markdown_content must be non-empty. Hosted and gated templates should include {{resource_link}} unless confirm_missing_magnet_url is set to true.
Fields you leave out start from the account defaults, the same way a new magnet in the dashboard does: text_template from the default private DM preset, not_connected_reply from the default public reply preset, followups from the default sequence per pathway, and engagement_scope from the magnet defaults (see Account defaults). "", null, and [] are explicit choices and are kept. PATCH never applies defaults.
Direct example:
curl -sS "https://app.leadpanther.ai/api/v1/lead-magnets" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"delivery_mode": "hosted_content",
"keyword": "GUIDE",
"resource_name": "Example Guide",
"description": "A public-safe guide description.",
"text_template": "Here is the guide: {{resource_link}}",
"not_connected_reply": "Connect with me and I will send it over.",
"linked_posts": [],
"followups": [
{
"enabled": true,
"trigger": "resource_dm_delivered",
"channel": "dm",
"delay_minutes": 1440,
"message_template": "Did you get a chance to use the guide?",
"skip_if_replied": true
}
],
"confirm_missing_magnet_url": false,
"confirm_legacy_placeholder": false,
"hosted_page": {
"title": "Example Guide",
"markdown_content": "# Example Guide\n\nPublic-safe guide content.",
"external_resource_url": null,
"cta_label": null,
"cta_url": null,
"status": "published"
}
}'
Agency example:
curl -sS "https://app.leadpanther.ai/api/v1/clients/00000000-0000-4000-8000-000000000001/lead-magnets" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"delivery_mode": "hosted_content",
"keyword": "CHECKLIST",
"resource_name": "Example Checklist",
"description": "A public-safe checklist description.",
"text_template": "Here is the checklist: {{resource_link}}",
"linked_posts": [],
"followups": [],
"hosted_page": {
"title": "Example Checklist",
"markdown_content": "# Example Checklist\n\nPublic-safe checklist content.",
"external_resource_url": null,
"cta_label": null,
"cta_url": null,
"status": "published"
}
}'
Example response:
{
"data": {
"lead_magnet": {
"id": "00000000-0000-4000-8000-000000000003",
"delivery_mode": "hosted_content",
"keyword": "GUIDE",
"resource_name": "Example Guide"
},
"result": {
"status": "saved"
}
},
"request_id": "req_000000000000000000000003"
}
Lead Magnet Follow-Up Workflow#
Lead magnet follow-up rules are configured on the lead magnet create/update payload with followups. Including followups replaces the existing rules for that magnet. Omitting followups leaves existing rules unchanged on update.
See also conversation-playbook-product-overview.md for the full Conversation Playbook product model (dual pathways, reactions, engagement scope).
Conversation Playbook fields (lead magnet)#
| Field | Values | Notes |
|---|---|---|
engagement_scope | keyword_only, all_comments | Default keyword_only. all_comments engages every comment on linked posts for this magnet. |
response_mode | connection_aware, unified_public_only, unified_public_plus_dm | Default connection_aware. Public-only never sends a DM. |
unified_public_reply | string | Used by either unified public response mode. |
followups[] | pathway rules | Max 5 enabled steps per connection_path. |
Each follow-up rule supports:
connection_path:connectedornot_connected(DM forbidden onnot_connected)channel:comment,dm, orreactiondelay_minutes,delay_anchor(instant_responseorprevious_step)reactionconfig whenchannel = reaction(fixedorrandom_pool)- optional
trigger(derived fromchannelwhen omitted)
Account defaults for new magnets#
New magnets, in the dashboard and through POST /lead-magnets, inherit the account defaults. One GET returns all of them, resolved:
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-defaults" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
{
"data": {
"followups": null,
"resolved_followups": [ ... ],
"sources": { "connected": "account_preset", "not_connected": "built_in" },
"uses_product_defaults": false,
"messages": {
"private_dm": { "body": "Hey {{first_name}}, here it is: {{resource_link}}", "source": "account_preset" },
"public_reply": { "body": "Sent! Accept my connection request and it lands in your DMs.", "source": "built_in" }
},
"magnet": { "engagement_scope": "all_comments", "sources": { "engagement_scope": "account" } }
},
"request_id": "req_..."
}
messagesandresolved_followupscome from the presets markedis_default(see Magnet default presets), falling back to the legacy settings, then the built-ins. They are read-only here; change them by marking a preset default.magnetholds the defaults that are not messages.engagement_scopeis the default capture mode:keyword_only(only comments containing the keyword) orall_comments(every comment on a linked post). Set it with PATCH:
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-defaults" \
-X PATCH \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{ "magnet": { "engagement_scope": "all_comments" } }'
all_comments is refused with 400 when all-comments capture is not enabled for the workspace, on this endpoint and on magnet create alike. Changing the default never touches existing magnets; each magnet keeps its own copy.
Legacy default follow-up array#
Before presets, the default sequence lived in a single followups array on this endpoint. It is still accepted for compatibility and still wins over the built-in sequence when no default sequence preset exists:
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-defaults" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-defaults" \
-X PATCH \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"followups": [
{
"connection_path": "connected",
"channel": "comment",
"delay_minutes": 2880,
"delay_anchor": "instant_response",
"message_template": "Hey {{first_name}}, did you get the {{resource_name}}?",
"skip_if_replied": true
}
]
}'
LinkedIn safety requirement: do not configure lead magnet follow-ups so a large batch of DMs or public comments becomes due at the same time. Use randomized delays, spread actions across normal working hours, and avoid exact repeating intervals. See LinkedIn Safety for LeadPanther's provider-safe scheduling guidance.
Rules are limited to 5 enabled steps per connection_path. delay_minutes is measured from instant engage (instant_response) or chains from the prior step (previous_step). skip_if_replied defaults to true.
resolved_followups resolves the same way the dashboard seeds a new magnet: the default sequence preset for each pathway wins, then the legacy followups array saved here, then the stock sequence. sources.connected and sources.not_connected say which one applied (account_preset, company_preset, legacy, or built_in). For an agency-managed account, the default preset of its client company sits between the account preset and the legacy array; company_preset reports that, for messages as well as sequences. Company presets are managed in the agency dashboard, not through this API. Prefer presets for new integrations. A PATCH may send followups, magnet, or both; only the parts sent are echoed back.
Magnet default presets#
Presets are the named defaults shown under Lead Magnets → Defaults in the dashboard. Three kinds:
kind | Holds | pathway |
|---|---|---|
private_dm | body: the instant DM text | not set |
public_reply | body: the public comment reply | not set |
sequence | steps: 1 to 6 follow-up rules for one pathway (same rule shape as followups[], every step's connection_path must equal the preset's pathway; DM steps only on connected) | connected or not_connected |
One preset per kind (and pathway, for sequences) can be the default. Setting is_default: true on create or update clears the previous default. A new magnet starts from the defaults, in the dashboard and through POST /lead-magnets for any field the request leaves out. Deleting a preset never changes a magnet: magnets hold copies.
List, filtered to one kind:
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-presets?kind=sequence" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Create a not-connected sequence and make it the default:
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-presets" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"kind": "sequence",
"pathway": "not_connected",
"name": "Gentle nudge",
"is_default": true,
"steps": [
{
"connection_path": "not_connected",
"channel": "reaction",
"delay_minutes": 1,
"delay_anchor": "instant_response",
"reaction": { "mode": "random_pool", "reaction_types": ["celebrate", "love", "like"] }
},
{
"connection_path": "not_connected",
"channel": "comment",
"delay_minutes": 60,
"delay_anchor": "previous_step",
"message_template": "{{first_name}}, hope you enjoyed {{resource_name}}! Grab it here: {{resource_link}}."
}
]
}'
Save a DM preset, then rename it:
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-presets" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
--data '{ "kind": "private_dm", "name": "Friendly DM", "body": "Hey {{first_name}}! Here is the {{resource_name}}: {{resource_link}}" }'
curl -sS "https://app.leadpanther.ai/api/v1/account/playbook-presets/00000000-0000-4000-8000-000000000010" \
-X PATCH \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
--data '{ "name": "Friendly DM v2", "is_default": true }'
Responses are { "data": { "preset": { ... } } } and { "data": { "presets": [ ... ] } } (default first, then by name). A duplicate name within the same kind and pathway returns 409 conflict. Agency keys use /clients/{clientId}/playbook-presets with the same bodies.
When LeadPanther records a successful initial delivery, it materializes matching lead_magnet_followup_actions rows for the stamped pathway only. Customer API clients do not call the internal delivery-event pipeline directly.
Follow-up action list responses include connection_path, reaction_type, and source_comment (the prospect's original comment when available).
Supported legacy explicit trigger/channel pairs (still accepted when trigger is set):
trigger: "resource_dm_delivered"withchannel: "dm"trigger: "public_comment_replied"withchannel: "comment"
Reaction steps use channel: "reaction" and omit message_template.
The API exposes materialized actions for read/cancel:
curl -sS "https://app.leadpanther.ai/api/v1/lead-magnets/00000000-0000-4000-8000-000000000003/followup-actions?status=pending&limit=25&offset=0" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
curl -sS "https://app.leadpanther.ai/api/v1/lead-magnets/00000000-0000-4000-8000-000000000003/followup-actions/00000000-0000-4000-8000-000000000032/cancel" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Agency-client routes use the same shape under /clients/{clientId}/lead-magnets/{id}/followup-actions.
To queue a one-off historical follow-up sweep through LeadPanther, call the collection route with POST. Use dry_run: true first to validate targeting, channel selection, template variables, duplicate targets, and skipped reasons without creating actions.
curl -sS "https://app.leadpanther.ai/api/v1/lead-magnets/00000000-0000-4000-8000-000000000003/followup-actions" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: manual-followup-2026-05-31-001" \
--data '{
"dry_run": true,
"targets": [
{
"activity_id": "00000000-0000-4000-8000-000000000041"
},
{
"lead_id": "00000000-0000-4000-8000-000000000042"
}
],
"channel": "auto",
"message_template": "Hey {{first_name}}, did you have a chance to check out the {{resource_name}}?",
"skip_if_replied": true,
"pacing": {
"min_seconds": 20,
"max_seconds": 45
}
}'
When the dry run looks correct, repeat the same request with dry_run: false and the same idempotency key if it is the same logical queue. The initial release accepts up to 100 targets per request. skip_if_replied must stay enabled.
Manual queue behavior:
- The lead magnet in the URL path controls the
{{resource_name}},{{resource_link}}, and{{keyword}}values. - The request body chooses targets only; it cannot override the owner, client, lead magnet, or resource context.
- Supported variables are
{{first_name}},{{recipient_name}},{{keyword}},{{resource_name}},{{resource_link}},{{magnet_url}}, and{{magnet_link}}. channel: "auto"sends DM only when LeadPanther has safe DM evidence, such as prior DM delivery or an existing inbound DM conversation. Otherwise, it uses a comment target when available or skips the target.- Explicit
channel: "dm"requires a known DM conversation target. Explicitchannel: "comment"requires comment thread fields. - Actions are inserted into the same follow-up action model as native future follow-ups, so existing list and cancel routes continue to apply.
- Historical queues must be paced. Avoid scheduling large same-account batches at identical times; random spacing is part of the safety model.
Execution worker behavior: Convex registers process-lead-magnet-followups to run every minute. The worker claims due pending rows, marks them processing, renders {{first_name}}, {{recipient_name}}, {{keyword}}, {{resource_name}}, {{resource_link}}, and legacy {{magnet_url}}/{{magnet_link}}, checks skip_if_replied when enabled, records the rendered message before any provider send, sends through Unipile, then marks rows completed, skipped, or failed. Retryable checks before provider send, such as unknown reply state, may reschedule back to pending. Once a provider send attempt starts, unconfirmed send outcomes fail for manual review instead of auto-retrying. The worker claims up to 10 rows per run, processes one action per user per run, defers additional due actions for the same user, and enforces same-user send cooldowns across overlapping cron runs. Native rule actions use the default 3-minute same-user cooldown and 20-45 second in-process delay. Manual historical queue actions use their action-level pacing.min_seconds/pacing.max_seconds for same-user cooldown/defer timing, with a runtime cap on in-process sleeps so a cron run cannot stall for many minutes. These worker safeguards are not a substitute for safe scheduling; if every action is due at once, the backlog still creates an automation pattern. Unknown reply state fails closed when skip_if_replied is true. Stale processing rows are marked failed for manual review rather than blindly retried.
Dashboard UI exposure is implemented in the lead magnet create/edit flow. The dashboard uses the same API contract documented above: lead magnet create/update for rules, follow-up action list for queue visibility, and follow-up action cancel for pending cancellations. UI saves that include followups replace the full follow-up rule set for that magnet, while saves that omit followups leave existing rules unchanged. UI testing should use draft magnets or controlled future-dated pending actions and must not trigger real LinkedIn sends unless an explicit production smoke window has been approved.
Expected dashboard behavior:
- Configure DM and public comment follow-up steps from the lead magnet create/edit experience.
- Default
skip_if_repliedto enabled. - Convert days/hours controls into
delay_minutesbefore saving. - Encourage randomized follow-up timing rather than identical delay presets across large campaigns.
- Show action status, channel, trigger, scheduled time, executed time, recipient summary, retry count, and a trimmed error summary.
- Explain
skippedas a safety outcome where no follow-up was sent. - Treat
failedas requiring review, especially if provider send confirmation is ambiguous. - Allow cancellation only for
pendingactions and refresh the queue after cancellation. - Never expose API keys, service-role credentials, delivery-event secrets, Unipile credentials, or internal webhook secrets in browser-visible payloads.
Create Media Upload URL#
Use POST /media/upload-url for direct-user work. Use POST /clients/{clientId}/media/upload-url for agency-client work.
Required scope: media:write.
Allowed content types are image/jpeg, image/png, image/gif, image/webp, application/pdf, video/mp4, video/quicktime, and video/webm. Images can be up to 5 MB. PDFs can be up to 15 MB. Videos can be up to 100 MB and must be processed to 899 seconds or shorter before creating a post.
Direct example:
curl -sS "https://app.leadpanther.ai/api/v1/media/upload-url" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"filename": "example-image.png",
"content_type": "image/png",
"file_size": 102400
}'
Agency example:
curl -sS "https://app.leadpanther.ai/api/v1/clients/00000000-0000-4000-8000-000000000001/media/upload-url" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"filename": "example-document.pdf",
"content_type": "application/pdf",
"file_size": 204800
}'
Example response:
{
"data": {
"upload_url": "https://storage.example.com/upload/REDACTED",
"token": "REDACTED_UPLOAD_TOKEN",
"path": "api-uploads/00000000-0000-4000-8000-000000000001/example-image.png",
"public_url": "https://cdn.example.com/api-uploads/example-image.png",
"bucket": "content-images",
"content_type": "image/png"
},
"request_id": "req_000000000000000000000004"
}
The returned upload_url and token are sensitive and short-lived. Do not log them in places accessible to end users.
For video posts, upload or process the video and a JPEG thumbnail, then create the post with document_type: "video", video_url, video_thumbnail_url, video_duration_seconds, and video_processing_status: "ready". When uploading a thumbnail through this endpoint, include "purpose": "video_thumbnail"; it is stored under the dedicated video-thumbnails path and is not usable as an ordinary post image. After a successful publish, LeadPanther deletes the stored video object and keeps the thumbnail for previews.
Clean Image Post Workflow#
Use this workflow for a JPEG, PNG, or static WebP when Clean image uploads is enabled for the effective account.
The important distinction is that there are two tracks:
- A media job privately uploads and finalizes the image asynchronously.
- A normal post request schedules the post immediately after the upload is accepted for finalization.
You do not wait for a job GET to say ready before creating the post. After complete returns queued, processing, or ready, submit the post with the job ID. LeadPanther holds that post in processing_media, attaches the server-generated final image itself, and changes the post to scheduled only when every job is ready. There is no second post-create call and you must not copy a job’s final URL into images yourself.
Image job: awaiting_upload -> queued -> processing -> ready
\-> failed or expired
Post: POST/PATCH with job IDs -> processing_media -> scheduled -> publishing -> published
\-> failed
If the requested publish time cannot be met, the post becomes failed; LeadPanther never publishes it late.
Choose the Direct or Agency Route Family#
The following five steps are identical for direct users and agency-client work. Set the two route roots once, then use them in every call below.
| Phase | Direct-user route | Agency-client route |
|---|---|---|
| Create job | POST /media/image-finalization-jobs | POST /clients/{clientId}/media/image-finalization-jobs |
| Complete job | POST /media/image-finalization-jobs/{jobId}/complete | POST /clients/{clientId}/media/image-finalization-jobs/{jobId}/complete |
| Optional job status | GET /media/image-finalization-jobs/{jobId} | GET /clients/{clientId}/media/image-finalization-jobs/{jobId} |
| Create post | POST /posts | POST /clients/{clientId}/posts |
| Poll post | GET /posts/{postId} | GET /clients/{clientId}/posts/{postId} |
Direct-user setup:
API="https://app.leadpanther.ai/api/v1"
MEDIA_ROOT="$API/media"
POST_ROOT="$API/posts"
Agency-client setup:
API="https://app.leadpanther.ai/api/v1"
CLIENT_ID="00000000-0000-4000-8000-000000000001" # GET /clients returns this user_id
MEDIA_ROOT="$API/clients/$CLIENT_ID/media"
POST_ROOT="$API/clients/$CLIENT_ID/posts"
An agency key needs clients:read to discover an eligible client, plus media:write and posts:write. Use the same CLIENT_ID for the job and the post; a client job cannot be attached to another client’s post.
1. Create One Job Per Image#
Create a job for each image, in the same order that you want the post carousel to appear. The response is awaiting_upload and contains a short-lived, private upload target. Save data.id as JOB_ID and data.upload.url as UPLOAD_URL; do not log or expose the upload URL or token.
curl --fail-with-body -sS "$MEDIA_ROOT/image-finalization-jobs" \
-X POST \
-H "Authorization: Bearer ${LEADPANTHER_API_KEY}" \
-H "Content-Type: application/json" \
--data '{
"filename": "my-post-image.png",
"content_type": "image/png",
"file_size": 102400
}'
Representative response shape (the short-lived upload credential is intentionally omitted):
{
"data": {
"id": "00000000-0000-4000-8000-000000000010",
"status": "awaiting_upload",
"upload": {
"url": "https://private-upload.example/REDACTED",
"path": "REDACTED_PRIVATE_PATH",
"expires_at": "2030-01-15T15:05:00.000Z"
}
},
"request_id": "req_000000000000000000000010"
}
2. Upload the Raw Image Bytes to That Private Target#
This PUT goes to the returned private UPLOAD_URL, not to the LeadPanther API and not with your LeadPanther bearer key. Send the original bytes with the same MIME type declared when the job was created. A Supabase client may instead use the returned path and token with its signed-upload method.
curl --fail-with-body -sS "$UPLOAD_URL" \
-X PUT \
-H "Content-Type: image/png" \
-H "x-upsert: false" \
--data-binary "@./my-post-image.png"
3. Tell LeadPanther the Upload Is Complete#
Call complete after the private upload succeeds. It verifies the object exists and queues the finalizer. Repeating this call is safe: it returns the current job state and does not create a second final image.
curl --fail-with-body -sS "$MEDIA_ROOT/image-finalization-jobs/$JOB_ID/complete" \
-X POST \
-H "Authorization: Bearer ${LEADPANTHER_API_KEY}"
The usual result is data.status: "queued". It can also already be processing or ready. Any of those three states can be used in the next step. Do not wait for GET .../{jobId} before scheduling the post.
4. Create and Schedule the Post Once#
Submit the regular post request immediately after you call complete for every image job. Do not wait for finalization to finish. Use the ordered job IDs, set status to scheduled, and choose scheduled_at at least five minutes after the request reaches LeadPanther. The server, not the client clock, enforces that buffer.
Do not send images, media_url, document, or video fields in this request. A clean-image post create requires an Idempotency-Key; keep the same key and exact normalized payload if the network outcome is uncertain and you retry.
curl --fail-with-body -sS "$POST_ROOT" \
-X POST \
-H "Authorization: Bearer ${LEADPANTHER_API_KEY}" \
-H "Idempotency-Key: clean-image-post-0001" \
-H "Content-Type: application/json" \
--data '{
"text": "A clean-image post.",
"status": "scheduled",
"scheduled_at": "2030-01-15T15:15:00.000Z",
"image_finalization_job_ids": [
"00000000-0000-4000-8000-000000000010"
]
}'
The response is the normal post envelope, but initially looks like this:
{
"data": {
"id": "00000000-0000-4000-8000-000000000020",
"status": "processing_media",
"scheduled_at": "2030-01-15T15:15:00.000Z"
}
}
Save that post id as POST_ID. It is the resource to poll from now on. To attach clean images to an existing editable draft or scheduled post, use PATCH "$POST_ROOT/$POST_ID" with the same clean-image fields instead of this POST; the post then enters processing_media as well.
5. Poll the Post Until It Is Scheduled or Fails#
Use the ordinary post GET, not a second create call. While the finalizer works, it returns processing_media. When every job is ready, LeadPanther writes the final server-generated images and returns scheduled. The normal publisher will publish at scheduled_at later.
curl --fail-with-body -sS "$POST_ROOT/$POST_ID" \
-H "Authorization: Bearer ${LEADPANTHER_API_KEY}"
For example, a caller can poll at a modest interval and stop at a terminal state:
while true; do
STATUS=$(curl -sS "$POST_ROOT/$POST_ID" \
-H "Authorization: Bearer ${LEADPANTHER_API_KEY}" | jq -r '.data.status')
case "$STATUS" in
processing_media) sleep 5 ;;
scheduled) echo "Image is finalized and the post is scheduled."; break ;;
failed) echo "Finalization or the scheduling deadline failed; inspect the job/post response."; break ;;
*) echo "Unexpected post status: $STATUS"; break ;;
esac
done
GET "$MEDIA_ROOT/image-finalization-jobs/$JOB_ID" is optional. It is useful for showing an upload/finalization progress UI and returns data.media only when the job is ready, or data.failure when it failed. It is not a prerequisite for Step 4, and a ready job’s data.media.url is not a value to submit in a new post request.
Failures, Retries, and What Happens Next#
awaiting_upload: the rawPUTdid not finish. Upload the bytes, then callcomplete. If the upload target has expired, create a new job.queuedorprocessing: finalization is running. The post may already be safely held inprocessing_media; continue to poll the post.ready: final media exists. If the post is stillprocessing_media, the settlement worker will attach it; keep polling the post instead of creating another one.failedorexpiredjob: inspectdata.failurewhen present. Correct the input and create a new job; job IDs are one-use bindings.failedpost: a source job failed, expired, or the requested schedule could not be met. Do not blindly retry the same post create. Use new job IDs and a new scheduled post after correcting the cause; the original bound jobs cannot be reused.- A clean-image post create replay with the same
Idempotency-Keyand same payload returns the original post. A changed payload returns409 conflict; if the original post was deleted, the replay returns410 goneand never creates a replacement.
The five-minute rule is evaluated by the server at submission time. Fast Response, Team Boost, and post follow-ups cannot be configured on the same processing-media submission; add them after the post reaches scheduled.
While a post is processing_media, PATCH is deliberately narrow: update text, theme, lead-magnet template, notes, or move the requested publish time to the same or a later time. The later time must still be at least five minutes after the PATCH reaches the server; retaining the unchanged timestamp is allowed for a metadata-only edit. Status changes, direct media fields, replacement job IDs, Fast Response, Team Boost, and follow-up configuration are rejected while processing is active, so a settlement cannot overwrite a concurrent attachment or automation change.
For enabled accounts, POST /media/upload-url and its nested-client version return 409 with image_finalization_required for JPEG, PNG, and WebP upload requests, unless the request explicitly declares purpose: "video_thumbnail". This redirects ordinary clean-image ingestion to the private job workflow; it does not mean that still images are unsupported. The thumbnail exception is only for a video post’s video_thumbnail_url, not its images field. The finalizer accepts static WebP but rejects animated input. Documents, videos, GIFs, and disabled accounts retain the ordinary upload route.
Create or Schedule a Lead Magnet Post#
Use POST /posts for direct-user work. Use POST /clients/{clientId}/posts for agency-client work.
Required scope: posts:write.
To create a lead magnet post, set template_id to the lead magnet ID. Use status: "draft" to save a draft. Use status: "scheduled" with a future scheduled_at ISO datetime to schedule the post.
Direct draft example:
curl -sS "https://app.leadpanther.ai/api/v1/posts" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"text": "Public-safe post copy. Comment GUIDE and I will send the resource.",
"status": "draft",
"scheduled_at": null,
"theme_id": null,
"template_id": "00000000-0000-4000-8000-000000000003",
"images": [
{
"url": "https://cdn.example.com/api-uploads/example-image.png",
"type": "image"
}
],
"document_url": null,
"document_type": "image",
"notes": "Public-safe internal note.",
"followups": {
"reaction": {
"enabled": true,
"delay_minutes": 30,
"reaction_type": "like"
},
"repost": null,
"comments": [
{
"text": "Comment GUIDE if you want the resource.",
"delay_minutes": 60
}
]
}
}'
Agency scheduled example:
curl -sS "https://app.leadpanther.ai/api/v1/clients/00000000-0000-4000-8000-000000000001/posts" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"text": "Public-safe scheduled post copy. Comment CHECKLIST and I will send the resource.",
"status": "scheduled",
"scheduled_at": "2026-06-15T14:00:00.000Z",
"theme_id": null,
"template_id": "00000000-0000-4000-8000-000000000003",
"images": [],
"document_url": null,
"document_type": "image",
"notes": "Public-safe internal note.",
"followups": {
"reaction": {
"enabled": false,
"delay_minutes": 30,
"reaction_type": "like"
},
"repost": null,
"comments": []
}
}'
Example response:
{
"data": {
"id": "00000000-0000-4000-8000-000000000002",
"status": "scheduled",
"scheduled_at": "2026-06-15T14:00:00.000Z",
"template_id": "00000000-0000-4000-8000-000000000003",
"metrics": {},
"theme": null,
"followups": {
"reaction": {
"enabled": false,
"delay_minutes": 30,
"reaction_type": "like"
},
"repost": null,
"comments": []
}
},
"request_id": "req_000000000000000000000005"
}
Schedule Workspace Social Action#
Use POST /workspaces/{workspaceId}/social-actions.
Required scope: social_actions:write.
The API key owner must be an active workspace member or have an active workspace API grant with social_actions:write or *. The actor and target post owner must both be active workspace members. The target post must be a LeadPanther content-calendar post in either published or scheduled status.
LinkedIn safety requirement: each workspace comment or repost must have a deliberately randomized scheduled_for. Do not queue many actors to comment or repost at the same timestamp, at fixed minute offsets, or in a tight burst. Spread actions across working hours and keep enough randomness that the pattern does not look automated. See LinkedIn Safety.
Published targets create queued actions and require LinkedIn publish identifiers. Scheduled targets create pending_target actions, then transition to queued after the target post publishes and has LinkedIn publish identifiers.
If scheduled_for is omitted, published targets schedule for now plus 1 minute; scheduled targets schedule for the later of now plus 1 minute or the target post scheduled_at plus 1 minute. If provided, it must be at least 1 minute in the future. For scheduled targets, it must also be after the target post scheduled_at.
Idempotency-Key header and body idempotency_key are both accepted. If both are provided, they must match. Reusing a key with the same payload returns the existing queued action. Reusing a key with a different payload returns 409 conflict.
Comment example:
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: ws-comment-0001" \
--data '{
"actor_user_id": "00000000-0000-4000-8000-000000000001",
"target_post_id": "00000000-0000-4000-8000-000000000002",
"action_type": "comment",
"comment_text": "Great post.",
"scheduled_for": "2026-06-15T14:01:00.000Z"
}'
Repost example:
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"actor_user_id": "00000000-0000-4000-8000-000000000001",
"target_post_id": "00000000-0000-4000-8000-000000000002",
"action_type": "repost",
"repost_text": "Worth reading.",
"scheduled_for": "2026-06-15T14:05:00.000Z",
"idempotency_key": "ws-repost-0001"
}'
Example response:
{
"data": {
"id": "00000000-0000-4000-8000-000000000011",
"workspace_id": "00000000-0000-4000-8000-000000000012",
"actor_user_id": "00000000-0000-4000-8000-000000000001",
"target_post_id": "00000000-0000-4000-8000-000000000002",
"action_type": "comment",
"status": "queued",
"scheduled_for": "2026-06-15T14:01:00.000Z",
"comment_text": "Great post.",
"repost_text": null,
"created_at": "2026-06-15T14:00:00.000Z",
"updated_at": null
},
"request_id": "req_000000000000000000000010"
}
Pre-Schedule Workspace Cross-Engagement#
Use this workflow when a workspace wants to queue cross-engagement as soon as the parent posts are scheduled in LeadPanther.
Typical sequence:
- Schedule the parent posts through LeadPanther and keep each returned post
idandscheduled_at. - Build the workspace engagement matrix in your own system. For each target post, choose which workspace members should comment or repost. The target post owner should not be used as the actor unless that is intentional.
- For every planned action, choose a randomized
scheduled_forafter the target postscheduled_at. A safer pattern is comments staggered across a wider window after publish and reposts later, not all actions a few minutes after publish. - Call
POST /workspaces/{workspaceId}/social-actionsonce per planned comment or repost. Use a deterministicIdempotency-Keyper action so retries do not create duplicates. - Store the returned social action
id. - Use
GET /workspaces/{workspaceId}/social-actions?status=pending_targetto confirm actions are waiting on scheduled target posts. - If the plan changes before execution, call the cancel endpoint for any
pending_targetorqueuedaction.
For a scheduled target post, the create response returns status: "pending_target". LeadPanther automatically transitions the action to queued after the target post publishes and has LinkedIn publish identifiers. The normal worker then executes the action at scheduled_for; if that time is already due when the target publishes, it runs on the next worker pass.
If the target post is moved back to draft or fails to publish, LeadPanther cancels dependent pending_target actions and keeps the audit trail.
Comment against a scheduled target:
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: ws-000000000012-post-000000000002-actor-000000000001-comment" \
--data '{
"actor_user_id": "00000000-0000-4000-8000-000000000001",
"target_post_id": "00000000-0000-4000-8000-000000000002",
"action_type": "comment",
"comment_text": "Strong perspective here.",
"scheduled_for": "2026-06-15T14:05:00.000Z"
}'
Example scheduled-target response:
{
"data": {
"id": "00000000-0000-4000-8000-000000000011",
"workspace_id": "00000000-0000-4000-8000-000000000012",
"actor_user_id": "00000000-0000-4000-8000-000000000001",
"target_post_id": "00000000-0000-4000-8000-000000000002",
"action_type": "comment",
"status": "pending_target",
"scheduled_for": "2026-06-15T14:05:00.000Z",
"comment_text": "Strong perspective here.",
"repost_text": null,
"executed_at": null,
"created_at": "2026-06-15T13:00:00.000Z",
"updated_at": null
},
"request_id": "req_000000000000000000000011"
}
Check pending actions:
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions?status=pending_target&target_post_id=00000000-0000-4000-8000-000000000002" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Cancel a planned action:
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions/00000000-0000-4000-8000-000000000011/cancel" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Current limits:
- Bulk creation is not available yet. Create one action per API call.
- Arbitrary LinkedIn URLs are not supported for workspace social actions.
- Likes/reactions are not supported for workspace social actions.
- LeadPanther does not rewrite
scheduled_forvalues. The caller must randomize timing for each action before creating it.
Read Workspace Social Actions#
Use GET /workspaces/{workspaceId}/social-actions to list actions, or GET /workspaces/{workspaceId}/social-actions/{actionId} to retrieve one action.
Required scope: social_actions:read.
Supported list filters are status, action_type, actor_user_id, target_post_id, limit, and offset. status=pending_target returns actions waiting for scheduled target posts to publish.
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions?status=queued&limit=25&offset=0" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions/00000000-0000-4000-8000-000000000011" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
The response uses the same public action shape as the schedule response. It does not include API key IDs, idempotency keys, payload hashes, raw failure details, retry counters, or vendor result identifiers.
Cancel Workspace Social Action#
Use POST /workspaces/{workspaceId}/social-actions/{actionId}/cancel.
Required scope: social_actions:write.
Only pending_target and queued actions can be cancelled. Already-cancelled actions return 200. processing, completed, and failed actions return 409 conflict.
curl -sS "https://app.leadpanther.ai/api/v1/workspaces/00000000-0000-4000-8000-000000000012/social-actions/00000000-0000-4000-8000-000000000011/cancel" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Team Boost Workflow#
Use this workflow to boost a scheduled lead-magnet post with the workspace's Team engagement pod: consenting workspace members — and optionally the workspace's enrolled LinkedIn company pages — react, keyword-comment, and repost inside a bounded window after the post publishes. Boost-network comments remain normal lead-magnet inputs: the linked magnet's engagement_scope and response_mode decide their capture and reply behavior.
Read routes require pods:read; write routes require pods:write. Boost routes support both the direct owner subject and agency client subjects via /clients/{clientId}/posts/{id}/boost (the client is the post author; the agency owner is recorded as the requester). Company-actor routes, POST /linkedin/organizations/refresh, and takedown still resolve the direct owner subject only.
Typical sequence:
- Create or schedule the lead-magnet post and keep the returned post
id. The post must have a live magnet template (active, unarchived, non-empty keyword). You can configure Team Boost in that same create/update call with nestedteam_boost, or callPOST /posts/{id}/boostafterward. - If you did not nest
team_boost, callPOST /posts/{id}/boostwithworkspace_id,scope: "team",bundle, andwindow_minutes. Use anIdempotency-Keyso retries replay instead of duplicating; replays return200and payload mismatches return409. - Keep boosted team posts at least 60 minutes apart. If this post's
scheduled_atis within 60 minutes (before or after) of another post in the same workspace that holds a live or completed boost plan, the boost returns409witherror.details.reason = "team_boost_spacing_conflict", the closed window (blocked_from/blocked_until), andearliest_scheduled_at. Reschedule the post to that time or later, or publish it without Team Boost. Moving a post that already has a live plan viaPATCH /posts/{id}is held to the same rule. Two boosts closer than that share the same team accounts, which cuts the comment window short and creates a burst of activity LinkedIn treats as risky. - The plan stays
scheduleduntil the post publishes, then expands into member (and company-actor) actions inside the window. - Poll
GET /posts/{id}/boostfor plan state, counters, per-action progress, skip reasons,lead_capture.boost_comments: "included", and takedown events. Post reads also include an additiveteam_boostsummary. - Call
DELETE /posts/{id}/boostto stop a plan, or PATCH the post with{ "team_boost": { "enabled": false } }. Queued actions are cancelled; in-flight actions finish or fail authorization. Idempotent. - Use
POST /pods/actions/{actionId}/takedownto take down a completed pod comment via the engager's own account.
Configure Team Boost while scheduling a post (requires posts:write and pods:write). Omitted team_boost leaves any existing plan unchanged; the feature is never auto-enabled:
"team_boost": {
"enabled": true,
"bundle": "full",
"window_minutes": 15,
"include_company_actors": true
}
bundle is standard (reactions and keyword comments) or full (also adds reposts; default full). window_minutes is 15, 30, or 60 (default 15). include_company_actors defaults to the server default when omitted. workspace_id is optional; the server uses the caller's pod workspace when it can be inferred.
Create a boost after the post exists:
curl -sS "https://app.leadpanther.ai/api/v1/posts/00000000-0000-4000-8000-000000000002/boost" \
-X POST \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: boost-000000000002-team-standard" \
--data '{
"workspace_id": "00000000-0000-4000-8000-000000000012",
"scope": "team",
"bundle": "standard",
"window_minutes": 30
}'
Company actors#
Enrolled company pages act as additional pod actors: they like, keyword-comment, and bare-repost the boosted post as the page itself, under the same window, pacing, scheduling metadata, and audit trail as person actors. Their comments follow the linked Lead Magnet's normal engagement scope and response mode.
- Discovery is user-initiated only. Call
POST /linkedin/organizations/refresh(or use the dashboard) to discover the company pages the connected LinkedIn account controls. The endpoint is throttled per user; calls inside the cooldown return429 rate_limited. Failed refreshes never clobber the cached list. Organizations are never backfilled automatically. - Enrolment is explicit. Pages are added to the pod in the dashboard;
GET /pods/company-actorslists the enrolled actors andPUT /pods/company-actors/{orgId}updates enablement, per-action-type toggles, and the daily action cap. Disabling an actor or toggle cancels its queued pod actions atomically. - Owner consent is a precondition. Enabling a page requires the owner's accepted pod membership consent (
pod_consent_requiredotherwise) and a pinnable personal LinkedIn slug (linkedin_identifier_unpinnedotherwise). Enrolling a page never makes the owner a personal engager: the owner's personal toggles never gate the page, and the page's toggles never gate the owner. A solo author's own page can boost the author's post; person authors remain excluded. - Per-boost control.
POST /posts/{id}/boostaccepts an optionalinclude_company_actorsboolean. When omitted, enabled company actors are included by default. The field participates in the idempotency payload hash only when explicitly sent, so existing integrations that omit it keep byte-stable idempotency behavior. - Availability is server-gated. When the company-actor feature is off for the subject, company-actor routes and boost participation are invisible and inert; execution fails closed with no fallback to the owner's personal account.
GET /posts/{id}/boost identifies each action's actor:
{
"actions": [
{
"action_type": "comment",
"status": "completed",
"actor": {
"kind": "organization",
"organization_id": "123456",
"name": "Example Company"
}
},
{
"action_type": "reaction",
"status": "queued",
"actor": {
"kind": "person"
}
}
]
}
Organization-specific skip reasons can appear alongside person skip reasons: org_permissions_stale, org_permission_limited, org_stale, org_owner_consent_missing, org_owner_not_workspace_member, org_account_mismatch, org_disabled, org_scope_author_only, org_daily_cap_zero, and org_feature_disabled.
Company-actor error reasons (linkedin_not_connected, linkedin_organization_not_found, linkedin_organization_permission_denied, linkedin_organization_stale, linkedin_organization_provider_rejected, linkedin_organization_duplicate, linkedin_identifier_unpinned, pod_consent_required, rate_limited) are documented in the errors guide.
Get Post Engagement#
Use GET /posts/{id}/engagement for direct-user work. Use GET /clients/{clientId}/posts/{id}/engagement for agency-client work.
Required scope: analytics:read.
Direct example:
curl -sS "https://app.leadpanther.ai/api/v1/posts/00000000-0000-4000-8000-000000000002/engagement" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Agency example:
curl -sS "https://app.leadpanther.ai/api/v1/clients/00000000-0000-4000-8000-000000000001/posts/00000000-0000-4000-8000-000000000002/engagement" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Example response:
{
"data": {
"post_id": "00000000-0000-4000-8000-000000000002",
"published_url": "https://www.linkedin.com/posts/example-post",
"metrics_available": true,
"metrics": {
"impressions": 1200,
"reactions": 42,
"comments": 8,
"reposts": 2
}
},
"request_id": "req_000000000000000000000006"
}
Get Lead Magnet Analytics#
Use GET /lead-magnets/{id}/analytics for direct-user work. Use GET /clients/{clientId}/lead-magnets/{id}/analytics for agency-client work.
Required scope: analytics:read.
Direct example:
curl -sS "https://app.leadpanther.ai/api/v1/lead-magnets/00000000-0000-4000-8000-000000000003/analytics?limit=25&offset=0" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Agency example:
curl -sS "https://app.leadpanther.ai/api/v1/clients/00000000-0000-4000-8000-000000000001/lead-magnets/00000000-0000-4000-8000-000000000003/analytics?limit=25&offset=0" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Example response:
{
"data": {
"lead_magnet": {
"id": "00000000-0000-4000-8000-000000000003",
"keyword": "GUIDE",
"resource_name": "Example Guide"
},
"page": {
"id": "00000000-0000-4000-8000-000000000008",
"title": "Example Guide",
"slug": "example-guide",
"status": "published",
"published_at": "2026-06-15T14:00:00.000Z",
"created_at": "2026-06-15T14:00:00.000Z",
"updated_at": null
},
"metrics": {
"page_views": 300,
"unique_visitors": 240,
"unique_visitors_limited": false,
"unlocks": 24,
"conversion_rate": 0.1,
"content_access": 22,
"cta_clicks": 8,
"download_clicks": 16,
"captures": 24
},
"recent_captures": [
{
"id": "00000000-0000-4000-8000-000000000007",
"source": "comments",
"lead_id": "00000000-0000-4000-8000-000000000004",
"tracking_link_id": null,
"created_at": "2026-06-15T15:30:00.000Z"
}
],
"pagination": {
"limit": 25,
"offset": 0,
"has_more": false
}
},
"request_id": "req_000000000000000000000007"
}
List Leads#
Use GET /leads for direct-user work. Use GET /clients/{clientId}/leads for agency-client work.
Required scope: leads:read.
leads:read can return personal data, including submitted, work, or personal email fields. Public examples are redacted and do not represent the full sensitivity of production data.
Supported filters include limit, offset, keyword, source, has_email, has_intent, qualification, and search.
For full lead exports, keep limit and all filters unchanged for the duration of the run. Continue until pagination.has_more is false; do not infer completion from a fixed number of pages.
Direct example:
curl -sS "https://app.leadpanther.ai/api/v1/leads?limit=50&offset=0&source=comments&has_email=true" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Agency example:
curl -sS "https://app.leadpanther.ai/api/v1/clients/00000000-0000-4000-8000-000000000001/leads?limit=50&offset=0&keyword=GUIDE" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Example response:
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000004",
"linkedin_profile_url": "https://www.linkedin.com/in/example-profile",
"name": "Example Person",
"headline": "Example Headline",
"emails": {
"submitted": "person@example.com",
"work": null,
"personal": null
},
"submitted_name": "Example Person",
"submitted_at": "2026-06-15T15:30:00.000Z",
"submitted_via_magnet_id": "00000000-0000-4000-8000-000000000003",
"sources": {
"comments": true,
"dms": false,
"intent": false
},
"total_interactions": 1,
"latest_keyword": "GUIDE",
"keywords": ["GUIDE"],
"has_intent": true,
"intent_signals": ["Requested guide"],
"qualification_status": "qualified",
"last_delivery": {
"channel": "comment",
"event_at": "2026-06-15T15:46:00.000Z",
"template_id": "00000000-0000-4000-8000-000000000003"
},
"dm_capability": {
"can_dm": false,
"reason": "none",
"chat_id_available": false,
"last_seen_at": null
},
"first_seen_at": "2026-06-15T15:30:00.000Z",
"last_seen_at": "2026-06-15T15:30:00.000Z",
"created_at": "2026-06-15T15:30:00.000Z",
"updated_at": null
}
],
"pagination": {
"limit": 50,
"offset": 0,
"has_more": false
},
"request_id": "req_000000000000000000000008"
}
List Activity#
Use GET /activity for direct-user work. Use GET /clients/{clientId}/activity for agency-client work.
Required scope: activity:read.
activity:read can return message or comment content and platform identifiers. Public examples are redacted and do not represent the full sensitivity of production data.
Supported filters include limit, offset, source, status, keyword, and search.
Direct example:
curl -sS "https://app.leadpanther.ai/api/v1/activity?limit=50&offset=0&source=comments&status=new" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Agency example:
curl -sS "https://app.leadpanther.ai/api/v1/clients/00000000-0000-4000-8000-000000000001/activity?limit=50&offset=0&search=GUIDE" \
-H "Authorization: Bearer lp_live_REDACTED" \
-H "Accept: application/json"
Example response:
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000005",
"source": "comment",
"author_name": "Example Person",
"author_headline": "Example Headline",
"linkedin_profile_url": "https://www.linkedin.com/in/example-profile",
"content": "REDACTED_MESSAGE_CONTENT",
"keyword": "GUIDE",
"status": "new",
"occurred_at": "2026-06-15T15:45:00.000Z",
"metadata": {
"post_id": "00000000-0000-4000-8000-000000000002",
"comment_id": "REDACTED_PLATFORM_COMMENT_ID",
"is_lead_magnet_request": true,
"matched_template_id": "00000000-0000-4000-8000-000000000003",
"delivery_outcome": "comment_replied"
}
}
],
"pagination": {
"limit": 50,
"offset": 0,
"has_more": false
},
"request_id": "req_000000000000000000000009"
}
Unsupported Assumptions#
Do not guess endpoint names. The following are not implemented API v1 paths: /accounts, /users, /lists, /campaigns, /organizations, /team, and /lead-magnet-posts. Generic workspace management paths such as /workspaces and /workspaces/{workspaceId} are not implemented API v1 paths. Use only the documented workspace social action route: /workspaces/{workspaceId}/social-actions. The only organization-related route is POST /linkedin/organizations/refresh; do not infer generic organization endpoints from it.
Rate limits are not yet a published contract. Idempotency keys are documented only where explicitly specified, including published post mutations, Fast Response starts, and POST /workspaces/{workspaceId}/social-actions. Webhooks and SDKs are planned, not currently available as public API v1 features.