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
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 findings list does not mean the target is clean. A run still in progress has produced nothing yet. Check the run’s status from /runs before 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.