Programmatic access to Congressional stock trades, member profiles, federal contract leads, the lobbying filings that cite each bill, and committee seats. All responses follow a consistent envelope format with pagination and citation metadata.
https://www.capitolexposed.com/api/v1Free-tier requests require no authentication and are rate-limited by IP address. For higher limits, include your API key in the X-API-Key header.
curl -H "X-API-Key: cg_v1_your_key_here" \ https://www.capitolexposed.com/api/v1/stats
All responses return JSON with a standard envelope: status, data, meta (pagination + timestamp), and citation information. Field names use snake_case.
CapitolExposed has two AI surfaces. Quick Answer runs on /api/investigate for fast internal-data answers. Deep Research uses the saved notebook workflow under /api/investigate/deep plus thread, run, share, and export routes. These app routes are used by the Ask AI workspace rather than the public v1 API.
Best for fast database questions and quick follow-ups.
Free plan includes 5 runs per day, rate-limited by route.
Returns streaming chat output and tool results.
Best for source-backed investigations, timelines, comparisons, and source packs.
Pro includes 10 deep runs per month. Institutional and Enterprise plans are unlimited.
Saved notebooks persist runs, messages, artifacts, and source sets.
Uploads accept PDF, text, CSV, Markdown, and JSON files.
Each request supports up to 4 files and 12 MB per file.
PDF extraction reads up to 20 pages and trims extracted text to 50,000 characters.
Deep Research prioritizes uploaded evidence first, internal records second, and official allowlisted domains third.
14 official domains are enabled for retrieval, including congress.gov, senate.gov, house.gov, fec.gov, sec.gov, pdc.wa.gov, and leg.wa.gov.
The machine-readable Deep Research spec lives at /api/investigate/openapi.json.
| Route | Purpose | Access |
|---|---|---|
POST /api/investigate | Quick Answer streaming chat | Public, free tier limited to 5 runs per day |
POST /api/investigate/deep | Create a deep-research run and thread | Signed-in Pro or higher |
POST /api/investigate/uploads | Extract text from PDF and text-based uploads for notebook use | Public |
GET /api/investigate/runs/{runId} | Load run status, progress, and the current thread payload | Signed-in owner |
GET /api/investigate/runs/{runId}/stream | Resumable SSE progress stream for a deep run | Signed-in owner |
GET /api/investigate/threads/{threadId} | Load notebook state, sources, and artifacts | Signed-in owner |
PATCH /api/investigate/threads/{threadId} | Rename a saved notebook | Signed-in owner |
POST /api/investigate/artifacts/{artifactId}/share | Create a frozen public share snapshot | Signed-in owner |
GET /api/investigate/artifacts/{artifactId}/export | Export an artifact and its source set as JSON | Signed-in owner |
Rate limits are enforced per route and returned in response headers. Exceeding the active window returns 429 Too Many Requests with a Retry-After header.
| Route Class | Rate Limit | Authentication | Notes |
|---|---|---|---|
| List endpoints | 60 requests / minute | Optional; tracked by IP or API key | Members and trades |
| Search and contract leads | 30 requests / minute | Optional; tracked by IP or API key | Search, /api/v1/alerts (contract leads) |
| Bulk exports | 5 requests / 60 minutes per route | Optional; your plan comes from X-API-Key or your signed-in session, and sets the window | CSV / JSON export, newsroom ZIPs |
| Quick Answer | 5 requests / day on the free plan | Optional; signed-in paid plans skip the free cap | /api/investigate |
| Deep Research start | 20 requests / 60 minutes | Signed-in paid plans only | /api/investigate/deep |
| Upload ingestion | 20 requests / 60 minutes | Public; shared by Quick Answer and Deep Research uploads | /api/investigate/uploads |
All endpoints accept GET requests and return JSON. Click any endpoint to expand the response example.
/api/v1/statsReturns site-wide statistics including member count, trade count, lobbying filings, and the number of active federal contract leads (alert_count).
{
"api_version": "1.0",
"status": "success",
"data": {
"member_count": 538,
"trade_count": 15948,
"lobbying_filing_count": 116127,
"alert_count": 174,
"last_filing_date": "2026-02-24"
},
"meta": {
"timestamp": "2026-02-25T12:00:00Z"
},
"citation": {
"source": "CapitolExposed",
"url": "https://www.capitolexposed.com",
"license": "Public Records — Attribution Requested",
"accessed_at": "2026-02-25T12:00:00Z"
}
}/api/v1/membersList all members of Congress with basic profile information, party affiliation, chamber, and state.
| Name | Type | Required | Description |
|---|---|---|---|
chamber | string | optional | Filter by chamber: "house" or "senate" |
party | string | optional | Filter by party: "D", "R", or "I" |
state | string | optional | Filter by two-letter state code (e.g. CA, TX, NY) |
page | integer | optional | Page number (default: 1) |
per_page | integer | optional | Results per page, max 100 (default: 50) |
{
"api_version": "1.0",
"status": "success",
"data": [
{
"id": "m-001",
"slug": "nancy-pelosi",
"name": "Nancy Pelosi",
"party": "D",
"chamber": "house",
"state": "CA",
"district": 11,
"in_office": true,
"trade_count": 48
}
],
"meta": {
"total": 538,
"page": 1,
"per_page": 50,
"has_more": true,
"timestamp": "2026-02-25T12:00:00Z"
}
}/api/v1/members/{slug}Retrieve a single member profile: name, party, state, chamber, contact details, term dates and the number of disclosed trades.
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | required | URL-safe member identifier (e.g. nancy-pelosi) |
{
"api_version": "1.0",
"status": "success",
"data": {
"id": "m-001",
"slug": "nancy-pelosi",
"name": "Nancy Pelosi",
"party": "D",
"chamber": "house",
"state": "CA",
"district": 11,
"in_office": true,
"term_start": "2025-01-03",
"website": "https://pelosi.house.gov",
"trade_count": 48
}
}/api/v1/members/{slug}/tradesOne member's STOCK Act trades, newest transaction first. Each row has exactly the shape of a /api/v1/trades row: amount_min and amount_max are integer dollars, dates are YYYY-MM-DD, and a missing disclosure date is null.
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | required | URL-safe member identifier |
type | string | optional | Trade type: "purchase", "sale", "exchange" or "receive" |
ticker | string | optional | Filter by stock ticker symbol (e.g. AAPL, NVDA); case does not matter |
page | integer | optional | Page number (default: 1) |
per_page | integer | optional | Results per page, max 100 (default: 20) |
{
"api_version": "1.0",
"status": "success",
"data": [
{
"id": "tr-house-20035499-1",
"member_id": "m-S000250",
"member_name": "Pete Sessions",
"member_slug": "pete-sessions",
"ticker": "AAPL",
"asset_description": "Apple Inc. - Common Stock",
"asset_type": "Stock",
"transaction_type": "sale",
"transaction_date": "2026-09-22",
"disclosure_date": "2026-09-22",
"amount_min": 100001,
"amount_max": 250000,
"owner": "spouse",
"source": "house_clerk",
"source_url": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2026/20035499.pdf",
"filing_url": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2026/20035499.pdf",
"parser_confidence": 0.72,
"parser_version": "regex-v1",
"ingested_at": "2026-09-23T13:03:40.967764Z"
}
],
"meta": { "total": 48, "page": 1, "per_page": 20, "has_more": true }
}/api/v1/members/{slug}/votesRetrieve a member's roll-call votes, newest first, with the bill each vote was on.
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | required | URL-safe member identifier |
session | string | optional | Congressional session (e.g. 119) |
sector | string | optional | Only votes on bills tagged with this subject area (the bill's own tags) |
{
"api_version": "1.0",
"status": "success",
"data": [
{
"vote_id": "v-119-42",
"bill": "H.R. 4521",
"bill_title": "CHIPS and Science Act Extension",
"vote_date": "2026-01-28",
"vote_cast": "yea"
}
],
"meta": { "total": 12, "page": 1, "per_page": 50 }
}/api/v1/members/{slug}/donorsTop contributors of record for a member, aggregated from FEC receipts. The contributor of record is whoever the campaign committee reported, so a member's own joint fundraising committee and conduits such as ActBlue and WinRed appear here alongside PACs and individuals.
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | required | URL-safe member identifier |
cycle | string | optional | Two-year FEC cycle by its ending year, e.g. 2024 covers Jan 1 2023 to Dec 31 2024 |
min_amount | integer | optional | Minimum total from that contributor, in dollars, after aggregation |
{
"api_version": "1.0",
"status": "success",
"data": [
{
"donor_name": "AMERICAN DENTAL ASSOCIATION PAC",
"donor_type": "pac",
"employer": null,
"total_amount_cents": 1000000,
"total_amount_usd": 10000,
"donation_count": 2,
"first_donation_date": "2023-09-29",
"last_donation_date": "2024-11-05"
}
],
"meta": { "total": 82, "page": 1, "per_page": 50 }
}/api/v1/tradesList Congressional stock trades across all members. By default newest transaction first, paged with page/per_page. With sort=ingested it becomes the reconciliation feed behind the webhooks: oldest ingestion first, in a stable order, paged with an opaque cursor. A trade corrected or withdrawn after it was ingested does not reappear here; /api/v1/trades/changes lists those. amount_min and amount_max are dollars. Rows with parser_confidence at or below 0.7, or read off a scanned or handwritten filing (parser_version containing "vision", or source senate_ocr), are machine reading: check the filing before relying on one.
| Name | Type | Required | Description |
|---|---|---|---|
member | string | optional | Filter by member slug (e.g. pete-sessions) |
member_id | string | optional | Filter by member id (e.g. m-S000250) |
ticker | string | optional | Filter by stock ticker symbol |
type | string | optional | Trade type: "purchase", "sale", "exchange" or "receive" |
min_score | removed | optional | Removed on 2026-10-03, with min_conflict. Sending either returns 400: "min_score was removed on 2026-10-03: CapitolExposed no longer publishes a conflict score." |
ingested_since | ISO 8601 | optional | Only rows written after this instant, e.g. 2026-10-01T13:00:00Z. No offset means UTC; microseconds are honoured. |
disclosed_since | date | optional | Only rows disclosed on or after this date (YYYY-MM-DD); the ingestion date stands in where the disclosure date is missing |
sort | string | optional | "ingested" for oldest ingestion first, cursor-paged. Omit for newest transaction first. |
cursor | string | optional | meta.next_cursor from the previous sort=ingested page. Implies sort=ingested; page is ignored. |
page | integer | optional | Page number (default: 1) |
per_page | integer | optional | Results per page (default: 20). Max 100, or 500 with sort=ingested and a paid plan's API key. |
GET /api/v1/trades?sort=ingested&ingested_since=2026-09-23T00:00:00Z&per_page=100
{
"api_version": "1.0",
"status": "success",
"data": [
{
"id": "tr-house-20035499-1",
"member_id": "m-S000250",
"member_name": "Pete Sessions",
"member_slug": "pete-sessions",
"ticker": "AAPL",
"asset_description": "Apple Inc. - Common Stock",
"asset_type": "Stock",
"transaction_type": "sale",
"transaction_date": "2026-09-22",
"disclosure_date": "2026-09-22",
"amount_min": 100001,
"amount_max": 250000,
"owner": "spouse",
"source": "house_clerk",
"source_url": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2026/20035499.pdf",
"filing_url": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2026/20035499.pdf",
"parser_confidence": 0.72,
"parser_version": "regex-v1",
"ingested_at": "2026-09-23T13:03:40.967764Z"
}
],
"meta": {
"total": 412,
"page": 1,
"per_page": 100,
"has_more": true,
"next_cursor": "eyJ2IjoxLCJ0IjoiMjAyNi0wOS0yM1QxMzowMzo0MC45Njc3NjRaIiwiaSI6InRyLWhvdXNlLTIwMDM1NDk5LTEifQ",
"timestamp": "2026-10-01T14:00:00.000Z"
}
}/api/v1/trades/changesEvery change to a trade after it was published, oldest first: the polling twin of the trade.corrected and trade.withdrawn webhooks. Apply them in order: withdrawn means delete the trade by trade_id; corrected means overwrite it with current. A re-attribution to another member shows as a correction of member_id. The cursor only moves past changes whose transaction has committed, so a long correction that commits after you poll still appears on your next poll. previous is filled for plans with the data feed (Institutional and above) and null otherwise. Kept for 30 days.
| Name | Type | Required | Description |
|---|---|---|---|
since | ISO 8601 | optional | Where to start when you have no cursor: changes made after this instant. Ignored once you pass a cursor. |
cursor | string | optional | meta.next_cursor from the previous response. Every response has one, the last page included: store it and poll from it. |
type | string | optional | "corrected" or "withdrawn" |
per_page | integer | optional | Results per page (default: 20). Max 100, or 500 with a paid plan's API key. |
GET /api/v1/trades/changes?cursor=eyJ2IjoxLCJ4IjoiOTk2Nzk4IiwiaSI6IjAifQ&per_page=100
{
"api_version": "1.0",
"status": "success",
"data": [
{
"id": "1842",
"trade_id": "tr-senate-2b9e51c0-4",
"change_type": "corrected",
"changed_fields": ["member_id"],
"changed_at": "2026-10-02T14:30:11.950213Z",
"previous": {
"member_id": "m-D000001",
"member_name": "Jane Doe",
"member_slug": "jane-doe",
"ticker": "MSFT",
"asset_description": "Microsoft Corp",
"asset_type": "Stock",
"transaction_type": "purchase",
"transaction_date": "2026-08-14",
"disclosure_date": "2026-09-10",
"amount_min": 1001,
"amount_max": 15000,
"owner": "self",
"source_url": "https://efdsearch.senate.gov/search/view/ptr/2b9e51c0/",
"filing_url": "https://efdsearch.senate.gov/search/view/ptr/2b9e51c0/"
},
"current": {
"member_id": "m-R000002",
"member_name": "John Roe",
"member_slug": "john-roe",
"ticker": "MSFT",
"asset_description": "Microsoft Corp",
"asset_type": "Stock",
"transaction_type": "purchase",
"transaction_date": "2026-08-14",
"disclosure_date": "2026-09-10",
"amount_min": 1001,
"amount_max": 15000,
"owner": "self",
"source_url": "https://efdsearch.senate.gov/search/view/ptr/2b9e51c0/",
"filing_url": "https://efdsearch.senate.gov/search/view/ptr/2b9e51c0/"
}
},
{
"id": "1843",
"trade_id": "tr-house-20099999-2",
"change_type": "withdrawn",
"changed_fields": [],
"changed_at": "2026-10-02T15:02:40.118842Z",
"previous": { "member_id": "m-D000003", "ticker": "PAYC", "...": "..." },
"current": null
}
],
"meta": {
"per_page": 100,
"has_more": false,
"next_cursor": "eyJ2IjoxLCJ4IjoiOTk3MDQxIiwiaSI6IjAifQ",
"timestamp": "2026-10-02T15:05:00.000Z"
}
}/api/v1/alertsFederal contract leads, newest first: a member's reported trade in a company, a USAspending contract with that company acted on within 90 days of the trade, and the member's seat on a committee that oversees or funds the acting agency. Leads built from public records, not findings of wrongdoing. The conflict-score alert types were retired on 2026-10-03 and are not served; asking for one with type= is a 400.
| Name | Type | Required | Description |
|---|---|---|---|
severity | string | optional | Filter by severity: "high", "medium" or "low" |
member | string | optional | Filter by member slug |
status | string | optional | "active" (default) or "dismissed": leads withdrawn after publication, listed by id, member and dates only so a mirror can delete them |
type | string | optional | "contract_conflict", the only type served. committee_trade, trade_before_vote and volume_anomaly were retired on 2026-10-03 and return 400. |
page | integer | optional | Page number (default: 1) |
{
"api_version": "1.0",
"status": "success",
"data": [
{
"id": "alert-contract-5f0c2a9b7d3e41a6b8c9d0e1",
"alert_type": "contract_conflict",
"alert_type_label": "Federal Contract Lead",
"member_id": "m-X000001",
"member_name": "Jane Doe",
"member_slug": "jane-doe",
"status": "active",
"severity": "medium",
"title": "Bought LMT 12 days before the Defense Department acted on a $1.2B contract",
"summary": "Jane Doe (R-XX) bought $15,001 - $50,000 of Lockheed Martin Corp (LMT) on ... The records do not show who placed the trade or that anyone involved knew of the contract.",
"confidence": 0.65,
"evidence": [
{ "type": "trade", "entityId": "tr-house-20030001-3", "...": "..." },
{ "type": "contract", "entityId": "HQ0147...", "amount": 1200000000, "actionDate": "2025-06-14", "...": "..." },
{ "type": "committee", "committeeCode": "HSAS", "basis": "oversight", "...": "..." },
{ "type": "company_match", "ticker": "LMT", "...": "..." }
],
"trade_ids": ["tr-house-20030001-3"],
"created_at": "2025-06-14T12:00:00Z",
"updated_at": "2026-10-03T11:02:00Z"
}
],
"meta": { "total": 174, "page": 1, "per_page": 20, "has_more": true }
}/api/v1/graphSitting members with at least one disclosed trade, the tickers they traded and the committees they sit on, as nodes and edges for D3.js or a similar library. There are two kinds of edge, each a record: traded (member to ticker, from STOCK Act reports; weight is the number of trades and metadata.total_amount the sum of the top of each disclosed range, in dollars) and sits_on (member to committee, from the roster; weight 3 for a chair, otherwise 1). min_conflict was retired on 2026-10-03 and returns 400.
| Name | Type | Required | Description |
|---|---|---|---|
chamber | string | optional | "house" or "senate" |
party | string | optional | "D", "R" or "I" |
limit | integer | optional | Members to include, most trades first (default: 200, max: 500) |
{
"api_version": "1.0",
"status": "success",
"data": {
"nodes": [
{ "id": "m-X000001", "type": "member", "name": "Jane Doe", "party": "R", "size": 6, "metadata": { "state": "XX", "chamber": "house", "trade_count": 12 } },
{ "id": "c-LMT", "type": "company", "name": "LMT", "size": 5 },
{ "id": "HSAS", "type": "committee", "name": "Committee on Armed Services", "size": 8 }
],
"edges": [
{ "source": "m-X000001", "target": "c-LMT", "type": "traded", "weight": 2, "metadata": { "trade_count": 2, "total_amount": 65000 } },
{ "source": "m-X000001", "target": "HSAS", "type": "sits_on", "weight": 1, "metadata": { "role": "member" } }
]
},
"meta": { "total": 3 }
}/api/v1/searchSearch across members, trades, bills, lobbying filings, donations and filing documents. Supports lexical, semantic, and hybrid retrieval. Cross-reference documents, which paired a member with an offshore or FARA record by a shared name, were retired on 2026-10-03 and are not searched.
| Name | Type | Required | Description |
|---|---|---|---|
q | string | required | Search query (e.g. 'Pelosi NVIDIA', 'defense stocks') |
type | string | optional | Restrict to entity type: "member", "trade", "bill", "lobbying", "donation" or "document". "cross_reference" was retired on 2026-10-03 and returns 400. |
mode | string | optional | Search mode: "fts", "semantic", or "hybrid". semantic and hybrid fall back to fts when embeddings are unavailable. |
limit | integer | optional | Max results to return (default: 20, max: 100) |
{
"api_version": "1.0",
"status": "success",
"data": {
"results": [
{
"id": "m-W000816",
"type": "member",
"title": "Roger Williams",
"snippet": "<mark>Roger</mark> <mark>Williams</mark> - R TX",
"score": 0.991,
"url": "/members/roger-williams",
"extra": {
"party": "R",
"state": "TX",
"chamber": "house",
"in_office": true
}
}
],
"facets": {
"member": 1
}
},
"meta": { "total": 1, "query": "roger williams", "mode": "fts" }
}/api/v1/export/tradesBulk export of Congressional stock trades, newest disclosure first, as CSV or JSON. The window is on the disclosure date (the ingestion date where a filing gives none), not the transaction date: Free and anonymous 30 days, Pro 5 years, Institutional and Enterprise everything on file. Your plan comes from X-API-Key or, without one, your signed-in session. Columns: id, member_name, party, state, chamber, ticker, asset_description, transaction_type, transaction_date, disclosure_date, amount_min, amount_max (dollars), owner, member_slug, source_url, parser_confidence. More than 250,000 rows is a 413; narrow it with days or member.
| Name | Type | Required | Description |
|---|---|---|---|
format | string | optional | "csv" or "json" (default: json) |
days | integer | "all" | optional | Days back from today (default: 30). "all" is your plan's whole window. A longer request is cut to the plan's window and says so in X-Export-Limited-By-Plan. |
member | string | optional | A member slug, member id or Bioguide id. 404 if nothing matches. |
GET /api/v1/export/trades?format=json&days=all&member=pete-sessions
X-Export-Plan: enterprise
X-Export-Window-Days: all
X-Export-Limited-By-Plan: 0
X-Export-Rows: 214
{
"api_version": "1.0",
"status": "success",
"data": [ { "id": "tr-house-20035499-1", "member_name": "Pete Sessions", "...": "..." } ],
"meta": {
"days_covered": null,
"member": "pete-sessions",
"window": {
"plan": "enterprise", "plan_name": "Institutional", "days": null, "full_history": true,
"cap_days": null, "limited_by_plan": false,
"basis": "disclosure_date (falls back to the date ingested)"
},
"units": { "amount_min": "USD", "amount_max": "USD" },
"total": 214
}
}/api/v1/export/membersEvery member of Congress with trade_count and latest_trade. Free at every plan: the roster is public.
| Name | Type | Required | Description |
|---|---|---|---|
format | string | optional | "csv" or "json" (default: json) |
member | string | optional | One member, by slug, member id or Bioguide id. 404 if nothing matches. |
CSV download or JSON envelope with member records
/api/v1/export/lobbyingSenate LDA lobbying filings, newest filing first, windowed on the filing date: Free 90 days, Pro 5 years, Institutional and Enterprise everything on file. amount is in CENTS, as stored; amount_usd is the same figure in dollars. Filings name registrants and clients, not members, so member= is a 400. More than 250,000 rows is a 413.
| Name | Type | Required | Description |
|---|---|---|---|
format | string | optional | "csv" or "json" (default: json) |
days | integer | "all" | optional | Days back from today (default: 90). "all" is your plan's whole window. |
CSV download or JSON envelope; meta.units is { "amount": "cents", "amount_usd": "USD" }/api/v1/export/billsBills with lobbying_filing_count: the LD-2 reports that cite the bill by number and cover a quarter of its Congress (a report names a bill as hr-1234 with no Congress, so one on another Congress, which concerns a different bill with the same number, is not counted). Free: the current Congress only (any other session is a 403). Pro: any Congress, introduced in the last 5 years. Institutional and Enterprise: any Congress, no date limit. More than 50,000 rows is a 413.
| Name | Type | Required | Description |
|---|---|---|---|
format | string | optional | "csv" or "json" (default: json) |
session | integer | optional | Congress number, e.g. 119. Pro and above for anything but the current one. |
member | string | optional | Bills that member sponsored: slug, member id or Bioguide id. 404 if nothing matches. |
CSV download or JSON envelope with bill records
/api/v1/export/votesRoll-call votes, newest vote first, windowed on the vote date: Free 90 days, Pro 5 years, Institutional and Enterprise everything on file. More than 500,000 rows is a 413.
| Name | Type | Required | Description |
|---|---|---|---|
format | string | optional | "csv" or "json" (default: json) |
days | integer | "all" | optional | Days back from today (default: 90). "all" is your plan's whole window. |
member | string | optional | A member slug, member id or Bioguide id. 404 if nothing matches. |
CSV download or JSON envelope with vote records
/api/v1/export/accessWhat the caller may export: the plan (from X-API-Key, else the signed-in session), each export's window in days (null means everything on file) and whether newsroom bundles are included. A key that does not resolve to a live subscription is a 403.
{
"api_version": "1.0",
"status": "success",
"data": {
"plan": "pro",
"plan_name": "Pro",
"source": "api_key",
"windows": {
"basis": { "trades": "disclosure_date", "lobbying": "filing_date", "votes": "vote_date" },
"trades": { "days": 1825 },
"lobbying": { "days": 1825 },
"votes": { "days": 1825 },
"bills": { "congresses": "any", "days": 1825 }
},
"newsroom_bundle": false
}
}/api/v1/export/bundle/member/{slug}The newsroom bundle for one member, as a ZIP: README.md, trades.csv, donations.csv and lobbying.csv (LD-2 filings that cite a bill the member sponsored) where the member has them, committees.csv and citations.csv. Institutional and Enterprise, by API key or signed-in session; 401 anonymously, 403 on other plans. slug also accepts a member id or Bioguide id.
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | required | Member slug, member id or Bioguide id |
application/zip, Content-Disposition: attachment
/api/v1/export/bundle/thread/{threadId}One of your own Deep Research notebooks as a ZIP: README.md, transcript.md, artifacts/*.json and citations.csv. Signed-in session only (threads belong to an account, so an API key alone does not reach them), Institutional and Enterprise.
| Name | Type | Required | Description |
|---|---|---|---|
threadId | string | required | The notebook's id |
application/zip, Content-Disposition: attachment
/api/v1/top-tradersReturns members of Congress ranked by number of disclosed trades, with the date of the latest.
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | optional | Max results (default: 50, max: 100) |
chamber | string | optional | "senate" or "house" |
{
"data": [
{
"name": "David Perdue",
"slug": "david-perdue",
"party": "R",
"state": "GA",
"chamber": "senate",
"trade_count": 3121,
"latest_trade": "2020-12-30"
}
]
}/api/v1/lobbying-contributionsReturns LD-203 lobbying contribution records from the official Lobbying Disclosure Act database. Shows donations from registered lobbyists and PACs to members of Congress.
| Name | Type | Required | Description |
|---|---|---|---|
recipient | string | optional | Filter by recipient name (partial match) |
registrant | string | optional | Filter by lobbying firm name |
year | integer | optional | Filing year (e.g. 2025) |
page | integer | optional | Page number (default: 1) |
per_page | integer | optional | Results per page (default: 25, max: 100) |
{
"data": [
{
"id": "lda-c-abc123-0",
"registrant_name": "HOLLAND & KNIGHT LLP",
"lobbyist_name": "JOHN DOE",
"recipient_name": "Friends of Senator Smith",
"contribution_type": "FECA",
"amount": 5000,
"filing_year": 2025,
"filing_date": "2025-07-15"
}
]
}/api/oembedStandard oEmbed 1.0 endpoint for auto-embedding CapitolExposed member and stock pages in WordPress, Medium, Ghost, and other CMS platforms.
| Name | Type | Required | Description |
|---|---|---|---|
url | string | required | CapitolExposed URL to embed (e.g. https://www.capitolexposed.com/members/nancy-pelosi) |
format | string | optional | "json" (only format supported) |
maxwidth | integer | optional | Maximum embed width in pixels (default: 400) |
maxheight | integer | optional | Maximum embed height in pixels (default: 500) |
{
"version": "1.0",
"type": "rich",
"provider_name": "CapitolExposed",
"title": "Nancy Pelosi - Congressional Trading Profile",
"width": 400,
"height": 350,
"html": "<iframe src=\"https://www.capitolexposed.com/embed/nancy-pelosi\" ...></iframe>"
}Every route under /api/v1/export reads your plan from X-API-Key or, without one, from your signed-in session, so the Export buttons on the site and a script with your key get the same file. A key that does not resolve to a live subscription is a 403, not a quietly shorter file. Anonymous callers get the Free window.
days asks for that many days back; days=all asks for your plan's whole window, and anything longer than the plan allows is cut to it. Each response says what you got in X-Export-Plan, X-Export-Window-Days, X-Export-Limited-By-Plan and X-Export-Rows, and in meta.window on JSON. member takes a slug, member id or Bioguide id and is a 404 when nothing matches. An export over its row ceiling is a 413 asking you to narrow it. GET /api/v1/export/access tells you your windows without downloading anything.
| Export | Window on | Free and anonymous | Pro | Institutional, Enterprise | member= | Row ceiling |
|---|---|---|---|---|---|---|
trades | disclosure date (ingestion date if none) | 30 days | 5 years | everything | yes | 250,000 |
lobbying | filing date | 90 days | 5 years | everything | 400: filings name no member | 250,000 |
votes | vote date | 90 days | 5 years | everything | yes | 500,000 |
bills | Congress (session=) | current Congress | any Congress, introduced in 5 years | any Congress | bills sponsored | 50,000 |
members | none | everything | everything | everything | yes | none |
bundle/member/{slug} | none | no | no | ZIP of the member's records | path | none |
Trade amounts are dollars. Lobbying amount is cents, as stored, with amount_usd beside it in dollars. Trade exports carry member_slug, source_url and parser_confidence: a row at or below 0.7 is unverified machine reading.
On the Institutional and Enterprise plans, CapitolExposed POSTs every newly ingested trade to your HTTPS endpoint as a signed trade.new event, up to 100 trades per event in ingestion order, and tells you when a trade it already sent is later corrected or withdrawn, so a mirror of the feed stays in step with the site. Register endpoints at /account/webhooks or with your API key (below). Delivery is at least once: dedupe on the event id, treat trade.withdrawn as a delete by trade id and trade.new and trade.corrected as an upsert by trade id.
Public HTTPS only. Redirects are not followed, so register the final URL.
Verify the signature on the raw body, store or enqueue the event, then answer any 2xx within 10 seconds. Do the work afterwards.
A non-2xx, a timeout, a connection error or a redirect is a failure, retried after about 1 min, 5 min, 30 min, 2 h, 6 h, 24 h: 7 attempts over roughly 33 hours, then given up. Every retry carries the same event id and a byte-identical body, with a fresh timestamp and signature.
An endpoint is disabled after 10 consecutive failed attempts spanning at least 24 hours. Disabling drops its pending retries.
A retry can arrive after a newer event. Order by ingested_at and changed_at, not by arrival: an event about a trade you already hold a later timestamp for is stale.
Subscribe each endpoint to any of these; a new endpoint gets all of them unless you choose. You can change an endpoint's events at any time; the change applies to events created from then on.
| type | Sent when | What to do |
|---|---|---|
trade.new | Trades were ingested. Up to 100 per event, in ingestion order. | Upsert each trade by id. |
trade.corrected | A trade already published changed in a field you can see: re-attributed to another member, re-read from its filing, amended. Several changes to one trade are folded into one entry. | Upsert current by id. previous and changed_fields say what changed. |
trade.withdrawn | A trade already published was removed: quarantined after an audit of its source filing, or a duplicate of another row. | Delete the trade by id. |
alert.contract_lead | A new contract-conflict lead: a member traded a company whose federal contract an agency acted on near the trade, where the member sits on a committee that oversees or funds that agency. One lead per event, sent once, when it is first found. | A lead to report out, not a finding. See below. |
ping | Only when you ask for a test event. | Answer 2xx. |
The envelope is the same for every event type; api_version changes only if the shape does. A test event has "type": "ping" and no trades.
{
"id": "evt_5f0c2a9b7d3e41a6b8c9d0e1",
"type": "trade.new",
"api_version": "2026-10-03",
"created_at": "2026-09-23T13:03:42.118Z",
"data": {
"trades": [
{
"id": "tr-house-20035499-1",
"ticker": "AAPL",
"asset_description": "Apple Inc. - Common Stock",
"asset_type": "Stock",
"transaction_type": "sale",
"transaction_date": "2026-09-22",
"disclosure_date": "2026-09-22",
"amount_min": 100001,
"amount_max": 250000,
"owner": "spouse",
"filing_url": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2026/20035499.pdf",
"source": "house_clerk",
"parser_confidence": 0.72,
"parser_version": "regex-v1",
"ingested_at": "2026-09-23T13:03:40.967764Z",
"late_ingest": false,
"url": "https://www.capitolexposed.com/members/pete-sessions",
"member": {
"id": "m-S000250",
"bioguide_id": "S000250",
"name": "Pete Sessions",
"party": "R",
"state": "TX",
"chamber": "house",
"slug": "pete-sessions",
"url": "https://www.capitolexposed.com/members/pete-sessions"
}
}
]
}
}| Trade field | Meaning |
|---|---|
id | Trade id. Upsert on it: a trade can arrive more than once. |
transaction_date, disclosure_date | YYYY-MM-DD. disclosure_date is when the filing became public; null if the source gives none. |
amount_min, amount_max | Integers, in DOLLARS (not cents): the STOCK Act range the filer ticked. |
owner | self, spouse, joint or child. |
filing_url | The source filing on the House Clerk or Senate eFD site. |
source | Which ingest wrote the row: house_clerk, senate_efd, senate_ocr and so on. |
parser_confidence, parser_version | How the row was read. At or below 0.7, or a parser_version containing "vision" (a scanned or handwritten filing), or source senate_ocr: machine reading, so check the filing before relying on it. |
ingested_at | When the row was written, UTC, microsecond precision. The order sort=ingested pages in. |
late_ingest | True when the disclosure is more than 14 days older than ingestion: a backfilled or re-parsed old filing rather than a fresh disclosure. Still delivered, so a mirror stays complete. |
url, member.url | The member's page on CapitolExposed. |
A trade is announced again only if it changes in a field you can see, after you could have been told about it. A change to a trade still waiting for its trade.new sends nothing: the trade.new carries the current values. A trade removed before it was ever announced sends nothing either. Several changes to one trade between two deliveries arrive as one entry, comparing the trade as you were last told about it with the trade now, so a change that was made and then undone sends nothing at all.
Re-reading that produces identical values, and other bookkeeping, are not changes. A re-attribution, where a trade filed under the wrong member is moved to the right one, is a trade.corrected with member_id in changed_fields. Changes reach you within about a minute.
{
"id": "evt_9c41d2e07b6a4f13a8e5c2d1",
"type": "trade.corrected",
"api_version": "2026-10-03",
"created_at": "2026-10-02T14:31:05.402Z",
"data": {
"trades": [
{
"id": "tr-senate-2b9e51c0-4",
"changed_fields": ["member_id"],
"changed_at": "2026-10-02T14:30:11.950Z",
"previous": {
"member_id": "m-D000001",
"member": {
"id": "m-D000001", "bioguide_id": "D000001", "name": "Jane Doe", "party": "D",
"state": "CA", "chamber": "senate", "slug": "jane-doe",
"url": "https://www.capitolexposed.com/members/jane-doe"
},
"ticker": "MSFT",
"asset_description": "Microsoft Corp",
"asset_type": "Stock",
"transaction_type": "purchase",
"transaction_date": "2026-08-14",
"disclosure_date": "2026-09-10",
"amount_min": 1001,
"amount_max": 15000,
"owner": "self",
"filing_url": "https://efdsearch.senate.gov/search/view/ptr/2b9e51c0/"
},
"current": {
"id": "tr-senate-2b9e51c0-4",
"ticker": "MSFT",
"asset_description": "Microsoft Corp",
"...": "every trade.new field, as the trade stands now",
"member": {
"id": "m-R000002", "bioguide_id": "R000002", "name": "John Roe", "party": "R",
"state": "TX", "chamber": "senate", "slug": "john-roe",
"url": "https://www.capitolexposed.com/members/john-roe"
}
}
}
]
}
}{
"id": "evt_0b7f6a1c9e2d4b58a3c6d7e8",
"type": "trade.withdrawn",
"api_version": "2026-10-03",
"created_at": "2026-10-02T15:03:00.811Z",
"data": {
"trades": [
{
"id": "tr-house-20099999-2",
"changed_at": "2026-10-02T15:02:40.118Z",
"reason": null,
"previous": {
"member_id": "m-D000003",
"member": { "id": "m-D000003", "name": "Sam Doe", "slug": "sam-doe", "...": "..." },
"ticker": "PAYC",
"asset_description": "Paycom Software Inc",
"transaction_type": "purchase",
"amount_min": 15001,
"amount_max": 50000,
"...": "the same snapshot fields as trade.corrected"
}
}
]
}
}| Entry field | Meaning |
|---|---|
changed_fields | The fields that differ between previous and current, from: member_id, ticker, asset_description, asset_type, transaction_type, transaction_date, disclosure_date, amount_min, amount_max, owner, filing_url. member_id means the trade now belongs to a different member (current.member). |
previous | The published fields before the change: the trade as you were last told about it. In trade.withdrawn, the trade as it was removed. member is that member's record today, or null if it is no longer on file. |
current | trade.corrected only. The whole trade as it stands now, in exactly the trade.new shape: upsert it. |
changed_at | When the latest change in the entry was made. Retries can deliver an older event after a newer one: if you already hold a later changed_at (or ingested_at) for that trade, the event is stale. |
reason | trade.withdrawn only. Reserved; null today. |
A contract lead says that three public records line up: a member's STOCK Act report of a trade in a company, a federal contract with that company that an agency acted on within 90 days of the trade, and the member's seat on a committee that oversees that agency (basis: "oversight") or drafts its spending bill (basis: "funding"). These are leads, not findings. The records do not show who placed the trade, or that anyone involved knew of the contract, and the summary says so; report them out before publishing anything on the strength of one.
Read the award figures carefully. amount is the contract's total value in dollars, and action_date is the latest action USAspending listed for that contract when it was pulled on as_of: an option, a modification or the award itself. It is not a new award of that amount on that date; the contract may be years older. days_from_trade is action_date minus the nearest trade date.
Each lead is sent once, when it is first found. A lead that later gains an award or is re-scored is updated on its url page and is not re-sent. Included with Institutional and Enterprise.
{
"id": "evt_4e2a9d0c7b1f43e6a85d9c02",
"type": "alert.contract_lead",
"api_version": "2026-10-03",
"created_at": "2026-10-02T11:04:19.553Z",
"data": {
"alert": {
"id": "alert-contract-5f0c2a9b7d3e41a6b8c9d0e1",
"type": "contract_conflict",
"severity": "medium",
"title": "Bought EXDF 12 days before the Defense Department acted on a $48.0M contract",
"summary": "Jane Doe (D-CA) bought $15,001 - $50,000 of Example Defense Corp ... The records do not show who placed the trade or that anyone involved knew of the contract.",
"url": "https://www.capitolexposed.com/alerts/alert-contract-5f0c2a9b7d3e41a6b8c9d0e1",
"lead_date": "2026-09-30"
},
"member": { "id": "m-D000001", "name": "Jane Doe", "party": "D", "state": "CA" },
"company": { "id": "co-exdf", "ticker": "EXDF", "name": "Example Defense Corp" },
"trades": [
{
"id": "tr-house-20099998-1",
"transaction_type": "purchase",
"transaction_date": "2026-09-03",
"disclosure_date": "2026-09-30",
"amount_min": 15001,
"amount_max": 50000,
"owner": "spouse",
"source_url": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2026/20099998.pdf"
}
],
"awards": [
{
"award_id": "N0002426C0001",
"generated_internal_id": "CONT_AWD_N0002426C0001_9700_-NONE-_-NONE-",
"action_date": "2026-09-15",
"amount": 48000000,
"award_type": "DEFINITIVE CONTRACT",
"awarding_agency": "Department of Defense",
"recipient_name": "EXAMPLE DEFENSE CORPORATION",
"parent_name": "EXAMPLE DEFENSE CORP",
"description": "SHIPBOARD RADAR SUSTAINMENT",
"usaspending_url": "https://www.usaspending.gov/award/CONT_AWD_N0002426C0001_9700_-NONE-_-NONE-",
"days_from_trade": 12,
"as_of": "2026-09-28",
"committees": [
{ "code": "HSAS", "name": "House Committee on Armed Services", "basis": "oversight" }
]
}
]
}
}Content-Type | application/json |
X-CapitolExposed-Event-Id | evt_... The body's id. Identical on every retry of the event: dedupe on it. |
X-CapitolExposed-Event | The body's type: trade.new, trade.corrected, trade.withdrawn, alert.contract_lead, or ping for a test event |
X-CapitolExposed-Timestamp | Unix seconds when this attempt was signed. Reject anything more than 5 minutes from your clock. |
X-CapitolExposed-Signature | v1=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>. During a secret rotation, one v1= value per live secret, comma separated: accept if any matches. |
X-CapitolExposed-Delivery-Attempt | 1 to 7 |
User-Agent | CapitolExposed-Webhooks/2.0 |
Compute HMAC-SHA256 with your endpoint's secret over the timestamp header, a period, and the raw request body exactly as received (not re-serialized JSON). Compare in constant time against each v1= value, and reject timestamps more than 5 minutes from your clock so a captured request cannot be replayed.
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
// rawBody: the request body exactly as received (Buffer or string), before
// any JSON parsing. headers: Node's lower-cased header object.
export function verifyCapitolExposedWebhook(rawBody, headers, secret) {
const timestamp = String(headers["x-capitolexposed-timestamp"] || "");
const signatures = String(headers["x-capitolexposed-signature"] || "")
.split(",")
.map((part) => part.trim())
.filter((part) => part.startsWith("v1="))
.map((part) => Buffer.from(part.slice(3), "hex"));
if (!/^\d+$/.test(timestamp) || signatures.length === 0) {
throw new Error("Missing signature headers");
}
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
throw new Error("Timestamp outside the 5 minute tolerance");
}
const expected = createHmac("sha256", secret)
.update(timestamp + ".")
.update(rawBody)
.digest();
const valid = signatures.some(
(given) => given.length === expected.length && timingSafeEqual(given, expected),
);
if (!valid) throw new Error("Signature mismatch");
return JSON.parse(rawBody.toString("utf8"));
}
// Express: keep the raw bytes for this route only.
// app.post("/hooks/capitolexposed", express.raw({ type: "application/json" }), (req, res) => {
// const event = verifyCapitolExposedWebhook(req.body, req.headers, process.env.CE_WEBHOOK_SECRET);
// // Dedupe on event.id, enqueue the work, answer fast:
// res.sendStatus(200);
// });
import hashlib
import hmac
import json
import time
TOLERANCE_SECONDS = 300
def verify_capitolexposed_webhook(raw_body: bytes, headers, secret: str) -> dict:
"""raw_body: the request body bytes exactly as received.
headers: a case-insensitive mapping, e.g. Flask's request.headers."""
timestamp = headers.get("X-CapitolExposed-Timestamp", "")
signatures = [
part.strip()[3:].encode()
for part in headers.get("X-CapitolExposed-Signature", "").split(",")
if part.strip().startswith("v1=")
]
if not timestamp.isdigit() or not signatures:
raise ValueError("Missing signature headers")
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
raise ValueError("Timestamp outside the 5 minute tolerance")
expected = hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest().encode()
if not any(hmac.compare_digest(expected, given) for given in signatures):
raise ValueError("Signature mismatch")
return json.loads(raw_body)
# Flask:
# @app.post("/hooks/capitolexposed")
# def capitolexposed_hook():
# event = verify_capitolexposed_webhook(
# request.get_data(), request.headers, os.environ["CE_WEBHOOK_SECRET"])
# # Dedupe on event["id"], enqueue the work, answer fast:
# return "", 200
Rotating a secret keeps the old one signing alongside the new one for 24 hours, so you can deploy the new secret without dropping deliveries. End the overlap early from the account page or with {"expire_previous_secret": true}.
Webhooks get a trade to you seconds after ingestion. They cannot guarantee you never miss one: your endpoint can be down past the last retry, or disabled. So pair them with GET /api/v1/trades?sort=ingested&ingested_since=…, which returns every row written after an instant, oldest first, in a stable order with a cursor.
next_cursor while has_more is true. A paid key can ask for 500 rows a page.GET /api/v1/trades/changes alongside it: start with since=, then store the next_cursor every response returns and poll from it. Apply each change in order, withdrawn as a delete and corrected as an overwrite with current. That cursor only passes changes whose transaction has committed, so it needs no overlap. The log keeps 30 days: a mirror that has been offline longer re-syncs from a full sort=ingested pass.import requests
API = "https://www.capitolexposed.com/api/v1"
HEADERS = {"X-API-Key": "cg_v1_your_key_here"}
def reconcile_new(since_iso: str, upsert) -> str:
"""Upsert every trade ingested after since_iso, oldest first.
Returns the newest ingested_at seen, your next high-water mark."""
params = {"sort": "ingested", "ingested_since": since_iso, "per_page": 500}
high_water = since_iso
while True:
body = requests.get(f"{API}/trades", params=params, headers=HEADERS, timeout=30).json()
for trade in body["data"]:
upsert(trade) # keyed on trade["id"]
high_water = trade["ingested_at"]
if not body["meta"]["has_more"]:
return high_water
params = {"cursor": body["meta"]["next_cursor"], "per_page": 500}
def reconcile_changes(cursor: str | None, update_fields, delete) -> str:
"""Apply every correction and withdrawal after cursor, in order.
Returns the cursor to store; the first call can pass since= instead."""
params = {"per_page": 500, **({"cursor": cursor} if cursor else {"since": "2026-10-01T00:00:00Z"})}
while True:
body = requests.get(f"{API}/trades/changes", params=params, headers=HEADERS, timeout=30).json()
for change in body["data"]:
if change["change_type"] == "withdrawn":
delete(change["trade_id"])
else:
update_fields(change["trade_id"], change["current"]) # overwrite those fields
cursor = body["meta"]["next_cursor"]
if not body["meta"]["has_more"]:
return cursor # store it; poll from it next time
params = {"cursor": cursor, "per_page": 500}
# Every 15 minutes, and on startup:
# high_water = reconcile_new(high_water_minus_one_hour, upsert)
# changes_cursor = reconcile_changes(changes_cursor, update_fields, delete)The same operations as the account page, authenticated with your API key in X-API-Key (or a signed-in session). The plan must include webhooks.
GET /api/webhooks/config | List endpoints (secrets are never returned) |
POST /api/webhooks/config | Register { url, events }; events defaults to all of them. Returns the secret once. Up to 10 per account |
PATCH /api/webhooks/config?id= | { active }, { events: [...] } (applies to events created from then on), { rotate_secret: true } or { expire_previous_secret: true } |
DELETE /api/webhooks/config?id= | Delete an endpoint and its delivery history |
POST /api/webhooks/test | { endpoint_id }: send a signed ping now and report the response |
GET /api/webhooks/logs | Recent attempts with status code, error and retry state (30 days) |
POST /api/webhooks/logs | { log_id }: re-send that attempt's event now, same event id |
# Register an endpoint (the response holds the secret, once)
curl -X POST https://www.capitolexposed.com/api/webhooks/config \
-H "X-API-Key: cg_v1_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/capitolexposed",
"events": ["trade.new", "trade.corrected", "trade.withdrawn"]}'
# Change what an existing endpoint receives
curl -X PATCH "https://www.capitolexposed.com/api/webhooks/config?id=wh_..." \
-H "X-API-Key: cg_v1_your_key_here" \
-H "Content-Type: application/json" \
-d '{"events": ["trade.new", "trade.corrected", "trade.withdrawn", "alert.contract_lead"]}'
# Send it a signed test event
curl -X POST https://www.capitolexposed.com/api/webhooks/test \
-H "X-API-Key: cg_v1_your_key_here" \
-H "Content-Type: application/json" \
-d '{"endpoint_id": "wh_..."}'
# Rotate the secret (the old one keeps signing for 24 hours)
curl -X PATCH "https://www.capitolexposed.com/api/webhooks/config?id=wh_..." \
-H "X-API-Key: cg_v1_your_key_here" \
-H "Content-Type: application/json" \
-d '{"rotate_secret": true}'| Status | Meaning | Common Cause |
|---|---|---|
200 | OK | Request succeeded |
400 | Bad Request | Invalid query parameters or missing required fields |
401 | Unauthorized | Missing or invalid API key for a protected endpoint |
404 | Not Found | Member slug or resource ID does not exist |
429 | Too Many Requests | Rate limit exceeded. Check the Retry-After header. |
500 | Internal Server Error | Unexpected server error. Please retry or contact support. |
const res = await fetch(
"https://www.capitolexposed.com/api/v1/stats",
{
headers: {
"X-API-Key": "cg_v1_your_key_here"
}
}
);
const { data } = await res.json();
console.log(data.member_count);import requests
resp = requests.get(
"https://www.capitolexposed.com/api/v1/stats",
headers={"X-API-Key": "cg_v1_your_key_here"}
)
data = resp.json()["data"]
print(data["member_count"])curl -s \ -H "X-API-Key: cg_v1_your_key_here" \ https://www.capitolexposed.com/api/v1/stats \ | jq '.data'
library(httr2)
resp <- request("https://www.capitolexposed.com/api/v1/stats") |>
req_headers("X-API-Key" = "cg_v1_your_key_here") |>
req_perform() |>
resp_body_json()
resp$data$member_countPublic access includes route-specific limits surfaced in response headers. Upgrade plans unlock premium exports, embeds, and account-based API access.