IOC Feed API
A machine-readable feed of attacker-role indicators extracted from public CTI reporting. One endpoint, cursor-paginated, API-key authenticated. Indicators are returned refanged (usable). The examples here are defanged for display. OpenAPI spec →
Quickstart
Get a key from your API keys page (Pro or Team), then:
curl -H "Authorization: Bearer $SIGNALIS_API_KEY" \ "https://api.signalis.watch/api/feed/v1/iocs?limit=100&type=domain"
Auth & plans
Pass your key (prefix sig_live_) as a bearer token, or via X-API-Key. API access requires a paid plan. The free tier is web-only and 7-day delayed; Pro and Team are real-time.
| Pro | Team | |
| Price | $29/mo | $69/mo |
| Feed | real-time | real-time |
| Rate limit | 10 req/s | 50 req/s |
| Monthly quota | 10,000 calls | 100,000 calls |
| API keys | 5 | 20 |
The endpoint
GET https://api.signalis.watch/api/feed/v1/iocs
Attacker-role IOCs, oldest-first. Query params:
limit: 1–1000, default 100type: one IOC type (domain, url, ip_v4, ip_v6, hash_sha256, …)since: RFC 3339 timestamp; only IOCs first-seen at/after itcursor: opaque; pass the previous page'snext_cursor
Response:
{
"data": [
{
"id": 84213,
"value": "malicious-c2[.]example", // returned refanged (usable); defanged here
"type": "domain",
"role": "attacker",
"report_id": 5012,
"report_title": "Threat actor infrastructure roundup",
"source_name": "Example CTI Vendor",
"published_at": "2026-06-14T09:00:00Z",
"severity_level": "high",
"created_at": "2026-06-15T02:11:00Z" // first-seen; the pagination sort key
}
],
"next_cursor": "eyJ0cyI6...", // pass to ?cursor= for the next page
"has_more": true
}Pagination
Forward-only cursor. Loop while has_more is true, passing next_cursor back as ?cursor=. Don't construct cursors. For incremental sync, persist the max created_at you've seen and pass it as ?since= next run.
Rate limits & errors
- 429, per-second rate limit: includes
Retry-After: 1. Back off and retry. - 429, monthly quota exhausted: no
Retry-After; resets at the start of the month. - 503, feed paused (operator kill-switch, e.g. a data-quality incident): honor
Retry-After(3600). Your integration should treat this as transient, not fatal. - 401 invalid key · 403 plan lacks API access or key missing
feed:read· 422 bad parameter/cursor.
All error bodies are { "detail": "…" }.
Integration recipes
Python: poll & diff
import os, time, requests
API = "https://api.signalis.watch/api/feed/v1/iocs"
HEADERS = {"Authorization": f"Bearer {os.environ['SIGNALIS_API_KEY']}"}
def pull(since=None):
"""Yield every attacker IOC since a timestamp, walking the cursor. Honors backoff."""
cursor = None
while True:
params = {"limit": 500}
if since: params["since"] = since
if cursor: params["cursor"] = cursor
r = requests.get(API, headers=HEADERS, params=params, timeout=30)
if r.status_code in (429, 503):
time.sleep(int(r.headers.get("Retry-After", 5)))
continue
r.raise_for_status()
body = r.json()
yield from body["data"]
if not body["has_more"]:
break
cursor = body["next_cursor"]
if __name__ == "__main__":
# poll-and-diff: persist the max created_at and pass it as ?since next run
for ioc in pull(since="2026-06-01T00:00:00Z"):
print(ioc["type"], ioc["value"], "->", ioc["source_name"])MISP: PyMISP pull
Maps Signalis IOC types to MISP attribute types; skips types with no clean mapping (cve, yara_rule, crypto addresses beyond BTC).
import os, requests
from pymisp import PyMISP, MISPEvent, MISPAttribute
SIG = "https://api.signalis.watch/api/feed/v1/iocs"
misp = PyMISP(os.environ["MISP_URL"], os.environ["MISP_KEY"], ssl=True)
# Signalis IOC type -> MISP attribute type. Types with no clean MISP mapping are skipped.
MAP = {
"domain": "domain", "url": "url", "ip_v4": "ip-dst", "ip_v6": "ip-dst",
"email": "email-src", "hash_md5": "md5", "hash_sha1": "sha1",
"hash_sha256": "sha256", "hash_sha512": "sha512", "filename": "filename",
"file_path": "filename", "asn": "AS", "user_agent": "user-agent",
"bitcoin_address": "btc", "registry_key": "regkey", "mutex": "mutex",
}
def signalis(since):
cursor = None
while True:
p = {"limit": 500, "since": since}
if cursor: p["cursor"] = cursor
b = requests.get(SIG, headers={"Authorization": f"Bearer {os.environ['SIGNALIS_API_KEY']}"},
params=p, timeout=30).json()
yield from b["data"]
if not b["has_more"]:
break
cursor = b["next_cursor"]
event = MISPEvent(); event.info = "Signalis attacker IOCs"
for ioc in signalis("2026-06-01T00:00:00Z"):
t = MAP.get(ioc["type"])
if not t:
continue
a = MISPAttribute(); a.type = t; a.value = ioc["value"]; a.to_ids = True
a.comment = f"Signalis · {ioc['source_name']}"
event.add_attribute(**a)
misp.add_event(event)OpenCTI: generic import
A generic pull-and-create sketch via pycti. This is not a packaged connector (no dedup, bundling, or STIX relationships). A dedicated external-import connector is a separate effort. Adjust the observable-create call to your pycti version.
import os, requests
from pycti import OpenCTIApiClient
# GENERIC IMPORT (not a packaged connector; see the note below).
SIG = "https://api.signalis.watch/api/feed/v1/iocs"
octi = OpenCTIApiClient(os.environ["OPENCTI_URL"], os.environ["OPENCTI_TOKEN"])
OBS = { # Signalis type -> OpenCTI observable type
"domain": "Domain-Name", "url": "Url", "ip_v4": "IPv4-Addr", "ip_v6": "IPv6-Addr",
"email": "Email-Addr", "hash_md5": "StixFile", "hash_sha1": "StixFile", "hash_sha256": "StixFile",
}
def signalis(since):
cursor = None
while True:
p = {"limit": 500, "since": since}
if cursor: p["cursor"] = cursor
b = requests.get(SIG, headers={"Authorization": f"Bearer {os.environ['SIGNALIS_API_KEY']}"},
params=p, timeout=30).json()
yield from b["data"]
if not b["has_more"]:
break
cursor = b["next_cursor"]
for ioc in signalis("2026-06-01T00:00:00Z"):
t = OBS.get(ioc["type"])
if not t:
continue
octi.stix_cyber_observable.create(
simple_observable_key=f"{t}.value", simple_observable_value=ioc["value"],
x_opencti_description=f"Signalis · {ioc['source_name']}",
)Questions or a type mapping you need? The feed emits every type in the spec. Report detail is a member feature. see plans.