Every investigation Qevlar closes produces intelligence: the observables it touched, the verdicts it reached, and the evidence behind each verdict. Until now that knowledge stopped at the ticket.
The IOC endpoint turns each closed investigation into something you can reuse. It returns the observables an investigation proved malicious, with the evidence that produced each verdict, so you can act on them in the tooling you already run.
Every indicator carries extracted_from_alert so you can separate the two. true means your own tooling already had it, false means Qevlar found it. Filtering on false gives you the set your detection missed.
What counts as an indicator
An observable appears here when an investigation action classified it malicious. Qevlar's engine runs actions (CTI lookups, file analysis, log pivots, webpage inspection) and each one lands on a classification. Only malicious classifications produce indicators. Suspicious is a separate, lower tier and is not returned.
Four types come back: file hash, URL, domain, public IP address. These are the portable ones. You can put them in a blocklist, hand them to a partner, or look them up in someone else's platform, and they mean the same thing everywhere. Private and reserved IP ranges are dropped in the query itself, since an internal address means nothing outside the network it came from. Email addresses are not returned; they are context about an alert rather than something you can act on elsewhere.
The evidence behind each verdict
Each indicator lists every action that classified it malicious, in the order those actions ran:
"findings": [
{
"action_id": "6b1f9a72-0c48-4e13-b5d7-8f2a6c91e304",
"action_type": "ANALYZE_CTI",
"summary": "Domain flagged by 11/90 CTI engines"
},
{
"action_id": "9c04b3e5-7a16-4f28-8d9b-1e5c0a74f286",
"action_type": "QEVLAR_EYE_WEBPAGE",
"summary": "Credential harvesting page impersonating Microsoft 365 login"
}
]No indicator is a bare value. An analyst reviewing the list, a CTI team deciding whether to publish, or a client questioning a block can trace any entry to the investigation step that produced it and read what that step concluded.
That is what makes the contextual types safe to act on. A file hash is malicious wherever it appears, so it travels well on its own. An IP or a domain can be malicious in one role and benign in another: an address used for password spray is not malicious as mail infrastructure, and a consumer VPN endpoint is a compliance question rather than a malware one. The findings let you see why the verdict was reached rather than guessing from the value.
Where two independent actions reached the same verdict on one indicator, both appear, and two findings are a stronger signal than one.
Detection engineering
Every observable a closed investigation proves malicious is candidate detection material. An investigation does not only return a verdict, it returns evidence about what to watch for next.
The endpoint gives you that material already qualified. Because the set is filtered to malicious classifications rather than everything the investigation looked at, it is small relative to the observables in scope and needs no triage before it becomes a watchlist entry, a blocklist row or a detection rule. The evidence attached to each indicator tells you which of those it should be. A CTI reputation hit across dozens of engines is a different quality of finding from a single log pivot, and the difference is visible in the response.
Not every confirmed indicator warrants a standing rule. More rules mean more false positives and more cost, so the evidence is there for you to make that call rather than have it made for you.
The four types map onto detection surfaces without translation out of a proprietary format first: hashes to EDR, domains and URLs to proxy and DNS, public IPs to firewall and SIEM watchlists.
Threat hunting
The operational question is whether an indicator confirmed in one place has already touched another: another host, another tenant, another client. That is a retrospective hunt, and confirmation from an investigation is what makes it worth running.
Qevlar supplies the confirmed indicator and its evidence. Your orchestrator runs the hunt:
- An investigation confirms a true positive.
- Your SOAR or automation platform calls the API for that incident's indicators.
- It translates each one into a query for the platforms you cover, using the type to pick the field.
- It runs those queries across the environments in scope and collects hits.
- Hits go to an analyst, or back into Qevlar as new alerts for investigation.
Fidelity varies by type, and the response tells you which you are holding. A file hash retrospective hunt is high signal and safe to run broadly. A domain or IP usually needs a time window or a protocol constraint so the analyst is not buried.
For an MSSP running many client environments, this is the basis for hunting across the portfolio on intelligence that originates in your own operations rather than in a subscription.
Using the API
Endpoint and authentication
GET /v2/iocs Authorization: Bearer <your-profile-token>
The base URL on Qevlar's EU platform is https://api.qevlar.com. Self managed and BYOC deployments use their own hostname; the path is identical. Interactive OpenAPI documentation is served at /docs on the same host.
Tokens are issued per profile, and all results are scoped to that profile's tenant. Client level tokens are rejected here.
Scope
Pass incident_id, or alert_id, or neither. Omitting both searches the whole tenant.
Incident scope is usually the right choice. Qevlar correlates related alerts into an incident, and an indicator discovered while investigating one alert is evidence about the whole incident, so incident scope resolves to every alert in that group.
Results are deduplicated on type and value within the scope you request. An indicator seen in four alerts of one incident returns once, with the findings from all four merged, and extracted_from_alert set to true if it was present in the payload of any of them.
Parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
incident_id | UUID | none | Scope to one incident. Mutually exclusive with alert_id. |
alert_id | UUID | none | Scope to one alert. Mutually exclusive with incident_id. |
type | enum, repeatable | all four | File, URL, Domain, IP. Repeat to combine: ?type=File&type=URL. |
q | string | none | Case insensitive substring match on the indicator value. Trigram indexed, so patterns under three characters may bypass the index and run slowly on tenant scope. |
page | integer ≥ 1 | 1 | |
per_page | integer 1 to 100 | 25 |
Response
{
"iocs": [ ... ],
"pagination": { "page": 1, "per_page": 25, "offset": 0, "total": 63, "pages": 3 }
}Every indicator carries:
| Field | Type | Notes |
|---|---|---|
type | string | File, URL, Domain or IP. Discriminates the rest of the object. |
verdict | string | "Malicious". |
first_seen | timestamp | Earliest action that classified this indicator malicious in scope. |
first_seen_alert_id | UUID | Alert that action belonged to. |
extracted_from_alert | boolean | true if present in any source alert in scope, false if Qevlar discovered it. |
findings | array | action_id, action_type, summary. Earliest first. summary may be null. |
The indicator value sits in a type specific field rather than a generic value key, because a file hash has several possible representations:
File:algorithm, plussha256,sha1,md5. Which of the three are populated depends on what the source tooling provided, and all three can benullwhen the hash uses an algorithm outside those three.algorithmcomes from the source metadata, or is inferred from the value when the source did not record it.IP:ip_addressDomain:domain_nameURL:url
Results are ordered by first_seen descending, with ties broken on the indicator value. Investigations are asynchronous, so an incident still being investigated will return more indicators later.
Example
curl -sS \ -H "Authorization: Bearer $QEVLAR_TOKEN" \ "https://api.qevlar.com/v2/iocs?incident_id=3f2a6c18-9d41-4b7e-8a52-0c7d9e1b4a63"
{
"iocs": [
{
"type": "Domain",
"domain_name": "login-m365-verify.example",
"verdict": "Malicious",
"first_seen": "2026-07-21T09:14:02.881000Z",
"first_seen_alert_id": "b48c1e07-5f3a-4d92-9c61-2a7e8f0b3d15",
"extracted_from_alert": false,
"findings": [
{
"action_id": "6b1f9a72-0c48-4e13-b5d7-8f2a6c91e304",
"action_type": "ANALYZE_CTI",
"summary": "Domain flagged by 11/90 CTI engines"
}
]
},
{
"type": "File",
"algorithm": "SHA-256",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"sha1": null,
"md5": "9e107d9d372bb6826bd81d3542a419d6",
"verdict": "Malicious",
"first_seen": "2026-07-21T09:11:47.204000Z",
"first_seen_alert_id": "b48c1e07-5f3a-4d92-9c61-2a7e8f0b3d15",
"extracted_from_alert": true,
"findings": [
{
"action_id": "1d7e4f80-3b25-4a91-9e6c-5f8b2a0d7c14",
"action_type": "FILE_ANALYSIS",
"summary": "Sandbox execution: process injection and C2 beacon"
}
]
}
],
"pagination": { "page": 1, "per_page": 25, "offset": 0, "total": 2, "pages": 1 }
}Values above are illustrative.
Network indicators only, for a blocklist:
GET /v2/iocs?incident_id=$INCIDENT&type=IP&type=Domain&type=URL&per_page=100
Only what Qevlar discovered. Provenance is not a query parameter, so filter client side:
discovered = [ioc for ioc in body["iocs"] if not ioc["extracted_from_alert"]]
Pagination
Drive pagination from pagination.pages, not from the length of iocs. A page can legitimately contain fewer items than per_page, so a short page does not mean you have reached the end.
import httpx
def incident_iocs(base_url: str, token: str, incident_id: str) -> list[dict]:
out, page = [], 1
with httpx.Client(headers={"Authorization": f"Bearer {token}"}, timeout=30) as client:
while True:
r = client.get(
f"{base_url}/v2/iocs",
params={"incident_id": incident_id, "page": page, "per_page": 100},
)
r.raise_for_status()
body = r.json()
out.extend(body["iocs"])
if page >= body["pagination"]["pages"]:
return out
page += 1Paging is offset based on a live ordering. For bulk collection, page through incidents individually rather than walking tenant scope, since a live tenant shifts under a long walk as investigations complete.
Errors
| Status | Cause |
|---|---|
401 | Missing, invalid or expired token. |
403 | Client level token used on a profile endpoint. |
404 | incident_id or alert_id not found in your tenant. |
422 | Invalid filter combination or parameter value. |
404 is returned identically whether an ID does not exist or belongs to another tenant, so do not treat it as evidence that an ID is unused. The shape of the detail field on a 422 varies with which validation failed, so branch on the status code rather than parsing it.
Appendix: mapping to STIX 2.1
If your intelligence store expects STIX 2.1, you construct the indicator objects from the response. The mapping is direct.
| STIX field | Source |
|---|---|
type | Literal "indicator" |
spec_version | Literal "2.1" |
id | indicator--<uuid>, generated by you. Derive it deterministically from the pattern for idempotent republishing. |
created, modified | Your clock at build time |
pattern | Type specific value field. See below. |
pattern_type | Literal "stix" |
indicator_types | ["malicious-activity"] |
valid_from | first_seen |
name, description | findings[].summary, the first summary or the summaries joined |
external_references | first_seen_alert_id, for traceability back to the alert |
Patterns by type:
File [file:hashes.'SHA-256' = '<sha256>']
[file:hashes.'SHA-1' = '<sha1>']
[file:hashes.MD5 = '<md5>']
Domain [domain-name:value = '<domain_name>']
URL [url:value = '<url>']
IP [ipv4-addr:value = '<ip_address>']
[ipv6-addr:value = '<ip_address>']For IP, choose ipv4-addr or ipv6-addr by inspecting the value. For File, build the pattern from whichever hash field is populated.
Fields your own policy governs are set on your side at publication time: confidence, kill_chain_phases, decay and expiry. Qevlar does not return an ATT&CK tactic per indicator today, so kill_chain_phases has no source in the response.
Worked example
The Domain indicator from the example above becomes:
{
"type": "indicator",
"spec_version": "2.1",
"id": "indicator--3c2b1a09-8f7e-6d5c-4b3a-2918f7e6d5c4",
"created": "2026-07-21T10:02:00.000Z",
"modified": "2026-07-21T10:02:00.000Z",
"name": "Domain flagged by 11/90 CTI engines",
"description": "Domain flagged by 11/90 CTI engines",
"indicator_types": ["malicious-activity"],
"pattern": "[domain-name:value = 'login-m365-verify.example']",
"pattern_type": "stix",
"valid_from": "2026-07-21T09:14:02.881Z",
"external_references": [
{
"source_name": "qevlar-alert",
"external_id": "b48c1e07-5f3a-4d92-9c61-2a7e8f0b3d15"
}
]
}Questions about integration or unexpected responses: support@qevlar.com.