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.
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
cursorstringOpaque keyset cursor. Pass back next_cursor from the previous response, verbatim. This is the correct way to page: it resumes at an exact row, so it traverses groups of signals that share a detected_at. Supersedes since when both are sent. A cursor that cannot be decoded returns 400 rather than silently restarting the stream.
sincestringISO 8601 timestamp. Returns signals detected strictly after this time. Retained for existing integrations, but it CANNOT page through signals that share a detected_at — see Pagination. Prefer cursor.
limitintegerNumber of results. Range: 1–500. Default: 100. Applies to the merged, cross-type result — not per type.
signal_typestringComma-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).
tickerstringFilter to one ticker symbol. Applies across all requested signal_types.
directionstringinsider_cluster only. Filter by trade direction. Values: buy | sell
min_scoreintegerinsider_cluster only. Minimum cluster score (0–100). Filters out low-conviction signals.
min_gradestringinsider_cluster only. Minimum grade. Values: A | B | C. Equivalent to min_score 70 | 45 | 0 under score_version 2.
Response
{
"schema_version": "1.6",
"server_time": "2026-07-21T12:25:21.588Z",
"count": 3,
"next_since": "2026-06-16T18:07:38.445456+00:00",
"next_cursor": "MjAyNi0wNi0xNlQxODowNzozOC40NDU0NTYrMDA6MDB8YzFkMi4uLg",
"signals": [ /* Signal objects — shape varies by signal_type, see Signal Types */ ]
}schema_versionstring"1.6" as of this release, and the same value on all three endpoints. Bumped on additive changes.
countintegerNumber of signals in this response.
next_cursorstring|nullAdded in 1.5. Opaque cursor naming the last signal in this batch within the (detected_at, id) ordering. Pass as cursor on the next poll. Null when the response is empty — keep re-sending your previous cursor. This is the only field that can page through a group of signals sharing one detected_at.
next_sincestring|nulldetected_at of the last signal in this batch, across all requested types. Legacy pagination — lossy across signals sharing a detected_at. Prefer next_cursor. Null when the response is empty.
signalsarrayArray 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
Resolves an id of any signal type. An insider_cluster id comes back with full enrichment: live price data, earnings proximity, dilution risk, filing speed, and insider track records. That path makes external API calls — use it only for signals that pass your first-pass filter.
Every other type returns the same object the list endpoint emits, DB-only, with no price_context — there is no additional enrichment to fetch for those types. Added in 1.6: these ids previously returned 404, which is indistinguishable from a deleted signal, so a strategy gate reading a field off this endpoint failed closed on the entire type. Nothing a consumer needs is detail-only — every field below except price_context and the insider-only flags is already on the list payload.
GET /api/v1/signals/6251ee0d-c83b-452c-8531-85c951b769a6
Response
{
"schema_version": "1.6",
"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
"days_to_next_earnings": 76, // same as the list endpoint's field — use this to gate on an earnings blackout
"next_earnings_date": "2026-10-27",
"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"} only when no signal of any type carries that id. On a non-insider type, flags.dilution_risk, volume_spike and filing_speed_same_day_pct are null — absent, not measured-and-negative. An aligned_signal shares its id with the congressional cluster or contract award it was derived from, so those ids resolve here as congressional_cluster or gov_contract — the same underlying event, reported under its own type.
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.6",
"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",
"next_earnings_date": "2026-11-03", // added in 1.6 — on every type
"days_to_next_earnings": 83,
"earnings_proximity_days": null
}
],
"fda_catalysts": [ /* fda_catalyst signals, up to 100 */ ]
}company_namestring|nullFrom the enrichment cache. Null if not yet cached.
sectorstring|nullFrom the enrichment cache. Null if not yet cached.
*arrayEvery 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_typestringAlways "insider_cluster".
idstringUUID. Stable — use for deduplication and detail lookups.
tickerstringStock ticker symbol.
company_namestring|nullCompany name from the enrichment cache. Null if not yet cached.
sectorstring|nullGICS sector from the enrichment cache. Null if not yet cached.
directionstring"buy" or "sell".
gradestring"A" (score ≥ 70), "B" (≥ 45), or "C" (< 45) under score_version 2. Recomputed from score on read, so a rescore is reflected immediately.
scoreinteger0–100 conviction score. 0 on clusters detected before scoring was live.
score_versionintegerAdded in 1.3. Which scorer produced score and grade. 2 is current. Version 1 used a different scale on which 99% of clusters graded A and a grade C was arithmetically impossible — v1 and v2 grades are NOT comparable, so do not pool them in a backtest. Filter on this field when computing grade-sliced statistics.
score_breakdownobjectPer-dimension score components. Keys: insider_count, capital, seniority, win_rate (always 0), velocity, filing_speed.
window_startstringYYYY-MM-DD. First transaction date in the cluster window.
window_endstringYYYY-MM-DD. Last transaction date in the cluster window.
detected_atstringISO 8601. When the cluster was first detected by the poller.
aggregate_value_usdnumberSum of transaction values across all filings in the cluster.
insider_countintegerNumber of distinct insiders in the cluster.
is_short_radarbooleanTrue when direction=sell, insider_count ≥ 3, and aggregate_value_usd ≥ $250k. A higher-conviction sell signal worth extra scrutiny.
next_earnings_datestring|nullAdded in 1.6. The ticker's next known earnings date, YYYY-MM-DD. Null when the calendar has no upcoming date for the ticker (not yet scheduled, or not covered by the source — ETFs, most foreign issuers). Present on every signal type. Provided raw so you can apply your own proximity rule instead of ours.
days_to_next_earningsinteger|nullAdded in 1.6. Days from today to next_earnings_date, 0–120. This is the field to gate an earnings blackout on: it describes the ticker right now, which is what matters for a position you are opening today. Null means only that no upcoming date is known — never that the ticker is clear. Present on every signal type.
earnings_proximity_daysinteger|nullAdded in 1.2; extended to every signal type in 1.6. Days from this signal's own event date (window_end for a cluster, filing/award date otherwise) to next_earnings_date. Null outside -30/+120 days. This is a property of the SIGNAL — how close to a report the trading happened, i.e. whether the filers plausibly had forward visibility — not a tradeability check. On a backfilled type it is usually null because the event is long past: measured 2026-08-12 over the 200 newest congressional clusters, 5 had a value here while 176 had a days_to_next_earnings. Do not use it as a blackout gate; use days_to_next_earnings.
filingsarrayIndividual Form 4 filings in the cluster.
edgar_urlstring|nullSEC EDGAR archives URL for the first filing. Null if CIK or accession number unavailable.
Filing Object
insider_namestringFull name as reported on Form 4.
titlestring|nullOfficer title (e.g. CEO, CFO). Null if not reported.
transaction_codestringSEC transaction code. Common: "S" (open-market sale), "P" (open-market purchase), "A" (grant/award).
sharesnumber|nullNumber of shares transacted.
pricenumber|nullPrice per share.
value_usdnumber|nullTotal transaction value (shares × price).
transaction_datestringYYYY-MM-DD.
is_10b5_planbooleanTrue if the trade was executed under a pre-arranged 10b5-1 plan. Reduces signal strength.
is_cashless_exercisebooleanTrue 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, detected_at, and — since 1.6 — the three earnings fields (next_earnings_date, days_to_next_earnings, earnings_proximity_days), which are facts about the ticker and are derived identically for all eight types. The fields below are the type-specific ones. 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_countintegerDistinct members in the cluster.
window_startstringYYYY-MM-DD
window_endstringYYYY-MM-DD
total_amount_lownumber|nullSum of disclosed range lower bounds.
total_amount_highnumber|nullSum of disclosed range upper bounds.
membersarrayPer-member detail: name, party, state, source, committees, trade_date.
has_aligned_signalbooleanTrue if this cluster's trading window comes within 60 days of a same-direction corporate insider cluster's window on the same ticker — see aligned_signal. Compares real event dates, not ingestion times.
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).
detailsobjectShape 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|nullThe trademark text/name, when disclosed.
applicant_namestringFiling entity name as reported to USPTO.
goods_services_descriptionstring|nullWhat the mark covers.
filing_datestringYYYY-MM-DD
nice_classesarrayNice 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_amendmentbooleanTrue for a follow-up amendment rather than the original filing.
filer_namestringThe reporting person/entity taking the stake.
issuer_namestringThe company whose stock was acquired.
percent_ownershipnumber|nullPercent of class owned, as reported.
shares_ownednumber|nullAggregate shares beneficially owned.
filed_datestringYYYY-MM-DD
edgar_urlstringSEC EDGAR archives URL for the filing.
aligned_with_insider_clusterbooleanTrue if this filing's filed_date falls within 90 days of an insider buy cluster's trading window on the same ticker. Compares real event dates, not ingestion times, so it is stable across backfills.
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_namestringTracked fund/manager name.
change_typestring"new" | "exit" | "increase" | "decrease".
period_of_reportstringQuarter-end date for this filing, YYYY-MM-DD.
prior_periodstring|nullQuarter-end date this was compared against.
valuenumber|nullPosition value this quarter (USD). Null for exit.
prior_valuenumber|nullPosition value prior quarter (USD). Null for new.
sharesnumber|nullShares held this quarter. Null for exit.
prior_sharesnumber|nullShares held prior quarter. Null for new.
pct_changenumber|nullValue 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_namestringContract recipient as reported to USAspending.
awarding_agencystringFederal agency awarding the contract.
award_amountnumberAward amount in USD.
start_datestringYYYY-MM-DD. The award's period-of-performance start, used as its event date. It tracks the actual signing date within a few days for almost all awards, but is not the same field — 1.1% of awards have a period starting well before or after the award was made, including option periods dated years out.
aligned_with_congressional_tradebooleanTrue if a member on a relevant oversight committee traded this ticker within 60 days of the award — see aligned_signal for the detail. Always read alongside roster_current_only.
roster_current_onlybooleanAdded in 1.4. True when aligned_with_congressional_trade was decided against a committee roster that postdates the award. The roster source publishes the current Congress only, so for an award predating it the match answers ‘is this member on that committee today’, not ‘were they on it when the award landed’. Exclude or discount these rows in a backtest rather than treating them as measured.
usaspending_urlstringUSAspending.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_namestringLead sponsor (industry) as reported to clinicaltrials.gov.
titlestringTrial brief title.
phasestring|nulle.g. "PHASE3" or "PHASE2, PHASE3" for combined trials.
conditionstring|nullUp to 3 conditions/indications, comma-separated.
statusstring"RECRUITING" | "ACTIVE_NOT_RECRUITING" | "ENROLLING_BY_INVITATION".
primary_completion_datestring|nullYYYY-MM-DD — the key readout date. Day precision isn't always disclosed (defaults to the 1st of the month).
completion_datestring|nullYYYY-MM-DD — full trial completion, typically later than primary completion.
study_urlstringclinicaltrials.gov study page.
Errors
All error responses use the same shape:
{ "error": "human-readable description" }| Status | error value | Meaning |
|---|---|---|
| 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" | No signal of any type has that ID (detail endpoint only). Since 1.6 this no longer fires for non-insider types. |
| 429 | "Rate limit exceeded..." | 60 req/min per key exceeded. Back off and retry after the current minute. |
| 500 | varies | Server error. Retry with exponential backoff. |
Pagination
Results are ordered by detected_at ASC with id as a stable tiebreaker — together a total order, so every signal has exactly one position. Save next_cursor from each response and pass it back as cursor. An empty response means you are caught up; keep your last cursor and re-send it on the next poll.
Use cursor, not since. since filters on detected_at alone, and that value is not unique: a bulk historical ingest stamps thousands of signals within the same microsecond, and even routine batches produce groups of 100–200. When such a group spans a page boundary, next_since advances past the entire group and its remaining signals become unreachable — and if one group is larger than limit, pagination cannot advance at all. Draining the activist_stake corpus with since at limit=100 reaches 17.7% of it; with cursor it reaches all of it. since is retained so existing integrations keep working, but it cannot be made complete.
// Poll loop (pseudocode)
let cursor = null // persist this across restarts
while (true) {
const url = cursor
? `/api/v1/signals?cursor=${encodeURIComponent(cursor)}&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) {
// caught up — keep the existing cursor and wait
await sleep(60_000)
} else {
cursor = body.next_cursor
// 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
Earnings blackout gate (any signal type)
// days_to_next_earnings is on every signal from the list endpoint, so the gate
// never needs the detail endpoint and never branches on signal_type.
const BLACKOUT_DAYS = 3
function tradeable(signal) {
const d = signal.days_to_next_earnings
if (d === null) return FAIL_OPEN_ON_UNKNOWN // your policy — see below
return d > BLACKOUT_DAYS
}
// null means "no upcoming date is known for this ticker", never "the ticker is
// clear". Whether that blocks a trade is your call, but know the cost: the
// calendar source omits some tickers outright (ETFs, most foreign issuers, and
// a tail of ordinary names), so failing closed on null permanently blocks them
// rather than blocking them for a few days. Measured 2026-08-12: 18 of 202
// tickers across the congressional corpus had no date on file at all.
// Do NOT gate on earnings_proximity_days. It is measured from the signal's own
// event date, which on a backfilled type is often a year old — it is null for
// most rows and says nothing about today.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.62026-08-12Earnings context on every signal type, not just insider_cluster: next_earnings_date, days_to_next_earnings (from today — the field to gate an earnings blackout on) and earnings_proximity_days(from the signal's own event date). Earnings dates are a property of the ticker, so all three are now derived identically for all eight types on both the list and /tickers/:symbol/summary. Also: GET /signals/:id now resolves an id of any type instead of returning 404 for the seven non-insider ones, and all three endpoints report the same schema_version.v1.5Added cursor / next_cursor keyset pagination over (detected_at, id). since cannot page through signals sharing a detected_at; cursor can.v1.4Added roster_current_only to gov_contract signals, marking alignments decided against a roster that postdates the award.v1.3Added score_version to insider_cluster signals. Scoring recalibrated to v2 — v1 and v2 grades are not comparable.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/.