Developer Guide
How to consume the Zedmos CTI feed API from your firewall, SIEM, or EDR. Every endpoint is HTTPS + bearer token; every response carries audit headers.
1. Authentication
Every /v1/feeds/* and /v1/lookup/* endpoint requires a bearer token. Tokens are tenant-scoped and have one or more "kinds" (ti, security, waf) attached at issue time.
curl -H "Authorization: Bearer $TOKEN" \
https://cti.zedmos.net/v1/feeds/security/phishing/domains.txt | head
A few notes:
- The token must be at least 16 characters and sent as
Authorization: Bearer <token>. - Tokens are tenant-scoped — pulls from anywhere with the same token are billed/audited as one tenant.
- Rotation: receive a new token, switch your firewall config, then disable the old one.
1A. Self-service portal
Anyone can request a Zedmos CTI account through the public portal. There is no email-verification step right now — every new account is queued for admin review and contacted manually once approved.
- Sign up at /register with email + password (≥12 chars, mixed case, digit). Response:
202 pending_approval. - Wait for admin approval. An operator flips
enabled=trueviaPATCH /admin/v1/users/:id. You will be notified out-of-band. - Sign in at /login → receive a 7-day session JWT (stored in localStorage).
- Issue API tokens at /dashboard. Each token is shown once (copy immediately). Scope by
kinds(ti, security, waf) andtiers(verified, trusted, community). Max 10 active tokens per user. - Call the feed API with the token (see Section 1 above).
Security: bcrypt cost 12, zod strict-mode input validation, NoSQL-injection guard, 5/hour signup rate-limit, 10-attempt account lockout (15 min), 2-phase login (wrong password and pending account return distinguishable errors only to the password holder).
2. Pulling feeds
All feeds follow the URL convention:
/v1/feeds/<kind>/<category>/<format>
Common combinations:
| URL | Format |
|---|---|
/v1/feeds/security/phishing/domains.txt | plain domain list |
/v1/feeds/ti/botnet_cc/ips.txt | plain IP / CIDR list |
/v1/feeds/security/malware_virus/suricata.rules | Suricata 7 DNS-block rules with priority + threat metadata |
/v1/feeds/security/ransomware/sigma.yml | Sigma rule (MITRE ATT&CK tagged) |
/v1/feeds/ti/file_sha256_malware/yara.yar | YARA hash rule |
/v1/feeds/security/phishing/stix.json | STIX 2.1 bundle |
/v1/feeds/security/phishing/unbound.rpz | RPZ DNS sinkhole zone |
The full list of categories is available at GET /v1/public/sources (no auth required).
3. IOC scoring
Each IOC carries four 0-100 scores. The most useful in production is composite: a weighted aggregate of the other three.
| Score | Measures |
|---|---|
| confidence | Source quality × consensus × enrichment |
| threat | Severity × category weight × tier multiplier |
| popularity | Distinct sources × sightings × age window |
| composite | confidence × 0.45 + threat × 0.35 + popularity × 0.20 |
Filtering by score (?min_score=N)
Vendor-native feed exports (fortinet.txt, paloalto.txt, checkpoint.csv, etc.) accept a min_score query parameter (0-100). The value is matched against the per-IOC confidence score recorded in the metadata sidecar.
Plain domains.txt / ips.txt serve the pre-built tier snapshot file as-is — they ignore min_score. Use a vendor format if you need score-gated output, or call /v1/lookup/... for per-IOC composite scores.
# Full feed (default)
curl -H "Authorization: Bearer $TOKEN" \
https://cti.zedmos.net/v1/feeds/security/phishing/domains.txt
# Confidence ≥ 70 only (verified + corroborated)
curl -H "Authorization: Bearer $TOKEN" \
"https://cti.zedmos.net/v1/feeds/security/phishing/fortinet.txt?min_score=70"
# Response carries X-Tihub-Min-Score, X-Tihub-Row-Count, X-Tihub-Row-Limit headers
Filtering by tier (?tiers=...)
The default ship list is verified ∪ trusted. To opt in to single-source community indicators, or to restrict to a single tier, pass ?tiers=:
| Tier | Definition |
|---|---|
| verified | ≥ 2 distinct upstream sources OR active enrichment confirmed (GreyNoise / AbuseIPDB / VirusTotal) OR honeypot-observed OR operator-approved |
| trusted | single-source from a strict T1 feed: USOM (TR-CERT), CERT.pl, Spamhaus DROP, abuse.ch Feodo low-FP |
| community | single-source from T2–T5 feeds — useful wide-net but not independently corroborated; opt-in only |
# Only verified (highest precision)
curl -H "Authorization: Bearer $TOKEN" \
"https://cti.zedmos.net/v1/feeds/security/phishing/fortinet.txt?tiers=verified"
# Verified + trusted + community (widest net)
curl -H "Authorization: Bearer $TOKEN" \
"https://cti.zedmos.net/v1/feeds/security/phishing/fortinet.txt?tiers=verified,trusted,community"
Row caps + ?limit=N
Each firewall vendor has a documented EDL / block-list import limit. Zedmos applies a per-vendor default so the importer never silently truncates a too-large feed. Override with ?limit=N up to 500,000.
| Vendor format | Default cap | Source |
|---|---|---|
fortinet.txt | 128,000 | FortiOS 7.x EDL ceiling |
paloalto.txt (IPs) | 150,000 | PAN-OS EDL — IP list |
paloalto-domain.txt (Domains) | 50,000 | PAN-OS EDL — Domain list |
checkpoint.csv | 500,000 | Quantum Custom IOC recommended |
sophos.csv | 100,000 | Central CSV upload limit |
cisco-umbrella.txt | 250,000 | Umbrella block-list cap |
meraki.txt | 50,000 | MX custom URL list |
juniper-srx.txt | 100,000 | dynamic-address-set practical max |
| others (opnsense, mikrotik, pihole, unbound, suricata) | unlimited / format-specific | — |
When the cap kicks in, indicators with metadata-sidecar enrichment (malware family, threat actor, MITRE) ship first.
Caching contract — 202 "warming"
Heavy aggregate endpoints (/v1/public/stats, /v1/public/transparency, /v1/public/verification, /v1/public/cti/categories, /v1/public/mitre/coverage, /v1/public/dashboard) run against an 11M-row catalog. Right after a hub restart the in-process cache is empty and the first request would otherwise hang for 10-30 s.
To prevent timeouts, those endpoints return 202 Accepted with body {"status":"warming","retry_after_s":5} until the background prime job populates the cache. Subsequent calls return 200 OK in < 100 ms. Treat 202 as transient — retry after the suggested interval. Response header X-Tihub-Cache distinguishes warming / process-hit / stale-revalidating.
Per-IOC lookup
curl -H "Authorization: Bearer $TOKEN" \
https://cti.zedmos.net/v1/lookup/domain/example.com
Response includes tier, sources, categories, the four scores, effective MITRE techniques, and a recommendation string.
4. MITRE ATT&CK mapping
Each Zedmos category maps to a curated list of ATT&CK tactics + techniques (Enterprise v15). The mapping surfaces in:
- Sigma rules —
tags: attack.t1566.001, attack.initial-access - STIX bundles —
kill_chain_phases+external_referencesto attack.mitre.org - Lookup endpoint —
mitre: { tactics, techniques }in the JSON response
Coverage matrix (public)
curl https://cti.zedmos.net/v1/public/mitre/coverage | jq '.techniques[0:5]'
The same data rendered as a grid lives at /mitre-coverage.html — no authentication required.
5. Vendor-native exports
The same IOCs, packaged for direct paste into the firewall console. All endpoints accept ?min_score=N.
Fortinet FortiGate
config system external-resource
edit "zedmos-phishing"
set type domain
set resource "https://cti.zedmos.net/v1/feeds/security/phishing/fortinet.txt?min_score=70"
next
end
Palo Alto Networks (External Dynamic List)
PAN-OS uses two separate EDL types — register each as its own object:
# Objects → External Dynamic Lists → Add → Type: IP List
# Source: https://cti.zedmos.net/v1/feeds/security/phishing/paloalto.txt
# Repeat every: Hour
# Objects → External Dynamic Lists → Add → Type: Domain List
# Source: https://cti.zedmos.net/v1/feeds/security/phishing/paloalto-domain.txt
# Repeat every: Hour
Check Point Quantum
ioc_feeds add \
--feed_name Zedmos_Phishing \
--transport_type web_url \
--resource https://cti.zedmos.net/v1/feeds/security/phishing/checkpoint.csv \
--feed_action prevent \
--feed_format CSV
Sophos · Cisco Umbrella · Meraki · Juniper SRX · MikroTik · Pi-hole · OPNsense · Unbound RPZ
Config snippets for all 16 supported integrations: /integrations.
6. API reference
Public endpoints (no auth)
| Endpoint | Returns |
|---|---|
GET /v1/public/health | health probe (always 200, never throttled) |
GET /v1/public/stats | catalog summary, tier mix, feed health |
GET /v1/public/sources | per-source feed catalogue |
GET /v1/public/threats-by-country | top-25 source countries (cached 5 min) |
GET /v1/public/methodology | FP filter + tier rules (static) |
GET /v1/public/transparency | live FP filter counts + signing key info |
GET /v1/public/verification | per-tier counts, cross-validation result |
GET /v1/public/dashboard | aggregate stats for landing page |
GET /v1/public/quality-report | tier mix, per-feed contribution |
GET /v1/public/cti/categories | 33 category cards with IOC counts (5-min cached) |
GET /v1/public/mitre/coverage | MITRE ATT&CK coverage matrix (5-min cached) |
GET /v1/public/sightings/top | top observed indicators |
GET /v1/public/keys/sign | ed25519 public key (JSON) |
GET /v1/public/keys/sign.pem | same key (PEM) |
GET /v1/public/lookup/<value> | public IOC lookup (auto-detects type) |
POST /v1/public/lookup | bulk lookup {values:[...]} (≤ 200) |
GET /v1/public/ct/recent | recent Certificate Transparency hits |
GET /v1/integrations/vendors | vendor catalog (drives /integrations page) |
Self-service portal (session JWT or public)
| Endpoint | Returns |
|---|---|
POST /v1/portal/register | create account {email,password,name?} → 202 pending_approval |
POST /v1/portal/login | session JWT {email,password} → 200 + {token,user}; 403 if pending; 401 on bad creds |
POST /v1/portal/logout | audit-log logout intent (client drops JWT) |
GET /v1/portal/me | current user profile (requires Bearer session JWT) |
GET /v1/portal/tokens | list your API tokens with use_count, last_used_at, last_used_ip |
POST /v1/portal/tokens | create token {name,kinds[],tiers[]} → raw secret once; max 10 active per user |
POST /v1/portal/tokens/:id/revoke | revoke (token stays in audit log) |
DELETE /v1/portal/tokens/:id | hard-delete the token |
TAXII 2.1 / STIX (bearer)
| Endpoint | Returns |
|---|---|
GET /taxii2/ | discovery |
GET /taxii2/api1/ | API root |
GET /taxii2/api1/collections/ | collection list (one per kind+category+type) |
GET /taxii2/api1/collections/{id}/objects/ | STIX bundle (?limit, ?added_after) |
GET /taxii2/api1/collections/{id}/manifest/ | collection manifest |
GET /v1/stix/info | STIX version + signer info |
Sightings + closed loop (bearer)
| Endpoint | Returns |
|---|---|
POST /v1/sightings/bulk | submit firewall hit aggregates {sightings:[{value,type,count,…}]} |
POST /v1/sightings/backchannel | cti2 v2 backchannel events |
GET /v1/me | inspect your API token (scope, quota, expiry) |
GET /v1/feeds/index | list every shipped (kind, category, file, URL, etag, line_count) |
Bearer-token endpoints
| Endpoint | Returns |
|---|---|
GET /v1/feeds/<kind>/<cat>/domains.txt | plain domain list |
GET /v1/feeds/<kind>/<cat>/ips.txt | plain IP / CIDR list |
GET /v1/feeds/<kind>/<cat>/suricata.rules | Suricata rules (priority + threat metadata) |
GET /v1/feeds/<kind>/<cat>/sigma.yml | Sigma rule (MITRE tagged) |
GET /v1/feeds/<kind>/<cat>/yara.yar | YARA rule |
GET /v1/feeds/<kind>/<cat>/stix.json | STIX 2.1 bundle |
GET /v1/feeds/<kind>/<cat>/fortinet.txt | Fortinet External Block List |
GET /v1/feeds/<kind>/<cat>/paloalto.txt | PAN External Dynamic List (IP) |
GET /v1/feeds/<kind>/<cat>/paloalto-domain.txt | PAN External Dynamic List (Domain) |
GET /v1/feeds/<kind>/<cat>/checkpoint.csv | Check Point Custom IOC Feed |
GET /v1/feeds/<kind>/<cat>/sophos.csv | Sophos Active Threat Response |
GET /v1/feeds/<kind>/<cat>/cisco-umbrella.txt | Cisco Umbrella block list |
GET /v1/feeds/<kind>/<cat>/meraki.txt | Meraki MX URL list |
GET /v1/feeds/<kind>/<cat>/juniper-srx.txt | Junos SRX Dynamic Address Set |
GET /v1/feeds/<kind>/<cat>/mikrotik.rsc | MikroTik /import script |
GET /v1/feeds/<kind>/<cat>/pihole.txt | Pi-hole adlist |
GET /v1/feeds/<kind>/<cat>/opnsense.txt | OPNsense URL-table |
GET /v1/feeds/<kind>/<cat>/unbound.rpz | Unbound RPZ zone |
GET /v1/lookup/<type>/<value> | per-IOC lookup with scores + MITRE |
Response headers
Every feed response sets:
ETag— SHA-256 of the body (use withIf-None-Matchfor 304s — most firewall agents handle this natively so unchanged feeds re-download as zero bytes)Cache-Control: public, max-age=60X-Tihub-Min-Score— applied confidence gate (vendor formats only)X-Tihub-Row-Count— number of indicators in this responseX-Tihub-Row-Limit— the cap applied (vendor default or?limit=override)X-Tihub-Vendor— vendor id on the vendor-native endpointsX-Tihub-Body-Age-Ms— how stale the cached body is (0 on freshly-rebuilt)X-Tihub-Cache—process-hit/stale-revalidating/warmingfor cached aggregatesX-Tihub-Tier+X-Tihub-Tier-Meaning— on plaindomains.txt/ips.txt/verified-*/trusted-*/community-*
Need a token, a custom export format, or have a question? Contact your Zedmos representative.