API Documentation
Integrate real-time threat intelligence into your security workflows
Threat Radar API v1
The Threat Radar API provides programmatic access to real-time threat intelligence data from multiple sources. Integrate threat monitoring, vulnerability scanning, and security alerts directly into your applications, SIEM systems, or security workflows.
Quick Start
- Register for an account at radar.offseq.com/console
- Verify your email address
- Get your API key from the verification response or welcome email
- Complete your profile to manage the API key in the Console
- Start calling straight away — free keys are accepted at 30 requests/hour; upgrade in Console -> Billing for higher limits
- Start making API requests with your key
Base URL
https://radar.offseq.com/api/v1Response Format
application/jsonFeatures
- Real-time threat intelligence from multiple sources
- CVE vulnerability data with enriched metadata
- IoC (Indicators of Compromise) checking
- Product, vendor, and version-level monitoring
- Geographic threat distribution data (structured + AI-identified)
- RESTful API with JSON responses
- API access on every tier — Free included, at 30 requests/hour
- Per-tier rate limiting, from Free through Enterprise
What you can filter on
GET /threats supports 19 filter parameters plus aliases. Highlights:
- Identifier normalization —
2025-1974=cve-2025-1974=CVE-2025-1974 - Fuzzy version match — finds
affectedVersions=1.24.0even in unstructured news/Reddit posts - Multi-value —
tag=a,borseverity=critical&severity=high - CVSS range —
minCvss=9.0 - Exploitation & patch flags —
hasExploits=true,hasPatch=true - Date windows —
publishedAfter+publishedBeforeordays=7 - CWE filtering —
cwe=787 - Diagnostic headers — typos surface in
X-Unknown-Params
See the for the complete reference, or hit GET /api/v1/threats/filters for a machine-readable catalog.
Authentication
API Key Required
All API requests require a valid API key. Every tier can call the API, including Free — free keys are accepted at 30 requests/hour (75/day, 150/month). A Basic/Pro/Enterprise subscription or Pro Console Lifetime Access raises those quotas.
Pro Console-only access (no subscription) uses its own baseline of 45 requests/hour (150/day, 375/month) — see Rate Limits.
Header Authentication
Getting Your API Key
- Register at radar.offseq.com/console
- Verify your email address with the OTP sent to you
- Your API key will be provided in the verification response and welcome email
- Complete your profile with full name and primary use case to manage the key in Console
- Your key works immediately at the Free tier (30 requests/hour); activate a plan in Console -> Billing for higher limits
- Store your API key securely (treat it like a password)
Security Best Practices
- Never expose your API key in client-side code
- Store API keys in environment variables
- Regenerate keys if compromised (use the regenerate button in your account)
- Use HTTPS for all API requests
- Monitor your API usage regularly
Regenerating Your API Key
If your API key is compromised or you need a new one for any reason, you can regenerate it:
- Go to your account page
- After logging in, click the "Regenerate" button next to your API key
- Confirm the action (your old key will stop working immediately)
- Update your applications with the new API key
Warning: Regenerating your API key will immediately invalidate the old key. Make sure to update all your applications.
API Endpoints
/threatsRetrieve a paginated list of threats with optional filtering.
Supports 19 filter parameters with aliases, identifier normalization (CVE/CWE), and fuzzy version matching. See the for the complete reference. Quick summary below.
Most-used parameters
| Parameter | Type | Example |
|---|---|---|
| search | string | nginx |
| severity | low | medium | high | critical (multi) | critical |
| type | string (multi) | vulnerability |
| cve | string (normalized) | CVE-2025-1974 |
| cwe | string (multi, normalized) | CWE-787 |
| product | string (substring, case-insensitive) | nginx |
| vendor | string (substring, case-insensitive) | F5 |
| affectedVersions | string (multi, fuzzy text fallback) | 1.24.0 |
| tag | string (multi, exact) | rce |
| country | string (multi, structured + AI-identified) | US |
| source | string (multi, case-insensitive exact) | NVD |
| hasExploits | boolean | true |
| hasPatch | boolean | true |
| minCvss / maxCvss | number 0..10 | 9.0 |
| publishedAfter / publishedBefore | ISO date | 2025-01-01 |
| days | integer 1..3650 | 7 |
| sort | field name (prefix with - for desc) | -publishedDate |
| page / limit | integer (limit capped per tier) | 1 / 50 |
Example Request
curl -G "https://radar.offseq.com/api/v1/threats" \ -H "X-API-Key: tr_your_api_key_here" \ -d "severity=critical" \ -d "hasExploits=true" \ -d "minCvss=9" \ -d "days=7" \ -d "limit=20"
Response (200 OK)
{
"success": true,
"data": {
"threats": [
{
"_id": "6e1f2a...",
"slug": "cve-2025-1974-...",
"externalId": "CVE-2025-1974",
"source": "NVD",
"type": "vulnerability",
"title": "...",
"description": "...",
"cveId": "CVE-2025-1974",
"cvssScore": 9.8,
"severity": "critical",
"vendorProject": "...",
"product": "...",
"affectedVersions": ["=1.24.0", ">=1.25.0 <1.25.4"],
"cwes": ["CWE-787"],
"tags": ["rce", "rce-exploit"],
"affectedCountries": [],
"knownExploitsInWild": true,
"patchAvailable": true,
"patchLinks": ["https://..."],
"references": [{ "title": "...", "url": "...", "type": "advisory" }],
"publishedDate": "2025-...",
"enrichment": {
"enriched": true,
"aiGeneratedSummary": "...",
"aiGeneratedImpact": "...",
"aiGeneratedMitigation": "...",
"aiIdentifiedCountries": ["US", "DE"],
"confidence": 0.85
},
"reddit": { "postId": "...", "topComments": [ ... ] } // only on Reddit-sourced threats
}
],
"pagination": {
"page": 1, "limit": 20, "total": 142,
"pages": 8, "hasNext": true, "hasPrev": false
}
},
"meta": {
"timestamp": "2026-05-26T...",
"version": "v1",
"filterApplied": { "severity": "critical", "knownExploitsInWild": true, ... },
"sort": { "publishedDate": -1 },
"unknownParams": [] // present only if you sent unknown params
}
}meta.filterApplied echoes the exact Mongo filter applied — useful for debugging silent mismatches.
/threats/searchSearch-first variant of /threats. Requires q; accepts every filter the list endpoint supports. Use for autocomplete-style UIs where you want a smaller, flat response.
Query Parameters
q is required. All other parameters from the Filtering tab also work.
Example
curl -G "https://radar.offseq.com/api/v1/threats/search" \ -H "X-API-Key: tr_your_api_key_here" \ -d "q=nginx" \ -d "severity=critical" \ -d "hasExploits=true"
/threats/slug/{slug}Look up a single threat by the URL slug used on the public site (e.g. poc-code-published-for-critical-nginx-vulnerabilit-3d78edaa). Returns 404 with a JSON error if no match.
curl "https://radar.offseq.com/api/v1/threats/slug/poc-code-published-for-critical-nginx-vulnerabilit-3d78edaa" \ -H "X-API-Key: tr_your_api_key_here"
/threats/{id}Look up a single threat by 24-character Mongo ObjectId. Non-ObjectId values return a 400 with a hint pointing to the slug endpoint.
curl "https://radar.offseq.com/api/v1/threats/65d8f3a2c1e9b0a4d8f3a2c1" \ -H "X-API-Key: tr_your_api_key_here"
/threats/filtersself-describingMachine-readable catalog of every filter parameter, its aliases, and an example value. Use this in client code to discover supported filters at runtime instead of hard-coding.
curl "https://radar.offseq.com/api/v1/threats/filters" \
-H "X-API-Key: tr_your_api_key_here" | jq '.data.filters[] | {name, aliases, example}'/threats/feeds·GET/threats/stats/overview/feeds: per-source threat counts, latest update timestamp, severity breakdown, and exploited count for each ingested feed. /stats/overview: total threats, 24h/7d/30d additions, severity distribution, top affected countries, top types.
curl "https://radar.offseq.com/api/v1/threats/feeds" \ -H "X-API-Key: tr_your_api_key_here" | jq '.data.feeds[] | select(.count > 100)'
/threats/check-iocsCheck if your Indicators of Compromise match known threats.
Request Body
{
"indicators": [
{
"type": "ip",
"value": "192.168.1.100"
},
{
"type": "domain",
"value": "malicious-site.com"
},
{
"type": "hash",
"value": "a1b2c3d4e5f6..."
}
]
}/threats/monitorMonitor threats affecting specific products or vendors.
Request Body
{
"products": ["WordPress", "Apache", "nginx"],
"severities": ["high", "critical"],
"limit": 20
}/matchPrecise coordinate matching. Ask “is this exact package, at this exact version, affected?” bypurl,cpe, orpackage{name,ecosystem}. Version applicability is evaluated with each ecosystem’s native comparator (dpkg / rpm / apk / PEP 440 / semver), so Debian/Ubuntu/RHEL backport builds (e.g. 1.18.0-6+deb11u3) are not false-positived.
Request Body (any one form)
{ "purl": "pkg:npm/[email protected]" }
// or distro packages — include the release so revisions compare correctly:
{ "purl": "pkg:deb/debian/[email protected]+deb11u3?distro=bullseye" }
// or name + ecosystem + version (version separate):
{ "package": { "name": "lodash", "ecosystem": "npm" }, "version": "4.17.20" }
// or by CPE 2.3:
{ "cpe": "cpe:2.3:a:apache:http_server:2.4.48:*:*:*:*:*:*:*" }Supply the version inside the purl OR in the version field — not both (400 otherwise). By default the response includes coordinate matches whose version couldn’t be confirmed (confirmed:false); add "strict": true to return only version-confirmed hits.
Example Request
curl -X POST "https://radar.offseq.com/api/v1/match" \
-H "X-API-Key: tr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"purl":"pkg:npm/[email protected]"}'Response (200 OK)
{
"success": true,
"data": {
"query": "pkg:npm/lodash",
"mode": "purl",
"ecosystem": "npm",
"version": "4.17.20",
"totalCandidates": 3,
"matches": [
{
"id": "...", "cveId": "CVE-2021-23337",
"title": "lodash command injection", "severity": "high",
"cvssScore": 7.2, "slug": "lodash-command-injection-ab12cd",
"kev": null, // or { addedDate, dueDate, ransomwareUse, requiredAction }
"epss": { "score": 0.4, "percentile": 0.97 },
"knownExploitsInWild": false, "patchAvailable": true,
"fixedVersions": ["4.17.21"], // upgrade target(s), ecosystem-scoped
"remediation": "Upgrade to lodash 4.17.21 or later.",
"cwes": ["CWE-77"],
"references": ["https://nvd.nist.gov/vuln/detail/CVE-2021-23337"],
"matchBasis": "coordinate", // coordinate | cpe | coordinate-unconfirmed
"matchedRange": "<4.17.21",
"matchedCoordinate": "pkg:npm/lodash",
"confirmed": true // version confirmed inside an affected range
}
]
},
"meta": { "version": "v1", "strict": false }
}/match/batchBulk coordinate matching (OSV /querybatch style). Send a whole host’s asset inventory in one request; results[] align positionally to queries[]. Per-batch cap by tier: Free 25 · Pro Console 25 · Basic 200 · Pro 1,000 · Enterprise 4,000.
Request Body
{
"queries": [
{ "purl": "pkg:npm/[email protected]" },
{ "purl": "pkg:deb/ubuntu/[email protected]?distro=focal" },
{ "package": { "name": "django", "ecosystem": "PyPI" }, "version": "4.2.0" },
{ "cpe": "cpe:2.3:a:apache:http_server:2.4.48:*:*:*:*:*:*:*" }
]
}Response (200 OK)
{
"success": true,
"data": {
"results": [
{ "query": "pkg:npm/lodash", "mode": "purl", "ecosystem": "npm",
"version": "4.17.20", "totalCandidates": 3, "matches": [ /* ...as above... */ ] },
{ "query": "pkg:deb/ubuntu/openssl", "mode": "purl", "matches": [] },
{ "query": "PyPI/django", "mode": "package", "matches": [ /* ... */ ] },
{ "query": "...", "mode": "cpe", "matches": [ /* ... */ ] }
]
},
"meta": { "version": "v1", "queries": 4, "queriesWithMatches": 2, "strict": false }
}Tip: GET /api/v1/match/help returns the live, self-describing contract (forms, rules, matchBasis meanings, tier caps).
/inventory/registerContinuous monitoringRegister a host’s scanned coordinates (with network exposure) so Radar keeps watching: when a newly-ingested threat affects one of them, the account is alerted (email + realtime + webhook). The response returns the host’s current exposure-aware, risk-scored findings, inventory drift, and what’s new since the last scan — the two-way handshake threat-finder uses after a scan (it asks [Y/n/never]).
Request Body
{
"hostId": "<stable uuid>",
"hostname": "web-01",
"os": { "type": "linux", "distro": "ubuntu", "version": "22.04" },
"agentVersion": "0.1.4",
"monitor": true,
"assets": [
{ "purl": "pkg:deb/ubuntu/[email protected]?distro=jammy",
"exposure": "public", // public | private | loopback | none
"exposed": true, "runtime": true }
]
}Response (200 OK)
{
"success": true,
"data": {
"hostId": "...", "monitoring": true, "new": true, "assetCount": 1840,
"drift": { "added": 5, "removed": 2, "changed": 3 },
"summary": { "total": 12, "confirmed": 9, "kev": 2, "exposed": 3,
"bySeverity": { "critical": 1, "high": 4, "medium": 5, "low": 2 },
"byDecision": { "act-now": 2, "soon": 3, "schedule": 4, "track": 3 }, "actNow": 2 },
"top": [
{ "cveId": "CVE-2021-3711", "severity": "critical", "exposure": "public",
"exposed": true, "confirmed": true, "riskScore": 92, "decision": "act-now",
"coordinate": "pkg:deb/ubuntu/openssl", "fixedVersions": ["1.1.1f-1ubuntu2.17"],
"remediation": "Upgrade openssl…", "radarUrl": "https://radar.offseq.com/threat/..." }
],
"newSinceLast": [ /* findings new vs the previous scan of this host */ ],
"newSinceLastCount": 1
}
}GET /inventory — list your monitored hosts.GET /inventory/{hostId} — host detail + current findings.PATCH /inventory/{hostId} — toggle monitoring / rename.DELETE /inventory/{hostId} — deregister a host.Risk score (0–100) & decision fuse severity + EPSS + KEV + network exposure into an explainableact-now / soon / schedule / track band — the SSVC input most tools lack because they can’t see what’s actually running and exposed. Per-tier host caps: Free 2 · Pro Console 2 · Basic 5 · Pro 50 · Enterprise 500.
Filtering Reference
Every parameter the API accepts on GET /threats and GET /threats/search. Parameters combine with AND. Where indicated, a parameter accepts multiple values via comma (?tag=a,b) or repetition (?tag=a&tag=b).
Response headers help you debug filters
X-Unknown-Params— comma-separated list of parameters the API didn't recognize (typos surface here instead of silently returning the whole DB).X-Warnings— soft warnings (e.g. invalid sort field, malformed date) that didn't fail the request.X-Limit-Capped— set when your requestedlimitexceeded your tier cap (e.g.1000->50on Pro).
The JSON response also includes meta.filterApplied echoing the exact MongoDB filter — invaluable when a query doesn't match what you expect.
Complete parameter reference
| Parameter | Aliases | Type | Behavior | Example |
|---|---|---|---|---|
| search | q | string | Case-insensitive substring across title, description, cveId, product, vendorProject, tags, cwes, affectedVersions, AI summary, Reddit selftext. | nginx |
| type | types | string (multi) | vulnerability, exploit, malware, phishing, botnet, campaign, threat-actor, breach, indicator, security-tool, analysis, security-news. | vulnerability |
| severity | severities | enum (multi) | low | medium | high | critical. Invalid values silently dropped. | critical,high |
| cve | cveId | string (normalized) | "2025-1974", "cve-2025-1974", "CVE-2025-1974" all match the same record. Non-canonical input falls back to regex. | CVE-2025-1974 |
| cwe | cwes | string (multi, normalized) | "787" or "CWE-787" both resolve to CWE-787. | CWE-787,CWE-22 |
| product | — | string | Case-insensitive substring on structured Threat.product. Regex-safe. | nginx |
| vendor | vendorProject, vendors | string | Case-insensitive substring on structured Threat.vendorProject. | F5 |
| affectedVersions | version, versions, affectedVersion | string (multi) | Structured + fuzzy text match. Matches Threat.affectedVersions[] AND (by default) word-boundary regex of the version string in title, description, AI summary, Reddit selftext. Disable text fallback with ?fuzzy=false. | 1.24.0 |
| purl | — | string (multi) | Coordinate filter. Narrows to threats affecting this Package URL (indexed exact match on ecosystem+name). Does NOT compare versions — for version-aware applicability use POST /match. | pkg:npm/lodash |
| cpe | — | string (multi) | Coordinate filter by CPE 2.3 vendor/product. For version-range applicability use POST /match. | cpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:* |
| ecosystem | + product / packageName | string | OSV-style ecosystem (npm, PyPI, Go, Maven, Debian:11, …). Combine with product for a coordinate match. | ecosystem=npm&product=lodash |
| country | countries | string (multi) | Case-insensitive exact match against affectedCountries[] OR enrichment.aiIdentifiedCountries[]. | US |
| source | sources | string (multi) | Case-insensitive exact source name. Use /threats/feeds to enumerate available sources. | NVD |
| tag | tags | string (multi) | Case-insensitive exact match against tags[] element. | rce |
| hasExploits | exploited | boolean | true / false / 1 / 0 / yes / no. Matches knownExploitsInWild. | true |
| hasPatch | patched | boolean | true / false. Matches patchAvailable. | true |
| minCvss | — | number 0..10 | CVSS score ≥ N. | 9.0 |
| maxCvss | — | number 0..10 | CVSS score ≤ N. | 10 |
| publishedAfter | startDate, from | ISO 8601 date | publishedDate ≥ date. | 2025-01-01 |
| publishedBefore | endDate, to | ISO 8601 date | publishedDate ≤ date. | 2025-12-31 |
| days | — | integer 1..3650 | Threats ingested within the last N days (createdAt). | 7 |
| fuzzy | — | boolean | Only affects affectedVersions. Set to false to require a structured match (skip the text fallback). | false |
| page | — | integer ≥ 1 | Pagination. Default 1. | 2 |
| limit | — | integer | Items per page. Default 10. Capped per tier (Free 5 · Pro Console 25 · Basic 25 · Pro 50 · Enterprise 50). | 50 |
| sort | — | string | Allowed fields: publishedDate, modifiedDate, createdAt, updatedAt, cvssScore, severity, voteScore, viewCount. Prefix with - for descending. Multiple fields comma-separated. Default: -publishedDate. | -cvssScore |
| order | — | asc | desc | Used with sort when you don't want to prefix with -. | desc |
Don't remember which parameter takes which alias? Hit GET /api/v1/threats/filters to fetch the live catalog.
Spotlight: fuzzy version matching
Many threats — especially news articles, Reddit posts, and write-ups — describe affected versions in prose without populating the structured affectedVersions[] field. By default affectedVersions=1.24.0 matches:
- Threats whose structured
affectedVersions[]array contains the value, OR - The version string appears in
title,description,enrichment.aiGeneratedSummary,enrichment.aiGeneratedImpact, orreddit.selftextText
Word-boundary regex anchored on non-digit/non-dot ensures 1.24.0 doesn't accidentally match 11.24.05.
Stored values use a single canonical comparator grammar so ranges are machine-comparable: =1.24.0 (exact), <2.4.49 (fixed in 2.4.49), >=2.0.0 <2.5.2 (interval, upper bound exclusive), * (all versions). Operators are >= > <= < =; disjoint ranges are separate array entries. Lower bounds are inclusive, "fixed in X" is the exclusive upper bound <X — matching the OSV / CVE 5.x / semver convention. A query for affectedVersions=1.24.0 still matches the stored pin =1.24.0.
Need strict structured-only? Add &fuzzy=false.
Spotlight: coordinate matching (purl / CPE)
Free-text name search is noisy (name collisions) and lossy at scale. When you know a package’s exact coordinate, match by it instead:
- Filter the list endpoint with
?purl=/?cpe=/?ecosystem=&product=(indexed exact coordinate, no version compare). - Match a concrete version with
POST /match/POST /match/batch— see the Endpoints tab.
Threats now carry two structured coordinate fields (returned in every threat object):
"affectedPackages": [
{ "ecosystem": "npm", "purlType": "npm", "name": "lodash",
"purl": "pkg:npm/lodash", "ranges": ["<4.17.21"], "versions": [], "source": "cve" }
],
"cpes": [
{ "cpe23": "cpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:*",
"vendor": "apache", "product": "http_server", "versionEndExcluding": "2.4.49" }
]Distro-backport correctness: for pkg:deb / pkg:rpm / pkg:apk packages, POST /match compares versions with the distro’s own algorithm (dpkg / rpm), keeping the full revision (1.18.0-6+deb11u3). A build that backported the fix is correctly reported not affected — pass the ?distro= qualifier so the right release is used.
affectedVersions[] remains a canonical, ecosystem-agnostic superset of every package’s ranges — so the simple version filter keeps working everywhere.
Cookbook: common patterns
CISA-KEV-style watchlist (critical CVEs being exploited in the wild)
GET /api/v1/threats?severity=critical&hasExploits=true&hasPatch=true&days=30&sort=-cvssScore&limit=50
Find anything affecting nginx 1.24.0 (fuzzy text match included)
GET /api/v1/threats?product=nginx&affectedVersions=1.24.0&limit=50
Monitor my stack — multi-vendor with severity floor
GET /api/v1/threats?vendor=F5&severity=high,critical&days=14
Memory-corruption (CWE-787) RCE class, last quarter
GET /api/v1/threats?cwe=787&tag=rce&publishedAfter=2025-01-01&publishedBefore=2025-03-31
Specific CVE by any input form
GET /api/v1/threats?cve=2025-1974 GET /api/v1/threats?cve=cve-2025-1974 GET /api/v1/threats?cve=CVE-2025-1974
All three return the same record.
Page through all critical threats in chronological order
GET /api/v1/threats?severity=critical&sort=publishedDate&order=asc&page=1&limit=50
Find threats touching the US (structured or AI-identified)
GET /api/v1/threats?country=US&severity=critical&days=30
Error responses
| Status | When | Body |
|---|---|---|
| 400 | Bad request — e.g. /threats/{id} with a non-ObjectId, or missing q on /search. | {"success":false,"error":"...","message":"..."} |
| 401 | Missing or invalid API key. | {"error":"API key required"} |
| 403 | Profile incomplete or no Pro Console access. | {"error":"Pro Console access required"} |
| 404 | No matching threat (single-document endpoints). | {"success":false,"error":"Threat not found"} |
| 429 | Rate limit exceeded (hourly/daily/monthly). | See the Rate Limits tab — includes retry-after header. |
| 500 | Internal error. The response body's message field includes a short description. | {"success":false,"error":"...","message":"..."} |
Why does my unknown parameter not return an error?
For backward-compatibility, the API never 400s on unknown parameters — it accepts the request, ignores the typo, and surfaces it via the X-Unknown-Params response header and the meta.unknownParams JSON field. Inspect those during integration to catch typos early. If you'd like a strict-mode flag that 400s instead, contact support.
Code Examples
cURL
Get Latest Critical Threats
curl -X GET "https://radar.offseq.com/api/v1/threats?severity=critical&limit=10" \ -H "X-API-Key: tr_your_api_key_here"
Search for CVE
curl -X GET "https://radar.offseq.com/api/v1/threats/search?q=CVE-2024-0001" \ -H "X-API-Key: tr_your_api_key_here"
Check IoCs
curl -X POST "https://radar.offseq.com/api/v1/threats/check-iocs" \
-H "X-API-Key: tr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"indicators": [
{"type": "ip", "value": "192.168.1.100"},
{"type": "domain", "value": "suspicious-site.com"}
]
}'Filter by affected version (fuzzy text fallback)
Matches structured affectedVersions[] AND any text mention of 1.24.0 in title, description, AI summary, or Reddit selftext.
curl -G "https://radar.offseq.com/api/v1/threats" \ -H "X-API-Key: tr_your_api_key_here" \ -d "product=nginx" \ -d "affectedVersions=1.24.0" \ -d "limit=50"
CWE class + RCE tag + date range
curl -G "https://radar.offseq.com/api/v1/threats" \ -H "X-API-Key: tr_your_api_key_here" \ -d "cwe=787" \ -d "tag=rce" \ -d "publishedAfter=2025-01-01" \ -d "publishedBefore=2025-03-31" \ -d "sort=-cvssScore"
CISA-KEV-style: exploited critical with patches, last 30 days
curl -G "https://radar.offseq.com/api/v1/threats" \ -H "X-API-Key: tr_your_api_key_here" \ -d "severity=critical" \ -d "hasExploits=true" \ -d "hasPatch=true" \ -d "days=30" \ -d "minCvss=9.0" \ -d "sort=-publishedDate"
Discover the filter catalog at runtime
curl -X GET "https://radar.offseq.com/api/v1/threats/filters" \
-H "X-API-Key: tr_your_api_key_here" \
| jq '.data.filters[] | "\(.name) (\(.aliases // [] | join(",")))"'Inspect diagnostic headers (catch typos)
-D - dumps response headers. Watch for X-Unknown-Params and X-Warnings.
curl -G -D - "https://radar.offseq.com/api/v1/threats" \ -H "X-API-Key: tr_your_api_key_here" \ -d "severity=critical" \ -d "afected_versions=1.24.0" \ -o /dev/null \ -s | grep -i "^x-" # X-Unknown-Params: afected_versions # X-RateLimit-Remaining-Hourly: 224
Python
Basic Setup
import requests
import json
API_KEY = "tr_your_api_key_here"
BASE_URL = "https://radar.offseq.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}
def get_threats(severity="critical", limit=10):
url = f"{BASE_URL}/threats"
params = {"severity": severity, "limit": limit}
response = requests.get(url, headers=HEADERS, params=params)
response.raise_for_status()
return response.json()
# Get critical threats
threats = get_threats()
print(f"Found {len(threats['data']['threats'])} critical threats")Monitor Your Infrastructure
def monitor_products(products, severities=["high", "critical"]):
url = f"{BASE_URL}/threats/monitor"
data = {
"products": products,
"severities": severities,
"limit": 50
}
response = requests.post(url, headers=HEADERS, json=data)
response.raise_for_status()
return response.json()
# Monitor your tech stack
my_products = ["WordPress", "Apache", "MySQL", "Ubuntu"]
threats = monitor_products(my_products)
for threat in threats['data']['threats']:
print(f" {threat['severity'].upper()}: {threat['title']}")
if threat.get('cveId'):
print(f" CVE: {threat['cveId']}")
print(f" Product: {threat.get('product', 'N/A')}")
print()Advanced filtering with pagination
Combines vendor + severity floor + CVSS + recency + sort. Iterates through pages until exhausted, warns on unknown params.
def iterate_threats(**filters):
"""Generator yielding every matching threat, paging server-side."""
page = 1
while True:
r = requests.get(
f"{BASE_URL}/threats",
headers=HEADERS,
params={**filters, "page": page, "limit": 50},
)
r.raise_for_status()
unknown = r.headers.get("X-Unknown-Params")
if unknown:
print(f"[warn] API ignored unknown params: {unknown}")
body = r.json()
for threat in body["data"]["threats"]:
yield threat
if not body["data"]["pagination"]["hasNext"]:
break
page += 1
# Every exploited critical with CVSS >= 9 in last 30 days, F5 vendor
for t in iterate_threats(
vendor="F5",
severity="critical",
hasExploits="true",
minCvss=9.0,
days=30,
sort="-cvssScore",
):
print(t["cveId"], t["title"][:80])Lookup by URL slug
When you have a radar.offseq.com URL and need the underlying threat record.
from urllib.parse import urlparse
def fetch_by_url(threat_url):
slug = urlparse(threat_url).path.rsplit("/", 1)[-1]
r = requests.get(f"{BASE_URL}/threats/slug/{slug}", headers=HEADERS)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()["data"]
threat = fetch_by_url("https://radar.offseq.com/threat/poc-code-published-for-critical-nginx-vulnerabilit-3d78edaa")
print(threat["title"], threat.get("cvssScore"))JavaScript / Node.js
Basic client + advanced filters
const axios = require('axios');
const api = axios.create({
baseURL: 'https://radar.offseq.com/api/v1',
headers: { 'X-API-Key': process.env.RADAR_API_KEY }
});
// Composable filter helper — every filter from the Filtering tab works
async function searchThreats(filters) {
const { data, headers } = await api.get('/threats', { params: filters });
// Catch typos early — unknown params surface here
if (headers['x-unknown-params']) {
console.warn('Unknown params (ignored):', headers['x-unknown-params']);
}
if (headers['x-warnings']) {
console.warn('Warnings:', headers['x-warnings']);
}
if (headers['x-limit-capped']) {
console.warn('Limit capped:', headers['x-limit-capped']);
}
return data.data;
}
// CISA-KEV-style: exploited critical with patches, last 30 days
const kev = await searchThreats({
severity: 'critical',
hasExploits: 'true',
hasPatch: 'true',
minCvss: 9.0,
days: 30,
sort: '-cvssScore',
limit: 50
});
console.log(`${kev.pagination.total} matching threats`);
kev.threats.forEach(t => console.log(t.cveId, t.title));Lookup by slug + IoC matching
// Get a single threat by URL slug
async function fetchBySlug(slug) {
try {
const { data } = await api.get(`/threats/slug/${slug}`);
return data.data;
} catch (e) {
if (e.response?.status === 404) return null;
throw e;
}
}
// Check observed IoCs against the threat corpus
async function checkIoCs(indicators) {
const { data } = await api.post('/threats/check-iocs', { indicators });
return data.data.results;
}
// Usage
const threat = await fetchBySlug('poc-code-published-for-critical-nginx-vulnerabilit-3d78edaa');
if (threat) console.log(threat.title);
const matches = await checkIoCs([
{ type: 'ip', value: '192.168.1.100' },
{ type: 'domain', value: 'suspicious-site.com' }
]);Discover filters at runtime (useful for SDK auto-generation)
const { data } = await api.get('/threats/filters');
console.table(data.data.filters.map(f => ({
name: f.name,
aliases: (f.aliases || []).join(', '),
type: f.type,
example: f.example
})));Rate Limits
Rate limits are enforced to ensure fair usage and system stability. Every tier has its own quota — a Basic/Pro/Enterprise plan or Pro Console access raises it.
Pro Console-only access uses its own baseline limits until a subscription is active.
API Access
API access is included on every tier. Free accounts get 30 requests/hour; Basic/Pro/Enterprise subscriptions and Pro Console Lifetime Access raise the quotas shown below.
Free Tier
Pro Console (no subscription)
Basic Tier
Pro Tier
Enterprise
Rate Limit Headers (Pro Example)
API responses include rate limit information in the headers:
X-RateLimit-Limit-Hourly: 225 X-RateLimit-Remaining-Hourly: 220 X-RateLimit-Reset-Hourly: 1640995200 X-RateLimit-Limit-Monthly: 17500 X-RateLimit-Remaining-Monthly: 17460
Rate Limit Exceeded
When you exceed your rate limit, you'll receive a 429 status code:
{
"success": false,
"error": "Rate limit exceeded",
"message": "Monthly limit of 17500 requests exceeded. Upgrade your plan for higher limits.",
"rateLimits": {
"requestsPerHour": 225,
"requestsPerDay": 750,
"requestsPerMonth": 17500
}
}Need higher limits or enterprise throughput? Contact us to upgrade your plan.
Contact SupportSDKs & Integration Tools
Integrate Threat Radar into your security stack with these tools and examples.
The official open-source host scanner. It enumerates a machine’s running services, resolves each to an exact package coordinate, and batches them through POST /api/v1/match/batch — so version applicability (including Linux distro backports) is decided server-side. The fastest way to go from an API key to “what on my box is vulnerable.”
Install (Linux & macOS)
# Homebrew (macOS/Linux) — prebuilt, no toolchain brew install offseq/tap/threat-finder # or a prebuilt binary via cargo-binstall cargo binstall threat-finder # or build from crates.io cargo install threat-finder
Prefer a tarball? Grab a prebuilt release (x86_64 / arm64). Windows isn’t supported.
Run & gate CI
# scan this host (key via env var) export OFFSEQ_API_KEY=tr_your_api_key_here threat-finder # CI: no prompts, JSON + SARIF, fail on exposed exploited CVEs OFFSEQ_API_KEY=$KEY threat-finder --yes \ --json --output report.json --sarif results.sarif \ --fail-on exposed
Why it’s accurate: it sends package coordinates (purl/CPE), not just names, to the , so the server compares versions with each ecosystem’s native algorithm (dpkg / rpm / apk / PEP 440 / semver). See the for how purl/CPE applicability works.
SIEM Integrations
- Splunk App (custom)
- Elastic Security integration
- QRadar custom DSM
- ArcSight FlexConnector
Contact support for integration guides
Automation
- Slack/Mattermost notifications
- Jira ticket creation
- PagerDuty alerts
- Webhook integrations
Security Tools
- Vulnerability scanners
- Threat hunting platforms
- SOC automation tools
- Risk assessment systems
Languages
- Python (requests, httpx)
- JavaScript/Node.js (axios, fetch)
- Go (net/http)
- PowerShell (Invoke-RestMethod)
Python Helper Class
class ThreatRadarAPI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://radar.offseq.com/api/v1"
self.headers = {"X-API-Key": api_key}
def get_threats(self, **params):
response = requests.get(
f"{self.base_url}/threats",
headers=self.headers,
params=params
)
response.raise_for_status()
return response.json()
def search_threats(self, query, **params):
params['q'] = query
response = requests.get(
f"{self.base_url}/threats/search",
headers=self.headers,
params=params
)
response.raise_for_status()
return response.json()
def check_iocs(self, indicators):
response = requests.post(
f"{self.base_url}/threats/check-iocs",
headers=self.headers,
json={"indicators": indicators}
)
response.raise_for_status()
return response.json()
# Usage
api = ThreatRadarAPI("tr_your_api_key_here")
threats = api.get_threats(severity="critical", limit=10)Slack Notification Script
import requests
import json
def send_threat_alert_to_slack(webhook_url, threat):
message = {
"text": f"New {threat['severity'].upper()} Threat Detected",
"attachments": [
{
"color": "danger" if threat['severity'] == "critical" else "warning",
"fields": [
{"title": "Title", "value": threat['title'], "short": False},
{"title": "CVE", "value": threat.get('cveId', 'N/A'), "short": True},
{"title": "Severity", "value": threat['severity'].title(), "short": True},
{"title": "Product", "value": threat.get('product', 'N/A'), "short": True},
{"title": "Published", "value": threat.get('publishedDate', 'N/A'), "short": True}
]
}
]
}
requests.post(webhook_url, json=message)
# Monitor and alert
api = ThreatRadarAPI("tr_your_api_key_here")
threats = api.get_threats(severity="critical", limit=5)
for threat in threats['data']['threats']:
send_threat_alert_to_slack(SLACK_WEBHOOK_URL, threat)Webhook Integration
Webhooks allow you to receive real-time HTTP POST notifications when threats match your custom feeds. Configure webhooks in Console → Automations (Pro Console required).
Delivery Format
When a webhook fires, Threat Radar sends an HTTP POST request to your configured URL with a JSON body.
Request Headers
Content-Type: application/json User-Agent: ThreatRadar-Automations/1.0 X-Threat-Radar-Event: custom_feed_alert | starred_threat | test X-Threat-Radar-Secret: <your-webhook-secret>
Use the X-Threat-Radar-Secret header to verify requests originate from Threat Radar. Compare it against the secret shown in your webhook settings. Any custom headers you configured are also included.
Event Types
| Event | Description |
|---|---|
custom_feed_alert | New threats matched one of your custom feeds |
starred_threat | A threat was starred or unstarred |
test | Manual test event triggered from the Console |
Payload: custom_feed_alert
{
"event": "custom_feed_alert",
"generatedAt": "2026-05-18T12:00:00.000Z",
"user": {
"id": "abc123",
"email": "[email protected]"
},
"data": {
"feed": {
"id": "feed_id",
"name": "My Critical Alerts",
"description": "All critical vulns",
"filters": { "severity": ["critical"], "dateRange": "7d" },
"alertsEnabled": true,
"createdAt": "2026-05-01T00:00:00.000Z"
},
"threatCount": 3,
"threats": [
{
"id": "threat_id",
"title": "CVE-2026-12345: RCE in Example Product",
"slug": "cve-2026-12345-rce-in-example-abc123",
"severity": "critical",
"type": "vulnerability",
"source": "CVE Database V5",
"publishedAt": "2026-05-18T08:00:00.000Z",
"cve": "CVE-2026-12345",
"vendor": "ExampleCorp",
"product": "Example Product",
"description": "Remote code execution via ...",
"tags": ["cve", "rce"],
"url": "https://radar.offseq.com/threat/cve-2026-12345-..."
}
]
}
}Payload: starred_threat
{
"event": "starred_threat",
"generatedAt": "2026-05-18T12:00:00.000Z",
"user": { "id": "abc123", "email": "[email protected]" },
"data": {
"threat": {
"id": "threat_id",
"title": "CVE-2026-12345: RCE in Example Product",
"slug": "cve-2026-12345-rce-in-example-abc123",
"severity": "critical",
"type": "vulnerability",
"source": "CVE Database V5",
"publishedAt": "2026-05-18T08:00:00.000Z",
"cve": "CVE-2026-12345",
"vendor": "ExampleCorp",
"product": "Example Product",
"description": "Remote code execution via ...",
"tags": ["cve", "rce"],
"indicators": [],
"url": "https://radar.offseq.com/threat/cve-2026-12345-..."
},
"star": {
"notes": "Affects our prod servers",
"tags": ["internal", "p0"],
"priority": "critical",
"starredAt": "2026-05-18T12:00:00.000Z"
},
"action": "starred"
}
}Payload: test
{
"event": "test",
"generatedAt": "2026-05-18T12:00:00.000Z",
"user": { "id": "abc123", "email": "[email protected]" },
"data": {
"note": "Manual webhook test event",
"triggeredBy": "[email protected]"
}
}Receiver Example (Node.js / Express)
const express = require('express');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = 'trwh_your_secret_here';
app.post('/webhooks/threat-radar', (req, res) => {
// 1. Verify the request
const secret = req.headers['x-threat-radar-secret'];
if (secret !== WEBHOOK_SECRET) {
return res.status(401).json({ error: 'Invalid secret' });
}
const { event, data } = req.body;
// 2. Handle the event
switch (event) {
case 'custom_feed_alert':
console.log(`Feed "${data.feed.name}": ${data.threatCount} new threats`);
for (const threat of data.threats) {
console.log(` - [${threat.severity}] ${threat.title}`);
}
break;
case 'starred_threat':
console.log(`Threat ${data.action}: ${data.threat.title}`);
break;
case 'test':
console.log('Test event received');
break;
}
// 3. Respond with 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000);Receiver Example (Python / Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "trwh_your_secret_here"
@app.route("/webhooks/threat-radar", methods=["POST"])
def handle_webhook():
# 1. Verify the request
secret = request.headers.get("X-Threat-Radar-Secret", "")
if secret != WEBHOOK_SECRET:
return jsonify({"error": "Invalid secret"}), 401
payload = request.get_json()
event = payload.get("event")
data = payload.get("data", {})
# 2. Handle the event
if event == "custom_feed_alert":
feed = data["feed"]
print(f"Feed '{feed['name']}': {data['threatCount']} new threats")
for threat in data["threats"]:
print(f" - [{threat['severity']}] {threat['title']}")
elif event == "starred_threat":
print(f"Threat {data['action']}: {data['threat']['title']}")
elif event == "test":
print("Test event received")
# 3. Respond with 200
return jsonify({"received": True}), 200Important Notes
- Your endpoint must respond within 8 seconds or the delivery is marked as failed.
- Return any
2xxstatus code to acknowledge receipt. - Failed deliveries are not retried automatically. Check delivery logs in Console → Automations.
- Use the Test button in webhook settings to verify your endpoint before going live.
- The webhook secret is included as-is in the header (not HMAC). Rotate it via Console if compromised.