Every page here has a JSON twin. Nothing needs a session or a token. Answers are plain JSON; add Accept: text/plain (or ?format=text) to any report URL for a short summary meant to be read by a person or a language model.
curl -sk https://HOST:8443/api/whoami # JSON: TLS ClientHello, JA3/JA4, HTTP/2 preface, headers, findings curl -sk -H 'accept: text/plain' https://HOST:8443/api/whoami
https://HOST:8443/?tag=my-run. The page runs the probes and saves the report.GET /api/report/latest?tag=my-run&wait=60 long-polls until the probes finished.# any WebDriver browser (the CLI does exactly this)
python -m dactyloscope check --webdriver http://127.0.0.1:7000 --server https://127.0.0.1:8443 --tag ci --fail-on warn
# by hand
curl -s -X POST http://127.0.0.1:7000/session -d '{"capabilities":{}}'
curl -s -X POST http://127.0.0.1:7000/session/SID/url -d '{"url":"https://HOST:8443/?tag=my-run"}'
curl -sk 'https://HOST:8443/api/report/latest?tag=my-run&wait=60' -H 'accept: text/plain'
The browser must trust the Dactyloscope certificate: run it with SSL_CERT_FILE=/path/to/ca.pem (download the CA from /ca.pem, also on the plain port), or put Dactyloscope behind a certificate it already trusts.
| Endpoint | What it returns |
|---|---|
| GET / | The probe page. Loading it creates a report from the request's TLS/HTTP/2/header data and runs the in-page probes. ?tag= and ?label= are stored, ?noredirect=1 keeps the page from opening the report at the end. |
| GET /api/whoami | Network-only fingerprint and findings for this connection. |
| GET /api/report/<id>[?wait=s] | Full report. With wait, blocks until the probes are done (max 600 s). |
| GET /api/report/latest[?tag=&wait=&after=id] | Newest finished report, optionally for a tag; with wait, waits for one to appear (after a given id). |
| GET /api/report/<id>/summary | Plain-text summary. |
| GET /api/report/<id>/findings?min=warn | Only the findings at or above a severity. |
| DELETE /api/report/<id> | Remove a report. |
| GET /api/reports?tag=&limit= | Summaries, newest first. |
| GET /api/compare?a=&b= | Field-by-field diff. a/b are report ids, latest, or baseline:<name>. |
| GET /api/baselines, POST /api/baseline/<name>?report=<id>, DELETE /api/baseline/<name> | Baselines are reports you trust, e.g. from a real Chrome on a real Windows machine. |
| GET /api/known | The reference shapes the analysis compares against (loaded from captures/ plus embedded header expectations). |
| GET /ca.pem, GET /healthz | Local CA certificate; liveness. |
{
"id": "20260904-153000-a1b2c3", "created": "…", "done": true, "tag": "ci", "label": null,
"client": {"ip": "…", "port": 0},
"network": {
"scheme": "https", "http_version": "2",
"tls": {"ja3": {"hash", "raw"}, "ja3n": {…}, "ja4": {"hash", "raw", "original", "original_raw"},
"shape": {…same vocabulary as captures/*.json…}, "decoded": {…names…},
"negotiated": {"version", "cipher", "alpn"}, "match": {"known", "closest", "exact_matches", "differences"}},
"h2": {"akamai_fingerprint": "1:65536;…|15663105|0|m,a,s,p", "settings", "window_update", "pseudo_header_order", "frames", "match"},
"http": {"method", "path", "version", "headers": [["name","value"],…], "user_agent", "match": {"reference", "differences"}},
"requests": [ …subresource requests with their header sets… ]
},
"probes": {"navigator", "uadata", "screen", "timezone", "document", "engine", "apis", "automation", "integrity",
"canvas", "webgl", "audio", "fonts", "permissions", "media", "worker", "iframe", "storage", "timing"},
"analysis": {
"score": 0-100, "verdict": "clean|suspicious|automated", "counts": {"fail", "warn", "info"},
"identity": {"claimed": {"family","major","os","engine"}, "tls_family", "h2_family", "http_family", "js_engine", "js_engine_evidence"},
"findings": [{"id": "engine.mismatch", "severity": "fail", "category": "engine", "message": "…", "evidence": …}]
}
}
Ids are stable and dotted by area: tls.*, h2.*, http.*, nav.*, engine.*, apis.*, screen.*, tz.*, canvas.*, webgl.*, audio.*, fonts.*, auto.*, integrity.*, worker.*, iframe.*, media.*, storage.*, timing.*. A fail is something a detector can use on its own; a warn is a deviation from what the claimed browser does; info is context. Score = 20 per fail + 6 per warn, capped at 100.