Introduction
VULONE exposes its intelligence two ways: a REST API for asking questions one request at a time, and a TAXII 2.1 feed your SIEM subscribes to and re-pulls incrementally. One key works for both.
The REST API
Read-only JSON over HTTPS under https://app.vulone.com/api/v1. Search actors across every indexed forum dump, pull a profile with its recovered content, pivot a selector to every account that used it, read ransomware crews and their leak-site victims, query CVEs enriched with EPSS and KEV, and look up indicators with malware attribution.
curl -H "Authorization: Bearer vul_your_key" \
https://app.vulone.com/api/v1/search?q=intelbrokerimport requests
r = requests.get(
"https://app.vulone.com/api/v1/search?q=intelbroker",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/search?q=intelbroker", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();See Quickstart to make your first request.
The TAXII feed
Where the REST endpoints answer a question, a TAXII collection is something your SIEM subscribes to. It pulls the collection once, remembers the newest object it saw, and every pull after asks only for what is new. The feed speaks STIX 2.1, so Splunk, Microsoft Sentinel, MISP and OpenCTI ingest it without a custom parser.
REST base URL
https://app.vulone.com/api/v1
TAXII discovery
https://app.vulone.com/taxii2/
Next steps
- Quickstart: your first request and how to read the response.
- Authentication: how keys and scopes work.
- Pagination: the contract every list endpoint follows.
- Subscribing: wire the feed into your SIEM.
Quickstart
From nothing to a paged search result in three requests.
Create a key
Open the API tab in the dashboard and create a key. It is shown once and looks like vul_…. Keys inherit the plan of the account that created them.
Your first request
Check what the key can do and how much quota it has left:
curl -H "Authorization: Bearer vul_your_key" https://app.vulone.com/api/v1/meimport requests
r = requests.get(
"https://app.vulone.com/api/v1/me",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/me", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();{
"plan": "enterprise",
"scopes": ["*"],
"rate_limit": { "limit": 100000, "used_24h": 17, "remaining": 99982, "window": "24h" }
}Search for an actor
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/search?q=quake&per_page=50&page=0"import requests
r = requests.get(
"https://app.vulone.com/api/v1/search?q=quake&per_page=50&page=0",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/search?q=quake&per_page=50&page=0", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();{
"query": "quake",
"page": 0, "per_page": 50, "count": 45,
"total": 327, "total_exact": true,
"has_more": true, "next_page": 1,
"results": [
{
"username": "quake", "email": "…",
"forum": "BreachForums", "db": "d_bf_2022", "id": 37873,
"merged": 2,
"sources": [ { "db": "d_bf_2022", "id": 37873 }, { "db": "d_bf_to_users", "id": 37873 } ]
}
]
}Each result carries the db and id you pass to the actor endpoints. Follow next_page until has_more is false.
Pull the profile
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/actor?db=d_bf_2022&id=37873"import requests
r = requests.get(
"https://app.vulone.com/api/v1/actor?db=d_bf_2022&id=37873",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/actor?db=d_bf_2022&id=37873", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Next steps
- Pagination: why
countcan be belowper_page. - Actors: content, linked accounts and selectors for one profile.
- Errors: every failure is JSON with a stable
code.
Authentication
Every request carries an API key. Create one in the dashboard API tab; send it in whichever form your client prefers.
Send the key
# Bearer token (REST and TAXII)
curl -H "Authorization: Bearer vul_your_key" https://app.vulone.com/api/v1/search?q=intelbroker
# X-API-Key header
curl -H "X-API-Key: vul_your_key" https://app.vulone.com/api/v1/search?q=intelbroker
# HTTP Basic, key as the password (what most TAXII clients send)
curl -u "vulone:vul_your_key" https://app.vulone.com/taxii2/Scopes
Keys can be scoped. A key limited to search,actor is refused at the other endpoints with 403 and a required field naming the missing scope. A key with no scopes, or the scope *, has full access. The scopes are:
| Parameter | Description | |
|---|---|---|
search | Search and the forum list. | |
actor | Profiles, content, linked accounts, dossiers. | |
selector | Selector pivots and per-actor selectors. | |
aliases | Alias candidates. | |
graph | Private-message graph. | |
ransomware | Crews, victims and ATT&CK. | |
cve | Vulnerability records. | |
ioc | Indicators, C2 endpoints, the blocklist and revocations. | |
export | The offline artifact and the delta feed (Offline sync & exports). |
Keeping keys safe
A key is a bearer credential: anyone holding it is you. Keep it server-side, never in a browser or a mobile app, and revoke it from the dashboard the moment it leaks. Every request is logged against the key that made it.
Rate limits
Each key has a daily request quota set by its plan. Every response counts against it, including errors.
Quota
The window is a rolling 24 hours. The API tab shows your last day, and /api/v1/me returns the same numbers as JSON. The TAXII feed shares the ledger, so an incremental sync every few minutes is well within a normal quota; a full re-pull of the indicator corpus is not, which is exactly what added_after is for.
Headers
Every response carries the quota state:
X-RateLimit-Limit: 100000
X-RateLimit-Remaining: 99982
X-RateLimit-Window: 24h
X-RateLimit-Reset: 1758153600When you hit 429
The response is JSON with code: "rate_limited", a Retry-After header in seconds and X-RateLimit-Reset as a Unix timestamp. Requests that are refused with 429 do not count against the quota. Back off, and if you are polling, switch to since on REST lists or added_after on TAXII so each poll only asks for what changed.
Pagination
Every list endpoint follows one contract, so a client written against one of them works against all of them.
The contract
Lists take a zero-based page and a per_page, and answer with the fields below. Ordering is deterministic, so a row never appears on two pages and never disappears between them.
{
"page": 6,
"per_page": 50,
"count": 27,
"total": 327,
"total_exact": true,
"has_more": false,
"next_page": null
}| Parameter | Description | |
|---|---|---|
count | Items on this page. | |
total | Matching records across the whole result set. | |
total_exact | false when an exact count would be too expensive; total is then a lower bound. | |
has_more | Whether another page exists. This is the field to stop on. | |
next_page | The page to request next, or null on the last page. |
Walking a result set
page=0
while :; do
body=$(curl -s -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/ransomware/victims?since=2026-09-01&per_page=200&page=$page")
echo "$body" | jq '.victims[]'
[ "$(echo "$body" | jq .has_more)" = "true" ] || break
page=$((page+1))
doneMerged accounts
On /search, records of the same account on different dumps of one board are merged into a single result whose sources array lists every underlying record. Nothing is dropped: total counts records, so a page can carry fewer than per_page results after merging while has_more is still true. Pass dedupe=0 for one result per raw record.
Errors
Every error is JSON, including unknown paths. Never parse HTML from this API.
Shape
{
"error": "This key is not scoped for cve",
"code": "scope",
"status": 403,
"required": "cve"
}error is for humans and may change; code is stable and is what your client should branch on.
Codes
| Parameter | Description | |
|---|---|---|
400 | bad_request | A parameter is missing or malformed. The code names it, for example missing_q or bad_since. |
401 | unauthorized | Missing or invalid key. Check the header and that the key is not revoked. |
402 | payment_required | The plan behind the key does not include the API. |
403 | forbidden | The key is not scoped for this endpoint or collection. |
404 | not_found | Unknown endpoint, dump, actor, group or object. |
405 | method_not_allowed | Anything but GET. The v1 API is read-only. |
429 | rate_limited | Daily quota exceeded; see Rate limits. |
503 | unavailable | A backing search timed out or a feed is not initialised yet. Retry with a narrower query. |
Search
Accounts matching a handle or email across every indexed forum dump, merged across snapshots of the same board.
Search accounts
/api/v1/searchScope: search. Records of the same account on different dumps of one board are merged into one result with a sources array (see Pagination). Up to page 200; use narrower queries or the scope parameter to go deeper.
| Parameter | Description | |
|---|---|---|
q | required | Username, email or prefix. At least two characters. |
page | optional | Zero-based page. |
per_page | optional | Up to 100, default 50. |
scope | optional | all (default), username or email. Email scope accepts * wildcards. |
dedupe | optional | 0 returns one result per raw record with no merging. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/search?q=intelbroker&per_page=50&page=0"import requests
r = requests.get(
"https://app.vulone.com/api/v1/search?q=intelbroker&per_page=50&page=0",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/search?q=intelbroker&per_page=50&page=0", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Each result carries username, email, forum, db, id, registered, last_seen, posts, a match score and the url of the profile in the platform.
List forums
/api/v1/forumsScope: search. Every dump we index with its display name, account count and board. Dumps sharing a board value are snapshots of the same community; an account id is only meaningful inside one db.
Actors
One account: its profile, everything recovered for it, the accounts we treat as the same person, and every selector it used.
All actor endpoints identify an account by db and id from Search. actor (the table) and idcol are optional; they are filled from the dump's layout when omitted.
Profile
/api/v1/actorScope: actor. The profile fields, with passwords, salts and tokens withheld, plus linked_accounts.
| Parameter | Description | |
|---|---|---|
db | required | The dump, e.g. d_bf_2022. |
id | required | The account id within that dump. |
actor | optional | Table name, from Search. |
idcol | optional | Id column, from Search. |
Linked accounts
/api/v1/actor/linksScope: actor. Accounts on other dumps of the same board that we treat as the same person, each with a via:
email: the account carries an email that is unique on both dumps and passes placeholder checks.actor_link: a high-confidence link computed from shared selectors or PGP keys.
The same numeric id on two dumps is two different people unless it appears here.
Content
/api/v1/actor/contentScope: actor. Posts, private messages or threads recovered for the account. Private messages recovered from a linked account carry an account object naming the dump and id they came from.
| Parameter | Description | |
|---|---|---|
db, id | required | As above. |
type | optional | post (default), pm or thread. |
offset, limit | optional | Paging, up to 200 per call. Answers carry has_more and next_offset. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/actor/content?db=d_bf_2022&id=37873&type=pm&limit=50"import requests
r = requests.get(
"https://app.vulone.com/api/v1/actor/content?db=d_bf_2022&id=37873&type=pm&limit=50",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/actor/content?db=d_bf_2022&id=37873&type=pm&limit=50", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Selectors
/api/v1/actor/selectorsScope: selector. Every selector tied to the account: wallets, Jabber, Tox, Session, PGP fingerprints, emails, IPs, with where each was seen and how often. Each carries a pivot link into Selectors.
| Parameter | Description | |
|---|---|---|
db, id | required | As above. |
limit | optional | Up to 500. |
Aliases, graph and dossier
/api/v1/aliasesScope: aliases. Scored alias candidates across forums, each with the evidence behind the score. Scores are additive over independent signals and capped at 100; read the evidence before acting on a score.
/api/v1/graphScope: graph. Who the account exchanged private messages with. depth=2 returns the ego network as nodes and edges; limit up to 60.
/api/v1/dossierScope: dossier. The full analyst dossier for the account in one document.
Selectors
Pivot a selector, a wallet, a Jabber ID, an email, to every account that used it.
Pivot
/api/v1/selectorScope: selector. Exact match first; when nothing matches exactly, a prefix search. The answer says which with match.
| Parameter | Description | |
|---|---|---|
value | required | The selector to pivot on. |
kind | optional | Restrict the prefix search to one kind: btc, xmr, eth, pgp, jabber, tox, session, telegram, email, ipv4, … |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/selector?value=bc1qexampleaddress"import requests
r = requests.get(
"https://app.vulone.com/api/v1/selector?value=bc1qexampleaddress",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/selector?value=bc1qexampleaddress", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Each hit names the account (db, id, username, forum), where the selector was seen, how many times, and the surrounding context.
Ransomware
Crews, their published contacts, ATT&CK-mapped techniques, tooling and playbooks, and every leak-site victim claim we record.
List crews
/api/v1/ransomware/groupsScope: ransomware. Every tracked crew with aliases, status, origin, victim counts (total and last 30 days), technique and tool counts, logo and profile links.
| Parameter | Description | |
|---|---|---|
status | optional | Filter by active, defunct, disrupted, rebranded. |
q | optional | Name or alias substring. |
Crew profile
/api/v1/ransomwareScope: ransomware. One crew: summary, published contacts (PGP keys unwrapped), techniques with detections, tooling, playbook and sources. Omit group for a compact list.
| Parameter | Description | |
|---|---|---|
group | optional | A crew slug, e.g. lockbit3. |
Victims
/api/v1/ransomware/victimsScope: ransomware. Leak-site claims across every crew, newest first, one row per claim with sector, country, domain and the claim post. Use since to poll for new claims.
| Parameter | Description | |
|---|---|---|
group | optional | Crew slug. |
country | optional | ISO country code. |
sector | optional | Sector label as returned by the API. |
q | optional | Victim name or domain prefix. |
since | optional | ISO 8601 date or unix timestamp; claims discovered on or after it. |
page, per_page | optional | Up to 200 per page. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/ransomware/victims?since=2026-09-01&country=DE"import requests
r = requests.get(
"https://app.vulone.com/api/v1/ransomware/victims?since=2026-09-01&country=DE",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/ransomware/victims?since=2026-09-01&country=DE", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Claims are the crews' own assertions from their leak sites and are not verified breaches.
Vulnerabilities
Every CVE from NVD, synced every 30 minutes and enriched with EPSS, CISA KEV, ransomware linkage and automated analysis.
One record
/api/v1/cveScope: cve. With id, the full record: description, CVSS, EPSS, KEV state including known ransomware use, weaknesses, affected products, references, the ransomware crews whose playbooks reference it, and the automated analysis where present.
| Parameter | Description | |
|---|---|---|
id | optional | e.g. CVE-2024-3400. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/cve?id=CVE-2024-3400"import requests
r = requests.get(
"https://app.vulone.com/api/v1/cve?id=CVE-2024-3400",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/cve?id=CVE-2024-3400", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Listing and search
Without id, a paged list. since on last_modified is the right way to poll.
| Parameter | Description | |
|---|---|---|
q | optional | Keyword, product, vendor or CVE prefix. |
kev | optional | 1 for CISA KEV entries only. |
severity | optional | CRITICAL, HIGH, MEDIUM, LOW. |
min_cvss | optional | Numeric floor on the base score. |
vendor, product | optional | CPE vendor and product names, lower case. |
since | optional | Records modified on or after this date. |
sort | optional | modified (default), published, cvss, epss, kev. |
page, per_page | optional | Up to 100 per page. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/cve?kev=1&sort=kev&per_page=25"import requests
r = requests.get(
"https://app.vulone.com/api/v1/cve?kev=1&sort=kev&per_page=25",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/cve?kev=1&sort=kev&per_page=25", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Automated analyses are generated by VULONE's analysis model from the NVD record, KEV and EPSS data and are marked as such in the response. They are not human-reviewed.
Indicators & C2
Hashes, IPs, domains and URLs with malware attribution, and the live command-and-control endpoints behind them.
Look up an indicator
/api/v1/iocScope: ioc. With value, every feed that lists it, the C2 records for it, the sample it belongs to, and the union of attributed families. The value can be an IPv4, IPv6, domain, URL, SHA-256, SHA-1 or MD5; the type is detected for you. Private and reserved addresses answer listed: false immediately with note: private_or_reserved_address.
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/ioc?value=45.55.202.73"import requests
r = requests.get(
"https://app.vulone.com/api/v1/ioc?value=45.55.202.73",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/ioc?value=45.55.202.73", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Beyond listed, families, indicators[], c2[] and sample, a lookup carries the live verdict:
| Parameter | Description | |
|---|---|---|
effective_score | 0–100 after time decay. This is the number to threshold on. Decays with a per-source half-life; a stale indicator drifts towards 0 and climbs back when re-sighted. | |
status | active, aging, stale, expired, candidate or revoked. | |
blocklist_recommended | true when the score clears the blocklist threshold (60) and the observable is not suppressed. The same rule the blocklist uses. | |
suppressed, suppression_reason | true when the observable sits on an allowlist (Tranco top sites, AWS, GCP and Cloudflare ranges, public resolvers). Its score is capped below the blocklist threshold and it is excluded from the blocklist and the offline artifact's block set. Treat suppressed: true as never block, shared infrastructure, which is a different thing from a decayed score that may reactivate. The reason reads allowlist:<category>:<source>. | |
score_source | observable when the value itself is scored. related when the value is only known as part of other observables, for example an IP that appears solely inside C2 URLs: the top-level score and status are then taken from the strongest related row, so a bare score >= t check still blocks it. | |
confidence | The components behind the score: base_score, effective_score, half_life_hours, policy_version. | |
first_seen, last_seen | For the observable as a whole. Each row in indicators[] and c2[] also carries its own score and status. | |
backend, degraded | Normally vedette / false. If the scoring backend does not answer, the response is served from a static mirror with degraded: true: still correct for what it lists, but without scores or recent sightings. Alert on it rather than silently trusting the result. |
Where a C2 row and a lookup disagree by a fraction of a point for the same value, the lookup's effective_score is authoritative: rows are per-indicator snapshots, the lookup is the observable's current verdict.
List indicators
Without value, a paged list. Give at least one filter.
| Parameter | Description | |
|---|---|---|
type | optional | sha256, md5, sha1, ip, domain, url. |
family | optional | Malware family, e.g. Mirai or LummaStealer. |
source | optional | A feed source string, e.g. malwarebazaar:Mirai. |
since | optional | Indicators last seen on or after this date. |
page, per_page | optional | Up to 200 per page. |
Listing mode is served from the static mirror and is marked degraded: true. For change tracking use the delta feed; for a block-ready set use the blocklist.
C2 endpoints
/api/v1/c2Scope: ioc. Live command-and-control endpoints (indicators of threat type botnet_cc) with family, host, port, protocol, score and status, newest last_seen first.
| Parameter | Description | |
|---|---|---|
family | optional | Malware family, e.g. Vidar. |
q | optional | Address or host prefix, e.g. 45.55.. |
since | optional | ISO-8601 date/time or unix timestamp. Only endpoints with last_seen on or after it. Exact: total and has_more are computed after the cut, so a loop that stops on has_more: false terminates, and a future since returns total: 0. |
country | optional | ISO country code. Served from the static mirror (degraded: true, no scores); not yet available on the live backend. |
page, per_page | optional | Up to 200 per page. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/c2?since=2026-10-06T00:00:00Z&per_page=200"import requests
r = requests.get(
"https://app.vulone.com/api/v1/c2?since=2026-10-06T00:00:00Z&per_page=200",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/c2?since=2026-10-06T00:00:00Z&per_page=200", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Offline sync & exports
Everything an endpoint agent or EDR needs to evaluate indicators without calling us per event: a signed offline snapshot, an incremental delta feed, a revocation stream to un-block, and a plain blocklist.
The export endpoints need the export scope; blocklist and revocations use ioc. All of them bypass the response cache and stream straight from the scoring backend. On a failure they answer 503 with code: vedette_unavailable and degraded: true instead of serving stale data.
The offline artifact
/api/v1/export/offline/meta/api/v1/export/offlineA gzip-compressed SQLite database of the highest-scoring observables (currently 250,000 rows, about 4.7 MB) rebuilt daily. Suppressed (allowlisted) observables are excluded. Fetch meta first: if sha256 matches what you already have, skip the download.
| Parameter | Description | |
|---|---|---|
generated_at | When the artifact was built. | |
sha256, size_bytes, row_count | Verify the download against sha256; it is also sent as X-Vedette-Sha256 and as the ETag prefix. | |
policy_version, allowlist_version | Which scoring policy and allowlist generation produced it. | |
download | The URL of the artifact itself. |
curl -H "Authorization: Bearer vul_your_key" "https://app.vulone.com/api/v1/export/offline/meta"
curl -H "Authorization: Bearer vul_your_key" -o vedette-offline.sqlite.gz \
"https://app.vulone.com/api/v1/export/offline"
sha256sum vedette-offline.sqlite.gz # compare with meta.sha256
gzip -d vedette-offline.sqlite.gz
sqlite3 vedette-offline.sqlite "select value, effective_score, status from observables where value='45.55.202.73'"Tables: meta (policy, generated_at, row_count), observables (type, value, status, base_score, effective_score, first_seen, last_seen) and netblocks. Apply the same rule the API does: block when effective_score >= 60 and status = 'active'.
The delta feed
/api/v1/export/deltaObservables whose score, status or suppression changed since a watermark, as newline-delimited JSON (application/x-ndjson), oldest change first. Observables are never deleted: something that expired or was revoked shows up as a row with that status, so the delta is self-contained and you never need to diff snapshots.
| Parameter | Description | |
|---|---|---|
since | optional | Your watermark: ISO-8601 or unix timestamp. Omit it on the first pull after loading an artifact and use the artifact's generated_at. |
limit | optional | Rows per pull, default 10,000, up to 100,000. |
Each row: type, value, status, base_score, effective_score, suppressed, suppression_reason, first_seen, last_seen, updated_at. Two response headers drive the loop:
| Parameter | Description | |
|---|---|---|
X-Vedette-Next-Since | The watermark for your next pull. Store it; it is the updated_at of the last row, or your own since when nothing changed. | |
X-Vedette-Count | Rows in this response. When it equals limit, pull again immediately with the new watermark. | |
ETag | Send it back as If-None-Match; an unchanged window answers 304 with no body. |
# one incremental pull; SINCE is what the previous response returned in X-Vedette-Next-Since
curl -sD headers.txt -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/export/delta?since=$SINCE&limit=10000" > delta.ndjson
SINCE=$(grep -i x-vedette-next-since headers.txt | cut -d' ' -f2 | tr -d '\r')Polling every few minutes is fine; the feed is cheap when nothing changed. Do not reconstruct state from since values you compute yourself: always carry the header forward, and start over from a fresh artifact if you lose it.
Revocations
/api/v1/revocationsScope: ioc. Tombstones for indicators that were withdrawn by their source or revoked by an operator, newest first. Use it to un-block promptly even if you only refresh the artifact daily. Each item names the observable, the reason and revoked_at; next_since is the watermark for the next poll.
| Parameter | Description | |
|---|---|---|
since | optional | Only revocations at or after this instant. |
limit | optional | Default 500, up to 5,000. |
Blocklist
/api/v1/blocklistScope: ioc. Active observables with effective_score at or above a threshold, suppressed entries excluded, ready for a firewall, DNS sinkhole or EDR import. The same set the artifact and the lookup\'s blocklist_recommended describe.
| Parameter | Description | |
|---|---|---|
format | optional | txt (one value per line, default), jsonl or csv. |
min_score | optional | Default 60. Raise it for a tighter list. |
type | optional | ip, ipv6, domain, url, sha256, sha1, md5. |
limit | optional | Default 20,000, up to 100,000. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/blocklist?type=ip&min_score=80&format=txt" > block-ips.txtimport requests
r = requests.get(
"https://app.vulone.com/api/v1/blocklist?type=ip&min_score=80&format=txt",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/blocklist?type=ip&min_score=80&format=txt", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Putting it together
A typical agent: download the artifact once a day and verify its hash; pull the delta every five minutes carrying X-Vedette-Next-Since forward; poll revocations on the same cadence and un-block what they name; and fall back to /api/v1/ioc only for values the local snapshot has never seen. Everything above honours the rate limit; a delta pull counts as one request regardless of rows.
ATT&CK
The MITRE ATT&CK reference behind the ransomware knowledge base.
Technique
/api/v1/attackScope: ransomware. A technique by id: description, tactics, platforms, MITRE detection guidance, mitigations, and every crew documented using it with their procedure.
| Parameter | Description | |
|---|---|---|
id | optional | A technique id, e.g. T1486. |
q | optional | Free-text technique search when id is omitted. |
curl -H "Authorization: Bearer vul_your_key" \
"https://app.vulone.com/api/v1/attack?id=T1486"import requests
r = requests.get(
"https://app.vulone.com/api/v1/attack?id=T1486",
headers={"Authorization": "Bearer vul_your_key"},
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/api/v1/attack?id=T1486", {
headers: { Authorization: "Bearer vul_your_key" },
});
const data = await res.json();Account & stats
What your key can do, how much it has left, and platform-wide totals.
Index
/api/v1No key needed. A machine-readable list of every endpoint with its scope, parameters and summary.
Your key
/api/v1/meThe calling key's name, scopes, plan, rate limit and usage in the last 24 hours.
Platform stats
/api/v1/statsActor, selector and record totals; ransomware, indicator and CVE counts; and when they were generated. The vedette block carries the live scoring backend's own totals (observables by status, indicators), which are the figures the dashboard shows.
Subscribing
A TAXII 2.1 server your SIEM pulls incrementally. Subscribe once, remember the cursor, and every pull after only carries what is new.
Media types follow the standard: TAXII resources are application/taxii+json;version=2.1, object bundles are application/stix+json;version=2.1. Authenticate with HTTP Basic, username vulone and your key as the password, or a Bearer token.
Discovery
/taxii2/Returns the server title and its one API root, /taxii2/feeds/.
curl -u "vulone:vul_your_key" https://app.vulone.com/taxii2/import requests
r = requests.get(
"https://app.vulone.com/taxii2/",
auth=("vulone", "vul_your_key"),
)
r.raise_for_status()
data = r.json()const res = await fetch("https://app.vulone.com/taxii2/", {
headers: { Authorization: "Basic " + btoa("vulone:vul_your_key") },
});
const data = await res.json();Collections
/taxii2/feeds/collections/| Parameter | Description | |
|---|---|---|
c2-indicators | indicator | Live C2 endpoints as STIX indicators, each tied to the malware family it serves. |
iocs | indicator | The full indicator corpus. Large; built for incremental sync. |
ransomware-actors | threat-actor | Tracked crews as threat-actor and malware objects with relationships. |
Objects and sync
/taxii2/feeds/collections/{id}/objects/| Parameter | Description | |
|---|---|---|
added_after | optional | ISO 8601 timestamp. Return only objects added since. This is the incremental cursor. |
limit | optional | Objects per page, up to 5,000. |
next | optional | The pagination cursor from the previous page's next. |
# First pull: everything (paged)
curl -u "vulone:vul_your_key" \
"https://app.vulone.com/taxii2/feeds/collections/c2-indicators/objects/?limit=500"
# Every pull after: only what is new since your last sync
curl -u "vulone:vul_your_key" \
"https://app.vulone.com/taxii2/feeds/collections/c2-indicators/objects/?added_after=2026-08-27T00:00:00Z"Responses carry more and next. When more is true, request the next page with the next cursor. Object ids are stable STIX 2.1 UUIDv5, so re-pulling never creates duplicates. A /manifest/ endpoint returns the same page as lightweight descriptors when you only need to know what changed.
STIX objects
A page of c2-indicators is a self-contained bundle: each indicator, the malware family it points at, and the relationship between them.
An indicator
{
"type": "indicator",
"spec_version": "2.1",
"id": "indicator--f0ab67f5-0247-5474-89c8-bf7137e00787",
"name": "Cobalt Strike C2 115.191.53.193:443",
"pattern": "[ipv4-addr:value = '115.191.53.193']",
"pattern_type": "stix",
"valid_from": "2026-02-28T00:00:14.000Z",
"indicator_types": ["malicious-activity"],
"confidence": 100
}What the feed carries
Infrastructure and public crew profiles only. The feed never exposes the breach corpus, private messages or any person's identity; that stays behind the session and its entitlements. The machine feed and the investigative search are deliberately different surfaces.
Microsoft Sentinel
Sentinel ingests TAXII natively and tracks the cursor for you.
Add the connector
In Threat Intelligence → Add → TAXII, enter:
| Parameter | Description | |
|---|---|---|
API root URL | https://app.vulone.com/taxii2/feeds/ | |
Collection ID | c2-indicators (or iocs, ransomware-actors) | |
Username | vulone | |
Password | your API key |
Sentinel polls on its own schedule and remembers added_after between polls, so each pull only carries new objects.
MISP
Add the feed in the UI, or pull with the TAXII client.
In the UI
Under Sync Actions → Feeds, add a TAXII 2.1 feed with the discovery URL https://app.vulone.com/taxii2/ and Basic auth, username vulone, password your key.
From the CLI
taxii2client \
--url https://app.vulone.com/taxii2/ \
--user vulone --pass vul_your_keySplunk
Point the STIX/TAXII add-on at the discovery URL.
Configure the input
With the Splunk Add-on for STIX/TAXII (or TA-taxii2), create an input with:
| Parameter | Description | |
|---|---|---|
Discovery URL | https://app.vulone.com/taxii2/ | |
Collection | iocs or c2-indicators | |
Auth | Basic, vulone / your key |
OpenCTI
Native STIX in, stitched into the graph on stable ids.
TAXII connector
Use the built-in TAXII 2.1 connector with the discovery URL https://app.vulone.com/taxii2/ and Basic auth (vulone / your key). Objects arrive as native STIX and stitch into the OpenCTI graph on their stable UUIDv5 ids, so re-pulls never duplicate.
Questions: api@vulone.com.