@whoisxmlapidotcom/techrecon
v0.1.2
Published
TypeScript client for the TechRecon JA4 Enrichment API
Readme
@whoisxmlapidotcom/techrecon
TypeScript client for the TechRecon JA4 Enrichment API — JA4 fingerprint
lookups, per-domain technology detection, change feeds, bulk exports, and
account management. Types are generated from docs/openapi.yaml
and the client is a thin, dependency-free wrapper around fetch.
npm status:
@whoisxmlapidotcom/techrecon0.1.0 and 0.1.1 are published, but both were built with a default base URL missing the public/apiprefix (bare/v1/...paths 302 to the marketing site), so they fail out of the box unless you passbaseUrlexplicitly. Use 0.1.2 or later once published (default base URL fixed to the live/apiedge).
Install
npm install @whoisxmlapidotcom/techreconOr consume it directly from this path (e.g. via a workspace
reference or npm install ../path/to/sdk/typescript).
Get an API key
All endpoints except health/readiness/docs require an API key, sent as the
X-API-Key header (the client sets this for you). Obtain a key from your
TechRecon account, or mint one programmatically:
import { TechReconClient } from "@whoisxmlapidotcom/techrecon";
const client = new TechReconClient({ apiKey: process.env.TECHRECON_API_KEY! });
const { api_key } = await client.issueAPIKey({ label: "my-integration" });
console.log("Save this key now — it is shown only once:", api_key);Quickstart: first domain lookup
import { TechReconClient, TechReconError } from "@whoisxmlapidotcom/techrecon";
const client = new TechReconClient({
apiKey: process.env.TECHRECON_API_KEY!,
// baseUrl defaults to https://techrecon.whoisxmlapi.com/api; override for local dev:
// baseUrl: "http://localhost:8080",
});
async function main() {
try {
const result = await client.getDomain("wordpress.org");
console.log(result);
} catch (err) {
if (err instanceof TechReconError) {
console.error(`TechRecon API error (${err.status}):`, err.body?.error);
} else {
throw err;
}
}
}
main();Example response (GET /v1/domain/wordpress.org):
{
"domain": "wordpress.org",
"tech_count": 2,
"source": "postgres",
"ip": "198.143.164.252",
"country": "US",
"hosting_provider": "Automattic",
"security_score": 90,
"rank": 142,
"title": "WordPress.org — Your home in the fediverse",
"crawl_status": "ok",
"last_crawled_at": "2026-04-20T08:00:00Z",
"technologies": [
{
"technology": "WordPress",
"version": "6.5.0",
"confidence": 0.99,
"category": "cms",
"signals": ["html_pattern", "headers", "meta_tag"],
"verified": true,
"first_seen_at": "2026-04-20T08:00:00Z",
"last_seen_at": "2026-04-20T08:00:00Z",
"crawl_count": 1
},
{
"technology": "PHP",
"confidence": 0.95,
"category": "programming-language",
"signals": ["headers"],
"verified": true,
"first_seen_at": "2026-04-20T08:00:00Z",
"last_seen_at": "2026-04-20T08:00:00Z",
"crawl_count": 1
}
]
}More examples
JA4 fingerprint lookup
const result = await client.lookup({
ja4_fingerprint: "t13d1516h2_8daaf6152771_b0da82dd1658",
ip: "93.184.216.34",
});
// result.ja4_tech_match is null if no mapping was found.Bulk domain lookup
const { results, count } = await client.bulkDomainLookup([
"example.com",
"wordpress.org",
]);IP batch lookup
const { results, queried_count, found_count } = await client.ipBatchLookup({
ips: ["104.21.0.1", "172.67.0.1"],
include_categories: ["vpn"], // optional
});Change feed
const { changes, has_more, next_cursor } = await client.getChanges({
since: "2026-04-01T00:00:00Z",
technology: "WordPress",
});Change stream (Server-Sent Events)
const controller = new AbortController();
await client.streamChanges({
technology: "WordPress",
signal: controller.signal,
onChange: (event) => console.log("change:", event),
onError: (err) => console.error("stream error:", err),
});Bulk export
const job = await client.createExport({
technology: "WordPress",
format: "csv",
});
const status = await client.getExportStatus(job.id);
if (status.status === "complete") {
const csv = await client.downloadExport(job.id);
}Error handling
Every non-2xx response throws TechReconError (or one of its subclasses):
PaymentRequiredError— HTTP 402 (billing required)RateLimitError— HTTP 429 (rate limit or quota exceeded)
Each carries status (the HTTP status code) and body (the parsed
{"error": "..."} response, when present).
import {
TechReconError,
PaymentRequiredError,
RateLimitError,
} from "@whoisxmlapidotcom/techrecon";
try {
await client.getDomain("example.com");
} catch (err) {
if (err instanceof PaymentRequiredError) {
// handle billing
} else if (err instanceof RateLimitError) {
// back off and retry
} else if (err instanceof TechReconError) {
console.error(err.status, err.body?.error);
}
}Development
npm install
npm run build # emit dist/
npm run typecheck # tsc --noEmitRegenerate src/schema.d.ts from the spec after any docs/openapi.yaml change:
npx [email protected] ../../docs/openapi.yaml -o src/schema.d.tsThe pinned version must match the one in .github/workflows/sdk-drift.yml — the
SDK Drift check regenerates this file and fails the PR if the committed copy
differs. When you upgrade the generator, bump the version in both places and
commit the regenerated file in the same PR.
License
MIT — see LICENSE.
