Developers
DEVELOPERS · REST API

REST API reference

Base URL api.signalraven.ai/api/v1 — versioned, JSON, scoped to your workspace. Mostly reads, plus Intelligence runs, person monitoring, lists of specific posts and destination changes. Every call needs an OAuth bearer token.

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

ScopeGrants
read:signalsList + fetch signals
read:sourcesList monitored sources + metrics
read:watchlistList watchlist posts
read:icpRead the workspace ICP profile
read:intelligenceRead Account + Person Intelligence reports
read:usageRead workspace activity counts
read:destinationsRead where signals are delivered
write:intelligenceStart new Intelligence runs (spends credits)
write:destinationsCreate + change delivery destinations
write:sourcesCreate 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.

ParamTypeDefaultNotes
limitint251–100
offsetint0pagination
typestring—filter by signal type
minStrengthint—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

StatuserrorWhen
400invalid_bodya required field is missing or out of range
400too_many_inputsmore than 25 posts in one call
401unauthorizedmissing / invalid / expired token
403insufficient_scopetoken lacks the required scope
403forbiddentoken isn’t workspace-scoped, or a list write made without an API key
403client_deniedclient denied by SignalRaven
403workspace_inactivewrites need an active workspace
404not_foundresource doesn’t exist
409plan_destination_cap_reachedat the plan’s destination limit
409source_cap_reachedat the plan’s monitor limit
409name_conflicta list with this name exists
409list_pausedthe list, or the list with this name, is switched off; turn it back on in the portal first