ClusterSignal
ClusterSignal/API Referencev1.1 · stable

API Reference

Machine-readable access to ClusterSignal's full signal set — insider clusters, congressional trading clusters, cross-source alignment events, trademark filings, 13D/13G activist stakes, 13F fund position changes, government contract awards, and FDA/clinical-trial catalysts. Three endpoints: a paginated cross-type list for incremental polling, an enriched detail endpoint for individual insider-cluster signals, and a per-ticker summary across every signal type.

Get API access — $19/mo →

Base URL

https://www.clustersignal.app/api/v1

Always use the www. subdomain. clustersignal.app issues a 308 redirect that many HTTP clients drop the Authorization header on.

Authentication

All requests require an API key passed as a Bearer token. Keys require an active API subscription ($19/mo) and are generated at /account/api-keys.

Authorization: Bearer cs_live_<key>

Key format: cs_live_ prefix + 64 hex chars (256-bit random)

Storage: Raw key is shown once at generation and never stored. Only a SHA-256 hash is kept server-side.

Limit: 3 active keys per account. Revoke unused keys to free slots.

Rate Limits

60 requests per minute, per API key.

Enforced atomically across all serverless instances via Postgres. Exceeding the limit returns 429.

The list endpoint is DB-only and safe to poll every 60 seconds. The detail endpoint makes external API calls (Yahoo Finance, SEC EDGAR) — poll it sparingly.

GET /api/v1/signals

Returns a paginated, cross-type list of signals ordered by detected_at ascending. DB-only — safe to poll frequently.

Query Parameters

since
string

ISO 8601 timestamp. Returns signals detected after this time. Use next_since from the previous response to page forward.

limit
integer

Number of results. Range: 1–500. Default: 100. Applies to the merged, cross-type result — not per type.

signal_type
string

Comma-separated list of types to include — see Signal Types below. Default: insider_cluster only (unchanged from v1.0, so existing integrations see no shape change unless they opt in).

ticker
string

Filter to one ticker symbol. Applies across all requested signal_types.

direction
string

insider_cluster only. Filter by trade direction. Values: buy | sell

min_score
integer

insider_cluster only. Minimum cluster score (0–100). Filters out low-conviction signals.

min_grade
string

insider_cluster only. Minimum grade. Values: A | B | C. Equivalent to min_score 80 | 50 | 0.

Response

{
  "schema_version": "1.2",
  "server_time":    "2026-07-21T12:25:21.588Z",
  "count":          3,
  "next_since":     "2026-06-16T18:07:38.445456+00:00",
  "signals":        [ /* Signal objects — shape varies by signal_type, see Signal Types */ ]
}
schema_versionstring

"1.2" as of this release. Bumped on additive changes.

countinteger

Number of signals in this response.

next_sincestring|null

detected_at of the last signal in this batch, across all requested types. Pass as since on the next poll. Null when the response is empty.

signalsarray

Array of Signal objects, chronologically merged across every requested signal_type. Each object's signal_type field tells you which shape it is.

When requesting multiple signal_types, each type is fetched independently up to limit and the results are merged by detected_at before truncating to the page size, so pagination stays chronologically correct across types.

GET /api/v1/signals/:id

Returns a single signal with full enrichment: live price data, earnings proximity, dilution risk, filing speed, and insider track records. Makes external API calls — use only for signals that pass your first-pass filter.

GET /api/v1/signals/6251ee0d-c83b-452c-8531-85c951b769a6

Response

{
  "schema_version": "1.2",
  "server_time":    "2026-07-21T12:25:21.588Z",
  "signal": {
    /* All list Signal fields plus: */
    "flags": {
      "earnings_proximity_days":   null,   // same value as the list endpoint's field, null if unknown
      "dilution_risk":             false,  // >3 filings on same day = potential offering signal
      "volume_spike":              false,  // today's volume > 2× 20-day average
      "filing_speed_same_day_pct": 0       // fraction of insiders who filed same day as trade
    },
    "price_context": {
      "company_name":     "AppFolio, Inc.",
      "sector":           "Information Technology",
      "current_price":    155.145,
      "price_change_pct": null,
      "market_cap":       null,
      "week_52_high":     326.04,
      "week_52_low":      142.56,
      "price_chart": {
        "timestamps": [ /* Unix timestamps */ ],
        "closes":     [ /* Close prices  */ ],
        "volumes":    [ /* Daily volumes */ ]
      },
      "price_return": {
        "priceAtCluster": 159.68,
        "return_30d":     null,   // null until enough time has passed
        "return_60d":     null,
        "return_90d":     null
      },
      "win_rate":             null,
      "signal_summary":       "Signal Strength: ...",
      "insider_track_records": [
        {
          "insider_cik":       "2082975",
          "insider_name":      "Rigler Don",
          "officer_title":     null,
          "trade_direction":   "buy",  // this insider's own direction in this cluster — "first_time_buyer" is only ever true for "buy"
          "is_first_time_buyer": true,
          "prior_trades":      []      // prior buys for a "buy" insider, prior sales for a "sell" insider — never mixed
        }
      ]
    }
  }
}

Returns 404 with {"error":"not_found"} for unknown IDs.

GET /api/v1/tickers/:symbol/summary

Everything known about one ticker across every signal type, in a single response. DB-only — no external API calls, safe to poll.

GET /api/v1/tickers/PLTR/summary

Response

{
  "schema_version": "1.2",
  "server_time":    "2026-07-21T12:25:21.588Z",
  "ticker":         "PLTR",
  "company_name":   null,
  "sector":         null,
  "insider_clusters":       [ /* insider_cluster signals, up to 100 */ ],
  "congressional_clusters": [ /* congressional_cluster signals, up to 100 */ ],
  "aligned_signals":        [ /* aligned_signal signals, up to 100 */ ],
  "trademark_filings":      [ /* trademark_filing signals, up to 100 */ ],
  "activist_stakes":        [ /* activist_stake signals, up to 100 */ ],
  "fund_positions":         [ /* fund_position signals, up to 100 */ ],
  "gov_contracts": [
    {
      "signal_type":      "gov_contract",
      "id":               "40a0e34d-feb4-43ab-80d3-029512312c3c",
      "ticker":           "PLTR",
      "recipient_name":   "PALANTIR TECHNOLOGIES INC.",
      "awarding_agency":  "Department of Homeland Security",
      "award_amount":     45848616.8,
      "start_date":       "2026-06-26",
      "aligned_with_congressional_trade": false,
      "usaspending_url":  "https://www.usaspending.gov/award/CONT_AWD_...",
      "detected_at":      "2026-07-20T13:04:41.199363+00:00"
    }
  ],
  "fda_catalysts": [ /* fda_catalyst signals, up to 100 */ ]
}
company_namestring|null

From the enrichment cache. Null if not yet cached.

sectorstring|null

From the enrichment cache. Null if not yet cached.

*array

Every other top-level field is an array of Signal objects of that type (see Signal Types), each capped at 100 rows, most recent first by detected_at. Empty array — not an error — when there's nothing of that type for this ticker.

Unknown tickers return 200 with every array empty, not a 404 — there's no canonical ticker list to validate against.

Signal Object

Fields present on every signal (list and detail).

signal_typestring

Always "insider_cluster".

idstring

UUID. Stable — use for deduplication and detail lookups.

tickerstring

Stock ticker symbol.

company_namestring|null

Company name from the enrichment cache. Null if not yet cached.

sectorstring|null

GICS sector from the enrichment cache. Null if not yet cached.

directionstring

"buy" or "sell".

gradestring

"A" (score ≥ 80), "B" (≥ 50), or "C" (< 50). Final at detection — not updated retroactively.

scoreinteger

0–100 conviction score. 0 on clusters detected before scoring was live.

score_breakdownobject

Per-dimension score components. Keys: insider_count, capital, seniority, win_rate (always 0), velocity, filing_speed.

window_startstring

YYYY-MM-DD. First transaction date in the cluster window.

window_endstring

YYYY-MM-DD. Last transaction date in the cluster window.

detected_atstring

ISO 8601. When the cluster was first detected by the poller.

aggregate_value_usdnumber

Sum of transaction values across all filings in the cluster.

insider_countinteger

Number of distinct insiders in the cluster.

is_short_radarboolean

True when direction=sell, insider_count ≥ 3, and aggregate_value_usd ≥ $250k. A higher-conviction sell signal worth extra scrutiny.

earnings_proximity_daysinteger|null

Added in 1.2. Days from window_end to the ticker's next known earnings date (negative = earnings already passed since detection). Null if no earnings date is known within -30/+120 days. DB-only lookup against a daily-synced calendar — no external call, safe on the list endpoint.

filingsarray

Individual Form 4 filings in the cluster.

edgar_urlstring|null

SEC EDGAR archives URL for the first filing. Null if CIK or accession number unavailable.

Filing Object

insider_namestring

Full name as reported on Form 4.

titlestring|null

Officer title (e.g. CEO, CFO). Null if not reported.

transaction_codestring

SEC transaction code. Common: "S" (open-market sale), "P" (open-market purchase), "A" (grant/award).

sharesnumber|null

Number of shares transacted.

pricenumber|null

Price per share.

value_usdnumber|null

Total transaction value (shares × price).

transaction_datestring

YYYY-MM-DD.

is_10b5_planboolean

True if the trade was executed under a pre-arranged 10b5-1 plan. Reduces signal strength.

is_cashless_exerciseboolean

True if the sale was a cashless option exercise (sell-to-cover). Reduces signal strength.

Signal Types

Every object in signals carries signal_type, id, ticker, and detected_at — the fields below that are type-specific. ticker is null when the underlying filer/recipient/sponsor couldn't be matched to a public company (deliberately conservative — an exact-normalized-name match only, never fuzzy, to avoid misattributing a signal to the wrong ticker).

congressional_cluster

2+ members of Congress trading the same ticker/direction within a 60-day window. Source: House disclosures.

directionstring

"buy" or "sell".

member_countinteger

Distinct members in the cluster.

window_startstring

YYYY-MM-DD

window_endstring

YYYY-MM-DD

total_amount_lownumber|null

Sum of disclosed range lower bounds.

total_amount_highnumber|null

Sum of disclosed range upper bounds.

membersarray

Per-member detail: name, party, state, source, committees, trade_date.

has_aligned_signalboolean

True if this cluster also lines up with a corporate insider cluster — see aligned_signal.

aligned_signal

Cross-source corroboration — two independent signal sources point to the same ticker around the same time. A stronger signal than either source alone.

alignment_typestring

"political_corporate" (a congressional cluster lines up with a corporate insider cluster) or "contract_congressional" (a gov contract award lines up with a congressional trade in the same ticker by a member on a relevant oversight committee, within 60 days).

detailsobject

Shape depends on alignment_type — political_corporate carries the congressional_cluster fields (direction, member_count, window_start/end, members); contract_congressional carries the gov_contract fields (recipient_name, awarding_agency, award_amount, start_date, aligned_trades).

trademark_filing

New USPTO trademark applications from publicly traded companies — can signal unannounced products/brands.

mark_textstring|null

The trademark text/name, when disclosed.

applicant_namestring

Filing entity name as reported to USPTO.

goods_services_descriptionstring|null

What the mark covers.

filing_datestring

YYYY-MM-DD

nice_classesarray

Nice Classification codes (goods/services categories).

activist_stake

SEC Schedule 13D/13G beneficial-ownership filings — someone crossing the 5% ownership threshold.

filing_typestring

"13D" (activist intent) or "13G" (passive investor).

is_amendmentboolean

True for a follow-up amendment rather than the original filing.

filer_namestring

The reporting person/entity taking the stake.

issuer_namestring

The company whose stock was acquired.

percent_ownershipnumber|null

Percent of class owned, as reported.

shares_ownednumber|null

Aggregate shares beneficially owned.

filed_datestring

YYYY-MM-DD

edgar_urlstring

SEC EDGAR archives URL for the filing.

aligned_with_insider_clusterboolean

True if this filing landed on a ticker with an active insider buy cluster (last 90 days).

fund_position

Quarter-over-quarter position changes from ~20 tracked notable 13F filers (Berkshire Hathaway, Renaissance Technologies, Citadel, etc.) — new positions, full exits, and adds/trims of 20%+.

filer_namestring

Tracked fund/manager name.

change_typestring

"new" | "exit" | "increase" | "decrease".

period_of_reportstring

Quarter-end date for this filing, YYYY-MM-DD.

prior_periodstring|null

Quarter-end date this was compared against.

valuenumber|null

Position value this quarter (USD). Null for exit.

prior_valuenumber|null

Position value prior quarter (USD). Null for new.

sharesnumber|null

Shares held this quarter. Null for exit.

prior_sharesnumber|null

Shares held prior quarter. Null for new.

pct_changenumber|null

Value change fraction (0.25 = +25%). Null for new/exit.

gov_contract

New federal prime contract awards ≥ $10M from USAspending.gov, matched to a public recipient where possible.

recipient_namestring

Contract recipient as reported to USAspending.

awarding_agencystring

Federal agency awarding the contract.

award_amountnumber

Award amount in USD.

start_datestring

YYYY-MM-DD

aligned_with_congressional_tradeboolean

True if a member on a relevant oversight committee traded this ticker within 60 days of the award — see aligned_signal for the detail.

usaspending_urlstring

USAspending.gov award detail URL.

fda_catalyst

Forward-looking Phase 3 clinical trial readouts for publicly traded biotechs, from clinicaltrials.gov. PDUFA dates are not tracked — there's no official structured source for them.

sponsor_namestring

Lead sponsor (industry) as reported to clinicaltrials.gov.

titlestring

Trial brief title.

phasestring|null

e.g. "PHASE3" or "PHASE2, PHASE3" for combined trials.

conditionstring|null

Up to 3 conditions/indications, comma-separated.

statusstring

"RECRUITING" | "ACTIVE_NOT_RECRUITING" | "ENROLLING_BY_INVITATION".

primary_completion_datestring|null

YYYY-MM-DD — the key readout date. Day precision isn't always disclosed (defaults to the 1st of the month).

completion_datestring|null

YYYY-MM-DD — full trial completion, typically later than primary completion.

study_urlstring

clinicaltrials.gov study page.

Errors

All error responses use the same shape:

{ "error": "human-readable description" }
Statuserror valueMeaning
401"unauthorized"No Authorization header, or key not found / revoked.
403"API access requires..."Key is valid but access has been disabled for the associated account.
404"not_found"Signal ID does not exist (detail endpoint only).
429"Rate limit exceeded..."60 req/min per key exceeded. Back off and retry after the current minute.
500variesServer error. Retry with exponential backoff.

Pagination

The list endpoint uses a since cursor. Results are ordered by detected_at ASC with id as a stable tiebreaker. On each response, save next_since and pass it as since on the next poll. No results means you are caught up.

// Poll loop (pseudocode)
let since = null

while (true) {
  const url = since
    ? `/api/v1/signals?since=${since}&limit=100`
    : '/api/v1/signals?limit=100'

  const res  = await fetch(url, { headers })
  const body = await res.json()

  for (const signal of body.signals) {
    process(signal)
  }

  if (body.count === 0) {
    await sleep(60_000) // caught up — wait a minute
  } else {
    since = body.next_since
    // more pages may exist — loop immediately
  }
}

Examples

List signals (curl)

curl -s "https://www.clustersignal.app/api/v1/signals?limit=10&direction=buy&min_grade=B" \
  -H "Authorization: Bearer cs_live_<your_key>"

Fetch signal detail (curl)

curl -s "https://www.clustersignal.app/api/v1/signals/<signal_id>" \
  -H "Authorization: Bearer cs_live_<your_key>"

List signals (JavaScript)

const BASE = 'https://www.clustersignal.app/api/v1'
const KEY  = process.env.CLUSTERSIGNAL_API_KEY

async function fetchSignals(since?: string) {
  const url = new URL(`${BASE}/signals`)
  url.searchParams.set('limit', '100')
  if (since) url.searchParams.set('since', since)

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${KEY}` },
  })
  if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`)
  return res.json()
}

async function fetchDetail(id: string) {
  const res = await fetch(`${BASE}/signals/${id}`, {
    headers: { Authorization: `Bearer ${KEY}` },
  })
  if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`)
  return res.json()
}

Filter for high-conviction buys only

GET /api/v1/signals?direction=buy&min_grade=A&limit=50

Filter for short-radar sells

# Fetch sells, then filter is_short_radar === true client-side
GET /api/v1/signals?direction=sell&limit=100

# Or use min_score to tighten the set
GET /api/v1/signals?direction=sell&min_score=50&limit=100

Poll every new signal type at once

GET /api/v1/signals?signal_type=congressional_cluster,aligned_signal,trademark_filing,activist_stake,fund_position,gov_contract,fda_catalyst&limit=100

One ticker, one call, every signal type

curl -s "https://www.clustersignal.app/api/v1/tickers/PLTR/summary" \
  -H "Authorization: Bearer cs_live_<your_key>"

Changelog

v1.22026-07-21Added earnings_proximity_days to the insider_cluster Signal Object — now available on GET /signals (list), not just the detail endpoint's flags. DB-only lookup against a daily-synced earnings calendar, so the list endpoint stays safe to poll frequently.
v1.12026-07-21Added signal_type and ticker params to GET /signals — 7 new signal types: congressional_cluster, aligned_signal, trademark_filing, activist_stake, fund_position, gov_contract, fda_catalyst. Default behavior (no signal_type) is unchanged — insider_cluster only. Added GET /tickers/:symbol/summary.
v1.02026-06-29Initial release. GET /signals and GET /signals/:id.

Additive changes (new optional fields, new optional query params) happen within v1 and bump schema_version. Breaking changes go to /api/v2/.