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.
n— records that day, each counted once.c— category → severity → count. A record belongs to several categories, so this map deliberately sums to more thann. Use it for facet counts, never for a total.sv— severity → count, each record exactly once. Use this for totals.s— source → count, each record exactly once.
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:
- Element 0 — template id.
0means the record carries its own full text. - Element 1 — index into
ts. A whole fetch batch shares one timestamp, so this table is short. - Elements 2+ — the template's slots, or the full field list for a raw record.
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.