Editor’s note (updated 2026-09-29): This post originally announced our SDKs in November 2025. It has been rewritten to reflect the SDK as it ships today. Some things announced then never shipped (there is no Go SDK, and the PHP code is distributed as the WordPress plugin and the webdecoy/sensor package, not a webdecoy/wordpress-plugin Composer package), and several configuration options shown in the original do not exist. Everything below matches the current @webdecoy/node packages.

This post covers the WebDecoy Node.js SDK, explains what TLS fingerprinting is, walks through JA3 and JA4, and is straight about where each fingerprint can actually be measured, which matters more than any algorithm.

The WebDecoy Node.js SDK

The SDK is a family of npm packages:

PackageWhat it is
@webdecoy/nodeThe core: protect(), the local rules engine (rate limits, filters, tripwires, bot rules, Web Bot Auth), agent verification, and a self-hosted proof-of-work captcha
@webdecoy/expressExpress middleware
@webdecoy/fastifyFastify plugin
@webdecoy/nextjsNext.js middleware
@webdecoy/honoHono middleware (a thin wrapper over the fetch guard)
@webdecoy/clientThe browser-side counterpart

All of it is TypeScript with bundled type definitions. The framework adapters default to monitor mode: detections are recorded and reported, and requests are served normally until you set mode: 'enforce'.

On WordPress: WebDecoy ships a WordPress plugin with its PHP bundled inside. For WordPress and WooCommerce, the plugin is the supported route.

Understanding TLS Fingerprinting

TLS fingerprinting identifies clients by the characteristics of their TLS handshake: the ClientHello message a client sends when it opens an HTTPS connection.

Why TLS Fingerprinting Matters

A ClientHello advertises:

  • TLS version (1.2, 1.3)
  • Cipher suites (encryption algorithms it supports)
  • Extensions (additional TLS features)
  • Supported groups (elliptic curves for key exchange)
  • Signature algorithms
  • ALPN protocols (Application-Layer Protocol Negotiation)

Real browsers like Chrome, Firefox, and Safari send very specific combinations. HTTP libraries such as curl, wget, python-requests, and Go’s net/http send different ones. And unlike a User-Agent header, whoever writes the User-Agent string has no say in the ClientHello: it comes from the TLS library.

The Problem with Traditional Bot Detection

Traditional bot detection relies on:

  • User-Agent strings (trivially spoofed)
  • IP reputation (VPNs, proxies, and residential proxy pools get around it)
  • Behavioral analysis (automation can imitate human interaction)

TLS fingerprinting looks at a lower layer. It is not unforgeable (tools like uTLS and curl-impersonate exist precisely to copy browser handshakes), but it raises the cost of passing as a browser considerably.

JA3 Fingerprinting: The Foundation

JA3 is a method for creating TLS fingerprints published by Salesforce in 2017.

How JA3 Works

JA3 builds a string from the ClientHello:

  1. TLS Version
  2. Cipher Suites (in order)
  3. Extensions (in order)
  4. Elliptic Curves (supported groups)
  5. EC Point Formats

Values within a field are joined with hyphens, fields with commas, and the result is MD5 hashed:

771,4866-4865-4867,0-11-10-35-23,29-23-24,0

↓ MD5 ↓

a 32-character hex hash

Limitations of JA3

  • Extension order randomization: Chrome has randomized its extension order since version 110, so one browser produces many JA3 hashes
  • GREASE values: Randomized placeholder values have to be filtered out or they change the hash
  • Evasion: Tools like uTLS can reproduce a browser’s ClientHello

This is where JA4 comes in.

JA4 Fingerprinting: The Next Generation

JA4, released by FoxIO in 2023, is also computed from the TLS ClientHello, but it is designed to survive the problems above:

  • It sorts cipher suites and extensions before hashing, so extension-order randomization no longer changes the fingerprint
  • It ignores GREASE values
  • It is human-readable in part: a JA4 looks like t13d1516h2_8daaf6152771_b0da82dd1658, where the first section encodes protocol, TLS version, SNI presence, cipher and extension counts, and the first ALPN value, and the other two are truncated hashes of the sorted ciphers and of the extensions plus signature algorithms

JA4 is part of a larger JA4+ family that also fingerprints servers, HTTP requests, certificates, and TCP. WebDecoy uses JA3 and JA4 (the TLS client fingerprints); it does not compute the other JA4+ fingerprints.

Where a TLS Fingerprint Can Be Measured

Here is the part the original version of this post got wrong: a fingerprint can only be computed by whatever terminates the TLS connection, because that is the only place the raw ClientHello exists.

For most Node.js apps, that is not Node. A CDN, load balancer, or reverse proxy terminates TLS and forwards plain HTTP. And even when Node terminates TLS itself, its tls module does not expose the client’s offered cipher list or extensions, so a JA3 or JA4 cannot be computed from a standard TLSSocket.

So in the WebDecoy stack, fingerprints come from the layers that can see the handshake:

  • Decoys served by WebDecoy. The ingest service terminates TLS for decoy domains itself, captures the raw ClientHello, and stores a spec-compliant JA4 on the detection.
  • Your edge. Providers that terminate TLS can expose JA4 (for example Cloudflare with Bot Management, and AWS WAF). JA4 is the key WebDecoy’s actor-level WAF blocks use.

What the Node.js SDK Does With TLS Data

With that constraint in mind, here is exactly what the SDK does:

1. It forwards TLS parameters you supply

RequestMetadata has an optional tls_info field. If your stack can capture ClientHello parameters (for example, a TLS-terminating proxy you control that passes them along), you can hand them to protect(). With enableTLSFingerprinting on (the default), a request carrying tls_info is always sent to the WebDecoy service, which computes a JA3 hash from it, checks it against its known-bot fingerprint table, and compares it with the User-Agent for a browser mismatch.

import { WebDecoy } from '@webdecoy/node';

const webdecoy = new WebDecoy({ apiKey: process.env.WEBDECOY_API_KEY });

const decision = await webdecoy.protect({
  method: 'GET',
  path: '/api/products',
  ip: clientIp,
  user_agent: userAgent,
  headers,                      // Record<string, string>
  timestamp: Date.now(),
  tls_info: {
    version: 771,               // 0x0303, TLS 1.2 record version
    cipher_suites: [4865, 4866, 4867, 49195, 49199],
    extensions: [0, 23, 65281, 10, 11, 35, 16],
    supported_groups: [29, 23, 24],
    ec_point_formats: [0],
  },
});

if (!decision.allowed) {
  // blocked verdict
}

The service computes JA3 only on this path, not JA4: JA4 needs the raw ClientHello, and reconstructed parameters are not enough to produce a spec-compliant value. If you have no ClientHello data, leave tls_info out; the SDK works fine without it.

2. The self-hosted captcha checks JA3, and can read a forwarded JA4

The in-process Captcha (and the framework captcha endpoints built on it) accepts a client-supplied ja3Hash and matches it against a small built-in table: Python requests, Python urllib, Go net/http, curl, Wget, Java HttpClient, Node.js axios, Node.js node-fetch, and Puppeteer’s default. JA3 supplied by the client is spoofable, and the code treats it that way.

For JA4, the captcha has a trustedJA4Headers option: the names of headers your own reverse proxy sets with the JA4 it computed. The SDK only reads JA4 from headers you name, because a header any client can set proves nothing. Be aware that the SDK ships no built-in JA4 table, so today a forwarded JA4 is read but does not change the in-process score. Comprehensive fingerprint matching happens server side.

3. The server does the heavy lifting

When a request is sent to the WebDecoy service, fingerprint matching, enrichment (AbuseIPDB, GeoIP, VPN/proxy/datacenter detection), and scoring happen there, and the verdict comes back to protect(). The detection shows up in your dashboard with the signals that produced it.

Getting Started

Express

npm install @webdecoy/express @webdecoy/node
import express from 'express';
import { webdecoy } from '@webdecoy/express';

const app = express();

app.use(webdecoy({
  apiKey: process.env.WEBDECOY_API_KEY,
  // mode defaults to 'monitor': detect and report, serve the request.
  // Switch to 'enforce' once you've reviewed what it would block.
}));

Verify the install with the reserved test User-Agent, which always produces a labeled Test detection:

curl -A "WebDecoy-Test/1.0" http://localhost:3000/

Core SDK

For other frameworks, install @webdecoy/node and call webdecoy.protect(metadata) directly, or use @webdecoy/fastify, @webdecoy/nextjs, or @webdecoy/hono.

Verifying signed agents

TLS fingerprints tell you what software sent a request. For AI agents that sign their requests with Web Bot Auth, the SDK can also check who sent it, locally:

import { WebDecoy } from '@webdecoy/node';

const webdecoy = new WebDecoy({});

const verdict = await webdecoy.detectBot(request);
// verdict.status: 'verified' | 'impersonation' | 'claimed' | 'none'
if (verdict.status === 'verified') {
  console.log('verified agent:', verdict.agentName, verdict.category);
}

Key Takeaways

  • TLS fingerprinting identifies client software from its handshake, below the layer where headers are spoofed
  • JA3 is an MD5 of ClientHello fields and is disrupted by extension-order randomization
  • JA4 sorts and structures the same data, so it stays stable across randomized browsers
  • Fingerprints can only be measured where TLS terminates: WebDecoy’s own decoy endpoints and your edge, not a typical Node.js process
  • The Node.js SDK forwards TLS parameters you supply (JA3 server side), checks JA3 in its captcha, reads JA4 only from proxy headers you trust, and defaults to monitor mode

Get Started

Try WebDecoy for free, or read the full SDK documentation.

Want to see WebDecoy in action?

Get a personalized demo from our team.

Request Demo