Live Threat Feed
A WebSocket feed of threat intelligence as it is ingested: new entities, entities reported as malicious, and new relationships between entities.
The feed is live-only. It delivers what happens after you connect, not a replay of history — use Corpus Analytics GET /recent for the backlog.
Base URL: https://apis.threatwinds.com/api/analytics/v1
Why a ticket is required
Browsers cannot set request headers on a WebSocket handshake, so the usual Authorization header is not available. Putting a bearer token in the query string instead would leak it into access logs, proxy logs and Referer headers, and it would stay valid long after.
Instead you authenticate over ordinary HTTP to obtain a single-use ticket, then open the socket with it. The ticket expires in 30 seconds and is consumed the first time it is redeemed, so a captured ticket is near-worthless.
Step 1 — Mint a ticket
Endpoint: GET /live/ticket
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Optional | Bearer token from an active session. |
| api-key | string | Optional | API key. |
| api-secret | string | Optional | API secret. |
curl -H "Authorization: Bearer $TOKEN" \
"https://apis.threatwinds.com/api/analytics/v1/live/ticket"
{ "ticket": "8Jx1c...", "expires_in": 30 }
The ticket records the security groups of the caller at the moment it was minted. Those groups decide what the socket will deliver.
Step 2 — Open the socket
Endpoint: GET /live (WebSocket upgrade)
| Parameter | Type | Required | Description |
|---|---|---|---|
| ticket | string | Yes | The ticket from /live/ticket. |
const { ticket } = await fetch('/api/analytics/v1/live/ticket', {
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json())
const ws = new WebSocket(
`wss://apis.threatwinds.com/api/analytics/v1/live?ticket=${encodeURIComponent(ticket)}`,
)
ws.onmessage = (e) => {
const event = JSON.parse(e.data)
console.log(event.type, event.value, event.reputation)
}
Because a ticket is single-use, reconnecting requires minting a new one.
Event format
Each message is one JSON object.
| Field | Type | Description |
|---|---|---|
| type | string | entity.created, entity.malicious, or entity.linked. |
| time | string | RFC 3339 timestamp of the event. |
| entityId | string | The entity’s identifier. For entity.linked, the source entity. |
| entityType | string | Entity type, e.g. ip, domain, md5. |
| value | string | The entity’s value, e.g. 203.0.113.10. |
| reputation | int | Reported reputation, -3..+3. |
| tags | string[] | Tags supplied at report time, when present. |
| toEntityId | string | entity.linked only — the target entity. |
| mode | string | entity.linked only — association or aggregation. |
{
"type": "entity.malicious",
"time": "2026-07-27T19:24:17.588Z",
"entityId": "ip-ad0c2ed9a0a9b2382...",
"entityType": "ip",
"value": "203.0.113.10",
"reputation": -3,
"tags": ["c2"]
}
An entity reported with a negative reputation produces two events: entity.created and entity.malicious. Subscribe to whichever suits you and ignore the other — entity.malicious alone is the signal-only view.
Visibility
You only receive events for entities whose security groups intersect the groups recorded in your ticket. Unauthenticated tickets carry the public group and therefore see only public entities.
Security group membership is not disclosed in the event payload.
Connection handling
| Behaviour | Detail |
|---|---|
| Keepalive | The server sends a WebSocket ping every 30 seconds. Standard browser clients answer automatically. |
| Slow consumers | A client that cannot keep up has events dropped rather than delaying other subscribers. The feed is lossy under load by design. |
| Capacity | Each instance serves a bounded number of concurrent feeds and replies 503 when full. |
Error Codes
| Status Code | Description | Possible Cause |
|---|---|---|
| 401 | Unauthorized | Missing, expired, or already-redeemed ticket |
| 503 | Service Unavailable | Live feed disabled, or the instance is at capacity |