REST API for corporate BTC, ETH & SOL treasuries
Same data that powers the dashboard. SEC filings parsed in minutes, holdings reconciled nightly against bitbo.io, JSON + CSV bulk export. Keep your dashboards, spreadsheets, and models fed.
Prefer to work in a tool? The full API description lives at /api/v1/openapi.json (OpenAPI 3.1). Import it straight into Postman, Insomnia, or Swagger to browse every endpoint and generate a client.
Quick start
Authenticate with the x-api-key header:
curl https://www.corpstacking.com/api/v1/stats \
-H "x-api-key: cs_live_YOUR_KEY"All JSON responses are wrapped in { data, meta } — your payload is under data, and meta carries a timestamp, a request id, and (on list endpoints) the paging counts. The bulk downloads (/export and /export/signals) are the exception: they return a raw CSV / JSON file, not the wrapper.
Base URL: https://www.corpstacking.com/api/v1 (version v1).
We only ever addfields within v1 — your code won't break when we ship new ones, so ignore any field you don't recognize. A few field names are kept only for older integrations and now apply to every asset, not just Bitcoin: btc_amount, price_per_btc, and btc_holdings. Prefer the asset-neutral fields (amount, price_per_unit, holdings) in new code.
Rate limits by plan
| Plan | API access | Req/min | Req/month |
|---|---|---|---|
| Free | — | — | — |
| Pro ($9/mo) | ✓ | 60 | 1,000 |
| Pro+ ($19/mo) | ✓ | 300 | 10,000 |
| API Pro ($499/mo) | ✓ | 2,000 | 500,000 |
| Team (Contact sales) | ✓ | Custom | Custom |
Every response tells you where you stand on both your per-minute rate limit and your monthly quota:
Per-minute: x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset.
Monthly quota: x-quota-limit, x-quota-remaining, x-quota-reset.
When you hit either limit you get a 429 with a Retry-After header (seconds to wait) — wait that long before retrying. Every response also carries an x-request-id; include it if you contact support about a specific call.
Pagination
List endpoints (/treasuries, /activity) page with limit and offset. The total row count and the limit/offset you used come back under meta, so you can step through pages.
The signals feed (/signals) pages by cursor instead: read next_cursor from the response and pass it as ?cursor= on the next call, repeating while has_more is true. Offset is ignored on the signals feed.
Every number ships with its source
The treasury endpoints don't just hand you a holdings figure — they tell you how we verified it and link straight to the document it came from, so you can show your users provenance and filter the list by trust.
source_tier— how the current holdings figure was sourced:primary(direct from an SEC filing, the fund sponsor, or an on-chain audit),secondary(cross-checked against industry trackers), orresearch(our own estimate where no public disclosure exists yet).source_citation_url— a click-through to the primary document backing the figure (the SEC filing, the sponsor's NAV page, etc.). Present for primary-tier figures; may be empty for figures that came through a data feed without a public page.data_confidence— how well independent references agree on the number:high(two or more agree within 5%),med(one agrees),low(a recent primary source but no second opinion yet),none(awaiting review), orunknown(not yet checked). On the single-entity endpoint this expands to a block with the matching sources and any analyst override.coverage_class— the kind of disclosure that backs this entity, with a plain-Englishcoverage_class_reason. Examples:onchain_audit(on-chain verified),verified_events_sec(every change backed by an SEC filing),etf_sponsor_tracked(sponsor-published),aggregator_only(industry-tracker snapshot), and more.rank— the entity's position on the leaderboard for the requested token (rows come sorted by holdings, largest first). Pair the holdings amount with a token'ssupply_capfrom/tokensto get its share of total supply.
Per-event rows (in /activity, the purchases array, and the bulk exports) carry their own citation: filing_url + sec_accession link the exact filing, filing_date is when it was filed, and summary is a one-line description. On-chain wallet addresses are kept private and are not exposed through the API.
Endpoints
/api/v1/statsProTotal holdings, current USD value, cost basis, unrealized profit/loss, and entity count across the tracked universe (companies, ETFs, and governments combined). The token block carries the asset details: ticker, the unit it is displayed in, its maximum supply (for Bitcoin), and the live spot price used for the USD value.
tokenquerystringToken to roll up. Defaults to bitcoin. Non-bitcoin values require Pro+.{
"data": {
"token": {
"id": "bitcoin",
"symbol": "BTC",
"display_unit": "BTC",
"supply_cap": 21000000,
"price_usd": 83512.44
},
"entity_count": 248,
"total_holdings": 3830257,
"total_usd_value": 319920000000,
"total_cost_basis": 241020000000,
"unrealized_pnl": 78900000000,
"avg_cost_basis_usd": 62925,
"total_purchases": 7086,
"most_recent_purchase": "2026-04-13"
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…", "token": "bitcoin" }
}/api/v1/treasuriesProFull list of tracked entities (public companies, private companies, ETFs, and governments) with holdings, cost basis, treasury rank, and verification metadata. Sorted by holdings descending — rank is the absolute leaderboard position. Each row carries how its figure was verified (source_tier + a click-through source_citation_url, data_confidence, and the coverage_class plus a plain-English reason) so you can sort or filter the leaderboard by trust without a second call. Use entity_type to pull a single segment (e.g. just ETFs or just governments).
tokenquerystringFilter to entities whose primary treasury asset is this token. Defaults to bitcoin. Non-bitcoin values require Pro+.entity_typequerystringLimit to one segment. Omit (or pass all) to return every segment.limitqueryintegerMax rows to return. Default 100, max 200.offsetqueryintegerRow offset for pagination. Default 0.{
"data": [
{
"id": "uuid",
"ticker": "MSTR",
"name": "Strategy",
"slug": "microstrategy",
"country": "US",
"flag_emoji": "🇺🇸",
"logo_domain": "strategy.com",
"logo_initial": "S",
"entity_type": "public_company",
"sec_cik": "0001050446",
"website": "https://strategy.com",
"rank": 1,
"btc_holdings": 818869,
"avg_price_usd": 75577,
"total_cost_usd": 59013000000,
"last_purchase": "2026-04-13",
"source_tier": "primary",
"source_citation_url": "https://www.sec.gov/cgi-bin/browse-edgar?...",
"data_confidence": "high",
"coverage_class": "verified_events_sec",
"coverage_class_reason": "Every position change is backed by an SEC filing."
}
],
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…", "total": 248, "limit": 100, "offset": 0, "token": "bitcoin", "entity_type": "all" }
}/api/v1/treasuries/{slug}ProEverything we hold on one entity: holdings + cost basis (token-native amount, amount_usd, avg cost, total cost), analyst metrics (mNAV, sats/share, % of market cap, debt-to-coin, enterprise value — see /market for the spot price they use), full verification metadata (source_tier + source_citation_url, the data_confidence block with its computed tier / matching sources / any operator override, and coverage_class with a plain-English reason), and up to the last 100 discrete treasury events. Each event carries its primary-source citation (filing_url + sec_accession), the SEC filing date, and an AI one-line summary. Replace {slug} in the path with the slug from /treasuries. Pass ?token= (Pro+) to read a non-primary asset for the same entity.
slugpathstringrequiredTreasury slug (e.g. microstrategy).tokenquerystringRead a specific asset for this entity instead of its primary. Non-bitcoin values require Pro+.{
"data": {
"ticker": "MSTR",
"name": "Strategy",
"sec_cik": "0001050446",
"website": "https://strategy.com",
"btc_holdings": 818869,
"avg_price_usd": 75577,
"total_cost_usd": 59013000000,
"token": { "id": "bitcoin", "symbol": "BTC", "name": "Bitcoin" },
"amount": 818869,
"amount_usd": 68390000000,
"source_tier": "primary",
"source_citation_url": "https://www.sec.gov/cgi-bin/browse-edgar?...",
"coverage_class": "verified_events_sec",
"coverage_class_reason": "Every position change is backed by an SEC filing.",
"metrics": {
"mnav": 1.42, "satsPerShare": 1820, "btcPctOfMcap": 70.4,
"debtToBtcRatio": 0.12, "enterpriseValueUsd": 102400000000, "btcValueUsd": 68390000000
},
"data_quality": { "tier": "verified", "verified_rows": 98, "total_rows": 100 },
"data_confidence": {
"tier": "high", "computed_tier": "high",
"sources": ["bitbo", "coingecko", "bitcointreasuries"],
"updated_at": "2026-04-19T06:00:00Z", "operator_override": null
},
"purchases": [
{
"id": "uuid", "token": "bitcoin", "amount": 13927, "btc_amount": 13927,
"usd_amount": 1001000000, "price_per_unit": 71870, "price_per_btc": 71870,
"purchase_date": "2026-04-13", "filing_date": "2026-04-14",
"total_holdings_after": 818869, "source": "sec_edgar",
"filing_url": "https://www.sec.gov/Archives/edgar/...",
"sec_accession": "0000950170-26-000123",
"summary": "Strategy disclosed a purchase of 13,927 BTC for about $1.0B."
}
]
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…", "token": "bitcoin" }
}/api/v1/activityProGlobal feed of recent treasury events across every tracked entity. Includes both buys (positive amount) and sells (negative amount, e.g. ETF daily outflows). Each row is a discrete, citable disclosure and ships with its primary source: filing_url + sec_accession (resolve at sec.gov), the SEC filing_date, and an AI one-line summary. Note: governments report their holdings as official headline figures, not as individual dated transactions, so filtering this feed to a government returns an empty list — that is by design, not a gap. The current government position is on /treasuries/{slug}.
tokenquerystringFilter to a specific token. Defaults to bitcoin. Non-bitcoin values require Pro+.companyquerystringFilter to a single entity by its treasury slug.entity_typequerystringLimit to one segment. Governments have no per-event activity (see note above).sincequerystringISO date — only include events on or after this date.orderquerystringSort direction by event date. Default desc (newest first).limitqueryintegerPage size. Default 50, max 100.offsetqueryintegerRow offset for pagination. Default 0.{
"data": [
{
"id": "abc-123",
"token_id": "bitcoin",
"amount": 13927,
"btc_amount": 13927,
"usd_amount": 1001000000,
"price_per_btc": 71870,
"purchase_date": "2026-04-13",
"filing_date": "2026-04-14",
"total_holdings_after": 818869,
"source": "sec_edgar",
"sec_accession": "0000950170-26-000123",
"filing_url": "https://sec.gov/...",
"summary": "Strategy disclosed a purchase of 13,927 BTC for about $1.0B.",
"company": {
"ticker": "MSTR",
"name": "Strategy",
"slug": "microstrategy",
"country": "US",
"flag_emoji": "🇺🇸",
"logo_domain": "strategy.com",
"logo_initial": "S"
}
}
],
"meta": { "total": 3450, "limit": 50, "offset": 0, "token": "bitcoin", "entity_type": "all" }
}/api/v1/marketProLatest prices for the tracked tokens, plus global market cap, 24h volume, BTC/ETH dominance, and the Fear & Greed index (value plus its text label, e.g. "Greed").
{
"data": {
"tokens": [
{ "id": "bitcoin", "symbol": "BTC", "name": "Bitcoin", "price_usd": 83512.44, "price_change_24h": -1.23, "market_cap": 1650000000000, "volume_24h": 42000000000, "market_cap_rank": 1 }
],
"stats": {
"total_market_cap": 3200000000000,
"total_volume_24h": 180000000000,
"btc_dominance": 58.2,
"eth_dominance": 11.4,
"active_coins": 12,
"fear_greed_value": 62,
"fear_greed_label": "Greed"
}
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/tokensProList of tokens that have tracked treasury holdings. Pro+ unlocks non-bitcoin tokens.
{
"data": [
{
"id": "bitcoin",
"symbol": "BTC",
"name": "Bitcoin",
"display_unit": "BTC",
"supply_cap": 21000000,
"requires_pro_plus": false,
"unlocked_for_key": true,
"stats": { "entity_count": 248, "total_amount": 3830257, "total_cost_usd": 241020000000, "avg_cost_basis_usd": 62925, "most_recent_purchase": "2026-04-13" }
},
{
"id": "ethereum",
"symbol": "ETH",
"name": "Ethereum",
"requires_pro_plus": true,
"unlocked_for_key": false,
"stats": { "entity_count": 41, "total_amount": 4900000, "total_cost_usd": 14200000000, "avg_cost_basis_usd": 2898, "most_recent_purchase": "2026-04-11" }
}
],
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/exportProCSV or JSON bulk export of discrete treasury events (one row per disclosed buy/sell, with its per-(entity, token) data-confidence tier). Pro+ includes non-BTC tokens. Governments have no per-event rows here (their holdings are headline figures — use /treasuries).
formatquerystringrequiredExport format.tokenquerystringFilter to a specific token. Defaults to bitcoin. Non-bitcoin values require Pro+.entity_typequerystringLimit to one segment.companyquerystringFilter to a single entity by its treasury slug.sincequerystringISO date — only include events on or after this date.limitqueryintegerMax rows. Default 10,000, max 50,000.# format=csv returns a raw download (no { data, meta } wrapper):
company,ticker,country,token_id,amount,usd_amount,price_per_unit,purchase_date,total_holdings_after,source,data_confidence_tier,data_confidence_sources
Strategy,MSTR,US,bitcoin,13927,1001000000,71870,2026-04-13,818869,sec_edgar,verified,sec_edgar
BlackRock iShares Bitcoin Trust,IBIT,US,bitcoin,5859,...
# format=json returns the same rows as a JSON array (raw, also unwrapped)./api/v1/usageProLive limits for the calling API key — how many requests you can make each minute and each month, how many you have left in each window, and when each window resets. Reset times are Unix timestamps (seconds).
{
"data": {
"plan": "pro_plus",
"rate_limit": { "requests_per_minute": 300, "remaining": 297, "resets_at": 1745078460 },
"quota": { "requests_per_month": 10000, "remaining": 9588, "resets_at": 1746057600 }
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/signalsProReverse-chronological feed of treasury news and events: detected purchases and sales, newly disclosed adopters, capital raises, and executive changes. Pro+ also unlocks premium signals — live newswire coverage, ETF milestones, and large on-chain movements. Paginated by cursor: follow next_cursor while has_more is true.
tokenquerystringFilter to one token, or all (the default).typequerystringFilter to one signal type, or all (the default).companyquerystringFilter to a single entity by its treasury slug.sincequerystringOnly include signals on or after this time (ISO 8601 date-time).untilquerystringOnly include signals before this time (ISO 8601 date-time).min_confidencequerystringOnly include signals at or above this confidence score (0 to 1).cursorquerystringPagination cursor. Pass the next_cursor from the previous page to fetch the next one.limitqueryintegerPage size. Default 20, max 100.{
"data": {
"items": [
{
"id": "uuid",
"slug": "strategy-buys-13927-btc-2026-04-13",
"token_id": "bitcoin",
"company_id": "uuid",
"signal_type": "purchase",
"source_type": "sec_filing",
"source_url": "https://www.sec.gov/Archives/edgar/...",
"source_ref": "0000950170-26-000123",
"headline": "Strategy adds 13,927 BTC",
"summary": "Strategy disclosed a purchase of 13,927 BTC for about $1.0B.",
"occurred_at": "2026-04-13T00:00:00Z",
"ingested_at": "2026-04-13T13:42:00Z",
"confidence": 0.98,
"payload": {},
"tier": "free"
}
],
"next_cursor": "2026-04-13T00:00:00Z",
"has_more": true
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/signals/{slug}ProOne signal by its slug (from the feed). Premium signals are only visible to Pro+ keys; a Pro key requesting one gets a 404. Replace {slug} in the path.
slugpathstringrequiredSignal slug (from the feed).{
"data": {
"id": "uuid",
"slug": "strategy-buys-13927-btc-2026-04-13",
"token_id": "bitcoin",
"company_id": "uuid",
"signal_type": "purchase",
"source_type": "sec_filing",
"source_url": "https://www.sec.gov/Archives/edgar/...",
"source_ref": "0000950170-26-000123",
"headline": "Strategy adds 13,927 BTC",
"summary": "Strategy disclosed a purchase of 13,927 BTC for about $1.0B.",
"occurred_at": "2026-04-13T00:00:00Z",
"ingested_at": "2026-04-13T13:42:00Z",
"confidence": 0.98,
"payload": {},
"tier": "free",
"canonical_signal_id": null
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/export/signalsProBulk download of the signals feed as CSV or newline-delimited JSON (JSONL). Same content rules as /signals — Pro+ keys also get the premium signals. Returns a raw file download, not the { data, meta } wrapper. Caps at 10,000 rows per call; narrow with since/until for larger pulls.
formatquerystringDownload format. Default csv.tokenquerystringFilter to one token, or all (the default).typequerystringFilter to one signal type, or all (the default).sincequerystringOnly include signals on or after this time (ISO 8601 date-time).untilquerystringOnly include signals before this time (ISO 8601 date-time).limitqueryintegerMax rows. Default 1,000, max 10,000.# format=csv (raw download, no { data, meta } wrapper):
id,slug,token_id,company_id,signal_type,source_type,source_url,source_ref,headline,summary,occurred_at,ingested_at,confidence,tier
uuid,strategy-buys-13927-btc-2026-04-13,bitcoin,uuid,purchase,sec_filing,https://www.sec.gov/...,0000950170-26-000123,"Strategy adds 13,927 BTC","Strategy disclosed a purchase…",2026-04-13T00:00:00Z,2026-04-13T13:42:00Z,0.98,free
# format=jsonl returns one JSON object per line (also a raw download)./api/v1/webhooksAPI ProSubscribe an HTTPS endpoint to one or more event types. The response includes the signing secret exactly once — store it now; later requests only return a masked preview. You can have up to 10 subscriptions per account. Subscribing to a premium signal event (newswire, ETF milestone, or on-chain movement) requires Pro+ or higher.
urlquerystringrequiredYour HTTPS endpoint (sent in the JSON body). Public, non-loopback addresses only.eventsquerystringrequiredArray of event types to subscribe to (sent in the JSON body). See the full list below.descriptionquerystringOptional label (up to 280 characters), sent in the JSON body.{
"data": {
"subscription": {
"id": "uuid",
"url": "https://your-app.example/corpstacking-webhook",
"events": ["purchase.detected", "holding.updated"],
"description": "prod pipeline",
"active": true,
"created_at": "2026-04-19T14:00:00Z",
"secret": "whsec_cs_…",
"secret_notice": "Store this secret now — it will not be shown again."
}
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/webhooksAPI ProList the subscriptions on your account. The signing secret is shown only as a masked preview here — the full value is returned just once, when you create the subscription.
{
"data": {
"subscriptions": [
{
"id": "uuid",
"url": "https://your-app.example/corpstacking-webhook",
"events": ["purchase.detected"],
"secret": "whsec_cs_********",
"description": "prod pipeline",
"active": true,
"paused_reason": null,
"paused_at": null,
"consecutive_failures": 0,
"last_delivered_at": "2026-04-19T13:55:00Z",
"created_at": "2026-04-19T14:00:00Z"
}
]
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/webhooks/{id}API ProRemove a subscription and its delivery history. Replace {id} with the subscription id from the list endpoint.
idpathstringrequiredSubscription id.{
"data": { "deleted": true, "id": "uuid" },
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…" }
}/api/v1/webhooks/eventsAPI ProThe delivery log for one subscription — every attempt, its status, the response code we got back, and any error. Useful for diagnosing a subscription that has stopped receiving events. Filter by status to see only failed deliveries.
subscription_idquerystringrequiredSubscription to inspect.statusquerystringFilter to one delivery status.limitqueryintegerMax rows. Default 50, max 200.{
"data": {
"events": [
{
"id": "uuid",
"event_type": "purchase.detected",
"event_id": "9f0c…e2a1",
"status": "delivered",
"attempts": 1,
"last_attempt_at": "2026-04-19T14:00:01Z",
"next_retry_at": null,
"last_status_code": 200,
"last_error": null,
"created_at": "2026-04-19T14:00:00Z",
"delivered_at": "2026-04-19T14:00:01Z"
}
]
},
"meta": { "timestamp": "2026-04-19T14:00:00.000Z", "request_id": "…", "count": 1 }
}Live signals stream (Pro+)
/api/v1/signals/stream is a live feed of new signals using Server-Sent Events — hold the connection open and we push each new signal as it lands, instead of you polling. It requires Pro+ or higher; lower plans get a 402 tier_required.
curl -N https://www.corpstacking.com/api/v1/signals/stream \
-H "x-api-key: cs_live_YOUR_KEY"Each signal arrives as one event:
id: <signal_id>
event: signal.purchase
data: {"id":"uuid","slug":"…","signal_type":"purchase","token_id":"bitcoin","headline":"Strategy adds 13,927 BTC","occurred_at":"2026-04-13T00:00:00Z","confidence":0.98,"tier":"free"}The event: name is signal.<type> (e.g. signal.purchase, signal.wire_news), so you can route on it. We send a heartbeat comment every 25 seconds to keep the connection from idling out. If the connection drops, just reconnect — opening a fresh stream resumes new signals from that moment on.
Live playground
Stored in your browser session only. Never logged or transmitted beyond the request itself.
GET /api/v1/statsMCP server (AI agents)
For Claude Desktop, Claude Code, Cursor, ChatGPT custom GPTs, and any other client that speaks the Model Context Protocol, we host an MCP server at https://www.corpstacking.com/api/mcp/mcp. Same API key, same plan, same monthly quota — eight read-only tools wrapped around this same /v1 surface. Full reference and copy-paste configs for the popular clients at /mcp.
Embed widgets (free, no key)
Drop a live treasury widget on any page with a single <iframe>. No API key, no sign-up — the widget reads the same verified holdings shown on the site and refreshes on its own. Point it at any company by its page name:
<iframe src="https://www.corpstacking.com/api/embed/widget?company=strategy" width="420" height="180" frameborder="0" title="Strategy bitcoin treasury — CorpStacking"></iframe>
Options: company(the company's page name, e.g. strategy), theme (light or dark), variant, and layout for size. Prefer to point and click? Use the free builder with a live preview at /widgets.
Webhooks (API Pro)
API Pro subscribers can receive signed HTTP callbacks whenever a qualifying event fires. Delivery is at-least-once; each envelope carries a stable id you can use to dedupe. You can keep up to 10 subscriptions per account — creating an 11th returns a 409 subscription_limit_reached; delete one first.
Treasury & price events (any webhook tier):
purchase.detectedholding.updatedprice.thresholdSignal events (mirror the signals feed):
signal.purchase.createdsignal.sale.createdsignal.new_adopter.createdsignal.capital_raise.createdsignal.exec_change.createdsignal.wire_news.createdsignal.etf_milestone.createdThe premium signal events — signal.wire_news.created, signal.etf_milestone.created, and on-chain movements — require Pro+ or higher to subscribe to. Subscribing to one on a lower plan returns a 402 tier_required. The other events are available on any webhook-enabled (API Pro or higher) plan.
There's also a per-alert webhook (Pro and above) you can wire up under Settings → Notifications — it POSTs a purchase event to your endpoint whenever a company you follow files one. Its payload is documented at the bottom of this section.
curl -X POST https://www.corpstacking.com/api/v1/webhooks \
-H "x-api-key: cs_live_YOUR_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://your-app.example/corpstacking-webhook",
"events": ["purchase.detected", "holding.updated"],
"description": "prod pipeline"
}'The signing secret is returned at data.subscription.secret (prefix whsec_cs_) — exactly once, on this create call. Store it now; GET /webhooks only ever returns the masked preview (whsec_cs_********).
POST https://your-app.example/corpstacking-webhook
content-type: application/json
user-agent: CorpStacking-Webhooks/1.0
x-corpstacking-event: purchase.detected
x-corpstacking-event-id: 9f0c…e2a1
x-corpstacking-timestamp: 1745078400
x-corpstacking-signature: t=1745078400,v1=5a8b…c9d0
{
"id": "9f0c…e2a1",
"type": "purchase.detected",
"created_at": "2026-04-19T14:00:00Z",
"data": {
"company": { "id": "uuid", "ticker": "MSTR" },
"token_id": "bitcoin",
"amount": 1250,
"usd_amount": 82500000,
"price_per_unit": 66000,
"total_holdings_after": 214046,
"purchase_date": "2026-04-19",
"sec_accession": "0001104659-26-000...",
"filing_url": "https://sec.gov/..."
}
}Token-native throughout: token_id tells you the asset and amountis in that asset's units (BTC, ETH, or SOL). Every figure ships with its primary source — filing_url links the SEC document and sec_accession resolves at sec.gov — so you can verify it independently.
import { createHmac, timingSafeEqual } from 'crypto'
function verify(secret, rawBody, header) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=', 2)))
const t = parts.t, v1 = parts.v1
if (!t || !v1) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const a = Buffer.from(v1, 'hex'), b = Buffer.from(expected, 'hex')
return a.length === b.length && timingSafeEqual(a, b)
}
// Express handler — rawBody must be the unparsed request body bytes.
app.post('/corpstacking-webhook', express.raw({ type: '*/*' }), (req, res) => {
const ok = verify(process.env.CORPSTACKING_SECRET, req.body.toString(), req.header('x-corpstacking-signature'))
if (!ok) return res.sendStatus(400)
const event = JSON.parse(req.body.toString())
// ... dedupe on event.id, then process
res.sendStatus(200)
})Replay protection: the signature covers t.rawBody, so the signed timestamp can't be altered without breaking it. Reject any request whose x-corpstacking-timestampis outside a ±5-minute window of your clock — that's the tolerance we recommend (allows for clock skew + our retry delay while defeating replay of a captured POST). Pair it with deduping on the event id for at-least-once safety. We retry on 5xx/network with exponential backoff up to 24h; 4xx is treated as a permanent failure.
curl "https://www.corpstacking.com/api/v1/webhooks/events?subscription_id=SUB_ID&status=failed" \
-H "x-api-key: cs_live_YOUR_KEY"After 20 consecutive failures (~24h under the backoff curve), the subscription is auto-paused with paused_reason: too_many_failures. DELETE and recreate the subscription, or contact support to resume.
The per-alert webhook (configured under Settings → Notifications) POSTs this body when a company you follow files a purchase. It's signed with your channel secret in the X-CorpStacking-Signature header (sha256=<hex of the raw body>), and the send time plus a dedupe id are also echoed in the X-CorpStacking-Timestamp and X-CorpStacking-Event-Id headers.
POST https://your-app.example/corpstacking-alert
content-type: application/json
user-agent: CorpStacking-Webhook/1.0
x-corpstacking-event: purchase.detected
x-corpstacking-event-id: 7c1d…a90f
x-corpstacking-timestamp: 2026-04-19T14:00:00.000Z
x-corpstacking-signature: sha256=5a8b…c9d0
{
"event": "purchase.detected",
"version": "1",
"timestamp": "2026-04-19T14:00:00.000Z",
"event_id": "7c1d…a90f",
"data": {
"company": {
"id": "uuid",
"name": "MicroStrategy",
"ticker": "MSTR",
"slug": "strategy",
"primary_token_id": "bitcoin",
"token_holdings": 214046,
"btc_holdings": 214046 // DEPRECATED — alias of token_holdings
},
"purchase": {
"id": "7c1d…a90f",
"token_id": "bitcoin",
"token_amount": 1250,
"price_per_token": 66000,
"usd_amount": 82500000,
"purchase_date": "2026-04-19",
"total_holdings_after": 214046,
"source": "sec_edgar",
"citation": {
"url": "https://www.sec.gov/Archives/edgar/...",
"sec_accession": "0001104659-26-000...",
"source": "sec_edgar"
},
"btc_amount": 1250, // DEPRECATED — alias of token_amount
"price_per_btc": 66000 // DEPRECATED — alias of price_per_token
},
"token": { "id": "bitcoin", "name": "Bitcoin", "ticker": "BTC", "symbol": "₿" }
}
}Prefer the token-native fields — token_holdings, token_amount, price_per_token — they carry the right value for every asset. The btc_holdings / btc_amount / price_per_btcfields are kept only for older integrations: the name says "btc" but the value is the entity's primary asset (ETH or SOL for ETH/SOL entities), so reading them on a non-BTC entity is misleading. citation carries the primary source for the figure so you can verify it. Verify the signature the same way as above, but over the raw body alone (sha256=hmac(secret, rawBody)), and apply the same ±5-minute replay window against X-CorpStacking-Timestamp.
Errors
Errors are JSON shaped like { "error": { "code", "message", "request_id" } }. Treat the code as the source of truth — branch on it in your handlers; the message is human-readable and may change.
401 missing_api_key — no x-api-key header401 invalid_api_key_format — key must start with cs_live_403 invalid_api_key — key not recognized403 plan_required — needs a paid plan with API access403 treasuries_plus_required — Ethereum / Solana need Pro+402 tier_required — signals stream & premium webhook events need Pro+403 webhook_access_required — webhooks need API Pro429 rate_limit_exceeded — too many requests this minute; see Retry-After429 quota_exceeded — monthly quota used up; see Retry-After500 internal_error — something went wrong on our side