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.
Create key
Use an API key for server-side calls.
Choose endpoint
Discovery, feeds, posts, or webhooks.
Send payload
Every POST/PATCH body is JSON.
Handle response
Read data, meta, and error.
https://api.profileiq.reachrd.tech/v1Authentication
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.
Authorization: Bearer piq_sk_live_xxxAuthorization: Bearer <dashboard_jwt>
x-api-key-id: api_key_uuidResponse 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.
{
"data": {
"posts": []
},
"meta": {
"count": 0,
"requestedAt": "2026-06-05T10:00:00.000Z"
},
"error": null
}{
"data": null,
"meta": {},
"error": {
"code": "VALIDATION_ERROR",
"message": "At least one keyword or author headline term is required"
}
}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.
/discover/postsDiscover posts
Returns one JSON response after the search and post collection finishes.
Auth: API key, or dashboard JWT with x-api-key-id
/discover/posts/streamStream 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| limit | number | No | 10 | How many matching posts to return. Min 1, max 100. |
| postedWithinHours | number|null | No | 24 | Only return posts newer than this window. Use null for any age. Max is 4320 hours. |
| minReactions | number | No | 0 | Minimum reaction count required after collection. |
| region | string | No | "" | Author/profile location filter. When present, ProfileIQ verifies the author profile location. |
| keywords | string[] | Required* | [] | Post text/search terms. Up to 10 terms, max 80 chars each. OR inside the array. |
| authorHeadline | string[] | Required* | [] | Author headline terms such as founder, CEO, marketer. OR inside the array. |
Selectable options
| Posted within | postedWithinHours | Meaning |
|---|---|---|
| Any time | null | Do not apply a date window. |
| Today | 24 | Posts from the last 24 hours. |
| Last 3 days | 72 | Posts from the last 72 hours. |
| Last week | 168 | Posts from the last 7 days. |
| Last month | 720 | Posts from the last 30 days. |
| Last 90 days | 2160 | Posts from the last 90 days. |
| Last 6 months | 4320 | Maximum supported date window. |
| Min reactions | minReactions | Meaning |
|---|---|---|
| Any reactions | 0 | No minimum reaction filter. |
| 5+ reactions | 5 | Only posts with at least 5 reactions. |
| 10+ reactions | 10 | Only posts with at least 10 reactions. |
| 25+ reactions | 25 | Only posts with at least 25 reactions. |
| 50+ reactions | 50 | Only posts with at least 50 reactions. |
| 100+ reactions | 100 | Only posts with at least 100 reactions. |
| Region option | region payload | Meaning |
|---|---|---|
| Any region | omit 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
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 -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
{
"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
}| Field | Type | Description |
|---|---|---|
| post.url | string | Canonical LinkedIn post URL. |
| post.text | string | Collected post text. |
| post.type | string | Detected content type when available. |
| author.name | string|null | Author display name. |
| author.headline | string|null | Author headline from the post/profile. |
| author.profileUrl | string|null | LinkedIn profile URL when discovered. |
| author.details.location | string|null | Profile location when collected. |
| published.iso | string|null | Post publish time in ISO format when available. |
| engagement.likesCount | number | Reaction count used by minReactions filtering. |
| engagement.commentsCount | number | Comment count when available. |
| content.mediaType | string | Text, image, video, document, article, or unknown. |
| source.inputUrl | string | URL ProfileIQ used to collect the post. |
What to do next
Streaming response
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"]
}'{"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"}}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.
| Method | Endpoint | What it does |
|---|---|---|
| POST | /feeds | Create one feed |
| GET | /feeds | List feeds |
| GET | /feeds/:id | Retrieve one feed |
| PATCH | /feeds/:id | Update display name or tier |
| PATCH | /feeds/:id/pause | Pause collection |
| PATCH | /feeds/:id/resume | Resume collection |
| POST | /feeds/:id/run | Queue immediate collection |
| DELETE | /feeds/:id | Delete feed data |
Complete endpoint contracts
/feedsCreate one feedAuthorization: Bearer piq_sk_live_xxx{
"linkedinUrl": "https://www.linkedin.com/in/example/",
"linkedinUrn": "urn:li:fsd_profile:abc",
"displayName": "Example Feed",
"tier": "B"
}{
"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
}/feedsList feedsq, sortBy=newest|oldest, status=active|paused, platform=linkedin{
"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
}/feeds/:idRetrieve one feedid=feed_uuid// Same feed object shape as POST /feeds./feeds/:idUpdate display name or tierid=feed_uuid{
"displayName": "New Label",
"tier": "A"
}// Updated feed object with the same shape as POST /feeds./feeds/:id/pausePause collectionid=feed_uuid// Feed object with "status": "paused"./feeds/:id/resumeResume collectionid=feed_uuid// Feed object with "status": "active"./feeds/:id/runQueue immediate collectionid=feed_uuid{
"data": { "jobId": "job_uuid" },
"meta": {},
"error": null
}/feeds/:idDelete feed dataid=feed_uuid{
"data": {
"id": "feed_uuid",
"deleted": true,
"deletedPosts": 12,
"deletedEvents": 3,
"deletedDeliveries": 3,
"deletedMonitoringJobs": 1
},
"meta": {},
"error": null
}Create feed input
| Field | Type | Required | Description |
|---|---|---|---|
| linkedinUrl | string | Yes | Public LinkedIn /in/, /company/, or /school/ URL. Normalized by the API. |
| linkedinUrn | string | No | Known LinkedIn URN when your system already has it. |
| displayName | string | No | Human label shown in dashboards and webhook feed payloads. Max 160 chars. |
| tier | A|B|C | No | Priority tier for adaptive scheduling. Defaults to B. |
List feeds query
| Query | Type | Required | Description |
|---|---|---|---|
| q | string | No | Search feeds by display/source text. Max 200 chars. |
| sortBy | newest|oldest | No | Sort order. |
| status | active|paused | No | Filter by feed status. |
| platform | string | No | Usually linkedin. |
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"
}'{
"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
| Field | Type | Description |
|---|---|---|
| id | string | Feed id used by posts and webhooks. |
| platform | string | Usually linkedin. |
| linkedin_url | string | Canonical LinkedIn profile, company, or school URL. |
| display_name | string|null | Feed, company, or school display name. |
| headline | string|null | Profile headline when available. |
| profile_image_url | string|null | Avatar or logo URL. |
| status | string | active, paused, or deleted. |
| fetch_status | string | queued, fetching, active, or failed. |
| last_checked_at | string|null | Last collection timestamp. |
| next_check_at | string|null | Next scheduled collection timestamp. |
| tier | A|B|C | Creator priority. A is highest, B is default, C is lowest. |
| schedule_state | string | learning, strong_pattern, weak_pattern, no_pattern, or dormant. |
| pattern_confidence | number | Adaptive posting-pattern confidence from 0 to 100. |
| last_post_at | string|null | Most recent collected post timestamp. |
| predicted_next_post_at | string|null | Predicted next post timestamp when enough history exists. |
| drift_score | number | Pattern drift score used to downgrade stale predictions. |
List feeds 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 Drafts
Use drafts when a user pastes many LinkedIn URLs. ProfileIQ validates rows first, then confirm creates feeds for valid rows only.
| Method | Endpoint | What it does |
|---|---|---|
| POST | /feed/linkedin/drafts | Create validation draft |
| GET | /feed/linkedin/drafts/:id | Inspect progress and row status |
| POST | /feed/linkedin/drafts/:id/confirm | Create feeds for valid rows |
| DELETE | /feed/linkedin/drafts/:id | Cancel draft |
Complete endpoint contracts
/feed-platformsList supported platforms{
"data": [
{
"id": "linkedin",
"name": "LinkedIn",
"supportedInputs": ["profile", "company", "school"]
}
],
"meta": {},
"error": null
}/feed/:platform/draftsCreate validation draftplatform=linkedin{
"inputs": [
"https://www.linkedin.com/in/example-one/",
"https://www.linkedin.com/company/example-company/"
]
}{
"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
}/feed/:platform/drafts/:idInspect progress and row statusplatform=linkedin, id=draft_uuid{
"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
}/feed/:platform/drafts/:id/confirmCreate feeds for valid rowsplatform=linkedin, id=draft_uuid{
"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
}/feed/:platform/drafts/:idCancel draftplatform=linkedin, id=draft_uuid// Draft object with "status": "cancelled"./feed/:platform/drafts/:id/events-tokenMint short-lived token for the events streamplatform=linkedin, id=draft_uuid{
"data": { "expiresInSeconds": 300 },
"meta": {},
"error": null
}/feed/:platform/drafts/:id/eventsSubscribe to draft snapshots (SSE)platform=linkedin, id=draft_uuidlastEventId=0-0// 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
| Field | Type | Required | Description |
|---|---|---|---|
| inputs | string[] | Yes | LinkedIn URLs to validate. Min 1, max 50. Each item max 300 chars. |
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/"
]
}'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
{
"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
}Posts
Posts are collected from feeds and returned as structured author, engagement, published, content, and source objects.
| Method | Endpoint | What it does |
|---|---|---|
| GET | /feeds/:id/posts | List posts for a feed (cursor paginated) |
| GET | /posts/:id | Retrieve one post |
| POST | /posts/comments | Collect comments for a LinkedIn post |
Complete endpoint contracts
/feeds/:id/postsList posts for a feed (cursor paginated)id=feed_uuidlimit=10, cursor=opaque_token{
"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
}/posts/:idRetrieve one postid=post_uuid// One post object with the same shape as items[] in GET /feeds/:id/posts./posts/commentsCollect comments for a LinkedIn post{
"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"
}{
"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
| Query | Type | Required | Description |
|---|---|---|---|
| limit | number | No | Rows to return. Min 1, max 100. Default 10. |
| cursor | string | No | Opaque cursor from a previous response's nextCursor. Omit for the first page. |
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"{
"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
}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.
| Method | Endpoint | What it does |
|---|---|---|
| POST | /webhooks | Create webhook and receive the secret once |
| GET | /webhooks | List webhooks |
| PATCH | /webhooks/:id | Update URL or status |
| DELETE | /webhooks/:id | Delete webhook |
| GET | /webhooks/:id/feeds | List selected feed ids |
| PUT | /webhooks/:id/feeds | Replace selected feed ids |
| POST | /webhooks/:id/test | Send signed ping only |
| GET | /webhooks/deliveries | List deliveries |
| POST | /webhooks/deliveries/:deliveryId/retry | Retry delivery |
| POST | /webhooks/:id/rotate-secret | Rotate signing secret |
Complete endpoint contracts
/webhooksCreate webhook and receive the secret once{
"url": "https://example.com/profileiq/webhook"
}{
"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.
/webhooksList webhooks{
"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
}/webhooks/:idUpdate URL or statusid=webhook_uuid{
"url": "https://example.com/new-webhook",
"status": "disabled"
}{
"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
}/webhooks/:idDelete webhookid=webhook_uuid{
"data": { "deleted": true },
"meta": {},
"error": null
}/webhooks/:id/feedsList selected feed idsid=webhook_uuid{
"data": { "feedIds": ["feed_uuid"] },
"meta": {},
"error": null
}/webhooks/:id/feedsReplace selected feed idsid=webhook_uuid{
"feedIds": ["feed_uuid"]
}{
"data": { "feedIds": ["feed_uuid"] },
"meta": {},
"error": null
}/webhooks/:id/testSend signed test.ping deliveryid=webhook_uuid{
"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.
/webhooks/:id/rotate-secretRotate signing secretid=webhook_uuid{
"data": { "secret": "whsec_new_secret_shown_once" },
"meta": {},
"error": null
}/webhooks/deliveriesList deliveries across all webhooks{
"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
}/webhooks/:id/deliveriesList deliveries for one webhookid=webhook_uuid// Same delivery array shape as GET /webhooks/deliveries, scoped to one webhook./webhooks/deliveries/:deliveryId/retryRetry a failed deliverydeliveryId=delivery_uuid{
"data": {
"retried": true,
"deliveryId": "delivery_uuid"
},
"meta": {},
"error": null
}Webhook inputs
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Public HTTPS receiver URL. |
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | No | New public receiver URL. |
| status | active|disabled | No | Enable or disable delivery. |
| Field | Type | Required | Description |
|---|---|---|---|
| feedIds | uuid[] | Yes | Full replacement list of selected feed ids. Max 500. |
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"
}'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"]
}'curl -X POST "https://api.profileiq.reachrd.tech/v1/webhooks/webhook_uuid/test" \
-H "Authorization: Bearer piq_sk_live_xxx"{
"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
}{
"id": "event_uuid",
"type": "test.ping",
"createdAt": "2026-06-05T10:00:00.000Z",
"data": {
"test": true,
"mode": "ping"
}
}{
"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
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
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
| Header | Type | Description |
|---|---|---|
| content-type | string | Always application/json. |
| user-agent | string | Always ProfileIQ-Webhooks/1.0. |
| x-profileiq-timestamp | string | Unix seconds when ProfileIQ signed the request. Used in the signature input. |
| x-profileiq-signature | string | sha256=<hex> HMAC-SHA256 of `${timestamp}.${rawBody}` using your webhook secret. |
| x-profileiq-test | string | Only present on the test ping (value: true). Use it to skip ingestion side-effects. |
Request body envelope
| Field | Type | Description |
|---|---|---|
| id | string | Event id. Reuse for idempotent inserts. |
| type | string | linkedin.posts.batch for live deliveries, test.ping for the verification ping. |
| createdAt | string | ISO timestamp the event was created server-side (not the delivery time). |
| data | object | Event payload. Shape depends on type — see live and test examples below. |
expected = HMAC_SHA256(
webhook_secret,
timestamp + "." + raw_body
)
// then compare against the header value with "sha256=" strippedExample delivery
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
{
"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
{
"id": "event_uuid",
"type": "test.ping",
"createdAt": "2026-06-05T10:00:00.000Z",
"data": {
"test": true,
"mode": "ping"
}
}Reference receiver (Node + Express)
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
API Keys
Create, list, rotate, and revoke keys used by external systems.
| Method | Endpoint | What it does |
|---|---|---|
| POST | /api-keys | Create API key |
| GET | /api-keys | List keys |
| POST | /api-keys/:id/revoke | Revoke key |
| POST | /api-keys/:id/rotate | Rotate key |
Complete endpoint contracts
/api-keysCreate API key{
"label": "Production server"
}{
"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.
/api-keysList keys{
"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
}/api-keys/:id/revokeRevoke keyid=api_key_uuid{
"data": { "revoked": true },
"meta": {},
"error": null
}/api-keys/:id/rotateRotate keyid=api_key_uuid{
"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
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | Yes | Key label shown in the dashboard. Max 120 chars. |
{
"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
}Usage
Use usage endpoints to show API, discovery, feed, and webhook activity in your own dashboard.
| Method | Endpoint | What it does |
|---|---|---|
| GET | /usage/overview | Dashboard usage overview |
Complete endpoint contracts
/usage/overviewDashboard usage overview{
"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
}/usage/statsSame shape as /usage/overview// Same response body as GET /usage/overview.Usage response
{
"data": {
"feedsCount": 12,
"postsToday": 38,
"deliveries": {
"success": 120,
"failed": 2
},
"discoveryRequests": 9,
"discoveryRequestsLimit": 100
},
"meta": {},
"error": null
}