Developer guide
Stream ChamberFeed court signals straight into your trading and research pipelines.
Quickstart
Install the Python client and start streaming with your API key:
pip install "chamberfeed-client @ https://chamberfeed.io/static/sdk/chamberfeed_client-0.1.0-py3-none-any.whl"
export CHAMBERFEED_API_KEY=cf_live_...
chamberfeed-stream --tickers CRWD NFLX # JSON lines on stdout; pipe into Kafka, a file, anything
Or in your own code:
import asyncio
from chamberfeed_client import ChamberFeedClient
async def main():
client = ChamberFeedClient(api_key="cf_live_...")
async for event in client.stream(tickers=["CRWD"]):
print(event.type, event.ticker, event.data)
asyncio.run(main())
Authentication
Machine clients use the API key ChamberFeed issued to you. Send it as a header. Keys are shown once, can be rotated or revoked at any time, and stop working automatically when a trial or licence ends.
X-API-Key: cf_live_...
?api_key= and ?token= so
credentials can't leak into proxy or CDN logs. Store the key in a secrets manager, not in source control.Python client
chamberfeed-client (Python 3.10+) wraps the stream with the behaviour every production consumer needs:
- Automatic reconnect with exponential back-off and jitter.
- Catch-up: after a reconnect it back-fills anything persisted while you were offline from the REST feed
(events marked
event.replayed = True). - De-duplication so each event is delivered once, even if it arrives live and in a back-fill.
- Clean shutdown: raises
AccessEndedwhen your trial or licence ends, andAuthErrorfor a rejected key, instead of retrying forever.
from datetime import datetime, timezone
from chamberfeed_client import AccessEnded, AuthError, ChamberFeedClient
client = ChamberFeedClient(api_key=KEY)
# Resume from your own checkpoint after a restart:
async for ev in client.stream(tickers=["CRWD", "NFLX"], since=last_checkpoint):
if ev.type == "raw" and ev.data.get("signal"):
trade_signal(ev.ticker, ev.data["signal"], ev.data["confidence"])
elif ev.type == "enriched":
update_model(ev.data) # ticker, motion_type, outcome, sentiment
elif ev.type == "context":
attach_note(ev.id, ev.data["kind"], ev.data["text"])
save_checkpoint(ev.timestamp)
# History / back-fill:
rows = await client.feed(since=datetime(2026, 11, 1, tzinfo=timezone.utc), ticker="CRWD")
WebSocket stream
Endpoint: wss://chamberfeed.io/v1/stream. Available to Tier-1 (real-time) clients and active trials.
1. Connect and authenticate
Send X-API-Key on the upgrade request. (Browsers, which can't set headers, send
{"action":"auth","token":"<JWT>"} as the first frame within 5 seconds.) The server replies:
{"type": "welcome", "role": "client_premium", "tickers": null}
2. Optionally filter by ticker
→ {"action": "subscribe", "tickers": ["CRWD", "NFLX"]} // [] = everything
← {"type": "subscribed", "tickers": ["CRWD", "NFLX"]}
3. Receive events
Every message is JSON with a type. Ignore types you don't recognise; new ones may be added.
import asyncio, json, websockets
async def main():
async with websockets.connect("wss://chamberfeed.io/v1/stream",
additional_headers={"X-API-Key": KEY}) as ws:
print(json.loads(await ws.recv())) # welcome
await ws.send(json.dumps({"action": "subscribe", "tickers": ["CRWD"]}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] in ("raw", "enriched", "context"):
handle(msg)
asyncio.run(main())
// Node.js (ws package)
const WebSocket = require("ws");
const ws = new WebSocket("wss://chamberfeed.io/v1/stream", { headers: { "X-API-Key": process.env.KEY } });
ws.on("open", () => ws.send(JSON.stringify({ action: "subscribe", tickers: ["CRWD"] })));
ws.on("message", (buf) => { const m = JSON.parse(buf); if (["raw","enriched","context"].includes(m.type)) handle(m); });
Message types
| type | When | What it contains |
|---|---|---|
raw | Instantly (milliseconds) when a runner taps a ruling button or a scraper detects a change | The signal as captured: signal + confidence for one-tap rulings, or free text. Act on this for speed. |
enriched | A fraction of a second later | Same id, plus ticker, motion_type, outcome, sentiment, confidence. |
context | Seconds to minutes later | Stage-2 detail linked by event_id: kind = context (dictated notes) or
retraction (the signal was sent in error; unwind it). |
heartbeat | Every 20 s when idle | Keep-alive. No action needed. |
access_ended | When a trial/licence ends or a key is revoked | The server then closes the socket (code 1008). Don't reconnect. |
Examples
{"type": "raw", "broadcast_timestamp_utc": "2026-11-09T15:14:02.081Z",
"event": {"id": "4f1c…", "case_name": "Acme Holdings LLC v. CrowdStrike, Inc.", "ticker": "CRWD",
"court_jurisdiction": "NY Commercial Div Part 53 (Borrok)", "signal": "MOTION_DENIED",
"confidence": "HIGH", "motion_type": "motion_to_dismiss",
"ruling_summary": "Motion to dismiss denied from the bench.",
"chamberfeed_timestamp": "2026-11-09T15:14:02.003Z", "source": "manual"}}
{"type": "enriched", "event": {"id": "4f1c…", "ticker": "CRWD", "motion_type": "motion_to_dismiss",
"outcome": "denied", "sentiment": "bearish", "confidence": 0.95, "signal": "MOTION_DENIED", …}}
{"type": "context", "event": {"event_id": "4f1c…", "kind": "context", "ticker": "CRWD",
"text": "Court found jurisdiction adequately pleaded on count two; expedited discovery ordered.",
"timestamp": "2026-11-09T15:15:10Z"}}
Field reference
| Field | Values |
|---|---|
signal | MOTION_DENIED, MOTION_GRANTED, MOTION_GRANTED_IN_PART,
STAY_LIFTED, INJUNCTION_GRANTED, INJUNCTION_DENIED, DECISION_RESERVED;
null for free-text and scraper events |
confidence (raw) | HIGH (runner certain) or MEDIUM (fairly sure) |
confidence (enriched) | 0–1 (HIGH = 0.95, MEDIUM = 0.7 for one-tap signals) |
motion_type | motion_to_dismiss, preliminary_injunction, summary_judgment,
motion_to_lift_stay, tro, class_certification, calendar_update, other |
outcome | granted, denied, granted_in_part, leaning_grant,
leaning_deny, reserved, unknown |
sentiment | bullish, bearish, neutral, unknown (for the named public company) |
source | manual (courtroom runner) or scraper (court website change) |
chamberfeed_timestamp | UTC capture time, microsecond precision; the basis of all latency measurements |
raw as the fastest directional indicator and enriched as the
structured record. Always honour retraction.Reconnect & catch-up
The Python client does all of this for you. If you write your own consumer:
- Reconnect with back-off: 0.5 s, 1 s, 2 s … capped at 30 s, with random jitter.
- Track the latest
chamberfeed_timestampyou processed. - After reconnecting, call
GET /v1/feed?since=<last timestamp − 30 s>and process anything you haven't seen. - De-duplicate on
(type, id); forcontextuse(event_id, timestamp). - Read fast. Each connection has a buffer of 1,000 messages. A consumer that falls further behind is disconnected (close code 1013) and should reconnect and catch up. Hand events to a queue rather than doing slow work inside the read loop.
- Close code 1008 means access was refused or ended. Don't retry; check your key or contact us.
REST feed
curl -H "X-API-Key: $KEY" "https://chamberfeed.io/v1/feed?since=2026-11-09T14:00:00Z&ticker=CRWD&limit=500"
curl -H "X-API-Key: $KEY" "https://chamberfeed.io/v1/events/<id>"
| Parameter | Meaning |
|---|---|
since | ISO-8601 time; only events captured at or after it |
ticker | Filter to one ticker |
limit | 1–1000 (default 100); results are newest first |
Responses are the enriched records, including context_updates, retracted_at and benchmark
fields. Interactive reference: /docs.
Webhooks
Prefer push to your own HTTPS endpoint? Ask us to register a webhook URL. Each enriched event is POSTed as JSON and signed with your webhook secret (shown once when configured). Failed deliveries are retried briefly; use the REST feed to reconcile.
POST /your/endpoint
Content-Type: application/json
X-ChamberFeed-Timestamp: 1762701242
X-ChamberFeed-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>
# Python
from chamberfeed_client import verify_webhook
ok = verify_webhook(raw_body_bytes, WEBHOOK_SECRET,
headers["X-ChamberFeed-Timestamp"], headers["X-ChamberFeed-Signature"])
if not ok:
return 401
// Node.js
const crypto = require("crypto");
function verify(rawBody, secret, ts, sig) {
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // reject replays
const mac = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return sig.length === mac.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mac));
}
Access & limits
- Tier 1 (real-time): WebSocket stream, webhooks and REST with no delay.
- Tier 2 (delayed): REST only; events become visible 15 minutes after capture.
- Trials: full real-time access for the trial period; keys stop working automatically at the end.
- Data is licensed for your internal use only; redistribution is not permitted (Data License Agreement §2).
Compliance teams: see the data sourcing & compliance statement. Questions or a key rotation? Contact your ChamberFeed representative.