Build with the API / Trending threat actors
Trending threat actors
Which threat actors, ransomware crews and malware families are rising this week?
What you get
Prints the trending APT groups, ransomware groups and malware families of the last 7 days: how many clusters mention them, the change versus the previous window, whether they are new to the corpus, and the ThreatCluster entity page for each.
- Endpoints
/entities/trending- Credits per run
- 1
- Key
- Works on the free key
Step by step
1. Get a key
Sign in, open Settings, then API & Feeds, and select Generate API key. The key is shown once, so copy it. Every account has one and the free key covers this recipe.

The API page. The key itself is created in Settings, under API & Feeds. 2. Download the script and set the key
The script is one file with one dependency. It reads the key from the environment, so the key never goes into the file.
curl -sO https://raw.githubusercontent.com/Jam0k/Cyber-Threat-Intelligence-API/main/examples/recipes/trending-actors.py pip install requests export THREATCLUSTER_API_KEY=tc_live_your_key_here3. Run it
No arguments. The script prints the most-mentioned threat actors, ransomware groups and malware of the last 7 days, with the change on the week before.
python3 trending-actors.py
Run on 29 September 2026. ShinyHunters led with 21 mentions. 4. Open a profile
Each name links to its profile: an overview, objectives, the techniques it uses and recent coverage.

The profile for the top row of the run above. 5. Pick the types you track
Pass the entity types you want. The run costs one credit.
python3 trending-actors.py --types apt_group malware cve
The code
trending-actors.py, 101 lines. Python 3, needs requests.
#!/usr/bin/env python3
"""trending-actors: which threat actors, ransomware crews and malware families are rising this week?
What it does
Prints the trending APT groups, ransomware groups and malware families of the last
7 days: how many clusters mention them, the change versus the previous window, whether
they are new to the corpus, and the ThreatCluster entity page for each.
Endpoints
GET /entities/trending?time_filter=7d&limit=10 (1 credit)
Cost: 1 credit per run. Works on the free tier.
Usage
python3 trending-actors.py
python3 trending-actors.py --types apt_group malware cve
"""
import argparse
from urllib.parse import quote
import json
import os
import sys
import time
import requests
API_BASE = os.environ.get("THREATCLUSTER_API_BASE", "https://threatcluster.io/api/public/v1").rstrip("/")
API_KEY = os.environ.get("THREATCLUSTER_API_KEY", "")
SITE = os.environ.get("THREATCLUSTER_SITE", "https://threatcluster.io") # where the linked pages live
PACE_SECONDS = 2.1 # free keys get 30 requests/min; ~28/min keeps loops clear of 429s
_credits = {"used": 0, "remaining": None}
def _auth_headers():
# A tc_... key is sent as X-API-Key; anything else is a short-lived bearer (e.g. from `tc login`).
if not API_KEY:
sys.exit("THREATCLUSTER_API_KEY is not set. Free keys: https://threatcluster.io/about/api")
if API_KEY.startswith("tc_"):
return {"X-API-Key": API_KEY}
return {"Authorization": "Bearer " + API_KEY}
def api_get(path, _allow=(), **params):
"""GET one endpoint. Exits non-zero with the API's own error body, unless the
status is listed in _allow (per-item lookups where a 404 is itself an answer)."""
r = requests.get(API_BASE + path, headers=_auth_headers(), params=params, timeout=60)
if "X-Request-Cost" in r.headers:
_credits["used"] += int(r.headers["X-Request-Cost"])
if "X-RateLimit-Remaining" in r.headers:
_credits["remaining"] = r.headers["X-RateLimit-Remaining"]
if r.status_code >= 400 and r.status_code not in _allow:
try:
body = json.dumps(r.json())
except ValueError:
body = r.text[:500]
sys.exit("HTTP %d from %s: %s" % (r.status_code, path, body))
return r
def cluster_url(cluster):
# The free payload carries `slug` (canonical page) and `cluster_id`; the last 8 hex
# chars of the id also resolve, via a 301 to the slug, when slug is missing.
ident = cluster.get("slug") or cluster.get("short_id") or cluster.get("cluster_id", "").replace("-", "")[-8:]
return "%s/cluster/%s" % (SITE, ident)
def report_credits():
# Stderr, so it never pollutes piped output (brief.md, blocklists, JSON).
# Keys without a daily cap send no remaining-credits header.
left = ", remaining today: %s" % _credits["remaining"] if _credits["remaining"] is not None else ""
sys.stderr.write("[threatcluster] credits used this run: %d%s\n" % (_credits["used"], left))
DEFAULT_TYPES = ["apt_group", "ransomware_group", "malware"]
def main():
ap = argparse.ArgumentParser(description="Trending actors/malware this week")
ap.add_argument("--types", nargs="+", default=DEFAULT_TYPES, help="entity types to show")
ap.add_argument("--limit", type=int, default=10, help="rows per type (free tier caps at 10)")
args = ap.parse_args()
data = api_get("/entities/trending", time_filter="7d", limit=args.limit).json()
trending = data.get("trending", {})
for etype in args.types:
rows = trending.get(etype) or []
print("== %s (last %s) ==" % (etype, data.get("time_filter", "7d")))
if not rows:
print(" nothing trending\n")
continue
print(" %-28s %8s %8s %s" % ("entity", "mentions", "change", "page"))
for r in rows:
change = "NEW" if r.get("is_new") else ("%+.0f%%" % r["change"] if r.get("change") is not None else "-")
# quote(safe='') because entity values can contain '/', '#' or spaces.
print(" %-28s %8s %8s %s/entities/%s/%s" % ((r.get("value") or "")[:28], r.get("frequency", "?"), change,
SITE, etype.replace("_", "-"), quote(str(r.get("value") or ""), safe="")))
print()
report_credits()
if __name__ == "__main__":
main()
The shell one-liner, trending-actors.sh:
#!/usr/bin/env bash
# trending-actors.sh: trending APT groups, ransomware groups and malware of the last 7 days. 1 credit. Free tier OK.
# usage: ./trending-actors.sh
set -euo pipefail
: "${THREATCLUSTER_API_KEY:?set THREATCLUSTER_API_KEY (free keys: https://threatcluster.io/about/api)}"
BASE="${THREATCLUSTER_API_BASE:-https://threatcluster.io/api/public/v1}"
SITE="${THREATCLUSTER_SITE:-https://threatcluster.io}"
# A tc_... key goes in X-API-Key; anything else is a bearer (e.g. from `tc login`).
if [[ "$THREATCLUSTER_API_KEY" == tc_* ]]; then AUTH="X-API-Key: $THREATCLUSTER_API_KEY"; else AUTH="Authorization: Bearer $THREATCLUSTER_API_KEY"; fi
# tc_get <path> [curl -G args...]: prints the body; on HTTP >= 400 prints the API error to stderr and fails.
tc_get() { local path=$1; shift; local out code
out=$(curl -sS -G -H "$AUTH" -w $'\n%{http_code}' "$BASE$path" "$@"); code=${out##*$'\n'}
if [ "$code" -ge 400 ]; then echo "HTTP $code from $path: ${out%$'\n'*}" >&2; return 1; fi; printf '%s' "${out%$'\n'*}"; }
tc_get /entities/trending --data-urlencode time_filter=7d --data-urlencode limit=10 \
| jq -r '.trending | to_entries[] | select(.key == "apt_group" or .key == "ransomware_group" or .key == "malware")
| "== \(.key) ==", (.value[] | " \(.value)\t\(.frequency) mentions\t\(if .is_new then "NEW" else "\(.change)%" end)")'
Output
From a run on 29 September 2026. Yours will differ as the corpus moves.
== apt_group (last 7d) == entity mentions change page ShinyHunters 21 +2000% https://threatcluster.io/entities/apt-group/ShinyHunters Lazarus Group 10 +100% https://threatcluster.io/entities/apt-group/Lazarus%20Group Salt Typhoon 6 +20% https://threatcluster.io/entities/apt-group/Salt%20Typhoon Apt28 2 +100% https://threatcluster.io/entities/apt-group/Apt28 Unc3886 6 NEW https://threatcluster.io/entities/apt-group/Unc3886 Wizard Spider 5 NEW https://threatcluster.io/entities/apt-group/Wizard%20Spider Water Hydra 3 NEW https://threatcluster.io/entities/apt-group/Water%20Hydra SideCopy 3 NEW https://threatcluster.io/entities/apt-group/SideCopy Unc6240 2 NEW https://threatcluster.io/entities/apt-group/Unc6240 Storm-2992 2 NEW https://threatcluster.io/entities/apt-group/Storm-2992 == ransomware_group (last 7d) == entity mentions change page Clop 28 +300% https://threatcluster.io/entities/ransomware-group/Clop Cl0p 10 +233% https://threatcluster.io/entities/ransomware-group/Cl0p Ryuk 10 +150% https://threatcluster.io/entities/ransomware-group/Ryuk SilentRansomGroup 5 +400% https://threatcluster.io/entities/ransomware-group/SilentRansomGroup Pear 5 +400% https://threatcluster.io/entities/ransomware-group/Pear Krybit 3 +50% https://threatcluster.io/entities/ransomware-group/Krybit Medusa 2 +100% https://threatcluster.io/entities/ransomware-group/Medusa Booba Project 6 NEW https://threatcluster.io/entities/ransomware-group/Booba%20Project Orova 3 NEW https://threatcluster.io/entities/ransomware-group/Orova MedusaLocker 2 NEW https://threatcluster.io/entities/ransomware-group/MedusaLocker == malware (last 7d) == entity mentions change page Closedquorum 10 +150% https://threatcluster.io/entities/malware/Closedquorum Pegasus 9 +125% https://threatcluster.io/entities/malware/Pegasus TraderTraitor 6 +200% https://threatcluster.io/entities/malware/TraderTraitor SecTopRAT 5 +400% https://threatcluster.io/entities/malware/SecTopRAT BigDiskBuster 4 +300% https://threatcluster.io/entities/malware/BigDiskBuster Predator 5 +67% https://threatcluster.io/entities/malware/Predator Emotet 4 +100% https://threatcluster.io/entities/malware/Emotet Trickbot 4 +33% https://threatcluster.io/entities/malware/Trickbot Mini Shai-Hulud 2 +100% https://threatcluster.io/entities/malware/Mini%20Shai-Hulud Stuxnet 2 +100% https://threatcluster.io/entities/malware/Stuxnet [threatcluster] credits used this run: 1
The file on GitHub: trending-actors.pyand trending-actors.sh.
Goes well with
- Weekly executive brief: The week's top 10 threat clusters as a Markdown brief.
- A lookup tool for LLM agents: A tool-calling function `threatcluster_lookup(query)` for LLM agents.
- Brand and domain dark-web monitor: Are my brands or domains showing up on the dark web?
- Bash integration: curl and jq in shell scripts and cron
All recipes: /build. Endpoint reference: /api/public/v1/docs.