Reconnaissance API
What the internet knows about a target, and what has already leaked about it.
Every other ThreatWinds API reads from a corpus the platform owns. This one does not. It is a metered front for external intelligence sources — infrastructure, DNS and registry data on one side, dark-web exposure, vulnerability scanning and brand monitoring on the other — and that shapes how you should use it: each uncached request spends a finite resource, so results are cached, costs are reported back to you, and allowances are bounded per day as well as per minute.
Base URL: https://apis.threatwinds.com/api/reconnaissance/v1
Authentication
Required on every route, all of which take the user role. There is no anonymous tier and no public endpoint: each route reaches a metered external provider, so every request must belong to an account that can be billed for it.
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes* | Bearer token from an active session. |
| api-key | string | Yes* | API key. |
| api-secret | string | Yes* | API secret. |
* Either a bearer token, or an api-key/api-secret pair.
Understanding cost
Two response headers tell you what a request actually bought.
| Header | Meaning |
|---|---|
X-Recon-Cached | true if the answer came from cache — this call spent nothing |
X-Recon-Upstream-Cost | Credits or billed searches consumed, 0 on a cache hit |
Cached answers do not count against your daily allowances. Polling faster than the cache TTL buys no fresher data and only consumes your per-minute allowance:
| Data | Cached for |
|---|---|
| Host lookups and searches | 1 hour |
| DNS records | 6 hours |
| Dark-web searches | 15 minutes |
| Registry records | 24 hours |
Infrastructure
Look up an address
GET /host/{ip}
Returns open ports, service banners, TLS details, the organization and ASN that announce the address, and any CVEs associated with the services found on it.
| Parameter | In | Description |
|---|---|---|
ip | path | IPv4 or IPv6 address |
history | query | true returns every historical observation rather than the latest |
curl -H "Authorization: Bearer $TOKEN" \
"https://apis.threatwinds.com/api/reconnaissance/v1/host/1.1.1.1"
Size a query before running it
GET /search/count?query={query}
Returns the number of matching hosts and nothing else. This is free — it does not consume a query credit or your daily allowance. Run it first: a query matching four million hosts is almost never the query that was intended.
| Parameter | In | Description |
|---|---|---|
query | query | Search expression, e.g. org:"Acme Corp" port:443 |
facets | query | Comma-separated breakdowns, e.g. port:10,country:5 |
Search
GET /search?query={query}&page={page}
Same query syntax as /search/count, but returns the matching host documents. Spends a query credit on a cache miss. Pages hold 100 results; page starts at 1.
DNS
Subdomains and records
GET /dns/domain/{domain}
Returns subdomains and DNS records accumulated over time rather than resolved live — so it includes names that no longer resolve, which is usually the interesting part for reconnaissance. Spends a query credit on a cache miss.
Resolve and reverse-resolve
GET /dns/resolve?hostnames=one.example.com,two.example.com
GET /dns/reverse?ips=1.1.1.1,8.8.8.8
Both are free upstream and count only against your per-minute allowance. Up to 100 entries per request.
Dark web
Search the breach corpus
GET /dark-web/search?field={field}&search={value}
| Parameter | In | Description |
|---|---|---|
field | query | One of domain, email, username, phone, ipAddress |
search | query | The value to search for |
{
"breaches": [
{
"id": "...",
"breachId": "...",
"email": "someone@example.com",
"domain": "example.com",
"leakName": "...",
"source": "...",
"compromisedData": "email, password",
"timestamp": "2024-11-02T00:00:00Z"
}
],
"truncated": false,
"cached": false
}
truncated is true when the result set was larger than one request returns. Treat a truncated list as incomplete — narrow the search rather than assuming you have seen everything.
An empty breaches array is a real answer: it means the corpus holds nothing for that value, not that the search failed.
This is the most expensive route on the service. Each distinct search is billed by the upstream, which is why its daily allowance is the tightest one and why identical repeat searches are served from cache for 15 minutes.
There is no organization parameter, by design. Dark-web results are scoped by the value you search for and nothing else. Searches never span or address another account’s data.
Registry
Authoritative registration records — who a name or address range is actually allocated to. This is not the same as the organization a scanner observed on a banner: an address can be announced by a hosting provider while remaining allocated to the customer behind it.
GET /registry/domain/{domain}
GET /registry/ip/{ip}
GET /registry/asn/{asn}
Free. These consume only the per-minute allowance.
{
"object": "domain",
"name": "example.com",
"registrar": "IANA",
"status": ["client transfer prohibited"],
"registered": "1995-08-14T04:00:00Z",
"expires": "2025-08-13T04:00:00Z",
"nameservers": ["a.iana-servers.net", "b.iana-servers.net"],
"contacts": [{"roles": ["abuse"], "email": "abuse@iana.org"}],
"cached": false
}
Most registries redact contact details, so an empty contacts list is normal rather than an error. status carries the EPP codes — that is where you learn a domain is locked, expiring, or pending deletion.
Vulnerability scanning
Scanning is asynchronous. Define a scan, run it, then poll for findings.
Define a scan
POST /vulnerability/scans
{"name": "quarterly review", "targets": ["1.1.1.1", "example.com"]}
Targets are IP addresses or domains, up to 50 per scan. Returns 201 with the scan id.
Run it
POST /vulnerability/scans/{scanId}/runs
Returns 202 — accepted, not finished.
Collect the results
GET /vulnerability/scans/{scanId}/runs
GET /vulnerability/scans/{scanId}/runs/{runId}/results
An empty
findingslist does not mean the target is clean. A run still in progress has produced nothing yet. Check the run’s status from/runsbefore drawing any conclusion.
Scans belong to the account that created them. Presenting a scan id belonging to another account returns 404.
Brand and workforce monitors
Both answers come from scanners that need minutes to produce anything, so a monitor is a handle you poll rather than a lookup that returns.
POST /monitors
{"domain": "acme.com", "kind": "brand"}
kind | Returns |
|---|---|
brand | Lookalike and impersonating domains registered against the brand |
workforce | Identities the intelligence scanner associates with the domain |
Returns 202 with a monitor id. Then poll:
GET /monitors/{id}
{
"id": "…",
"kind": "brand",
"domain": "acme.com",
"status": "pending",
"createdAt": 1754563200,
"expiresAt": 1754570400,
"impersonations": []
}
The status field is what distinguishes “nothing found” from “nothing yet”:
| Status | Meaning |
|---|---|
pending | Scanners are still working. An empty result means not yet. |
ready | Findings are available. |
expired | The window closed. No new findings will arrive. |
Release a monitor early
DELETE /monitors/{id}
Returns 204. Deletes the monitor and releases the upstream resources behind it. Optional: monitors expire automatically two hours after creation and are cleaned up whether or not you call this.
Note again the absence of an organization parameter — a monitor is addressed by an opaque id scoped to the account that created it.
Upstream spend
Not exposed by this API. Consumption is visible in the upstream providers’ own dashboards, and duplicating that here would be a route nobody reads.
Rate limits
Two axes, because there are two distinct constraints. The per-minute allowance bounds how fast you can ask; the daily allowances bound how much you can spend against finite upstream resources that are purchased monthly.
| Allowance | Window | Applies to |
|---|---|---|
recon_lookup_per_minute | minute | Every route |
recon_query_per_day | day | /host, /search, /dns/domain |
recon_darkweb_per_day | day | /dark-web/search |
recon_scan_per_day | day | POST /vulnerability/scans, POST .../runs, POST /monitors |
Free routes — /search/count, /dns/resolve, /dns/reverse, every /registry lookup — and cached responses consume only the per-minute allowance. So does reading scan results back: only starting work counts against recon_scan_per_day, so a scan you paid for can always be collected.
Allowances during beta
This API is in beta and is not currently rate limited. Every paid tier — Free through Enterprise Max — is provisioned as unlimited while we learn what real usage looks like.
Usage is still recorded per account throughout. When beta ends, published allowances will be set from that data rather than estimated, so you will not be surprised by a number nobody validated.
Two things remain true today and will not change:
- Unauthenticated requests get nothing. Every route requires a session, so there is no anonymous allowance.
- Caching still applies. Polling faster than the TTL returns the same answer and, once limits return, will consume allowance for no fresher data.
Once limits return, exceeding one gives 429 with a Retry-After header. The authoritative values are always at /rate-limits — read them there rather than from documentation, which can lag.
Errors
Standard ThreatWinds error envelope. Two behaviours worth knowing:
| Status | Meaning |
|---|---|
| 400 | A malformed target, rejected before anything was spent. The response names the offending parameter. |
| 404 | The upstream holds no information for that target — a real answer, not a failure. |
| 429 | A rate limit was exceeded. Honour Retry-After. |
| 503 | An upstream is unavailable or not configured. This is never about your credentials — retry later. |
A 503 specifically does not mean re-authenticate. If ThreatWinds’ own upstream credential is rejected, that surfaces as 503 rather than as a 401 you could do nothing about.