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:
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. The Java and .NET SDKs have one exception: a request with an absent or rejected browser posture outcome on a path listed in the project setting postureReportPaths is reported too. 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 client's TLS fingerprint, evidence the SDK never receives, so the two rows can disagree on class when that evidence decides the verdict. 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, and the engine's score there measures how strongly that evidence indicates automation. With no other evidence, a plausible browser request defaults to human at a low confidence: 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. It's still classified human, but in a higher band, because those surfaces score a human verdict from the in-browser evidence they collect, such as whether the visitor interacted with the page.
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.
The Gateway and Pixel/Beacon rows for a visit use the same IP-and-user-agent session key. An SDK row can join that session only when its adapter supplies the same authenticated visitor address. An origin's immediate socket can instead identify a proxy egress. The updated Express adapter withholds that socket when Express regards it as a proxy. Self-hosted Next normally supplies no client address because NextRequest exposes no socket peer.
For Express behind a Gateway or load balancer, select trusted-proxy and authenticate the actual origin peer with an address/CIDR allowlist or peer predicate. Configure the exact forwarding-chain length: an authenticated two-address X-Forwarded-For chain uses trustedHops: 1. An invalid chain returns no address. Existing Express trust proxy settings alone do not select this SDK profile.
Next's trusted-proxy and cloudflare profiles cannot authenticate a peer through NextRequest and return no address. Use the managed vercel profile on Vercel, or authenticate the address at a transport-aware ingress. Legacy gateway, Java, and .NET adapters retain separate contracts; this change does not qualify their address provenance. Treat SDK rows as attribution-incomplete until their ingress supplies the visitor address. See client-IP deployment guidance.
Pixel and Beacon share one session id: both read the first-party _as_beacon_session cookie, and whichever loads first mints it. On a Shopify storefront, a visitor who hasn't allowed analytics sends no pixel events. See the sessionId row in Installation Surfaces for the full identifier table.
No single identifier joins all four surfaces' rows for one physical request today. correlation_id joins the Gateway to a caller that threads traceparent, including Java-instrumented origins since checkpoint-java 0.2.0. TypeScript and .NET reports do not forward that correlation id. IP-and-user-agent session joins require an authenticated visitor address on both rows. An explicit proxy profile and peer allowlist can provide it in Express; a profile name alone cannot provide it in Next. Classification can agree when the surfaces receive equivalent evidence, even while address attribution remains incomplete.
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. The Beacon adds a continuous interaction-integrity stream the Pixel doesn't carry, which helps it separate a person from a script driving a real browser.
Use the Beacon to catch driven-browser automation
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
- Installation Surfaces: what each surface can see and enforce, and the full correlation and IP/geo reference.
- Marketing Pixel and JavaScript Beacon: client surface setup.
- Middleware and Gateway: server-side enforcement surfaces.
- Gateway Monitoring: health endpoints and dashboard troubleshooting, including the ingest-lag note above.
