Custom JA3
WithCustomFingerprint takes a raw JA3 string. The preset still drives HTTP/2 SETTINGS, headers, and the priority table, but the TLS ClientHello gets rebuilt from your JA3 on every connection.
When to use this
Reach for WithCustomFingerprint (JA3 only) when:
- You captured a JA3 from a real browser session and want to mirror that ClientHello exactly.
- You're testing what JA3 hashes look like for a given cipher / extension / curve permutation.
- You only care about TLS, the preset's headers and H2 are fine.
Reach for the JSON Preset Builder instead when:
- You need to tweak HTTP/2 SETTINGS along with the JA3.
- You want to change the User-Agent, sec-ch-ua list, or any other HTTP header.
- You want the change saved as a named preset for reuse.
JA3 is a one-liner. The JSON builder's a few more, but worth it the second you need anything beyond TLS.
JA3 string format
TLSVersion,CipherSuites,Extensions,EllipticCurves,PointFormats
Five comma-separated lists. Inside each list, values are dash-separated decimal IDs. Chrome 148 example:
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-21,29-23-24,0
771: TLS 1.2 protocol field (everything modern advertises TLS 1.2, then upgrades to 1.3 inside extensions).4865-4866-...: cipher suite IDs (TLS_AES_128_GCM_SHA256is 4865, etc).0-23-65281-...: TLS extension IDs (0=server_name, 23=session_ticket, 65281=renegotiation_info, etc). Order matters for the JA3 hash, but real Chrome shuffles this list per connection.29-23-24: supported groups / curves (29=x25519, 23=secp256r1, 24=secp384r1).0: EC point formats (0=uncompressed).
DevTools straight-up won't show you what TLS extensions Chrome put on the wire, and the order it shows is a lie since Chrome shuffles per connection. Capture from tls.peet.ws or a passive observer if you want the truth.
API
CustomFingerprint carries the JA3 plus a few uTLS-level extras. Setting JA3 flips the session into TLS-only mode automatically. Preset HTTP headers stop being applied, so you supply your own per request.
- Go
- Python
- Node.js
- .NET
package main
import (
"context"
"fmt"
"io"
"github.com/sardanioss/httpcloak"
)
func main() {
s := httpcloak.NewSession("chrome-148-windows",
httpcloak.WithCustomFingerprint(httpcloak.CustomFingerprint{
JA3: "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-21,29-23-24,0",
// optional uTLS extras:
ALPN: []string{"h2", "http/1.1"},
SignatureAlgorithms: []string{"ecdsa_secp256r1_sha256", "rsa_pss_rsae_sha256", "rsa_pkcs1_sha256"},
CertCompression: []string{"brotli"},
}),
)
defer s.Close()
resp, _ := s.Get(context.Background(), "https://tls.peet.ws/api/all")
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(body))
}
import httpcloak
with httpcloak.Session(
preset="chrome-148-windows",
ja3="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-21,29-23-24,0",
extra_fp={
"tls_alpn": ["h2", "http/1.1"],
"tls_signature_algorithms": ["ecdsa_secp256r1_sha256", "rsa_pss_rsae_sha256", "rsa_pkcs1_sha256"],
"tls_cert_compression": ["brotli"],
},
) as s:
r = s.get("https://tls.peet.ws/api/all")
print(r.json())
const { Session } = require("httpcloak");
const s = new Session({
preset: "chrome-148-windows",
ja3: "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-21,29-23-24,0",
extraFp: {
tls_alpn: ["h2", "http/1.1"],
tls_signature_algorithms: ["ecdsa_secp256r1_sha256", "rsa_pss_rsae_sha256", "rsa_pkcs1_sha256"],
tls_cert_compression: ["brotli"],
},
});
const r = await s.get("https://tls.peet.ws/api/all");
console.log(r.json());
s.close();
using HttpCloak;
using var s = new Session(
preset: "chrome-148-windows",
ja3: "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-21,29-23-24,0",
extraFp: new Dictionary<string, object> {
["tls_alpn"] = new[] { "h2", "http/1.1" },
["tls_signature_algorithms"] = new[] { "ecdsa_secp256r1_sha256", "rsa_pss_rsae_sha256", "rsa_pkcs1_sha256" },
["tls_cert_compression"] = new[] { "brotli" },
});
var r = await s.GetAsync("https://tls.peet.ws/api/all");
Console.WriteLine(r.Text);
Verification
Send the request, read the response, look at tls.ja3 and tls.ja3_hash. They mirror your input exactly:
INPUT JA3: 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-21,29-23-24,0
INPUT akamai: 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
OUTPUT ja3: 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-21,29-23-24,0
OUTPUT ja3_hash: cd08e31494f9531f560d64c695473da9
OUTPUT ja4: t13d1516h2_8daaf6152771_f37e75b10bcc
OUTPUT akamai: 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
OUTPUT akamai_hash: 52d84b11737d980aef856699f885ca86
The reflected ja3 is byte-identical to the input. ja3_hash is a stable MD5 of that string, so it stays stable across runs (unlike preset Chrome where extensions shuffle). ja4 differs from the underlying chrome-148-windows (d8a2da3f94cd vs f37e75b10bcc) because we used a different extension list. That's expected and proves the override took effect.
TLS-only mode is automatic
Setting JA3 flips the session into TLS-only mode. The preset's HTTP headers go away, the preset's User-Agent goes away, and you set everything per request:
- Go
- Python
req := &httpcloak.Request{
Method: "GET",
URL: "https://tls.peet.ws/api/all",
Headers: map[string][]string{
"User-Agent": {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"},
"Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
"Accept-Language": {"en-US,en;q=0.9"},
},
}
resp, _ := s.Do(context.Background(), req)
r = s.get(
"https://tls.peet.ws/api/all",
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
},
)
Want JA3 override and preset headers? Use the JSON Preset Builder approach instead. Describe the preset, override tls.ja3 in the JSON, leave headers alone, register, send.
Limitations
- JA3 is deprecated for a reason. Modern anti-bot stacks key on JA4 / peetprint / akamai. Mirroring a Chrome JA3 while inheriting Chrome's H2 SETTINGS gets you the right JA4 / peetprint / akamai too, that's the case shown above. But if your JA3 says one browser and your H2 says another, you're inconsistent and easy to flag.
- Setting
JA3clears the preset'sclient_helloID. The session rebuilds a ClientHello from the JA3 every connection. uTLS handles it, but it's lossy compared to a real browser ClientHelloID. JA3 doesn't capture extension data like ALPS, key share groups, application-settings, so the resulting handshake is close-but-not-identical to real Chrome. For byte-exact Chrome bytes, use a preset. - The
extras(ALPN, SignatureAlgorithms, CertCompression, PermuteExtensions) are uTLS-specific knobs that ride alongside the JA3. They don't show up inside the JA3 string itself, but they do hit the wire.