OIXLY open threat intelligence

Use the data

There is no API to sign up for, because there is no API. The data is a handful of static JSON files served from the same place as this page. Fetch them directly — no key, no quota, no authentication.

Files

/data/index.json          manifest: sources, totals, per-day counts
/data/d/YYYY-MM-DD.json   one shard per UTC day

Start with the manifest. It lists every available day and its record counts, so you can fetch only the days you need. It is a few kilobytes, and it is enough on its own to answer "how many critical indicators were there last Tuesday" without downloading a single record.

Manifest

{
  "v": 2,
  "generated": "2026-08-16T09:23:34.474Z",
  "retentionDays": 90,
  "sources": ["Blocklist.de", "NVD", "..."],
  "totals": {
    "records": 176564,
    "byCategory": { "botnet": 112552, "nvd": 26896, "..." : 0 },
    "bySeverity": { "medium": 123637, "high": 47311, "..." : 0 }
  },
  "days": [
    {
      "d": "2026-08-16",
      "n": 7263,
      "c":  { "phishing": { "high": 288 }, "bruteforce": { "medium": 5767 } },
      "sv": { "high": 1222, "medium": 5767, "low": 195 },
      "s":  { "OpenPhish": 288, "Blocklist.de": 5767 }
    }
  ]
}

days is ordered newest first.

Day shards

Shards are compact on purpose. About four fifths of the corpus is boilerplate — the same sentence with a different IP in it — so records that fit a known pattern store only their variable parts. A day shard looks like this:

{
  "v": 2,
  "d": "2026-08-16",
  "ts": [1786872213319, 1786872213333],
  "r": [
    [1, 0, "50.6.20.135"],
    [4, 1, "https://bad.example/login", "bad.example", "op-34f140b5b70a"]
  ]
}

Each record in r is a positional array:

Raw records

Template id 0 means the remaining elements are, in order:

[0, tsIndex, id, title, summary, fields, action,
    categoryMask, severityIndex, sourceIndex, sourceUrl, indicators]

fields is an array of [key, value] pairs. sourceIndex indexes the manifest's sources array, and severityIndex indexes ["critical","high","medium","low","none"].

categoryMask is a bitmask, not an index — a record usually belongs to several categories at once. Bit i is set when the record is in category i of this list, in order:

["nvd-cve","cisa-kev","zero-day","ransomware",
 "phishing","malware","botnet","bruteforce","scanner","tor",
 "apt","breach"]

// every category of a record
const cats = CATEGORIES.filter((_, i) => mask & (1 << i));

// is it a zero-day?
const isZeroDay = Boolean(mask & (1 << CATEGORIES.indexOf("zero-day")));

Some categories imply others and are always set together: cisa-kev and zero-day both imply nvd-cve (a KEV entry is a published CVE), and zero-day also implies cisa-kev. You never have to expand these yourself — the mask already contains them.

Templates

Template definitions live in js/core/templates.js, which is a plain ES module you can import directly. It is the single source of truth used by both the build pipeline and this site, so it cannot drift from the data.

Decoding without writing a decoder

Import the same modules this page uses:

import { expand, setSourceTable } from 'https://oixly.com/js/core/decode.js';

const index = await (await fetch('https://oixly.com/data/index.json')).json();
setSourceTable(index.sources);

const day = index.days[0].d;
const shard = await (await fetch(`https://oixly.com/data/d/${day}.json`)).json();

const threats = shard.r.map(rec => expand(rec, shard));
console.log(threats[0]);
// { id, title, summary, fields, action, category, severity,
//   source, sourceUrl, indicators, timestamp }

Python, no dependencies

import json, urllib.request

def get(path):
    with urllib.request.urlopen("https://oixly.com" + path) as r:
        return json.load(r)

index = get("/data/index.json")

# Every critical indicator from the last 7 days, using the manifest to
# skip days that contain none.
for day in index["days"][:7]:
    has_critical = any("critical" in sevs for sevs in day["c"].values())
    if not has_critical:
        continue
    shard = get(f"/data/d/{day['d']}.json")
    for rec in shard["r"]:
        if rec[0] == 0 and rec[8] == 0:      # raw record, severity "critical"
            print(day["d"], rec[2], rec[3])

CATEGORIES = ["nvd-cve","cisa-kev","zero-day","ransomware",
              "phishing","malware","botnet","bruteforce","scanner","tor",
              "apt","breach"]

def categories_of(rec):
    """Raw records carry a category bitmask at index 7."""
    mask = rec[7]
    return [c for i, c in enumerate(CATEGORIES) if mask & (1 << i)]

Bulk export

For a one-off pull, the feed page's JSON and CSV buttons export whatever your current filters match, up to 50,000 rows. The cap is there because building a larger file in the browser will freeze the tab. For more than that, fetch the shards.

Fair use

The files are static and cached at the edge, so polling costs nothing much — but the pipeline only runs every three hours, so fetching more often than that gets you the same bytes. Check generated in the manifest before pulling shards.

If you redistribute this data, carry the source field with it. The feed operators did the actual work, and several of their licences require attribution.

Stability

The v field in both the manifest and each shard is the format version. It is 1. If the layout changes incompatibly, that number changes with it — pin against it rather than assuming.