Authentication
Create an API key in the portal (app.signalraven.ai → Settings → API keys), then use the OAuth client_credentialsgrant to get a bearer token (≈30 min lifetime), and call the API with it.
# 1. Get a token
curl -s -X POST https://auth.signalraven.ai/oauth2/token \
-d grant_type=client_credentials \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d 'scope=read:signals read:sources read:icp'
# 2. Call the API
curl -s https://api.signalraven.ai/api/v1/signals?limit=5 \
-H "Authorization: Bearer eyJ…"Scopes
| Scope | Grants |
|---|---|
| read:signals | List + fetch signals |
| read:sources | List monitored sources + metrics |
| read:watchlist | List watchlist posts |
| read:icp | Read the workspace ICP profile |
| read:intelligence | Read Account + Person Intelligence reports |
| read:usage | Read workspace activity counts |
| read:destinations | Read where signals are delivered |
| write:intelligence | Start new Intelligence runs (spends credits) |
| write:destinations | Create + change delivery destinations |
| write:sources | Create lists of specific posts and add posts to them (spends credits). API keys only for now |
A token only works on endpoints matching its scopes; anything else returns 403 insufficient_scope. A key’s scopes are set when you create it, so a key made before a scope existed doesn’t have it: create a new key that includes it. Every response carries a _meta.mode of "live" or "sample" (see approved vs. sample data).
GET /signals
List signals, newest first.
| Param | Type | Default | Notes |
|---|---|---|---|
| limit | int | 25 | 1–100 |
| offset | int | 0 | pagination |
| type | string | — | filter by signal type |
| minStrength | int | — | strength ≥ this (0–10) |
Each signal carries ownCompanyPost: company when the post behind it was published by your own company page, person when it was published by someone who currently works at your company, none when neither, and null when the post could not be resolved or the signal has no post (a job change, for example).
{
"data": [
{
"id": "8af4c6c7-…",
"type": "KEYWORD_SEARCH_REACTION",
"strength": 9,
"person": { "company": "Globex", "linkedinUrl": "https://…", "location": "New York, US" },
"whyItMatters": "Publicly weighing a multi-CDN migration …",
"suggestedOpener": "Saw your note on tail-latency …",
"talkingPoints": ["Multi-CDN failover", "Live-event QoE"],
"ownCompanyPost": "none",
"createdAt": "2026-01-01T00:00:00.000Z"
}
],
"total": 866,
"limit": 25,
"offset": 0,
"_meta": { "mode": "live" }
}GET /signals/{id}
Fetch one signal by id. Returns { data, _meta }, or 404 if not found.
GET /sources
List monitored sources with activity metrics. Query period = 7, 30 (default), or all (days). Each source includes type, linkedinUrl, displayName, isActive, strengthScore, signalCount, totalPosts, qualifiedPosts. A list of specific posts (type post_list) also carries postList: days, commentsOnly, totalPosts, activePosts and pendingPosts; it is null for every other type.
GET /watchlist
List watchlist posts (limit 1–100, offset). Each post: sourceId, url, author, preview, relevanceScore, lifecycleStatus, createdAt. Pass sourceIdto list one monitor’s posts.
POST /post-lists
Create a list of specific posts: LinkedIn posts you already know about, watched for the people who engage with them (“Track specific posts” in the portal). Body: name (1–100 characters, the monitor name on its signals), and optionally urls (up to 25 posts, added in the same call), days (7, 14 or 30, default 14: how long each post is watched from the add) and commentsOnly (collect comments only, never reactions). Returns 201 with list (id, name, days, commentsOnly, isActive, totalPosts, activePosts, pendingPosts, createdAt) and the accepted and skipped posts described below. A list takes one monitor slot. The API never turns a switched-off list back on: a name that belongs to one returns 409 list_paused.
Lists are created and filled with an API key that includes write:sources. MCP connectors cannot create lists or add posts yet.
POST /post-lists/{id}/posts
Add up to 25 posts to a list. Body: urls, and optionally days. A URL can be any LinkedIn link to a post: the lnkd.in short link from “Copy link to post”, a /posts/… link, a /feed/update/urn:li:… link, or the URN on its own (urn:li:activity, urn:li:share or urn:li:ugcPost). The links of one post count as one post, whatever their form. Each post is handled on its own. accepted lists the posts taken, each fetched within about a minute (id, urn, url, expiresAt, reopened when a post watched before starts a new window, and requeued when a post still waiting for its fetch is fetched again). The urn is the post’s urn:li:activity; a post given only by its share or ugcPost id keeps that URN until the fetch returns the activity id. skipped lists the rest with a reason: duplicate, invalid_format (with a hint when a short link does not lead to a post), already_monitored (with the monitorName watching it), list_full (100 posts watched), watchlist_full or credits_exhausted.
Each post costs 1 credit when the fetch finds it; a deleted or private post costs nothing. After that, credits are spent as for any monitor: per engager captured, per engager evaluated and per signal delivered.
GET /post-lists/{id}/posts
Every post of a list, most recently added first (limit 1–100, offset), with a status: fetching, waiting (first harvest scheduled), harvesting, done, stopped or paused; or, for a post that was never watched, not_found, unreadable, no_credits, duplicate (given by its share link, and the fetch showed it already in this list) or already_monitored (the same, for a post another monitor watches). None of those is charged. Each post also carries the comments and reactions the fetch saw, engagersCaptured, signals, postedAt, addedAt and windowEndsAt. Beside the posts, listgives the list’s days, commentsOnly, isActive and autoHarvest: when it is false, a waiting post is harvested only once a harvest is started for it. Needs read:sources.
GET /icp
The workspace’s default ICP profile: minEmployees, maxEmployees, targetIndustries, targetTitles, targetPersonas, targetSeniority, additionalContext. 404 if none configured.
GET /intelligence
The Account and Person Intelligence reports run in your workspace, newest first (limit 1–100, offset, type of all | account | person, q free-text on the name). Every row carries id, type, slug, name, status and createdAt; account rows add disposition, industry, employeeCount and committeeSize, person rows add currentTitle, currentCompany and signalsCount. Fields belonging to the other family are null, so branch on type.
GET /intelligence/accounts/{id}
One Account Intelligence report: firmographics, disposition and dispositionReason, and the buyingCommittee array. 404if the report isn’t yours or doesn’t exist.
GET /intelligence/people/{id}
One Person Intelligence snapshot: profile, aiRead (classification + reasoning), notable (hiring, openToWork, recentJobChange), the signals it surfaced, and rawActivityCount. Raw activity is returned as a count only — the same thing the product shows a human.
POST /intelligence/accounts
Start an Account Intelligence run. Body: companyUrl, and optionally refresh: true. Returns 202 with { id, slug, status, cached:false } when a run was queued, or 200 with cached:true when a report already exists — in which case nothing is spent. An existing report is returned until you send refresh: true, which is honored once the report is at least 7 days old; until then the response carries refreshAvailableAt. Poll GET /intelligence/accounts/{id} until status is ready.
POST /intelligence/people
Same shape for a person. Body: personUrl, optionally sourceCompanyReportId to keep the lineage from an account report, and refresh as above.
GET /usage
Workspace activity counts: signalsThisMonth, signalsLastMonth, sourcesActive, postsMonitored, postsThisMonth, discoveryRunsToDate, plus systemStatus. Counts only — no billing figures.
GET /destinations
Where your signals are delivered, with type, name, isActive, health, consecutiveFailures and routing. Connection settings are never returned — a read scope should not hand back webhook URLs or tokens.
POST / PATCH / DELETE /destinations
Create (destinationType, name, config, optional routing), update (name, isActive, routing), or remove one. DELETE deactivates rather than hard-deleting, so delivery history survives. Creating returns 409when the plan’s destination limit is reached.
Errors
| Status | error | When |
|---|---|---|
| 400 | invalid_body | a required field is missing or out of range |
| 400 | too_many_inputs | more than 25 posts in one call |
| 401 | unauthorized | missing / invalid / expired token |
| 403 | insufficient_scope | token lacks the required scope |
| 403 | forbidden | token isn’t workspace-scoped, or a list write made without an API key |
| 403 | client_denied | client denied by SignalRaven |
| 403 | workspace_inactive | writes need an active workspace |
| 404 | not_found | resource doesn’t exist |
| 409 | plan_destination_cap_reached | at the plan’s destination limit |
| 409 | source_cap_reached | at the plan’s monitor limit |
| 409 | name_conflict | a list with this name exists |
| 409 | list_paused | the list, or the list with this name, is switched off; turn it back on in the portal first |