What You Will Achieve
TAXII 2.1 is just HTTP + JSON. You do not need a commercial TAXII client, a TIP license, or an integration marketplace to consume — or publish — standards-based threat intelligence. By the end of this playbook you will have:
- Discovery and collection listing against a live TAXII 2.1 server with plain
curl - STIX 2.1 bundle pulls parsed with
jq, paginated with thenexttoken - Incremental sync with
added_after, including revocation handling - A cron job that keeps a local indicator file in sync, once a day
- The same feed connected to Splunk/Elastic and OpenCTI/MISP native TAXII clients
- Your own org's indicators shared back over the same standard (
org-reported-*collections)
Everything below runs against https://api.ismalicious.com/taxii (also mirrored at /taxii2 — same handlers, for clients that hardcode that path).
Prerequisites
| Requirement | Details |
| ---------------- | --------------------------------------------------------------------------------------- |
| API key + secret | From Account Settings; used for every TAXII call |
| Plan | Pro or Enterprise — lower plans get 403 UPGRADE_REQUIRED on the feed endpoints |
| Tooling | curl, jq, cron (or a systemd timer) — that is the whole client |
Authentication — the server accepts three equivalent forms, so any standards-compliant client fits:
X-API-KEY: <base64(apiKey:apiSecret)>— single colon, no spaces- HTTP Basic with username =
apiKey, password =apiSecret(curl -u apiKey:apiSecret) - HTTP Basic with any username and the Base64 credential as the password — for clients that only expose one secret field (legacy TAXII clients, Sentinel, MISP)
export CREDENTIALS=$(printf '%s' "${API_KEY}:${API_SECRET}" | base64)
One thing worth knowing before you write a scheduler: on Pro and Enterprise, TAXII polls do not consume your monthly request quota. The feed is meant to be polled; only your plan's per-minute burst limit applies.
Step 1: Discover the API root
TAXII 2.1 starts with a discovery document that tells you where the API roots live. No vendor SDK — one GET:
curl -sS \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Accept: application/taxii+json;version=2.1" \
"https://api.ismalicious.com/taxii" | jq .
The response points at the single API root:
{
"title": "isMalicious",
"api_roots": ["/taxii/api-root"]
}
Fetch the root itself to see its capabilities (including max_content_length, which is why large pulls come back paginated rather than as one giant body):
curl -sS \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Accept: application/taxii+json;version=2.1" \
"https://api.ismalicious.com/taxii/api-root" | jq .
Step 2: List the collections
curl -sS \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Accept: application/taxii+json;version=2.1" \
"https://api.ismalicious.com/taxii/api-root/collections" | jq -r '.collections[].id'
Nine shared collections cover the catalogue, and three are scoped to your organization:
| Collection | Contents |
| ------------------------------------------------------------------------ | ---------------------------------------------- |
| malicious-domains / malicious-subdomains | Malicious domains, and subdomains only |
| malicious-ips | Malicious IP addresses |
| malicious-urls | Malicious URLs |
| malicious-file-hashes | MD5 / SHA-1 / SHA-256 file hashes |
| malware-iocs / ransomware-iocs | Category-filtered mixed IOCs |
| c2-indicators | C2 / botnet infrastructure (IPs + domains) |
| phishing-indicators | Phishing domains and URLs |
| org-reported-ips / org-reported-domains / org-reported-file-hashes | Indicators your org submitted (see Step 9) |
Step 3: Pull a STIX bundle with curl and jq
Objects come back as a STIX 2.1 bundle: one identity object plus indicator objects.
curl -sS -D /tmp/taxii-headers.txt \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Accept: application/taxii+json;version=2.1" \
"https://api.ismalicious.com/taxii/api-root/collections/malicious-domains/objects?limit=100" \
> /tmp/page.json
# Extract the STIX patterns
jq -r '.objects[] | select(.type == "indicator") | .pattern' /tmp/page.json
Patterns follow the standard STIX form, e.g. [domain-name:value = 'evil.example'], so any STIX-aware consumer parses them without custom code.
Page sizing: limit counts STIX objects (identity + indicators, so indicators cap at limit − 1). The default is 50; Pro accepts up to 1001 objects per page; Enterprise has no page cap — but responses are still split by a server-side size budget, so a very large request comes back as 200 with a next token rather than one oversized body. Either way, the pattern is the same: keep following next.
Step 4: Follow the next token to walk the whole collection
Pagination lives in both the response body and the headers:
- Body:
"more": trueand a"next"token inside the bundle - Headers:
X-TAXII-Has-More: trueandX-TAXII-Next
Pass the token back as the next query parameter:
NEXT=$(jq -r '.next // empty' /tmp/page.json)
curl -sS \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Accept: application/taxii+json;version=2.1" \
"https://api.ismalicious.com/taxii/api-root/collections/malicious-domains/objects?limit=100&next=${NEXT}" \
> /tmp/page2.json
Two rules keep a walk correct:
- A short page does not mean the end. Each request scans a fixed budget of backend keys, so a page can return fewer than
limitindicators whilemoreis stilltrue. Stop only whennextis absent. - Repeat the same
added_after/added_beforeon every page of a filtered walk — the token continues the scan, the filters define it.
Step 5: Sync incrementally with added_after
For the first full ingestion, omit the date filters entirely — that returns the most complete dataset, including indicators without intel timestamps (a date filter has to exclude anything it cannot place in a time window).
After that, poll with added_after set to your last successful sync:
curl -sS \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Accept: application/taxii+json;version=2.1" \
"https://api.ismalicious.com/taxii/api-root/collections/malicious-domains/objects?limit=1001&added_after=2026-08-23T06:00:00.000Z" \
| jq -r '.objects[] | select(.type == "indicator") | .pattern'
Incremental pulls also carry revocations: the first page includes revoked: true indicators for entities removed from the dataset (false-positive cleanup) since your added_after timestamp. Honor them — drop the matching indicator from your local store:
# Revoked patterns to delete locally
jq -r '.objects[] | select(.type == "indicator" and .revoked == true) | .pattern' /tmp/page.json
A full sync (no added_after) contains no revocations — there is nothing to revoke when you are pulling the current truth.
How often to poll: the shared collections are rebuilt by a nightly reload that starts at 02:00 UTC, so once a day around 06:00 UTC picks up the whole day's new indicators in one pass. Polling more often is allowed (and does not touch your quota) but returns the same set until the next rebuild. The exception is the org-reported-* collections, which update as your org submits — poll those as often as your workflow needs.
Step 6: Schedule a daily cron pull into a file
This script does everything above — full sync on first run, incremental with revocation handling afterwards — and appends newline-delimited STIX indicators to a dated file your downstream tooling can tail:
#!/usr/bin/env bash
set -euo pipefail
B64=$(printf '%s' "${ISMALICIOUS_API_KEY:?}:${ISMALICIOUS_API_SECRET:?}" | base64)
BASE="https://api.ismalicious.com/taxii/api-root/collections/${COLLECTION:-malicious-domains}/objects"
DIR="/var/lib/ismalicious/taxii"
STATE="${DIR}/since"
OUT="${DIR}/indicators-$(date -u +%F).ndjson"
mkdir -p "$DIR"
NOW=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
SINCE=$(cat "$STATE" 2>/dev/null || true)
NEXT=""
while :; do
QS="limit=1001"
[[ -n "$SINCE" ]] && QS="${QS}&added_after=${SINCE}" # first run: full sync, no filter
[[ -n "$NEXT" ]] && QS="${QS}&next=${NEXT}"
PAGE=$(curl -sS -f \
-H "X-API-KEY: ${B64}" \
-H "Accept: application/taxii+json;version=2.1" \
"${BASE}?${QS}")
jq -c '.objects[] | select(.type == "indicator")' <<<"$PAGE" >>"$OUT"
NEXT=$(jq -r '.next // empty' <<<"$PAGE")
[[ -z "$NEXT" ]] && break
done
echo "$NOW" >"$STATE"
echo "Synced $(wc -l <"$OUT") indicators into ${OUT}"
Schedule it after the nightly rebuild:
0 6 * * * COLLECTION=malicious-domains /usr/local/bin/ismalicious-taxii-pull.sh
That is a complete standards-based ingestion pipeline: one shell script, zero vendor tools.
Step 7: Point a SIEM native TAXII input at the feed
Splunk and Elastic both ingest TAXII through their own threat-intelligence tooling (a TAXII client add-on on Splunk; the TAXII-capable threat intel integration on Elastic Agent). Whatever the client, a TAXII 2.1 input asks for the same five values — configure them as follows:
| Setting | Value |
| -------------- | ---------------------------------------------------- |
| Discovery URL | https://api.ismalicious.com/taxii |
| API root URL | https://api.ismalicious.com/taxii/api-root/ |
| TAXII version | 2.1 |
| Collection | e.g. malicious-domains (one input per collection) |
| Authentication | HTTP Basic — username apiKey, password apiSecret |
If the client only exposes a single credential/token field, use the legacy Basic form: any username, and Base64(apiKey:apiSecret) as the password. Set the poll interval to daily (see Step 5) — anything tighter just re-reads the same rebuild.
Step 8: Connect OpenCTI or MISP
Both platforms ship native TAXII 2.1 clients, so there is no connector to write — give them the API root and credentials from Step 7 and select the collections you want. Follow the platform-specific walkthroughs rather than duplicating them here:
- OpenCTI integration — TAXII 2.1 ingestion into OpenCTI
- MISP integration — feed and TAXII setup for MISP
Because both consume the standard STIX 2.1 bundles from Steps 3–5, the indicators land with their patterns, labels, and revocation semantics intact — version-superseding included (revocations reuse the original indicator ID with a newer modified timestamp).
Step 9: Share your own indicators over the same standard
Sharing is the other half of standards-based threat intel, and it works without vendor tools too. Submit indicators observed in your environment:
curl -sS -X POST \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Content-Type: application/json" \
"https://api.ismalicious.com/org/indicators" \
-d '{
"indicators": [
{ "type": "ip", "value": "203.0.113.10", "category": "malware", "comment": "IPS correlation" }
]
}'
- Batches of up to 100 indicators per call; duplicates upsert by (organization, type, value)
- Submissions are auto-approved and isolated to your organization — they never enter the shared catalogue
- Daily caps: Pro 200/day, Enterprise 100,000/day
Approved indicators are mirrored into the org-reported-ips, org-reported-domains, and org-reported-file-hashes TAXII collections. That means every consumer you wired up in Steps 6–8 — the cron job, the SIEM input, OpenCTI, MISP — can subscribe to your org's own intel through the exact same TAXII 2.1 interface, and these collections update as you submit rather than on the nightly rebuild. One standard, both directions.
Validation
Confirm the whole chain with a filtered one-day window:
curl -sS -D - -o /tmp/check.json \
-H "X-API-KEY: ${CREDENTIALS}" \
-H "Accept: application/taxii+json;version=2.1" \
"https://api.ismalicious.com/taxii/api-root/collections/malicious-ips/objects?limit=10&added_after=$(date -u -d 'yesterday' +%Y-%m-%dT06:00:00.000Z)" \
| grep -i '^x-taxii'
jq '{objects: (.objects | length), more, next: (.next != null)}' /tmp/check.json
You should see Content-Type: application/stix+json;version=2.1, the X-TAXII-Has-More / X-TAXII-Next headers, and a bundle with indicators from the last rebuild. A 403 UPGRADE_REQUIRED means the key's plan is below Pro; a 401 means the credential encoding is wrong (single colon, no spaces).
Related resources
- API documentation — STIX/TAXII — full endpoint reference, parameters, and response headers
- STIX/TAXII feed overview — collections and plan details
- Blocklist download playbook — plain-text lists when you do not need STIX
- SIEM enrichment playbook — per-alert reputation lookups to pair with feed ingestion