Concepts

Detection Semantics

What a persisted detection means per surface: row volume, confidence, session identity, absence, and how long a row takes to land

Installation Surfaces covers what each surface can see and enforce. This page covers what happens once you run more than one at the same time: how many rows one visit produces, whether their confidence numbers mean the same thing, what ties them together, and how long a row takes to show up after the request that produced it.

These are measured behaviors, not a spec. Install the same surfaces the same way and you'll see the same shape.

Row volume: one visit, several rows

A single page load on a site running all four surfaces (Gateway, SDK middleware, Pixel, Beacon) does not produce four rows. It produces a different count on each surface, because each one reports on a different trigger:

SurfaceWhat produces a rowRows per human page load
GatewayEvery request that reaches itOne per request: the page plus every asset and API call, typically 4-5
PixelOne event per page load1
BeaconA pageview event, plus an unload event if the tab closes cleanly1-2
SDK middlewareDetected entities only (see below)0

The SDK middleware's zero is deliberate, not a gap. Its enforcement planner reports a request only when it classifies as a bot or AI agent. For human traffic, the request passes through and nothing gets reported. That keeps the SDK's reported telemetry limited to bot and AI-agent traffic, the requests an operator actually wants alerted on, but it also means the SDK middleware contributes no rows for human visitors. The Gateway (if you run it in front of the same origin) is the surface that reports every request regardless of classification.

Bot and AI-agent traffic flips this: a request classified as an agent produces a row on every surface that sees it and can report. A single bot request through a Gateway-fronted, SDK-instrumented origin produces two rows: one from the Gateway, one from the SDK. When the classification comes from evidence both surfaces read (a matched user-agent pattern, for example), the two rows carry identical class, agent, and confidence, because both surfaces ran the same detection logic against the same request headers. That parity has one exception: the Gateway also reads the TLS handshake's JA4 fingerprint, evidence the SDK never receives, so a request whose verdict depends on JA4 (a browser-UA spoofer presenting non-browser TLS, for instance) can classify incomplete_data on the Gateway and human on the SDK. Neither row counts as the other regardless; if you're summing "requests seen" across surfaces, count each surface separately rather than adding Gateway and SDK rows together.

Rows scale linearly with load on every surface: a burst of 20 bot requests produces 20 rows per reporting surface, not more. There's no batching-induced row multiplication to account for.

Confidence is scoped to the surface, not the visitor

Every detection carries a confidence score from 0 to 100, banded from Low to Very High. That band describes how much evidence the surface that produced the row actually had, not how confident you should be about the visitor overall.

The same real browser visit, unmodified, can land in different confidence bands on different surfaces at the same moment:

  • The wire (a Gateway request, or the SDK's in-process headers) has only the request itself to go on. With no other evidence, a plausible browser request defaults to human at a low confidence, typically around 10: the absence of suspicious signals, not proof of humanity.
  • The Pixel and Beacon run in the browser and see device and behavioral signals the wire never gets. The identical visit reads a noticeably higher confidence on those surfaces, often around 40. It's still classified human, but a full tier higher (Medium instead of Low) because there's more corroborating evidence to draw on.

Class and agent identity agree wherever two surfaces have equivalent evidence. The clearest case is a bot request, where the Gateway and SDK see the same headers and land on identical class, agent, and confidence. Confidence diverges specifically where evidence depth diverges, which is normal and expected, not a bug in either surface.

Don't threshold across surfaces with one confidence cutoff

A policy or alert tuned against Gateway/SDK confidence will not fire at the same point against Pixel/Beacon confidence for equivalent traffic. The two families of surface start from different evidence baselines. Checkpoint does not yet define a taxonomy that normalizes confidence across surfaces: treat a confidence number as scoped to the surface that produced it, and set per-surface thresholds rather than one shared cutoff.

Evidence depth can also change the class, not just the number. When one surface's evidence stream catches something the others structurally cannot see (see the next section), that surface's classification can legitimately disagree with the others for the same visit. That's the surfaces doing their job, not a stitching failure.

What ties rows together, and what doesn't

Installation Surfaces documents correlation_id resolution: an inbound traceparent trace id wins, then an x-kya-correlation-id header, then a caller-supplied fallback, then a freshly minted id. The Gateway follows that order and echoes the result back to the client as KYA-Request-Id, so a caller who threads traceparent through their own backend and into a direct /detect or /enforce call gets the same correlation_id on both ends.

Whether the SDK middleware participates in that thread depends on which one you're running. The Java middleware (checkpoint-java 0.2.0+) derives a correlation id from the same inbound traceparent / x-kya-correlation-id precedence and sends it on its telemetry report, so a Java-instrumented request that carried a traceparent lands on the same correlation_id as the Gateway row for that request. The TypeScript and .NET middleware don't: their telemetry reports carry no correlationId and no inbound traceparent field at all, so the platform always mints a fresh id for those reports, even when the original request carried one. Practically: check which SDK produced a row before assuming it shares a correlation_id with a Gateway row for the same physical request.

This is a gap in what the TypeScript and .NET middleware forward on their telemetry report, not a gap in the correlation-id logic itself. The same precedence order applies to every surface that does supply a traceparent or correlationId, Java's SDK telemetry included.

Session identity follows a similar split. The Gateway and Pixel/Beacon rows for a visit fold into one consolidated session, keyed on IP address and user agent. That's why Gateway and client-surface rows for the same visitor show up together in Activity. The SDK middleware's rows use that same IP+user-agent key, but when a request reaches the SDK by way of the Gateway rather than hitting the origin directly, the IP the SDK records is the Gateway's own egress address, not the visitor's. The SDK's IP-trust profile defaults to trusting the request's immediate socket peer, and behind a proxy that peer is the proxy, not the browser.

The consequence: a Gateway-proxied, SDK-instrumented origin's SDK rows fold into their own session cluster, distinct from the Gateway rows for the same visitors, as long as the SDK is left on its default IP-trust profile. That egress address can even rotate across requests within a single burst from one visitor, fragmenting the SDK-side session further. Every SDK middleware can fix this today: the Gateway forwards the real visitor IP as a two-token X-Forwarded-For header, so selecting the existing trusted-proxy profile with one trusted hop resolves the visitor's actual IP on any adapter, TypeScript and .NET included. Java's jdk-httpserver adapter also has a Gateway-specific alternative, RemoteIpResolver.gateway(), which trusts the Gateway's KYA-Client-IP header directly after verifying the direct peer is a Cloudflare edge address (the Java servlet adapter ships trusted-proxy/vercel/cloudflare but not this Gateway-specific profile). Treat Gateway-proxied SDK rows as attribution-incomplete until you've configured one of these profiles.

Pixel and Beacon each mint their own session id independently (sessionStorage for the Pixel, a first-party cookie for the Beacon), and nothing propagates between them. See the sessionId row in Installation Surfaces for the full identifier table.

Put together: no single identifier joins all four surfaces' rows for one physical request today. correlation_id joins the Gateway to a caller that threads traceparent, and, since checkpoint-java 0.2.0, to a Java-instrumented origin on the same request chain; the TypeScript and .NET SDKs never join it. Consolidated sessions join the Gateway to Pixel/Beacon by IP+UA; an SDK middleware behind the Gateway joins that cluster too once it's configured with a trusted-proxy profile (or, on Java's jdk-httpserver adapter, RemoteIpResolver.gateway()), but not on its default direct profile. What does hold, consistently, is classification agreement: wherever two surfaces see equivalent evidence for the same traffic, they land on the same class, agent, and (usually) confidence tier. Attribution fragments before classification does.

Absence is not evidence, and Pixel and Beacon aren't interchangeable

A blocked or absent Pixel never lowers the wire score. If a visitor's ad blocker or content-security policy stops the Pixel request, the Gateway and SDK still classify that visitor from the request alone, and a human visitor with a blocked Pixel scores exactly as human as one without a blocker. Treating "no Pixel event" as suspicious would misclassify every privacy-conscious human as a bot. The same holds for a Beacon that fails to load or is misconfigured: the origin and its wire-side detection keep working unaffected, because Pixel and Beacon absence is not a wire-side signal today.

Absence within the client surfaces is where Pixel and Beacon diverge, and this is a real capability boundary, not a tuning gap. Headed browser automation driven over the Chrome DevTools Protocol, with navigator.webdriver suppressed, is invisible to the signals the Pixel collects: it reads as an ordinary human browser. The Beacon catches the same session, because it carries an interaction-integrity stream the Pixel does not: behavioral evidence of automation that only a richer, continuously-sampling client surface can gather.

The Pixel alone won't catch driven-browser automation

If distinguishing a human from a script driving a real, headed browser matters for your traffic, install the Beacon alongside or instead of the Pixel. The Pixel remains the right choice for lightweight marketing analytics where that distinction doesn't matter.

When a row actually appears

Rows don't land at a uniform speed. The SDK middleware, Pixel, and Beacon each write their event through a direct request to Checkpoint's ingest API, so a row from any of them is queryable within roughly a second of the request that produced it.

The Gateway's path is different: it queues each detection and flushes queued detections in batches, on a timer capped at 30 seconds (or sooner, once a batch fills). In practice, a Gateway row can take up to 30-40 seconds to become queryable: the 30-second timer plus the batch write itself.

If you diff surfaces within the first 30 seconds of a request, expect to see SDK, Pixel, or Beacon rows with no matching Gateway row yet. That's the batching window, not a missing detection. Requery after it, or use the SDK/Pixel/Beacon rows for anything that needs a near-real-time read.

Where to go next