Using Dactyloscope from a script or an agent

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.

Check the network layer only (no browser needed)

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

Inspect a browser and read the result

  1. Point the browser at https://HOST:8443/?tag=my-run. The page runs the probes and saves the report.
  2. Fetch it: 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.

Endpoints

EndpointWhat 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/whoamiNetwork-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>/summaryPlain-text summary.
GET /api/report/<id>/findings?min=warnOnly 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/knownThe reference shapes the analysis compares against (loaded from captures/ plus embedded header expectations).
GET /ca.pem, GET /healthzLocal CA certificate; liveness.

Report shape

{
  "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": …}]
  }
}

Finding ids

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.