Custom fingerprints
Build a fingerprint from a JA3 string, HTTP/2 settings and TLS extensions.
When no built-in profile matches, describe the fingerprint yourself. Start from a capture of the client you want to copy, e.g. what tls.peet.ws reports for it, and fill in the matching options:
const session = new Session({
// TLS ClientHello: version, ciphers, extensions, curves, point formats
ja3string:
"771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
supportedSignatureAlgorithms: [
"ECDSAWithP256AndSHA256",
"PSSWithSHA256",
"PKCS1WithSHA256",
"ECDSAWithP384AndSHA384",
"PSSWithSHA384",
"PKCS1WithSHA384",
"PSSWithSHA512",
"PKCS1WithSHA512",
],
supportedVersions: ["GREASE", "1.3", "1.2"],
keyShareCurves: ["GREASE", "X25519"],
certCompressionAlgos: ["brotli"],
alpnProtocols: ["h2", "http/1.1"],
alpsProtocols: ["h2"],
// HTTP/2 SETTINGS frame, window update and header frame priority
h2Settings: {
HEADER_TABLE_SIZE: 65536,
ENABLE_PUSH: 0,
INITIAL_WINDOW_SIZE: 6291456,
MAX_HEADER_LIST_SIZE: 262144,
},
h2SettingsOrder: [
"HEADER_TABLE_SIZE",
"ENABLE_PUSH",
"INITIAL_WINDOW_SIZE",
"MAX_HEADER_LIST_SIZE",
],
connectionFlow: 15663105,
pseudoHeaderOrder: [":method", ":authority", ":scheme", ":path"],
headerPriority: { streamDep: 0, exclusive: true, weight: 255 }, // weight: 0–255
});
const seen = await (await session.get("https://tls.peet.ws/api/all")).json();
console.log(seen.http2.akamai_fingerprint); // 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,pclientIdentifier wins
The custom options only apply when the session has a ja3string and no
clientIdentifier. If both are set, clientIdentifier is used and all of the
options below are silently ignored.
The pieces
| Part of the fingerprint | Options |
|---|---|
| TLS ClientHello | ja3string (TLS version, cipher suites, extensions, curves, point formats) |
| TLS extension contents | supportedSignatureAlgorithms, supportedVersions, keyShareCurves, certCompressionAlgos, alpnProtocols, alpsProtocols, supportedDelegatedCredentialsAlgorithms, recordSizeLimit |
| Extension order | randomTlsExtensionOrder: true shuffles it per connection, as Chrome does |
| HTTP/2 connection | h2Settings + h2SettingsOrder (SETTINGS frame), connectionFlow (WINDOW_UPDATE), priorityFrames |
| HTTP/2 requests | pseudoHeaderOrder, headerPriority |
| HTTP/3 | h3Settings, h3SettingsOrder, h3PseudoHeaderOrder, h3PriorityParam, h3SendGreaseFrames. See HTTP/3 |
| Encrypted Client Hello | echCandidatePayloads, echCandidateCipherSuites |
Every option is listed with its type in SessionOptions; allowed string values are in Types.
Reading a capture
tls.peet.ws shows the values in the same shape:
tls.ja3is theja3string.http2.akamai_fingerprintisSETTINGS|WINDOW_UPDATE|PRIORITY|pseudo-header order. For1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p:1:65536;2:0;…givesh2Settingsandh2SettingsOrder. Ids: 1HEADER_TABLE_SIZE, 2ENABLE_PUSH, 3MAX_CONCURRENT_STREAMS, 4INITIAL_WINDOW_SIZE, 5MAX_FRAME_SIZE, 6MAX_HEADER_LIST_SIZE.15663105givesconnectionFlow.0means nopriorityFrames.m,a,s,pgivespseudoHeaderOrder(:method,:authority,:scheme,:path).
- The
extensionslist gives the signature algorithms, versions, key shares and ALPN/ALPS values.
The example above reproduces exactly that Akamai fingerprint.
Gotchas
GREASE. JA3 strings from Chrome contain GREASE values (e.g.2570). List"GREASE"insupportedVersionsandkeyShareCurveswhere the browser sends it.- Weights are 0–255. A
weightof 256 or more inheaderPriorityorpriorityFramesfails every request (status: 0, "cannot unmarshal number 256"). headerPriority. When unset, a zeroed priority is sent, as in earlier versions. Passnullto send none.ENABLE_PUSH. Use0/1; booleans are converted.certCompressionAlgosdefaults to["zlib"]. The singularcertCompressionAlgois deprecated.- Extension 51764 (trust anchors) requires
trustAnchorsPayload, which the 1.16.0 native binaries ignore.
Check every change against a fingerprint echo service: a typo produces a valid-looking but different fingerprint.