Complete API guide

ProfileIQ API Documentation

Start with an API key, send JSON to a REST endpoint, read the standard response envelope, then use feeds or webhooks to keep data flowing into your system.

1

Create key

Use an API key for server-side calls.

2

Choose endpoint

Discovery, feeds, posts, or webhooks.

3

Send payload

Every POST/PATCH body is JSON.

4

Handle response

Read data, meta, and error.

Base URL
https://api.profileiq.reachrd.tech/v1
Required for every request

Authentication

Use API keys for server-to-server integrations. Dashboard JWTs are for dashboard-owned requests. Discovery can also be called from the dashboard with a JWT plus x-api-key-id.

API key header
Authorization: Bearer piq_sk_live_xxx
Dashboard selected key
Authorization: Bearer <dashboard_jwt>
x-api-key-id: api_key_uuid
All normal JSON endpoints

Response Format

Successful responses put records in data and operational details in meta. Failed requests return data as null and include an error code plus message.

Success envelope
{
  "data": {
    "posts": []
  },
  "meta": {
    "count": 0,
    "requestedAt": "2026-06-05T10:00:00.000Z"
  },
  "error": null
}
Error envelope
{
  "data": null,
  "meta": {},
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "At least one keyword or author headline term is required"
  }
}
Find posts before creating feeds

Discovery

Discovery searches public LinkedIn post candidates, collects the matching posts, applies your filters, and returns structured post objects. Use it when you do not yet know which feeds you want to track.

POST/discover/posts

Discover posts

Returns one JSON response after the search and post collection finishes.

Auth: API key, or dashboard JWT with x-api-key-id

POST/discover/posts/stream

Stream discovery progress

Returns NDJSON progress events while ProfileIQ searches, collects, filters, and emits matching posts.

Auth: API key, or dashboard JWT with x-api-key-id

Required rule

Send at least one term in keywords or authorHeadline. You can send both. Terms inside each array are OR conditions; filters across fields are combined together.

Request body

FieldTypeRequiredDefaultDescription
limitnumberNo10How many matching posts to return. Min 1, max 100.
postedWithinHoursnumber|nullNo24Only return posts newer than this window. Use null for any age. Max is 4320 hours.
minReactionsnumberNo0Minimum reaction count required after collection.
regionstringNo""Author/profile location filter. When present, ProfileIQ verifies the author profile location.
keywordsstring[]Required*[]Post text/search terms. Up to 10 terms, max 80 chars each. OR inside the array.
authorHeadlinestring[]Required*[]Author headline terms such as founder, CEO, marketer. OR inside the array.

Selectable options

Posted withinpostedWithinHoursMeaning
Any timenullDo not apply a date window.
Today24Posts from the last 24 hours.
Last 3 days72Posts from the last 72 hours.
Last week168Posts from the last 7 days.
Last month720Posts from the last 30 days.
Last 90 days2160Posts from the last 90 days.
Last 6 months4320Maximum supported date window.
Min reactionsminReactionsMeaning
Any reactions0No minimum reaction filter.
5+ reactions5Only posts with at least 5 reactions.
10+ reactions10Only posts with at least 10 reactions.
25+ reactions25Only posts with at least 25 reactions.
50+ reactions50Only posts with at least 50 reactions.
100+ reactions100Only posts with at least 100 reactions.
Region optionregion payloadMeaning
Any regionomit field or send ""Do not verify author location.
United States"United States"Preset used in the app.
United Kingdom"United Kingdom"Preset used in the app.
Canada"Canada"Preset used in the app.
Australia"Australia"Preset used in the app.
Germany"Germany"Preset used in the app.
France"France"Preset used in the app.
India"India"Preset used in the app.
Pakistan"Pakistan"Preset used in the app.
United Arab Emirates"United Arab Emirates"Preset used in the app.
Singapore"Singapore"Preset used in the app.
Custom region"Dubai" or "Berlin"Backend accepts any 2-80 character region string.

Payload variants

Accepted discovery payload shapes
Keyword-only discovery
{
  "limit": 25,
  "postedWithinHours": 24,
  "keywords": ["hiring", "raised", "launching"]
}

Author-headline-only discovery
{
  "limit": 25,
  "postedWithinHours": 168,
  "authorHeadline": ["founder", "ceo"]
}

Keyword + author + region + reactions
{
  "limit": 10,
  "postedWithinHours": 72,
  "minReactions": 25,
  "region": "United States",
  "keywords": ["fundraising", "series a"],
  "authorHeadline": ["founder", "partner"]
}

Any time, custom region
{
  "limit": 50,
  "postedWithinHours": null,
  "minReactions": 0,
  "region": "Dubai",
  "keywords": ["launching"]
}

Validation rules
- limit: 1-100.
- postedWithinHours: null or 1-4320.
- minReactions: 0 or higher.
- region: omit, empty string, or any 2-80 char string.
- keywords: max 10 terms, 1-80 chars each.
- authorHeadline: max 10 terms, 1-80 chars each.
- At least one of keywords or authorHeadline is required.

Copy-ready request

curl
curl -X POST "https://api.profileiq.reachrd.tech/v1/discover/posts" \
  -H "Authorization: Bearer piq_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 10,
    "postedWithinHours": 24,
    "minReactions": 5,
    "region": "United States",
    "keywords": ["fundraising", "hiring"],
    "authorHeadline": ["founder", "ceo"]
  }'

Response body

200 response
{
  "data": {
    "posts": [
      {
        "platform": "linkedin",
        "post": {
          "url": "https://www.linkedin.com/posts/example_activity-123",
          "type": "text",
          "text": "We just raised our seed round...",
          "audience": "PUBLIC"
        },
        "author": {
          "name": "Ava Malik",
          "headline": "Founder at Example",
          "profileUrl": "https://www.linkedin.com/in/ava-malik/",
          "details": {
            "location": "United States"
          }
        },
        "published": {
          "iso": "2026-06-05T08:12:00.000Z",
          "timestamp": 1780647120000,
          "relative": "2h"
        },
        "engagement": {
          "likesCount": 82,
          "commentsCount": 14,
          "sharesCount": 3
        },
        "content": {
          "mediaType": "text",
          "mediaUrl": null
        },
        "source": {
          "platform": "linkedin",
          "inputUrl": "https://www.linkedin.com/posts/example_activity-123"
        }
      }
    ]
  },
  "meta": {
    "count": 1,
    "requested": 10,
    "returned": 1,
    "limit": 10,
    "candidateCount": 42,
    "attemptedCount": 18,
    "skippedCount": 17,
    "collectErrorCount": 0,
    "filterSkips": {
      "min_reactions": 7,
      "posted_within_hours": 10
    },
    "stopReason": "no_more_posts_found",
    "requestedAt": "2026-06-05T10:00:00.000Z"
  },
  "error": null
}
FieldTypeDescription
post.urlstringCanonical LinkedIn post URL.
post.textstringCollected post text.
post.typestringDetected content type when available.
author.namestring|nullAuthor display name.
author.headlinestring|nullAuthor headline from the post/profile.
author.profileUrlstring|nullLinkedIn profile URL when discovered.
author.details.locationstring|nullProfile location when collected.
published.isostring|nullPost publish time in ISO format when available.
engagement.likesCountnumberReaction count used by minReactions filtering.
engagement.commentsCountnumberComment count when available.
content.mediaTypestringText, image, video, document, article, or unknown.
source.inputUrlstringURL ProfileIQ used to collect the post.

What to do next

Use author.profileUrl to create a feed when you want ongoing posts.
Use post.url as your dedupe key if you store discovery results.
Use meta.stopReason to explain why fewer results came back than requested.
Use the stream endpoint when your UI needs progress updates.

Streaming response

NDJSON request
curl -N -X POST "https://api.profileiq.reachrd.tech/v1/discover/posts/stream" \
  -H "Authorization: Bearer piq_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 5,
    "keywords": ["launching"],
    "authorHeadline": ["founder"]
  }'
NDJSON events
{"type":"start","at":"2026-06-05T10:00:00.000Z"}
{"type":"search-start","queries":["site:linkedin.com/posts launching founder"]}
{"type":"candidates","count":42}
{"type":"collect-post","index":1,"total":42,"postUrl":"https://www.linkedin.com/posts/..."}
{"type":"collect-post-done","index":1,"total":42,"authorName":"Ava Malik","reactions":82}
{"type":"filter-match","postUrl":"https://www.linkedin.com/posts/...","resultIndex":1}
{"type":"result","post":{"post":{"url":"https://www.linkedin.com/posts/..."}}}
{"type":"done","count":5,"meta":{"returned":5,"stopReason":"requested_limit_reached"}}
Track feeds, companies, and schools

Feeds

A feed is the saved source ProfileIQ keeps checking. After a feed is created, ProfileIQ queues collection, stores posts, and can send webhook events for selected feeds.

MethodEndpointWhat it does
POST/feedsCreate one feed
GET/feedsList feeds
GET/feeds/:idRetrieve one feed
PATCH/feeds/:idUpdate display name or tier
PATCH/feeds/:id/pausePause collection
PATCH/feeds/:id/resumeResume collection
POST/feeds/:id/runQueue immediate collection
DELETE/feeds/:idDelete feed data

Complete endpoint contracts

POST/feedsCreate one feed
Auth
Authorization: Bearer piq_sk_live_xxx
Request body
{
  "linkedinUrl": "https://www.linkedin.com/in/example/",
  "linkedinUrn": "urn:li:fsd_profile:abc",
  "displayName": "Example Feed",
  "tier": "B"
}
201 response
{
  "data": {
    "id": "feed_uuid",
    "platform": "linkedin",
    "source_url": "https://www.linkedin.com/in/example/",
    "external_id": "urn:li:fsd_profile:abc",
    "linkedin_url": "https://www.linkedin.com/in/example/",
    "linkedin_urn": "urn:li:fsd_profile:abc",
    "display_name": "Example Feed",
    "profile_image_url": null,
    "status": "active",
    "fetch_status": "queued",
    "last_checked_at": null,
    "next_check_at": "2026-06-05T10:00:00.000Z",
    "tier": "B",
    "schedule_state": "learning",
    "pattern_confidence": 0,
    "last_post_at": null,
    "predicted_next_post_at": null,
    "drift_score": 0,
    "created_at": "2026-06-05T10:00:00.000Z",
    "updated_at": "2026-06-05T10:00:00.000Z"
  },
  "meta": { "monitoringQueued": true },
  "error": null
}
GET/feedsList feeds
Query
q, sortBy=newest|oldest, status=active|paused, platform=linkedin
200 response
{
  "data": [
    {
      "id": "feed_uuid",
      "platform": "linkedin",
      "source_url": "https://www.linkedin.com/in/example/",
      "external_id": "urn:li:fsd_profile:abc",
      "linkedin_url": "https://www.linkedin.com/in/example/",
      "linkedin_urn": "urn:li:fsd_profile:abc",
      "display_name": "Example Feed",
      "profile_image_url": "https://media.licdn.com/...",
      "status": "active",
      "fetch_status": "active",
      "last_checked_at": "2026-06-05T10:00:00.000Z",
      "next_check_at": "2026-06-05T16:00:00.000Z",
      "tier": "B",
      "schedule_state": "strong_pattern",
      "pattern_confidence": 87,
      "last_post_at": "2026-06-05T12:00:00.000Z",
      "predicted_next_post_at": "2026-06-06T12:00:00.000Z",
      "drift_score": 0,
      "created_at": "2026-06-05T09:45:00.000Z",
      "updated_at": "2026-06-05T10:00:00.000Z"
    }
  ],
  "meta": {},
  "error": null
}
GET/feeds/:idRetrieve one feed
Params
id=feed_uuid
200 response
// Same feed object shape as POST /feeds.
PATCH/feeds/:idUpdate display name or tier
Params
id=feed_uuid
Request body
{
  "displayName": "New Label",
  "tier": "A"
}
200 response
// Updated feed object with the same shape as POST /feeds.
PATCH/feeds/:id/pausePause collection
Params
id=feed_uuid
200 response
// Feed object with "status": "paused".
PATCH/feeds/:id/resumeResume collection
Params
id=feed_uuid
200 response
// Feed object with "status": "active".
POST/feeds/:id/runQueue immediate collection
Params
id=feed_uuid
200 response
{
  "data": { "jobId": "job_uuid" },
  "meta": {},
  "error": null
}
DELETE/feeds/:idDelete feed data
Params
id=feed_uuid
200 response
{
  "data": {
    "id": "feed_uuid",
    "deleted": true,
    "deletedPosts": 12,
    "deletedEvents": 3,
    "deletedDeliveries": 3,
    "deletedMonitoringJobs": 1
  },
  "meta": {},
  "error": null
}

Create feed input

FieldTypeRequiredDescription
linkedinUrlstringYesPublic LinkedIn /in/, /company/, or /school/ URL. Normalized by the API.
linkedinUrnstringNoKnown LinkedIn URN when your system already has it.
displayNamestringNoHuman label shown in dashboards and webhook feed payloads. Max 160 chars.
tierA|B|CNoPriority tier for adaptive scheduling. Defaults to B.

List feeds query

QueryTypeRequiredDescription
qstringNoSearch feeds by display/source text. Max 200 chars.
sortBynewest|oldestNoSort order.
statusactive|pausedNoFilter by feed status.
platformstringNoUsually linkedin.
Create feed request
curl -X POST "https://api.profileiq.reachrd.tech/v1/feeds" \
  -H "Authorization: Bearer piq_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "linkedinUrl": "https://www.linkedin.com/in/example/",
    "displayName": "Example Feed",
    "tier": "B"
  }'
Create feed response
{
  "data": {
    "feed": {
      "id": "feed_uuid",
      "platform": "linkedin",
      "linkedin_url": "https://www.linkedin.com/in/example/",
      "display_name": "Example Feed",
      "status": "active",
      "fetch_status": "queued",
      "tier": "B",
      "schedule_state": "learning",
      "pattern_confidence": 0
    }
  },
  "meta": {},
  "error": null
}

Feed object

FieldTypeDescription
idstringFeed id used by posts and webhooks.
platformstringUsually linkedin.
linkedin_urlstringCanonical LinkedIn profile, company, or school URL.
display_namestring|nullFeed, company, or school display name.
headlinestring|nullProfile headline when available.
profile_image_urlstring|nullAvatar or logo URL.
statusstringactive, paused, or deleted.
fetch_statusstringqueued, fetching, active, or failed.
last_checked_atstring|nullLast collection timestamp.
next_check_atstring|nullNext scheduled collection timestamp.
tierA|B|CCreator priority. A is highest, B is default, C is lowest.
schedule_statestringlearning, strong_pattern, weak_pattern, no_pattern, or dormant.
pattern_confidencenumberAdaptive posting-pattern confidence from 0 to 100.
last_post_atstring|nullMost recent collected post timestamp.
predicted_next_post_atstring|nullPredicted next post timestamp when enough history exists.
drift_scorenumberPattern drift score used to downgrade stale predictions.

List feeds response

200 response
{
  "data": {
    "feeds": [
      {
        "id": "feed_uuid",
        "platform": "linkedin",
        "linkedin_url": "https://www.linkedin.com/in/example/",
        "display_name": "Example Feed",
        "headline": "Founder",
        "profile_image_url": "https://media.licdn.com/...",
        "status": "active",
        "fetch_status": "active",
        "last_checked_at": "2026-06-05T10:00:00.000Z",
        "next_check_at": "2026-06-05T16:00:00.000Z",
        "tier": "B",
        "schedule_state": "strong_pattern",
        "pattern_confidence": 87
      }
    ]
  },
  "meta": {
    "count": 1
  },
  "error": null
}
Bulk feed onboarding

Bulk Feed Drafts

Use drafts when a user pastes many LinkedIn URLs. ProfileIQ validates rows first, then confirm creates feeds for valid rows only.

MethodEndpointWhat it does
POST/feed/linkedin/draftsCreate validation draft
GET/feed/linkedin/drafts/:idInspect progress and row status
POST/feed/linkedin/drafts/:id/confirmCreate feeds for valid rows
DELETE/feed/linkedin/drafts/:idCancel draft

Complete endpoint contracts

GET/feed-platformsList supported platforms
200 response
{
  "data": [
    {
      "id": "linkedin",
      "name": "LinkedIn",
      "supportedInputs": ["profile", "company", "school"]
    }
  ],
  "meta": {},
  "error": null
}
POST/feed/:platform/draftsCreate validation draft
Params
platform=linkedin
Request body
{
  "inputs": [
    "https://www.linkedin.com/in/example-one/",
    "https://www.linkedin.com/company/example-company/"
  ]
}
201 response
{
  "data": {
    "id": "draft_uuid",
    "platform": "linkedin",
    "status": "validating",
    "totalCount": 2,
    "processedCount": 0,
    "validCount": 0,
    "failedCount": 0,
    "createdAt": "2026-06-05T10:00:00.000Z",
    "updatedAt": "2026-06-05T10:00:00.000Z",
    "confirmedAt": null,
    "expiresAt": "2026-06-06T10:00:00.000Z",
    "items": []
  },
  "meta": {},
  "error": null
}
GET/feed/:platform/drafts/:idInspect progress and row status
Params
platform=linkedin, id=draft_uuid
200 response
{
  "data": {
    "id": "draft_uuid",
    "platform": "linkedin",
    "status": "ready",
    "totalCount": 2,
    "processedCount": 2,
    "validCount": 2,
    "failedCount": 0,
    "createdAt": "2026-06-05T10:00:00.000Z",
    "updatedAt": "2026-06-05T10:01:00.000Z",
    "confirmedAt": null,
    "expiresAt": "2026-06-06T10:00:00.000Z",
    "items": [
      {
        "id": "item_uuid",
        "input": "https://www.linkedin.com/in/example-one/",
        "normalizedUrl": "https://www.linkedin.com/in/example-one/",
        "externalId": "example-one",
        "displayName": "Example One",
        "profileImageUrl": "https://media.licdn.com/...",
        "status": "valid",
        "reason": null,
        "metadata": {},
        "createdAt": "2026-06-05T10:00:00.000Z",
        "updatedAt": "2026-06-05T10:01:00.000Z"
      }
    ]
  },
  "meta": {},
  "error": null
}
POST/feed/:platform/drafts/:id/confirmCreate feeds for valid rows
Params
platform=linkedin, id=draft_uuid
200 response
{
  "data": {
    "draftId": "draft_uuid",
    "feeds": [
      {
        "id": "feed_uuid",
        "platform": "linkedin",
        "linkedin_url": "https://www.linkedin.com/in/example-one/",
        "display_name": "Example One",
        "status": "active",
        "fetch_status": "queued"
      }
    ]
  },
  "meta": { "monitoringQueued": 1 },
  "error": null
}
DELETE/feed/:platform/drafts/:idCancel draft
Params
platform=linkedin, id=draft_uuid
200 response
// Draft object with "status": "cancelled".
POST/feed/:platform/drafts/:id/events-tokenMint short-lived token for the events stream
Params
platform=linkedin, id=draft_uuid
200 response
{
  "data": { "expiresInSeconds": 300 },
  "meta": {},
  "error": null
}
GET/feed/:platform/drafts/:id/eventsSubscribe to draft snapshots (SSE)
Params
platform=linkedin, id=draft_uuid
Query
lastEventId=0-0
Response
// Response: text/event-stream of draft snapshots.
// Each "data:" line is a JSON object matching GET /feed/:platform/drafts/:id.

Note: Requires the events-token issued above in a query param.

Create draft input

FieldTypeRequiredDescription
inputsstring[]YesLinkedIn URLs to validate. Min 1, max 50. Each item max 300 chars.
Create draft
curl -X POST "https://api.profileiq.reachrd.tech/v1/feed/linkedin/drafts" \
  -H "Authorization: Bearer piq_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "inputs": [
      "https://www.linkedin.com/in/example-one/",
      "https://www.linkedin.com/company/example-company/"
    ]
  }'
Confirm draft
curl -X POST "https://api.profileiq.reachrd.tech/v1/feed/linkedin/drafts/draft_uuid/confirm" \
  -H "Authorization: Bearer piq_sk_live_xxx" \
  -H "Content-Type: application/json"

Draft response

GET /feed/linkedin/drafts/:id response
{
  "data": {
    "draft": {
      "id": "draft_uuid",
      "platform": "linkedin",
      "status": "ready",
      "total": 2,
      "valid": 2,
      "invalid": 0,
      "rows": [
        {
          "input": "https://www.linkedin.com/in/example-one/",
          "status": "valid",
          "normalizedUrl": "https://www.linkedin.com/in/example-one/",
          "displayName": "Example One"
        }
      ]
    }
  },
  "meta": {},
  "error": null
}
Read collected content

Posts

Posts are collected from feeds and returned as structured author, engagement, published, content, and source objects.

MethodEndpointWhat it does
GET/feeds/:id/postsList posts for a feed (cursor paginated)
GET/posts/:idRetrieve one post
POST/posts/commentsCollect comments for a LinkedIn post

Complete endpoint contracts

GET/feeds/:id/postsList posts for a feed (cursor paginated)
Params
id=feed_uuid
Query
limit=10, cursor=opaque_token
200 response
{
  "data": {
    "items": [
      {
        "id": "post_uuid",
        "feedId": "feed_uuid",
        "platform": "linkedin",
        "post": {
          "url": "https://www.linkedin.com/posts/example_activity-123",
          "urn": "urn:li:activity:123",
          "type": "text",
          "text": "Post text",
          "audience": "PUBLIC"
        },
        "author": {
          "name": "Example Feed",
          "profileUrl": "https://www.linkedin.com/in/example/",
          "headline": "Founder",
          "avatarUrl": "https://media.licdn.com/..."
        },
        "published": {
          "iso": "2026-06-05T08:12:00.000Z",
          "timestamp": 1780647120000,
          "relative": "2h"
        },
        "engagement": {
          "likesCount": 10,
          "commentsCount": 2,
          "sharesCount": 1,
          "comments": [],
          "reactions": []
        },
        "permissions": {},
        "content": {
          "mediaType": "text",
          "mediaUrl": null,
          "article": null,
          "document": null,
          "resharedPost": null,
          "attributes": []
        },
        "source": {
          "platform": "linkedin",
          "inputUrl": "https://www.linkedin.com/in/example/"
        },
        "createdAt": "2026-06-05T10:00:00.000Z",
        "updatedAt": "2026-06-05T10:00:00.000Z"
      }
    ],
    "nextCursor": "eyJ0IjoiMjAyNi0wNi0wNVQwODoxMjowMC4wMDBaIiwiaWQiOiJwb3N0X3V1aWQifQ"
  },
  "meta": {},
  "error": null
}
GET/posts/:idRetrieve one post
Params
id=post_uuid
200 response
// One post object with the same shape as items[] in GET /feeds/:id/posts.
POST/posts/commentsCollect comments for a LinkedIn post
Request body
{
  "postId": "urn:li:activity:123",
  "postUrl": "https://www.linkedin.com/posts/example_activity-123",
  "authors": [
    {
      "profileUrn": "urn:li:fsd_profile:abc",
      "profileUrl": "https://www.linkedin.com/in/example/",
      "name": "Example Feed"
    }
  ],
  "matchMode": "authored_only"
}
200 response
{
  "data": {
    "comments": [
      {
        "id": "comment_id",
        "text": "Great post",
        "author": {
          "name": "Example Feed",
          "profileUrl": "https://www.linkedin.com/in/example/"
        },
        "createdAt": "2026-06-05T10:00:00.000Z",
        "parentId": null
      }
    ]
  },
  "meta": {},
  "error": null
}

List posts query

QueryTypeRequiredDescription
limitnumberNoRows to return. Min 1, max 100. Default 10.
cursorstringNoOpaque cursor from a previous response's nextCursor. Omit for the first page.
List posts for a feed
curl "https://api.profileiq.reachrd.tech/v1/feeds/feed_uuid/posts?limit=10" \
  -H "Authorization: Bearer piq_sk_live_xxx"

# Next page — pass the nextCursor from the previous response
curl "https://api.profileiq.reachrd.tech/v1/feeds/feed_uuid/posts?limit=10&cursor=eyJ0Ijo..." \
  -H "Authorization: Bearer piq_sk_live_xxx"
List posts response
{
  "data": {
    "items": [
      {
        "id": "post_uuid",
        "feedId": "feed_uuid",
        "post": {
          "url": "https://www.linkedin.com/posts/example_activity-123",
          "text": "Post text",
          "type": "text"
        },
        "author": {
          "name": "Example Feed",
          "profileUrl": "https://www.linkedin.com/in/example/"
        },
        "published": {
          "iso": "2026-06-05T08:12:00.000Z"
        },
        "engagement": {
          "likesCount": 10,
          "commentsCount": 2,
          "sharesCount": 1
        }
      }
    ],
    "nextCursor": "eyJ0IjoiMjAyNi0wNi0wNVQwODoxMjowMC4wMDBaIiwiaWQiOiJwb3N0X3V1aWQifQ"
  },
  "meta": {},
  "error": null
}
Send data to your app

Webhooks

Webhooks deliver signed post batch events for selected feeds. The test endpoint is only a signed ping so you can verify the URL, secret, timestamp, and signature logic.

MethodEndpointWhat it does
POST/webhooksCreate webhook and receive the secret once
GET/webhooksList webhooks
PATCH/webhooks/:idUpdate URL or status
DELETE/webhooks/:idDelete webhook
GET/webhooks/:id/feedsList selected feed ids
PUT/webhooks/:id/feedsReplace selected feed ids
POST/webhooks/:id/testSend signed ping only
GET/webhooks/deliveriesList deliveries
POST/webhooks/deliveries/:deliveryId/retryRetry delivery
POST/webhooks/:id/rotate-secretRotate signing secret

Complete endpoint contracts

POST/webhooksCreate webhook and receive the secret once
Request body
{
  "url": "https://example.com/profileiq/webhook"
}
201 response
{
  "data": {
    "id": "webhook_uuid",
    "user_id": "user_uuid",
    "url": "https://example.com/profileiq/webhook",
    "status": "active",
    "created_at": "2026-06-05T10:00:00.000Z",
    "secret": "whsec_shown_once"
  },
  "meta": {},
  "error": null
}

Note: The secret is only returned in this response — store it immediately.

GET/webhooksList webhooks
200 response
{
  "data": [
    {
      "id": "webhook_uuid",
      "url": "https://example.com/profileiq/webhook",
      "status": "active",
      "created_at": "2026-06-05T10:00:00.000Z",
      "updated_at": "2026-06-05T10:00:00.000Z",
      "feed_ids": ["feed_uuid"]
    }
  ],
  "meta": {},
  "error": null
}
PATCH/webhooks/:idUpdate URL or status
Params
id=webhook_uuid
Request body
{
  "url": "https://example.com/new-webhook",
  "status": "disabled"
}
200 response
{
  "data": {
    "id": "webhook_uuid",
    "url": "https://example.com/new-webhook",
    "status": "disabled",
    "created_at": "2026-06-05T10:00:00.000Z",
    "updated_at": "2026-06-05T10:05:00.000Z"
  },
  "meta": {},
  "error": null
}
DELETE/webhooks/:idDelete webhook
Params
id=webhook_uuid
200 response
{
  "data": { "deleted": true },
  "meta": {},
  "error": null
}
GET/webhooks/:id/feedsList selected feed ids
Params
id=webhook_uuid
200 response
{
  "data": { "feedIds": ["feed_uuid"] },
  "meta": {},
  "error": null
}
PUT/webhooks/:id/feedsReplace selected feed ids
Params
id=webhook_uuid
Request body
{
  "feedIds": ["feed_uuid"]
}
200 response
{
  "data": { "feedIds": ["feed_uuid"] },
  "meta": {},
  "error": null
}
POST/webhooks/:id/testSend signed test.ping delivery
Params
id=webhook_uuid
200 response
{
  "data": {
    "success": true,
    "mode": "ping",
    "deliveryId": "delivery_uuid",
    "eventId": "event_uuid",
    "isTest": true
  },
  "meta": {},
  "error": null
}

Note: Does not send any feed or post data — only the ping payload.

POST/webhooks/:id/rotate-secretRotate signing secret
Params
id=webhook_uuid
200 response
{
  "data": { "secret": "whsec_new_secret_shown_once" },
  "meta": {},
  "error": null
}
GET/webhooks/deliveriesList deliveries across all webhooks
200 response
{
  "data": [
    {
      "id": "delivery_uuid",
      "webhook_id": "webhook_uuid",
      "event_id": "event_uuid",
      "event_type": "linkedin.posts.batch",
      "status": "success",
      "response_status": 200,
      "attempts": 1,
      "created_at": "2026-06-05T10:00:00.000Z",
      "updated_at": "2026-06-05T10:00:02.000Z"
    }
  ],
  "meta": {},
  "error": null
}
GET/webhooks/:id/deliveriesList deliveries for one webhook
Params
id=webhook_uuid
200 response
// Same delivery array shape as GET /webhooks/deliveries, scoped to one webhook.
POST/webhooks/deliveries/:deliveryId/retryRetry a failed delivery
Params
deliveryId=delivery_uuid
200 response
{
  "data": {
    "retried": true,
    "deliveryId": "delivery_uuid"
  },
  "meta": {},
  "error": null
}

Webhook inputs

FieldTypeRequiredDescription
urlstringYesPublic HTTPS receiver URL.
FieldTypeRequiredDescription
urlstringNoNew public receiver URL.
statusactive|disabledNoEnable or disable delivery.
FieldTypeRequiredDescription
feedIdsuuid[]YesFull replacement list of selected feed ids. Max 500.
Create webhook
curl -X POST "https://api.profileiq.reachrd.tech/v1/webhooks" \
  -H "Authorization: Bearer piq_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/profileiq/webhook"
  }'
Select feeds for live events
curl -X PUT "https://api.profileiq.reachrd.tech/v1/webhooks/webhook_uuid/feeds" \
  -H "Authorization: Bearer piq_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "feedIds": ["feed_uuid_one", "feed_uuid_two"]
  }'
Verify endpoint and signature
curl -X POST "https://api.profileiq.reachrd.tech/v1/webhooks/webhook_uuid/test" \
  -H "Authorization: Bearer piq_sk_live_xxx"
Create webhook response
{
  "data": {
    "webhook": {
      "id": "webhook_uuid",
      "url": "https://example.com/profileiq/webhook",
      "status": "active",
      "feed_ids": [],
      "created_at": "2026-06-05T10:00:00.000Z",
      "secret": "whsec_shown_once"
    }
  },
  "meta": {},
  "error": null
}
Test ping delivery payload
{
  "id": "event_uuid",
  "type": "test.ping",
  "createdAt": "2026-06-05T10:00:00.000Z",
  "data": {
    "test": true,
    "mode": "ping"
  }
}
Live post batch delivery payload
{
  "id": "event_uuid",
  "type": "linkedin.posts.batch",
  "createdAt": "2026-06-05T10:00:00.000Z",
  "data": {
    "mode": "live",
    "feedId": "feed_uuid",
    "feed": {
      "id": "feed_uuid",
      "platform": "linkedin",
      "sourceUrl": "https://www.linkedin.com/in/example/",
      "externalId": "urn:li:fsd_profile:abc",
      "linkedinUrl": "https://www.linkedin.com/in/example/",
      "linkedinUrn": "urn:li:fsd_profile:abc",
      "displayName": "Example Feed",
      "headline": "Founder at Example",
      "profileImageUrl": "https://media.licdn.com/...",
      "followerCount": 12450,
      "location": "United States",
      "about": "Building Example.",
      "currentCompany": "Example",
      "currentTitle": "Founder",
      "connections": 500,
      "education": [
        {
          "school": "Stanford University",
          "degree": "BS Computer Science",
          "field": "Computer Science",
          "startYear": 2010,
          "endYear": 2014
        }
      ],
      "metadata": {},
      "status": "active",
      "fetchStatus": "active",
      "lastCheckedAt": "2026-06-05T09:45:00.000Z",
      "nextCheckAt": "2026-06-05T10:00:00.000Z",
      "tier": "B",
      "scheduleState": "strong_pattern",
      "patternConfidence": 87,
      "lastPostAt": "2026-06-05T08:30:00.000Z",
      "predictedNextPostAt": "2026-06-06T08:30:00.000Z",
      "driftScore": 0,
      "createdAt": "2026-05-01T10:00:00.000Z",
      "updatedAt": "2026-06-05T10:00:00.000Z"
    },
    "posts": [
      {
        "id": "post_uuid",
        "feedId": "feed_uuid",
        "changeType": "created",
        "platform": "linkedin",
        "post": {
          "urn": "urn:li:activity:123",
          "url": "https://www.linkedin.com/posts/example_activity-123",
          "type": "image",
          "text": "We just raised our seed round...",
          "isRepost": false,
          "isActivity": false,
          "rootShare": true,
          "audience": "PUBLIC"
        },
        "author": {
          "type": "Person",
          "name": "Example Feed",
          "headline": "Founder at Example",
          "profileUrl": "https://www.linkedin.com/in/example/",
          "profileSlug": "example",
          "urn": "urn:li:fsd_profile:abc",
          "followersCount": 12450,
          "avatarUrl": "https://media.licdn.com/...",
          "details": {
            "location": "San Francisco, California, United States"
          }
        },
        "published": {
          "timestamp": 1780647120000,
          "iso": "2026-06-05T08:12:00.000Z",
          "relative": "2h"
        },
        "engagement": {
          "likesCount": 82,
          "commentsCount": 14,
          "sharesCount": 3,
          "comments": [
            {
              "id": "urn:li:comment:(activity:123,456)",
              "text": "Congrats!",
              "author": {
                "name": "Jane Reviewer",
                "profileUrl": "https://www.linkedin.com/in/jane/",
                "headline": "Investor",
                "avatarUrl": "https://media.licdn.com/..."
              },
              "createdAt": "2026-06-05T08:30:00.000Z",
              "likesCount": 3,
              "parentId": null
            }
          ],
          "reactions": [
            {
              "type": "LIKE",
              "actor": {
                "name": "Jane Reviewer",
                "profileUrl": "https://www.linkedin.com/in/jane/",
                "headline": "Investor",
                "avatarUrl": "https://media.licdn.com/..."
              }
            }
          ]
        },
        "permissions": {},
        "content": {
          "mediaType": "image",
          "mediaUrl": "https://media.licdn.com/dms/image/...",
          "article": null,
          "document": null,
          "resharedPost": null,
          "attributes": [
            { "type": "HASHTAG", "value": "fundraising" },
            { "type": "MENTION", "value": "urn:li:fsd_profile:xyz" }
          ]
        },
        "source": {
          "platform": "linkedin",
          "inputUrl": "https://www.linkedin.com/in/example/",
          "profileUrl": "https://www.linkedin.com/in/example/",
          "collector": "profile_posts"
        },
        "createdAt": "2026-06-05T10:00:00.000Z",
        "updatedAt": "2026-06-05T10:00:00.000Z"
      }
    ],
    "count": 1
  }
}

Webhook behavior

Live webhook events send new posts after the feed is selected for that webhook.
The first feed collection can send discovered posts to selected webhooks once posts arrive.
Test webhook sends test.ping only. It does not send feed or post data.
ProfileIQ retries non-2xx and timed-out deliveries with exponential backoff.
Verify signed deliveries

Webhook Receiver

ProfileIQ POSTs JSON to your URL with an HMAC SHA-256 signature over the timestamp and raw body. Your receiver must verify the signature, dedupe by event id, and respond 2xx within 30 seconds.

Complete endpoint contracts

Webhook receiver
Request line and headers
POST <your receiver URL>
content-type: application/json
user-agent: ProfileIQ-Webhooks/1.0
x-profileiq-timestamp: <unix seconds>
x-profileiq-signature: sha256=<hex digest>
x-profileiq-test: true   # only present on the test ping

Signature
expected = HMAC_SHA256(
  webhook_secret,
  timestamp + "." + raw_request_body
)
Compare with constant-time equality after stripping the "sha256=" prefix.

Body envelope (every event type)
{
  "id": "event_uuid",
  "type": "linkedin.posts.batch" | "test.ping",
  "createdAt": "2026-06-05T10:00:00.000Z",
  "data": { ... }
}

Expected response
- 2xx status within 30 seconds → delivery marked success.
- Non-2xx, network error, or timeout → delivery marked failed and retried with
  exponential backoff up to the configured max attempts.
- Response body is captured (first 1000 chars) and shown on the deliveries page,
  so plain-text error messages help debugging.

Idempotency
- The same event.id can arrive more than once on retries. Persist it (with a
  unique constraint) and treat duplicates as no-ops.
- test.ping events should be verified and acknowledged but never stored as posts.

Request headers

HeaderTypeDescription
content-typestringAlways application/json.
user-agentstringAlways ProfileIQ-Webhooks/1.0.
x-profileiq-timestampstringUnix seconds when ProfileIQ signed the request. Used in the signature input.
x-profileiq-signaturestringsha256=<hex> HMAC-SHA256 of `${timestamp}.${rawBody}` using your webhook secret.
x-profileiq-teststringOnly present on the test ping (value: true). Use it to skip ingestion side-effects.

Request body envelope

FieldTypeDescription
idstringEvent id. Reuse for idempotent inserts.
typestringlinkedin.posts.batch for live deliveries, test.ping for the verification ping.
createdAtstringISO timestamp the event was created server-side (not the delivery time).
dataobjectEvent payload. Shape depends on type — see live and test examples below.
Signature input
expected = HMAC_SHA256(
  webhook_secret,
  timestamp + "." + raw_body
)
// then compare against the header value with "sha256=" stripped

Example delivery

Raw HTTP request
POST https://example.com/profileiq/webhook
content-type: application/json
user-agent: ProfileIQ-Webhooks/1.0
x-profileiq-timestamp: 1780647200
x-profileiq-signature: sha256=2b7c...e9f1
x-profileiq-test: true   # only present on the test ping

{
  "id": "event_uuid",
  "type": "test.ping",
  "createdAt": "2026-06-05T10:00:00.000Z",
  "data": {
    "test": true,
    "mode": "ping"
  }
}

Live post batch payload

event.type = linkedin.posts.batch
{
  "id": "event_uuid",
  "type": "linkedin.posts.batch",
  "createdAt": "2026-06-05T10:00:00.000Z",
  "data": {
    "mode": "live",
    "feedId": "feed_uuid",
    "feed": {
      "id": "feed_uuid",
      "platform": "linkedin",
      "sourceUrl": "https://www.linkedin.com/in/example/",
      "externalId": "urn:li:fsd_profile:abc",
      "linkedinUrl": "https://www.linkedin.com/in/example/",
      "linkedinUrn": "urn:li:fsd_profile:abc",
      "displayName": "Example Feed",
      "headline": "Founder at Example",
      "profileImageUrl": "https://media.licdn.com/...",
      "followerCount": 12450,
      "location": "United States",
      "about": "Building Example.",
      "currentCompany": "Example",
      "currentTitle": "Founder",
      "connections": 500,
      "education": [
        {
          "school": "Stanford University",
          "degree": "BS Computer Science",
          "field": "Computer Science",
          "startYear": 2010,
          "endYear": 2014
        }
      ],
      "metadata": {},
      "status": "active",
      "fetchStatus": "active",
      "lastCheckedAt": "2026-06-05T09:45:00.000Z",
      "nextCheckAt": "2026-06-05T10:00:00.000Z",
      "tier": "B",
      "scheduleState": "strong_pattern",
      "patternConfidence": 87,
      "lastPostAt": "2026-06-05T08:30:00.000Z",
      "predictedNextPostAt": "2026-06-06T08:30:00.000Z",
      "driftScore": 0,
      "createdAt": "2026-05-01T10:00:00.000Z",
      "updatedAt": "2026-06-05T10:00:00.000Z"
    },
    "posts": [
      {
        "id": "post_uuid",
        "feedId": "feed_uuid",
        "changeType": "created",
        "platform": "linkedin",
        "post": {
          "urn": "urn:li:activity:123",
          "url": "https://www.linkedin.com/posts/example_activity-123",
          "type": "image",
          "text": "We just raised our seed round...",
          "isRepost": false,
          "isActivity": false,
          "rootShare": true,
          "audience": "PUBLIC"
        },
        "author": {
          "type": "Person",
          "name": "Example Feed",
          "headline": "Founder at Example",
          "profileUrl": "https://www.linkedin.com/in/example/",
          "profileSlug": "example",
          "urn": "urn:li:fsd_profile:abc",
          "followersCount": 12450,
          "avatarUrl": "https://media.licdn.com/...",
          "details": {
            "location": "San Francisco, California, United States"
          }
        },
        "published": {
          "timestamp": 1780647120000,
          "iso": "2026-06-05T08:12:00.000Z",
          "relative": "2h"
        },
        "engagement": {
          "likesCount": 82,
          "commentsCount": 14,
          "sharesCount": 3,
          "comments": [
            {
              "id": "urn:li:comment:(activity:123,456)",
              "text": "Congrats!",
              "author": {
                "name": "Jane Reviewer",
                "profileUrl": "https://www.linkedin.com/in/jane/",
                "headline": "Investor",
                "avatarUrl": "https://media.licdn.com/..."
              },
              "createdAt": "2026-06-05T08:30:00.000Z",
              "likesCount": 3,
              "parentId": null
            }
          ],
          "reactions": [
            {
              "type": "LIKE",
              "actor": {
                "name": "Jane Reviewer",
                "profileUrl": "https://www.linkedin.com/in/jane/",
                "headline": "Investor",
                "avatarUrl": "https://media.licdn.com/..."
              }
            }
          ]
        },
        "permissions": {},
        "content": {
          "mediaType": "image",
          "mediaUrl": "https://media.licdn.com/dms/image/...",
          "article": null,
          "document": null,
          "resharedPost": null,
          "attributes": [
            { "type": "HASHTAG", "value": "fundraising" },
            { "type": "MENTION", "value": "urn:li:fsd_profile:xyz" }
          ]
        },
        "source": {
          "platform": "linkedin",
          "inputUrl": "https://www.linkedin.com/in/example/",
          "profileUrl": "https://www.linkedin.com/in/example/",
          "collector": "profile_posts"
        },
        "createdAt": "2026-06-05T10:00:00.000Z",
        "updatedAt": "2026-06-05T10:00:00.000Z"
      }
    ],
    "count": 1
  }
}

Test ping payload

event.type = test.ping
{
  "id": "event_uuid",
  "type": "test.ping",
  "createdAt": "2026-06-05T10:00:00.000Z",
  "data": {
    "test": true,
    "mode": "ping"
  }
}

Reference receiver (Node + Express)

server.js
import crypto from "node:crypto";
import express from "express";

const app = express();

// IMPORTANT: capture the raw body before JSON parsing — the signature is
// computed over the exact bytes ProfileIQ sent.
app.post(
  "/profileiq/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const timestamp = req.header("x-profileiq-timestamp");
    const signatureHeader = req.header("x-profileiq-signature") || "";
    const signature = signatureHeader.replace(/^sha256=/, "");
    const rawBody = req.body.toString("utf8");

    if (!timestamp || !signature) return res.status(400).end();

    // Reject anything older than 5 minutes to stop replay attacks.
    const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
    if (Number.isNaN(ageSeconds) || ageSeconds > 300) return res.status(400).end();

    const expected = crypto
      .createHmac("sha256", process.env.PROFILEIQ_WEBHOOK_SECRET)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

    const ok =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    if (!ok) return res.status(401).end();

    const event = JSON.parse(rawBody);

    // Use event.id for idempotency — retries reuse the same id.
    if (event.type === "test.ping") return res.status(204).end();

    if (event.type === "linkedin.posts.batch") {
      for (const post of event.data.posts) {
        // post.changeType is "created" or "updated"
        // post.id is the ProfileIQ post id, post.post.url is the LinkedIn URL
      }
    }

    return res.status(200).end();
  }
);

Receiver checklist

Capture the raw body before JSON.parse — signing is byte-exact.
Reject missing or stale timestamps (older than 5 minutes is a safe default).
Compare signatures with timing-safe equality after stripping the sha256= prefix.
Persist event.id with a unique constraint and treat duplicates as no-ops.
For test.ping, verify and return 2xx without storing post data.
Return 2xx within 30 seconds or the delivery will be marked failed and retried.
Credential management

API Keys

Create, list, rotate, and revoke keys used by external systems.

MethodEndpointWhat it does
POST/api-keysCreate API key
GET/api-keysList keys
POST/api-keys/:id/revokeRevoke key
POST/api-keys/:id/rotateRotate key

Complete endpoint contracts

POST/api-keysCreate API key
Request body
{
  "label": "Production server"
}
201 response
{
  "data": {
    "id": "api_key_uuid",
    "label": "Production server",
    "key_prefix": "piq_sk_live_abcd",
    "created_at": "2026-06-05T10:00:00.000Z",
    "last_used_at": null,
    "revoked_at": null,
    "key": "piq_sk_live_full_key_shown_once"
  },
  "meta": {},
  "error": null
}

Note: The full key is returned once. Store it immediately.

GET/api-keysList keys
200 response
{
  "data": [
    {
      "id": "api_key_uuid",
      "label": "Production server",
      "key_prefix": "piq_sk_live_abcd",
      "created_at": "2026-06-05T10:00:00.000Z",
      "last_used_at": "2026-06-05T10:30:00.000Z",
      "revoked_at": null
    }
  ],
  "meta": {},
  "error": null
}
POST/api-keys/:id/revokeRevoke key
Params
id=api_key_uuid
200 response
{
  "data": { "revoked": true },
  "meta": {},
  "error": null
}
POST/api-keys/:id/rotateRotate key
Params
id=api_key_uuid
200 response
{
  "data": {
    "id": "api_key_uuid",
    "label": "Production server",
    "key_prefix": "piq_sk_live_wxyz",
    "created_at": "2026-06-05T11:00:00.000Z",
    "last_used_at": null,
    "revoked_at": null,
    "key": "piq_sk_live_new_full_key_shown_once"
  },
  "meta": {},
  "error": null
}

Note: The previous key is revoked atomically with rotation.

Create API key input and response

FieldTypeRequiredDescription
labelstringYesKey label shown in the dashboard. Max 120 chars.
201 response
{
  "data": {
    "apiKey": {
      "id": "api_key_uuid",
      "label": "Production server",
      "prefix": "piq_sk_live",
      "status": "active",
      "created_at": "2026-06-05T10:00:00.000Z",
      "key": "piq_sk_live_shown_once"
    }
  },
  "meta": {},
  "error": null
}
Limits and reporting

Usage

Use usage endpoints to show API, discovery, feed, and webhook activity in your own dashboard.

MethodEndpointWhat it does
GET/usage/overviewDashboard usage overview

Complete endpoint contracts

GET/usage/overviewDashboard usage overview
200 response
{
  "data": {
    "feedsTracked": 12,
    "feedsLimit": 100,
    "postsDetectedToday": 38,
    "webhooksDelivered": 120,
    "discoveryRequests": 9,
    "discoveryRequestsLimit": 100,
    "monitoringHealth": 100,
    "timeseries": [
      {
        "date": "06-05",
        "posts": 38,
        "deliveries": 120,
        "requests": 9
      }
    ]
  },
  "meta": {},
  "error": null
}
GET/usage/statsSame shape as /usage/overview
200 response
// Same response body as GET /usage/overview.

Usage response

GET /usage/overview response
{
  "data": {
    "feedsCount": 12,
    "postsToday": 38,
    "deliveries": {
      "success": 120,
      "failed": 2
    },
    "discoveryRequests": 9,
    "discoveryRequestsLimit": 100
  },
  "meta": {},
  "error": null
}