Documentation

Device risk

A visitor ID for every browser on your sign-in, sign-up, password-reset and approval pages, like Fingerprint's visitorId, and the risk engine's view of it. The page identifies with your public site key and gets { visitorId, requestId, confidence }; your server reads the trusted result by requestId and passes it to sign-ins. A paid add-on, off until you turn it on (Device risk in the dashboard).

PieceRule
visitorIdtqv_…, this browser in your workspace only: another Touchque customer sees another one for the same browser. It survives cleared site data, private windows where the browser shows its traits, and browser updates.
confidence0.99 sealed by the browser's device-bound key, 0.95 its stored id, up to 0.9 a match of its traits (lower for identical fleet laptops), 0.5 to 0.6 a first sight.
requestIddre_…, one identification: for one account, for 15 minutes. Another account gets device_report_replayed.
networkThe address of the identification as the risk engine saw it: country, region and city (never coordinates), the operator (ASN), the kind of network (isp, hosting, tor) and whether the browser's clock fits the address (timezoneMismatch: a VPN or proxy in another country).
velocityThis visitor's counts: accounts in 24 h and 30 days, sign-ups and countries in 24 h, stored-id resets in 7 days (the risk engine's), identifications in the last hour and day.
botOne answer to "a bot?": detected (the browser says it is driven: webdriver, a headless browser), suspected (the pointer moved like software, a zero-size window, software graphics) or not_detected, with the reasons. A summary of the signals: the risk engine still decides.
Site key and originsThe site key (tqpk_…) is public; Touchque answers only pages on your organization's verified domains (Organization → Domains) and your listed origins. identify is a text/plain POST, so no CORS preflight: allow your Touchque API host in connect-src.
One line on the page, one on your server

The script tag (script.js, with your site key and its version's integrity hash from Device risk → Settings; a page that loads …/device.js, its first name, keeps working with the same hash) identifies the browser when the page loads and puts the requestId into a hidden touchque_request_id field of every form, forms added later too; it takes a new one after 10 minutes and after each submit. On submit it adds how the pointer and the fingers moved, after the requestId and a dot: 18 characters of counts the browser computed (presses on a jumped pointer and how long they lasted, how straight long strokes and swipes were, how long taps lasted), never a position or a path; data-pointer="off" leaves it out. Your server passes that field to deviceRisk.check as it came. Single-page apps read window.TouchqueDevice (ready, value(), requestId, refresh()); the 1.6.0 tag set window.TouchQueDevice.

<script src="https://cdn.touchque.com/device/v2.0.0/script.js" data-site-key="tqpk_…" data-purpose="sign_in"
        integrity="sha384-a69uLWQn3eccd/RGOZIKDLN16dOQIXvX2eGDlOUYpmMMq4QVyqiTcuoRgT17JGLG" crossorigin="anonymous" async></script>
Removing the tag gets a step-up, never a way in. requestIds are made only by Touchque's server, used for one account, bound to your site's origin and to 15 minutes. The page's value is never trusted: your server reads the verdict by requestId and the risk engine decides. A page without the tag, or with it blocked, sends no requestId: the check still answers (report: missing, unknown or expired) with device_identification_missing, which only ever asks for a number-matched step-up and starts in shadow like the others.
One decision maker. Touchque's risk engine counts per visitor (accounts, sign-ups, countries, stored-id resets), checks the browser's clock against the address and decides, for sign-ins (login.request, risk.check with deviceRiskRequestId) and for pages before an account (POST /device-risk/check). Every signal only ever asks for more: a number-matched approval or a passkey, on a sign-up your own extra check. None blocks on its own, none lets a check be skipped, and a sign-in with one is never silent. Signals start in shadow (reported, enforced: false); a visitor your admin marked "Step up always" asks at once.
Signals
codeMeaningAsks for
device_tamperedBrowser functions replaced by script, or traits that contradict each other (an anti-detect browser, a spoofing extension).number_match
device_virtualA virtual machine, software graphics, a headless or automated browser.number_match
device_geo_mismatchThe browser's clock is 2 h or more off the address's time zone (a proxy, VPN or remote access).passkey
device_many_accounts3 accounts in 24 h, or 6 in 30 days, on one visitor (not counted for identical fleet laptops).number_match
device_signup_burst3 sign-ups in 24 h from one visitor.number_match
device_id_resetsThe stored id cleared twice in 7 days and back as the same visitor (private windows not counted).number_match
device_many_countriesAddresses in 3 countries in 24 h for one visitor.number_match
device_wrong_siteThe identification was made on a site that is not yours (a look-alike or proxy page).passkey
device_report_replayedAn identification used for another account, or a report sent twice.passkey
device_private_windowA private window: context only, never asks on its own.none
device_step_up_by_adminYour admin marked the visitor "Step up always": enforced at once.number_match
device_identification_missingYour server checked a page that sent no identification, or one that does not verify (unknown, expired, another workspace's): the tag was removed or blocked. Ad blockers do that to honest people too.number_match
device_pointer_syntheticThe pointer or a finger moved like software, not a hand: every press landed where the pointer had just jumped and lasted under 10 ms, taps of under 10 ms, or ruler-straight strokes or swipes (Playwright, Puppeteer, Selenium, AI browser agents). From the counts the tag sends with the form, never a position or a path. No movement at all (a keyboard, a screen reader) is never a sign; Voice Control and Switch Control move like this too, so it only asks.number_match
What is read and what is kept

The page reads coarse traits (OS, browser family and major version, time zone, languages, screen size, cores, memory class, touch points, the graphics card's name), short hashes of how the browser draws a test text, plays a test sound, which of 25 common fonts it has, a few Math results and its WebGL limits, and findings (replaced browser functions, a virtual machine, a headless browser, a private window where detectable). Touchque keeps no raw trait: keyed hashes with a key of your workspace alone, coarse labels, the account, the country and a keyed hash of the address's network, for 30 to 90 days as you set; the address itself for a day at most. Never a MAC address, a serial number or a cross-customer device graph.

Tell your users. Use it only on sign-in, sign-up, password-reset and approval pages, and say it in your privacy notice. In Turkey (KVKK) account security can rest on legitimate interest (art. 5/2(f)); in the EU, reading the device needs consent unless it is strictly necessary for the security of a sign-in the user asked for (ePrivacy art. 5(3)), and a check that lets a known device do less does not count. Ask your lawyer. A starting text is under Device risk → Settings.
From your own domain

Ad blockers and browser privacy lists drop scripts and calls to known third-party fingerprinting hosts, honest visitors' too (they then get Identification missing, a step-up at most). Large sites serve the tag and its identify call from their own domain instead, like Fingerprint's proxy integrations: GET /_tq/device/vX.Y.Z/script.js goes to https://cdn.touchque.com/device/vX.Y.Z/script.js (the same bytes, so the same integrity hash) and POST /_tq/device-risk/identify to https://api.touchque.com/device-risk/identify. The tag then points at your domain, and your Content-Security-Policy needs only 'self':

<script src="https://www.example.com/_tq/device/v2.0.0/script.js" data-site-key="tqpk_…" data-purpose="sign_in" data-api="https://www.example.com/_tq"
        integrity="sha384-a69uLWQn3eccd/RGOZIKDLN16dOQIXvX2eGDlOUYpmMMq4QVyqiTcuoRgT17JGLG" crossorigin="anonymous" async></script>

The proxy secret: behind your proxy every visitor reaches Touchque from your proxy's address. Send the visitor's address as X-Touchque-Client-IP with your workspace's proxy secret as X-Touchque-Proxy-Secret (tqps_…, Device risk → Settings for owners and admins; GET /device-risk/settings with the API key). Touchque then records and scores the visitor's address and counts the per-address limit (60 identifications a minute) on it, not on your proxy. Keep the secret on the server like the API key, never in a page or in git. A wrong one is 403 proxy_secret_invalid; replacing the site key replaces it. Without it the proxy still works, but every visitor counts against the proxy's one address: small sites only. Drop your own cookies on the way, pass Origin on as it came (Touchque checks it against your domains and origins), and pass the visitor's address as clientIp to deviceRisk.check on your server.

location /_tq/device/ {
    proxy_pass https://cdn.touchque.com/device/;
    proxy_ssl_server_name on;
    proxy_set_header Cookie "";
    proxy_set_header Authorization "";
}
location = /_tq/device-risk/identify {
    limit_except POST { deny all; }
    proxy_pass https://api.touchque.com/device-risk/identify;
    proxy_ssl_server_name on;
    proxy_set_header Cookie "";
    proxy_set_header Authorization "";
    proxy_set_header X-Forwarded-For "";
    proxy_set_header X-Touchque-Proxy-Secret "tqps_…";   # Device risk → Settings; keep this file out of git
    proxy_set_header X-Touchque-Client-IP $remote_addr;  # behind a load balancer: set_real_ip_from + real_ip_header first
}

Limits: a visitor's address gets 60 identifications a minute; a site key, and a workspace's calls to deviceRisk.check, get 1,200 a minute each. Above a limit the call answers 429 with Retry-After. Device risk → Settings shows your number (ratePerMinute in GET /device-risk/settings); a bigger site asks support (support@touchque.com) with its expected traffic and Touchque raises it for the workspace.

Vercel rewrites cannot add the proxy secret: use them for small sites, or the Next.js middleware (also on Vercel) for the identify path at volume. With a verified Touchque Login custom domain there is nothing to proxy: Touchque serves both paths on it, and Device risk → Settings shows the tag with data-api="https://login.example.com/_tq".

A busy site has millions of identifications, so nothing lists them all: GET /device-risk/events (deviceRisk.searchEvents in every SDK, the Identifications tab in the dashboard) answers one page, newest first, for a date range of at most 90 days within your retention (7 by default), filtered by signal, stepUp, page (kind), origin, ip (an address or a CIDR range like 203.0.113.0/24; never returned), account, visitorId or requestId, each on an index. Pass nextCursor back as cursor until it is null (1 to 100 a page); do not page by moving from/to. A search that would read too much answers 422 search_too_broad: narrow the range or add a filter. For everything at once: a data request (Organization → Data requests, JSON or CSV, one row per identification, no address), the webhooks and the log stream.

Monitoring and alerts

Device risk → Overview shows the last 30 days, 90 days or 12 months per day (identifications with the step-ups, signals with the enforced part; from daily counts kept 400 days, longer than the identifications themselves) and the recent identifications, updated every 10 seconds while the page is open. Alerts (Device risk → Settings → Alerts) tell your people when something is going on; each rule has an on/off and a threshold and alerts at most once an hour. They go to your webhook and by email to your organization's owners and admins (once a mail sender is set up). An alert never changes a sign-in: the risk engine decides those.

AlertGoes off whenDefault
signup_burstOne visitor signed up this many times in 24 h5
many_accountsOne visitor on this many accounts in 24 h5
tamper_spikeThis many identifications with a tampered or virtual browser in an hour20
Webhooks and log streams

Your webhook (Developers → Webhooks) gets DEVICE_RISK_SIGNAL when an identification or a check raises a signal that asks for more (enforced or in shadow; page loads of the same visitor with the same signals once in 10 minutes, every check), and DEVICE_RISK_ALERT when an alert goes off. Signed like every event. They carry the requestId, visitorId, page, account when known, confidence, signals and the engine's verdict with its case code; never a trait. For an SSO organization with a log stream (Splunk, Datadog, Sentinel, webhook, S3), the same events also go to that stream, category device_risk.

Endpoints
EndpointWho
POST /device-risk/identifyThe page (site key)
GET /device-risk/events/{requestId}Your server: the trusted identification and the engine's verdict
POST /device-risk/checkYour server: an identification for an account (sign-up, reset)
login.request, risk.checkdeviceRiskRequestId
GET /device-risk/eventsYour server or the dashboard: search identifications (filters, an opaque cursor)
GET /device-risk/trendThe Overview's chart over 30, 90 or 365 days
GET /device-risk/recentThe latest identifications (the Overview's live feed)
DEVICE_RISK_SIGNAL, DEVICE_RISK_ALERTWebhook events to your server
// The page: when the sign-up, sign-in, reset or approval form is sent (without the script tag)
import { deviceRisk, REQUEST_ID_FIELD } from '@touchque/web';
const id = await deviceRisk.identify({ siteKey: 'tqpk_…', purpose: 'sign_up' });   // { visitorId, requestId, confidence }
form.elements[REQUEST_ID_FIELD].value = id?.requestId ?? '';   // the hidden touchque_request_id field, the one the tag fills

// Your server (Node): never trust the page's copy; missing or empty is an answer too
const r = await tq.deviceRisk.check(req.body.touchque_request_id, { event: 'sign_up', externalUsername: email, clientIp: req.ip });
if (r.stepUp) askForEmailCode();          // ask for more, never refuse
if (r.requestId) {
  const event = await tq.deviceRisk.getEvent(r.requestId);   // the trusted identification
}

// For sign-ins, instead of check: pass the field to login.request as it came
await tq.login.request({ externalUsername, type: 'LOGIN', clientIp: req.ip, deviceRiskRequestId: req.body.touchque_request_id });