Skip to content
API 9 min read

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

  1. Sign in and go to Settings → CLI.
  2. Click Mint key, tick the scopes you need, choose an expiry.
  3. 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