API Reference
Access ThreatCluster data programmatically. Query threats, entities, feeds, IOCs, vulnerabilities, and dark-web data via the REST API. Scoped credentials and short-lived bearer JWTs.
ThreatCluster exposes its data through a public REST API at
/api/public/v1/*. All responses are JSON.
For new integrations we recommend the CLI (tc), which wraps this
API with credential hygiene and bearer-token caching. Use the raw API directly
if you're integrating from a non-Python language or building a server-side
client.
Base URL
https://api.threatcluster.io/api/public/v1
All endpoint paths below are relative to this base.
Authentication
The API accepts two credential shapes, both interchangeable on data endpoints:
Option A — Bearer JWT (recommended)
Mint a short-lived (15-minute) bearer from a long-lived agent key, then send it on every request:
POST /api/auth/agent/token
X-API-Key: tc_agent_…
Content-Type: application/json
{}
{
"access_token": "eyJhbGciOiJIUzI1NiI…",
"token_type": "Bearer",
"expires_in": 900,
"scopes": ["threats:read", "iocs:read", "..."]
}
GET /api/public/v1/threats?limit=5
Authorization: Bearer eyJhbGciOiJIUzI1NiI…
If a bearer leaks, it dies in 15 minutes. If the agent key leaks, revoke it in Settings → CLI and all derived bearers die within 15 minutes too.
See Agent keys & scopes for the full design.
Option B — Direct X-API-Key (legacy / simple)
For one-off scripts or CI, you can send the agent key (or a legacy tc_live_*
key) directly on every request:
GET /api/public/v1/threats?limit=5
X-API-Key: tc_agent_…
The server mints a bearer internally and discards it. Functionally identical to option A but exposes the long-lived credential on every request — use only when you can't easily cache a bearer.
Getting a key
- Sign in and go to Settings → CLI.
- Click Mint key, tick the scopes you need, choose an expiry.
- Copy the
tc_agent_…value. Shown once.
You can also use a legacy tc_live_* key from Settings → API Access —
those keep working with the full scope set.
Programmatic mint (Auth0 user token required):
POST /api/public/v1/agent-keys
Authorization: Bearer <auth0-user-token>
Content-Type: application/json
{
"name": "my-laptop",
"scopes": ["threats:read", "iocs:read"],
"expires_in_days": 90
}
Scopes
Every endpoint requires exactly one scope. Mint your key with the scopes you need:
| Scope | Endpoint families |
|---|---|
threats:read |
/threats/*, /stats/* |
iocs:read |
/iocs/*, /threats/{id}/iocs |
entities:read |
/entities/* |
vulns:read |
/vulnerabilities/* |
darkweb:read |
/darkweb/* |
feeds:read |
/feeds/* |
Missing scope → 403 with:
{
"detail": {
"error": "insufficient_scope",
"message": "This endpoint requires the 'darkweb:read' scope.",
"required_scope": "darkweb:read",
"granted_scopes": ["threats:read", "iocs:read"]
}
}
Scopes are orthogonal to subscription tier. A free-tier user with
darkweb:read can call dark-web endpoints, but the endpoint will then enforce
the tier gate (e.g. /darkweb/keyword-hits is Business).
Rate limits
Rate limits are per-key, per-minute, scaled by your subscription tier:
| Tier | Requests / minute |
|---|---|
| Free | 30 |
| Starter | 120 |
| Business | 600 |
When you exceed the limit, the API returns 429 Too Many Requests with a
Retry-After: 60 header.
You can also set a per-bearer cap (max_requests claim) when minting a
bearer; once exceeded, every subsequent request gets 429 regardless of the
tier limit.
Pagination
List endpoints use limit/offset:
| Parameter | Type | Default | Max |
|---|---|---|---|
limit |
int | 50 | 100 |
offset |
int | 0 | — |
Each list response includes count (results in this page); some include
total for global pagination.
Error format
All errors return a JSON body. Rich errors include a structured detail
object:
{
"detail": {
"error": "insufficient_scope",
"message": "This endpoint requires the 'darkweb:read' scope.",
"required_scope": "darkweb:read"
}
}
Simpler errors are a string:
{ "detail": "Invalid or revoked API key" }
| Code | Meaning |
|---|---|
400 |
Bad request (malformed body, invalid query) |
401 |
Missing / invalid / expired credential |
403 |
Authenticated but lacks scope or subscription tier |
404 |
Resource not found |
429 |
Rate limit or per-session budget exceeded |
500 |
Server error — retry, then file a bug |
Auth endpoints
POST /api/auth/agent/token
Mint a bearer JWT from an agent key.
Headers: X-API-Key: tc_agent_…
Body (all optional):
| Field | Type | Description |
|---|---|---|
scopes |
string[] | Strict subset of refresh's scopes. Defaults to all of them. |
session_id |
string | Identifier for per-session budgeting. |
max_requests |
int | Cap requests for this session. Subsequent requests 429. |
Returns {access_token, token_type, expires_in, scopes}.
POST /api/auth/agent/self-revoke
Revoke the presented refresh credential. No Auth0 needed — same-key auth = same-key blast radius.
Headers: X-API-Key: tc_agent_…
Returns {revoked: "tk_..."}. New bearer mints fail immediately; existing
bearers die at their exp (≤15 min).
POST /api/auth/ci/exchange
Exchange a CI-issued OIDC JWT (GitHub Actions / GitLab) for a 1-hour bearer. See Agent keys & scopes — CI OIDC exchange.
POST /api/public/v1/agent-keys
Mint an agent key. Auth0 user token required.
GET /api/public/v1/agent-keys
List your agent keys (scopes, request_count, last_used_at, expires_at). Auth0 user token required. Never returns the secret value.
GET /api/public/v1/agent-keys/{key_id}/usage
Per-key counters for spotting abuse. Auth0 user token required.
DELETE /api/public/v1/agent-keys/{key_id}
Revoke a key by its public id (tk_xxxxxxxx). Auth0 user token required.
Data endpoints
Each endpoint lists its required scope.
Threats
GET /threats · threats:read
List threat clusters.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 50 | Max 100 |
offset |
int | 0 | Pagination |
time_filter |
string | 7d |
1h, 24h, 7d, 30d |
sort_by |
string | trending |
trending, new |
keyword |
string | — | Filter by keyword |
Returns {threats: [...], count, limit, offset, time_filter, sort_by}.
GET /threats/{id} · threats:read
Detail for one cluster (UUID or 8-char short id).
GET /threats/{id}/iocs · iocs:read
IOCs scoped to one cluster.
| Parameter | Default | Description |
|---|---|---|
types |
all |
Comma-separated IOC types or all |
format |
json |
json, csv, stix |
GET /threats/{id}/stix · threats:read
STIX 2.1 export for one cluster.
IOCs
GET /iocs/feed · iocs:read
Plaintext IOC feed (one per line). Suitable for SIEM/firewall blocklists.
| Parameter | Description |
|---|---|
type |
ip, domain, sha256, url, … |
since |
ISO date or relative (24h) |
GET /iocs/export · iocs:read
Bulk export across all clusters.
| Parameter | Default | Description |
|---|---|---|
format |
json |
json, csv, stix |
Entities
GET /entities/search · entities:read
Search threat actors, malware, tools, ransomware groups, etc.
| Parameter | Description |
|---|---|
q |
Search term |
entity_type |
Filter (apt_group, malware, ransomware_group, …) |
limit |
Max 100 |
GET /entities/{type}/{value} · entities:read
Detail for one entity. URL-encode the value if it contains slashes.
GET /entities/{type}/{value}/related · entities:read
Entities co-occurring with the given one.
GET /entities/trending · entities:read
Entities trending across the platform.
| Parameter | Default | Description |
|---|---|---|
window |
24h |
1h, 24h, 7d, 30d |
Returns {trending: {apt_group: [...], malware: [...], ...}}.
Vulnerabilities
GET /vulnerabilities · vulns:read
List tracked CVEs.
| Parameter | Description |
|---|---|
severity |
LOW, MEDIUM, HIGH, CRITICAL |
since |
ISO date or relative |
limit |
Max 200 |
Returns {cves: [...]}.
GET /vulnerabilities/{cve_id} · vulns:read
Detail for one CVE (e.g. CVE-2026-1234).
GET /vulnerabilities/stats · vulns:read
Aggregate counts by severity, source, etc.
Dark web
GET /darkweb/ransomware/victims · darkweb:read
Ransomware leak-site victims.
| Parameter | Default | Description |
|---|---|---|
days |
30 |
Max 90 |
group |
— | Filter by group (e.g. lockbit, akira) |
country |
— | ISO-2 country |
limit |
100 |
Max 500 |
Returns {victims: [{id, group, name, discovered, country, sector, ...}], count}.
GET /darkweb/ransomware/groups · darkweb:read
Active and historical ransomware groups with victim counts.
GET /darkweb/ransomware/group/{name} · darkweb:read
Detail for one group, including recent victims and infrastructure.
GET /darkweb/ransomware/victim/{victim_id} · darkweb:read
Detail for one victim. victim_id is the 16-char hex id from the list.
GET /darkweb/breaches · darkweb:read
Dark-web breach data.
GET /darkweb/markets · darkweb:read
Active dark-web markets.
GET /darkweb/market/{name} · darkweb:read
Detail for one market.
GET /darkweb/keyword-hits · darkweb:read (Business)
Hits from your tracked keywords on dark-web crawl. Tier-gated.
GET /darkweb/trends · darkweb:read
Aggregated trend data across markets, breaches, ransomware.
GET /darkweb/stats · darkweb:read
Platform-wide dark-web counters.
Stats
GET /stats/overview · threats:read
Top-level stats (cluster count, article count, IOC count, etc.).
GET /stats/entities/timeline · threats:read
Entity mention counts over time.
Feeds
GET /feeds · feeds:read
Your custom intel feeds.
GET /feeds/{id}/entities · feeds:read
Entities in one feed.
Examples
Python
import os, time, requests
BASE = "https://api.threatcluster.io"
REFRESH = os.environ["TC_REFRESH_TOKEN"] # tc_agent_… or tc_live_…
# Mint a bearer once, reuse until ~30s before expiry
r = requests.post(
f"{BASE}/api/auth/agent/token",
headers={"X-API-Key": REFRESH, "Content-Type": "application/json"},
json={"session_id": "my-script", "max_requests": 500},
).json()
bearer, exp_at = r["access_token"], time.time() + r["expires_in"] - 30
# Use it
threats = requests.get(
f"{BASE}/api/public/v1/threats",
headers={"Authorization": f"Bearer {bearer}"},
params={"limit": 10, "keyword": "ransomware"},
).json()
for t in threats["threats"]:
print(f"[{t['threat_score']}] {t['ai_title']}")
curl
# Mint a bearer
TC_REFRESH=tc_agent_…
TC_BEARER=$(curl -s -X POST https://api.threatcluster.io/api/auth/agent/token \
-H "X-API-Key: $TC_REFRESH" -H "Content-Type: application/json" \
-d '{}' | jq -r .access_token)
# Use it
curl -H "Authorization: Bearer $TC_BEARER" \
"https://api.threatcluster.io/api/public/v1/threats?limit=5" | jq
# Or skip the bearer dance for a one-off:
curl -H "X-API-Key: $TC_REFRESH" \
"https://api.threatcluster.io/api/public/v1/threats?limit=5" | jq
Shell (tc)
The recommended way for interactive and shell use:
pipx install threatcluster-cli
tc auth login # paste your tc_agent_…
tc threats list --limit 5 | jq -r '.threats[].ai_title'
See CLI (tc).
Interactive API docs
OpenAPI / Swagger UI at:
https://api.threatcluster.io/api/public/v1/docs
ReDoc at:
https://api.threatcluster.io/api/public/v1/redoc
Raw OpenAPI JSON at /api/public/v1/openapi.json.
See also
- CLI (
tc) — the recommended client - Agent keys & scopes — credential design and threat model
- IOC exports — STIX / CSV / JSON formats in detail
- Custom feeds — what
/feedsreads