TRIX API

TRIX API reference

Vulnerability and threat intelligence over HTTPS: every CVE with its TRIS score, KEV, EPSS, exploit signals, the threat actors with documented use, and exploit prerequisites. JSON in, JSON out.

Get started in three steps

  1. Create a free account with Google, GitHub, or your email. Email sign-ups confirm the address first.
  2. On the account page, accept the TRIX API terms and issue a key. It starts with cvk_ and is shown once, so copy it then.
  3. Make your first call:
curl -H "Authorization: Bearer $TRIX_KEY" \
  https://trix.cveasyai.com/v1/cve/CVE-2021-44228

The base URL for every route is https://trix.cveasyai.com. GET https://trix.cveasyai.com/v1 needs no key and lists the routes and tiers, so a script can check what exists before it authenticates.

Authentication

Send your key as a bearer token on every request:

Authorization: Bearer cvk_...

We store only a hash of the key, so we cannot show it to you again. If you lose it, rotate it on the account page: the old key stops working the moment the new one is issued. Keep keys out of source control and browser code; call the API from a server or a script. Pro and above hold several keys at once, so each service can have its own and you can rotate or revoke one without touching the rest.

A request with no key, a revoked key, or an expired key gets 401.

Tiers and limits

CommunityDeveloperProScaleEnterprise
Price a monthFree$49$495, or included in CVEasy AI$1,490From $2,500, annual contract
Billed yearly$490$4,950$14,900Annual
Calls per minute601206001,5003,000
Calls per day (UTC)1,00010,000100,000500,0002,000,000
List records per day5,00050,0001,000,0005,000,000Unlimited
Largest list page502001,0001,0001,000
Lookups a day on the website3002,0005,00010,00010,000
Active keys per account1131025
CVE records, TRIS baseline, KEV, EPSS, delta syncYesYesYesYesYes
Actors with documented use, chain factsYesYesYesYesYes
Heuristic actor matchesNoYesYesYesYes
Chain resolution and validated chainsNoYesYesYesYes
Contextual TRIS against your asset contextNoNoYesYesYes
Bulk snapshotsNoNoYesYesYes
Score history per CVENoNoNoYesYes
ACT alert webhooksNoNoNoYesYes
BASzy offensive intelNoNoNoNoPer contract
LicensePersonal, research, your own organizationPersonal, research, your own organizationCommercial: products and client servicesCommercial, plus showing TRIX data to your own customersCustom, including redistribution
SupportDocsEmailEmail, one business dayPriority supportNamed contact, SLA by contract

Every response carries x-ratelimit-limit-day and x-ratelimit-remaining-day. Over a limit you get 429 with Retry-After and a body that says which window you hit (minute or day). Below Enterprise it also names next_tier, the tier that lifts the limit, and an upgrade_url. Days reset at 00:00 UTC.

List records count the CVE rows that GET /v1/cve list pages return; point lookups do not count against them. They are what stop one key copying the whole catalog in an afternoon. Upgrade from the account page: your existing keys move to the new limits within a minute of payment, with no change to your code. Moving between paid tiers works the same way, up or down.

CVEs

GET /v1/cve/{id}Community

One CVE, enriched. Fields include tris_baseline (0 to 100) and tris_band (ACT, ATTEND, TRACK, MONITOR), a one line summary, products (up to 10, primary first, with products_total), CVSS score and vector, cwe_ids, epss_score and epss_percentile, is_kev with its dates, has_public_poc, and coverage when we have written about it.

{
  "id": "CVE-2021-44228",
  "tris_baseline": 95,
  "tris_band": "ACT",
  "summary": "Apache Log4j2 2.0-beta9 through 2.15.0 JNDI features ...",
  "products": ["Apache Log4j", "Siemens Capital", "..."],
  "products_total": 20,
  "severity": "CRITICAL",
  "cvss_score": 10,
  "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
  "epss_score": 0.99999,
  "epss_percentile": 1,
  "is_kev": 1,
  "kev_date_added": "2021-12-10",
  "has_public_poc": 1
}

Unknown CVE: 404. New CVEs appear after NVD publishes them.

GET /v1/cveCommunity

A filtered list. Query parameters, all optional:

ParameterMeaning
kev=1Only CVEs on the CISA KEV list
min_epss=0.5EPSS score at or above this. A number from 0 to 1.
since=2026-09-01Changed on or after this date. This is how you keep a local copy in sync. Community reaches back 30 days, Developer 90, Pro and above any date.
limit=50Page size: up to 50 on Community, 200 on Developer, 1,000 on Pro and above.
cursor=...Where to resume. Pass next_cursor from the previous page back unchanged.
{ "count": 50, "next_cursor": "...", "results": [ { "id": "...", ... } ] }

Results are ordered by CVE ID, or by the field you filter on when you send since (change date) or min_epss (EPSS score). Treat next_cursor as an opaque string: do not build or edit it. It is null on the last page. Every row a list page returns counts against your list records for the day.

Context and scoring

GET /v1/context/{cve}Community

Everything about one CVE in one call, grouped as threat (description, products, references), risk (CVSS, TRIS baseline and band, EPSS, KEV, ransomware and wormable signals, and a plain English reasoning), exploits (public exploit, prerequisites: what an attacker needs and what they gain) and intel (actors with documented use).

POST /v1/tris/scoreCommunity for baselinePro with context

Score up to 100 CVEs in one call. Without a context, every tier gets the baseline. With a context, Pro keys get tris_contextual: the same CVE scored for your asset.

curl -X POST https://trix.cveasyai.com/v1/tris/score \
  -H "Authorization: Bearer $TRIX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cve_ids": ["CVE-2021-44228", "CVE-2024-3094"],
    "context": {
      "asset_criticality": "critical",
      "internet_facing": true,
      "business_impact": "critical",
      "data_classification": "restricted",
      "compensating_controls": false,
      "reachable": true
    }
  }'

Every context field is optional, and each one you send adds a measured layer; the response lists them in measured_layers. Context is used for the request and never stored. A Community key that sends a context gets 402; drop the context for the baseline.

POST /v1/context/{cve}Pro

The fused context from the GET route, with tris_contextual filled in for the context you post. Same body shape as above, without cve_ids.

Threat actors

GET /v1/actor/cve/{id}Community

Actors seen using the CVE. By default only documented use counts: a named campaign, a vendor or government advisory, or MITRE ATT&CK. Each match has match_type, confidence, evidence and a ref_url when there is one.

{
  "cve_id": "CVE-2021-44228",
  "matches": [
    { "actor_id": "apt41", "actor_name": "APT41", "match_type": "direct_cve",
      "confidence": 0.95, "source": "mitre-attack,cveasy-curated",
      "evidence": "MITRE ATT&CK campaign C0017: ...",
      "ref_url": "https://www.mandiant.com/resources/apt41-us-state-governments" }
  ],
  "grade": "evidence only"
}

Add ?include_heuristic=1 on a Developer key or above for product and technique matches, always labelled with their match_type so they are never mistaken for evidence.

GET /v1/actor/resolve?name={alias}Community

Resolve any name or alias (APT29, Cozy Bear, Midnight Blizzard) to the canonical actor. ambiguous: true means two sources disagree, and both are returned.

GET /v1/actor/{id}Community

One actor with every known alias.

Exploit chains

GET /v1/chain/facts/{cve}Community

What exploiting the CVE requires and what it grants, from its CVSS vector: requires (for example none or low_priv), grants (for example code_exec, credentials), chain_role (for example entry) and the vector fields. basis says whether it came from CVSS v3 or v2.

GET /v1/chain/from/{cve}Developer

CVEs that could follow this one: candidate links from CVSS semantics and shared product. Add ?kev=1 to keep only known exploited next steps. These are marked derived: they could compose, not that anyone ran the sequence.

GET /v1/chain/validatedDeveloper

Named chains we have executed in the lab. Filter with ?platform=. Derived chains appear only with ?include_derived=1 and are labelled.

Board and lookup

GET /v1/boardCommunity

How the whole catalog splits across the four TRIS bands, plus every CVE in the ACT band, ranked. Refreshed daily. This is the data behind the threat intel board.

GET /v1/lookup/{id}Community

The CVE record, its documented actors and its chain facts in one response, as the website shows it: { "cve": ..., "actors": [...], "chain": ... }.

Bulk snapshots

GET /v1/bulk/manifestPro

Lists the newest snapshot of each dataset.

GET /v1/bulk/{dataset}Pro

Gzipped newline delimited JSON. Datasets: cves, actors, cve_actors, iocs, chain_facts. Load a snapshot once, then keep it current with GET /v1/cve?since=.

Score history

GET /v1/cve/{id}/historyScale

How a CVE's score moved over time: one entry per day it changed, each with day, tris_baseline, tris_band, epss_score and is_kev. ?days=90 sets the window, up to 365. Use it to show when a CVE crossed into ACT, or to back a trend line in your own reports.

curl -H "Authorization: Bearer $TRIX_KEY" \
  "https://trix.cveasyai.com/v1/cve/CVE-2024-3094/history?days=180"

ACT alert webhooks

Once a day, after the board is published, we compare today's ACT band with yesterday's and POST the CVEs that entered it to each of your webhooks. Up to five webhooks per account.

POST /v1/webhooksScale

curl -X POST https://trix.cveasyai.com/v1/webhooks \
  -H "Authorization: Bearer $TRIX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.example.com/trix", "secret": "a long random string" }'

The url must be https on a public host name. IP address literals and private or internal ranges are refused. The secret is what we sign each delivery with; keep it on your server. Leave it out and we generate one and return it in this response only; we never show it again.

GET /v1/webhooksScale

Your webhooks and their status.

DELETE /v1/webhooks/{id}Scale

Stop deliveries to one webhook.

POST /v1/webhooks/{id}/testScale

Send a signed test delivery now, so you can check your endpoint and your signature code before the next board.

What we send

POST /trix HTTP/1.1
Content-Type: application/json
x-trix-signature: t=1790000000,v1=5f2b...e91c

{ "event": "act.entered", "cves": ["CVE-2026-1234", "CVE-2026-5678"], "board_date": "2026-09-28" }

v1 is the hex HMAC-SHA256, keyed with your secret, of the timestamp, a dot, and the raw request body: t + "." + body. Answer with any 2xx. A delivery that fails is tried three times with backoff, and a webhook that fails every day for seven days is switched off.

Verify the signature

Check it on the raw body, before you parse the JSON. Compare in constant time, and refuse a timestamp more than five minutes from your clock so a captured delivery cannot be replayed later.

# Python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, sig = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
// Node 18 or later
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = parts.t ?? "", sig = parts.v1 ?? "";
  if (!/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > tolerance) return false;
  const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(sig);
  return a.length === b.length && timingSafeEqual(a, b);
}

In Express, read the body with express.raw({ type: "application/json" }) so rawBody is the exact bytes we signed.

Your usage

GET /v1/usageCommunity

Your key's calls per route per day for the last 30 days, and today's total against your daily limit:

{
  "tier": "community",
  "rate_per_min": 60,
  "today": { "day": "2026-09-28", "used": 212, "limit_per_day": 1000 },
  "usage": [ { "day": "2026-09-28", "route": "/v1/cve", "calls": 180 } ]
}

The account page shows the same numbers.

Errors

StatusMeaningWhat to do
400Malformed request, for example a bad CVE IDThe body says which field. CVE IDs look like CVE-2024-3094.
401No key, or the key is revoked or expiredCheck the header, or issue a new key
402The route or option needs a higher tierThe body names the tier that includes it, and where to upgrade
403The key cannot reach this routeUse a read key; browser sessions from the website reach only the board and lookups
404No such CVE, actor, or route
429Over the minute, day, or list records limitWait for Retry-After seconds, or upgrade to next_tier
5xxOur sideRetry with backoff; if it persists, write to us with the request id

A 402 looks like this:

{
  "error": "upgrade_required",
  "feature": "score_history",
  "required_tier": "scale",
  "upgrade_url": "https://cveasyai.com/account/"
}

required_tier uses the internal tier names: developer, commercial (sold as Pro), scale, and enterprise. Errors are JSON with an error field and usually a reason or detail. Every response carries x-request-id; include it when you contact us and we can find the request.

Examples

Python

import os, time, requests

BASE = "https://trix.cveasyai.com"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['TRIX_KEY']}"

def get(path, **params):
    while True:
        r = S.get(BASE + path, params=params, timeout=30)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        r.raise_for_status()
        return r.json()

cve = get("/v1/cve/CVE-2021-44228")
print(cve["tris_baseline"], cve["tris_band"], cve["summary"])

# Every KEV CVE, page by page
cursor = None
while True:
    # 50 is the Community page size; raise it to your tier's
    page = get("/v1/cve", kev=1, limit=50, **({"cursor": cursor} if cursor else {}))
    for c in page["results"]:
        print(c["id"], c["tris_baseline"])
    cursor = page["next_cursor"]
    if not cursor:
        break

JavaScript (Node 18 or later)

const BASE = "https://trix.cveasyai.com";
const headers = { Authorization: `Bearer ${process.env.TRIX_KEY}` };

const res = await fetch(`${BASE}/v1/actor/cve/CVE-2021-44228`, { headers });
if (!res.ok) throw new Error(`${res.status} ${res.headers.get("x-request-id")}`);
const { matches } = await res.json();
for (const m of matches) console.log(m.actor_name, m.match_type, m.ref_url ?? "");

Keep a local copy current

# Once a day: everything that changed since yesterday
curl -H "Authorization: Bearer $TRIX_KEY" \
  "https://trix.cveasyai.com/v1/cve?since=$(date -u -d yesterday +%F)&limit=50"

On macOS, use date -u -v-1d +%F for the date. Follow next_cursor until it is null, and raise limit to your tier's page size.

Terms and attribution

Community and Developer keys are for personal use, research, and protecting your own organization. Building the data into a product or a client service needs TRIX Pro. Showing TRIX data to your own customers inside your product needs TRIX Scale, and redistributing the data set itself needs TRIX Enterprise. Where you show TRIX data to others, credit it as "per CVEasy AI threat intel data" with a link to cveasyai.com/threat-intel/. EPSS scores come from FIRST, actor names from MITRE ATT&CK and the MISP galaxy, CVE records from NVD, and exploited status from CISA KEV. The full TRIX API terms apply.