Search.
Search runs the same engine as the Event Search page in the platform, and returns the same thing it does: Threat Events, not individual reports. Ask for ransomware and you get the occurrences, each with its reporting attached, not four write-ups of the same breach.
Requires the search.read scope; running a search also requires
threat-events.read, since the results are Threat Events. Listing and reading
your saved searches needs search.read alone. These are POST because
queries can be large, but they are reads, so a key with default (read-only)
scopes can use them.
Simple search
POST /api/v1/search/
curl -X POST "https://api.liberty91.com/api/v1/search/" \
-H "X-API-Key: $LIBERTY91_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"free_text": "ransomware",
"target_sectors": ["Healthcare"],
"target_countries": ["DE", "NL"],
"date_from": "2026-07-01T00:00:00Z"
}'Results use the Threat Event contract, cursor paginated, plus one extra key:
{
"next": null,
"previous": null,
"results": [ /* Threat Events */ ],
"unlinked_report_count": 2
}unlinked_report_count is reports that matched but do not yet belong to a
deduplicated occurrence. Without it, an empty results would be ambiguous
between "nothing matched" and "things matched but are not yet grouped".
Criteria
| Field | Type |
|---|---|
free_text | string |
date_from, date_to | ISO 8601 datetime, inclusive |
threat_actor_ids, malware_ids, vulnerability_ids, threat_cluster_ids | UUID lists |
asset_technology_ids, supplier_ids | UUID lists |
target_regions, target_countries, source_countries, target_states, target_sectors | string lists |
match_logic | ANY (default) or ALL |
query_tree | boolean condition groups, see below |
Resolve names to ids first with the
threat library (?name= or ?alias=).
Boolean queries
For questions the flat fields cannot express, send a query_tree. Conditions
inside a group must all match; the query matches when any group matches.
Say you want healthcare occurrences in Germany, or anything involving a specific actor anywhere:
{
"query_tree": {
"op": "OR",
"children": [
{
"op": "AND",
"children": [
{"field": "target_sector", "values": ["Healthcare"]},
{"field": "target_country", "values": ["DE"]}
]
},
{
"op": "AND",
"children": [
{"field": "threat_actor", "values": ["3e2d1c0b-..."]}
]
}
]
}
}Several values inside one condition mean "any of these". A target_country
condition listing DE and NL matches occurrences targeting either.
When query_tree is present it takes precedence over the flat fields, and
they are not combined.
Free text is capped at three leaves per query: each one triggers a broad scan.
Exceeding the cap, the depth limit or the node limit returns a 400 naming the
limit you hit.
Saved searches
Searches saved in the platform belong to the whole account, and you can read and run them over the API.
# what the team has saved
curl "https://api.liberty91.com/api/v1/searches/" -H "X-API-Key: $KEY"
# run one
curl -X POST "https://api.liberty91.com/api/v1/searches/$SEARCH_ID/run/" \
-H "X-API-Key: $KEY"Running a saved search executes its stored criteria and returns the same
Threat-Event-shaped page as POST /search/. This is the clean way to schedule
an analyst-authored query from your own tooling without duplicating the query
in two places.
What search can reach
Search runs across the reporting you are entitled to. Sources you do not hold a licence or an upload for are not searchable by you, and an occurrence covered only by reporting you are not entitled to does not appear at all, the same boundary described on Threat Events.
Frequently asked questions
Does the search API return reports or occurrences?
Occurrences. Several reports about the same breach come back as one Threat Event, the same way the platform's own search behaves.
Can I run my saved searches over the API?
Yes. List them at /searches/ and run one with POST /searches/{id}/run/,
which executes the stored criteria and returns Threat Events.
What does unlinked_report_count mean?
Reports that matched your query but do not yet belong to a deduplicated occurrence. It lets you tell "nothing matched" apart from "matches exist but are not yet grouped".