Beacon reference
Look up every Beacon method, session and receipt rule, data collection detail, posture verification step, delivery behavior, CSP directive, and browser requirement.
To install the Beacon and send your first event, start with Beacon setup.
How it works
Beacon API
new CheckpointBeacon(config)
Creates a beacon, wires the evidence-dispatch triggers, and sends one automatic pageview. Throws when config.projectId is missing or empty. Every other option has a default.
collect(eventType?)
Runs a collection round and sends one event immediately. eventType defaults to 'pageview'. The valid types are 'pageview', 'pageunload', 'evidence', 'heartbeat', 'custom', 'error', and 'performance'. The SDK doesn't produce 'heartbeat' on its own; the type stays for direct calls and already-deployed bundles.
Returns a promise that resolves once the send completes, ingest round trip included, so the posture cookie (when the server issues one) exists by then. Under Do Not Track it resolves without doing anything.
await beacon.collect('pageview');trackEvent(name, metadata?)
Sends a custom event. Metadata must be JSON-serializable. The beacon spreads it into the event envelope, then sets eventName, sessionId, timestamp, and identity, so those four keys cannot be overridden.
await beacon.trackEvent('button_click', {
buttonId: 'signup-cta',
page: '/home',
});The event name and your metadata travel with the event and pass validation, but the ingest
endpoint currently persists only the event type, the session id, and the identified user id from
the envelope. Custom metadata does not appear in the dashboard today. Use trackEvent() to add a
detection sample at a meaningful moment, not to store your own attributes.
prepareForAction({ action, maxPostureAgeMs? })
Refreshes posture evidence in the background and returns immediately, with nothing to await. It is a no-op when posture is already fresh (age under maxPostureAgeMs, default 30 seconds), a refresh is in flight or settled too recently, the beacon is already retrying a pending posture, or ingest withheld a token for a terminal reason such as not-enabled or origin-mismatch. Calling it on every keystroke or pointer move costs at most one request.
generateButton.addEventListener('focus', () => {
checkpoint.prepareForAction({ action: 'firefly_generate', maxPostureAgeMs: 30_000 });
});
async function onGenerateClick() {
checkpoint.prepareForAction({ action: 'firefly_generate', maxPostureAgeMs: 30_000 });
await callFireflyGenerate();
}Never await this method, or the work it starts, before running the protected action. Proceed immediately on whatever posture cookie is already on hand. That is the entire point of this API.
identify(identity?)
Tells the beacon who the visitor is once your site knows. Pass { userId }, your own id for the signed-in user, up to 255 characters. Longer values are rejected at ingest. Every later event carries it as metadata.identity.userId, and it is stored as the detection's user id. Earlier events in the session stay anonymous.
Call it with no argument on sign-out, so a shared device stops attributing the next visitor's activity to the person who left. If the identity is known at render, pass identity in the constructor instead.
beacon.identify({ userId: 'user_123' });
beacon.identify();getSessionId()
Returns the client session id the beacon stamps on every event. It is a pure read: it doesn't extend the session or touch the cookie, so polling it from a status footer is safe. The server assigns its own consolidated session id (see Sessions).
getPostureHeaders()
Returns the two request headers a backend needs to read the session's posture verdict, as a plain object. It is a pure read: no network call, no storage write, and it doesn't extend the session.
The latest token is the newest one: a late receipt doesn't replace a newer unexpired token for the same binding. The posture cookie follows the same rule: the newer issue time wins, and a later clean token doesn't replace an agent-driven one while that one is unexpired. Withdrawing consent through respectDoNotTrack, or destroying the last beacon, clears the token, and KYA-Posture-Session then follows getSessionId(). See Sending posture to another host.
const headers = beacon.getPostureHeaders();getRequestProofHeader(request)
Signs a proof that this browser holds the beacon's key, for one outgoing request. request is { method, url }. It returns a promise for the signed proof string. When the browser reports Do Not Track it resolves to undefined and signs nothing, whatever respectDoNotTrack is set to.
The key pair is a non-extractable P-256 pair. The beacon creates it on first use and keeps it in IndexedDB for the browser profile. Every call signs a new proof. If signing fails, for example when WebCrypto is unavailable, the promise rejects and your code decides what to do.
const proof = await beacon.getRequestProofHeader({
method: 'POST',
url: 'https://api.example.com/generate',
});trackPageUnload()
Records a pageunload event through the Unload path in How it works. Calling it from a pagehide handler is optional and safe (see Quick start).
updateConfig(updates)
Merges configuration at runtime.
- Changing
useWorker,enableWebWorker,workerUrl, orworkerConfigrestarts the worker. - Setting
browserSessionIdmoves the next send onto that session. flushIntervalandbatchSizeare accepted and ignored (deprecated no-ops).- Setting
respectDoNotTrack: truefor a DNT visitor mid-session releases the interaction listeners and evidence-dispatch triggers, expires the_as_beacon_sessioncookie, and starts a new in-memory session id. Setting it back re-wires them under that new id, so a consent manager can drive it.
The options it cannot change are in the read-once callout under Configuration options.
beacon.updateConfig({ respectDoNotTrack: false });addCollector(collector)
Registers an extra collector for a signal the default set doesn't carry. A collector implements ICollector (exported from the package): type, enabled, unloadSafe, isSupported(), collect(), and optional start() and dispose(). A second instance of an already registered class is refused with a warning. When two collectors write the same key, the first writer wins and the collision is logged.
destroy()
Removes the visibilitychange/pagehide listeners and the counted-interaction callback, disposes every collector (removing its listeners), terminates the worker, and drains the offline queue.
beacon.destroy();The Beacon is not an event emitter: there is no .on(...), no client-side detection event,
and no start(), stop(), or getDetectionResult() method.
More than one beacon on a page
A tag manager, a framework wrapper, and a hand-written snippet can each construct a beacon on the same page. Beacons that share a projectId share one running beacon. The second new CheckpointBeacon(...) attaches to the first and returns a handle to it. The handle's prototype is CheckpointBeacon.prototype, so instanceof is true.
The page gets one arrival pageview, one worker, one set of dispatch timers, and one DPoP key. Every method on the handle calls the shared beacon, so getSessionId() and getPostureHeaders() return the same values from either.
The match is on projectId character for character. acme-corp and the project's UUID are different keys even for the same project, so two beacons built with them run separately and each sends its own pageview. Use one id everywhere; the UUID is the form we recommend. Only copies that carry this behavior attach to each other, and an older release on the same page runs on its own.
When the second beacon's config differs from the first's:
How the privacy settings combine:
updateConfig on a handle follows the same rules, and a destroyed handle's updateConfig is ignored.
Sessions
The beacon keys every event to a visitor session stored in a first-party cookie, _as_beacon_session by default (sessionCookieName).
Receipts and resuming a session
onReceipt is called on the main thread for every 202 ingest response, in worker mode too, right after the beacon stores the posture cookie. It receives a BeaconReceipt:
An error thrown inside onReceipt is logged at warn and never affects delivery.
To keep one session across page loads without cookies, store browserSessionId wherever the page keeps state and pass it back at construction:
const beacon = new CheckpointBeacon({
projectId: '<project UUID>',
browserSessionId: loadStoredBrowserSessionId(),
onReceipt: ({ sessionId, browserSessionId, posture }) => {
saveBrowserSessionId(browserSessionId);
if (sessionId) recordCheckpointSession(sessionId);
if (posture) latestPosture = posture.token;
},
});loadStoredBrowserSessionId() returns undefined on a first visit. recordCheckpointSession() stands for your own code, which joins your record to the visit.
The option takes 1 to 255 characters of A-Z a-z 0-9 . _ ~ -. Anything else is ignored with a warning. The resumed id still rotates after sessionTimeout of inactivity. updateConfig({ browserSessionId }) switches a running beacon to a stored id, for a page that loads it after construction.
Data collection
Five collectors run on every collection round:
The fingerprint and lazy collectors are marked unsafe during unload and are skipped on the pageunload path.
The lazy chunk is injected as a <script> at low fetch priority after window.load and an idle period, and only when its URL can be resolved. The URL comes from lazySignalsUrl, otherwise from next to wherever beacon.min.js was loaded from, whatever the file is called. The script tag install needs no setup. A bundler does: see Self-host the companion files.
What leaves the page, alongside the signals above:
Redaction runs client-side, at collection time, before url and referrer are placed on the event. URL redaction and IP truncation (anonymizeIp, which stores only a /24 (IPv4) or /48 (IPv6) prefix) require @kya-os/checkpoint-beacon 1.2.0 or later. The package's full data-inventory declaration is NETWORK_CONTRACT.md, and a test asserts the SDK's real network behavior against it.
Do Not Track
When navigator.doNotTrack (or the window.doNotTrack and msDoNotTrack variants) reads 1 or yes and respectDoNotTrack is on (the default), the beacon collects and sends nothing. collect(), trackEvent(), and trackPageUnload() return immediately, no automatic arrival send happens, and the interaction listeners and evidence-dispatch triggers are never attached. Global Privacy Control is not read.
The session cookie for a Do Not Track visitor depends on the release:
The other identifiers the beacon stores (nonce keys, the DPoP keypair and its sent marker, the offline queue) are not cleared. Neither is a posture cookie already stored, which expires by itself within its 120 second lifetime.
Cookies the beacon writes
When ingest withholds a token, the 202 body says why in postureStatus, visible in the browser's network inspector on every build. The npm build also logs it once per reason at warn when logLevel allows.
From 1.4.3 a withheld reason on a queued receipt ends the ladder too. Earlier releases kept retrying on any queued receipt without a token.
Verifying the posture token
The posture token lets your server read the session's verdict without calling Checkpoint. The server SDKs (Java, .NET, Express, Next.js) verify it for you once browser posture is configured. Any other stack can verify the JWS directly.
Turning browser posture off (from the Installations page or the project posture API) deletes
the project's signing key. Turning it on again issues a new key id, so a server that pinned the
old key stops verifying until it takes the new one. A server that reads the keys endpoint instead
of pinning, such as a Java server configured with BrowserPostureOptions.remoteKeys, picks the
new key up on its own within about two minutes. Turning posture off is refused while human step-up
(Turnstile) is configured, because step-up signs its clearance with the same key. Clear the
Turnstile keys first.
Check the signature and kid against the pinned key, then typ, iss, aud, exp, and origin. Check that binding is the hex SHA-256 of the visitor's browser session id: the _as_beacon_session value, or the receipt's browserSessionId when cookies are blocked.
Treat a missing or invalid token as no evidence, never as a reason to allow.
Sending posture to another host
Both cookies are host-only. If the protected request goes to another host, such as an API subdomain, neither cookie goes with it. Attach the headers yourself with getPostureHeaders(). It reads the latest receipt, so it also works when cookies are blocked.
const beacon = new CheckpointBeacon({ projectId: '<project UUID>' });
await fetch('https://api.example.com/generate', {
method: 'POST',
headers: { ...beacon.getPostureHeaders() },
});The Java, .NET, Express, and Next.js server SDKs read these two headers, and each one wins over its cookie. A header sent twice is ignored rather than guessed at. For a cross-origin request, add both names to the API's Access-Control-Allow-Headers:
Access-Control-Allow-Headers: KYA-Posture, KYA-Posture-SessionA token lives for 120 seconds. After that, getPostureHeaders() still returns KYA-Posture-Session, leaves KYA-Posture out, and the server treats the request as having no posture evidence. Call prepareForAction shortly before a protected action: it starts a refresh without blocking, and the next getPostureHeaders() call returns the new token once its receipt arrives. To store the token yourself, keep the latest receipt from onReceipt.
Web Worker mode
Worker mode is on by default in a capable browser. Turn it off with useWorker: false (or the enableWebWorker: false alias). Network requests run in the worker. Collection always stays on the main thread.
The worker script must be on your own origin. If beacon.min.js loads from another host (a CDN, or your own tag loader) and you don't pass a same-origin workerUrl, the worker can't start and the beacon quietly uses the main thread. Set useWorker: false there to skip the attempt. With a bundler, workerUrl is required, because there's no script tag to find the worker from.
const beacon = new CheckpointBeacon({
projectId: '<project UUID>',
useWorker: true,
workerUrl: '/static/beacon.worker.js',
});workerConfig tunes the manager:
A Content Security Policy whose worker-src (or script-src) excludes the worker URL blocks creation, and the beacon falls back. For advanced setups, the BeaconWorkerClient class is exported from @kya-os/checkpoint-beacon.
Tuning dispatch
Three settings shape the automatic sends, plus a low-frequency backstop timer that only ever refreshes posture. None of them brings back a fixed-interval heartbeat.
With unload: false, trackPageUnload() still works for a listener you wire yourself. With arrival: false, call collect('pageview') yourself, and whatever has not been delivered still leaves on unload.
const beacon = new CheckpointBeacon({
projectId: '<project UUID>',
evidenceGrowthFactor: 3,
postureRefreshFraction: 0.25,
triggers: { signals: false },
proactiveRefreshMinIntervalMs: 45000,
proactiveRefreshMaxIntervalMs: 90000,
});Delivery: retries and offline
Network & CSP
Every batch is a POST to https://kya.vouched.id/api/v1/beacon (override it with endpoint), with Content-Type and X-Project-ID headers, plus X-Content-Encoding: gzip-base64 when compressed. Those headers mean every request is preflighted with OPTIONS. The endpoint answers with Access-Control-Allow-Origin: *.
From 1.4.3, onReceipt hands a 202's sessionId, posture, and postureStatus to your page (Receipts).
If your site restricts outbound requests (a Content Security Policy, a corporate proxy, or tracker blockers), allow these. The connect-src policy to copy is in Content Security Policy.
A policy with default-src also reports a style-src-attr violation from a fingerprint probe, which costs no signal. Releases before 1.4.3 also reported a worker-src violation for each blob: worker their capability probe started (one from the lazy chunk, and a second unless useWorker is off). From 1.4.3 the probe starts no worker, so the beacon needs no worker-src blob:.
Self-host the companion files
By default the beacon looks for beacon-lazy-signals.min.js and beacon.worker.js next to the beacon.min.js tag that loaded it. A bundled install has no such tag, so the worker and the lazy signals never load. Host both files yourself, name each one, and copy them from the dist/ of the installed package:
const beacon = new CheckpointBeacon({
projectId: '<project UUID>',
lazySignalsUrl: '/static/checkpoint/beacon-lazy-signals.min.js',
workerUrl: '/static/checkpoint/beacon.worker.js',
});- Copy both files from the same version as
beacon.min.js. A mismatched chunk fails the integrity check. - The worker must be same-origin. Pass a same-origin
workerUrl, or setuseWorker: false. Otherwise the worker can't start and the beacon uses the main thread. - Without
lazySignalsUrl, a bundled install never collects the 22 lazy signals. The chunk is also exported as@kya-os/checkpoint-beacon/lazy-signals.
lazySignalsUrl is read once at construction (Configuration options). A chunk on another origin must send Access-Control-Allow-Origin (CORS); a same-origin file needs nothing extra. Allow the chunk's host in script-src and the worker's origin in worker-src (see Network & CSP).
Integrity check on the lazy chunk
beacon.min.js and the npm build embed the SHA-384 hash of beacon-lazy-signals.min.js as it shipped in the same release. The beacon loads the chunk with that hash as its integrity and with crossorigin="anonymous". A chunk that differs by one byte never runs, and the beacon then behaves as it does when the chunk is blocked: the 22 lazy signals are missing.
Only the script build has the retry, and it only helps for a version already published to npm. To let it run when you load the beacon from a Checkpoint origin, allow https://cdn.jsdelivr.net in script-src. A host-allowlist policy without it blocks the retry, reports a violation, and leaves the chunk missing, exactly as without the retry. A policy with 'strict-dynamic' ignores the host list for scripts the beacon inserts, so the retry runs whatever the list says.
crossorigin="anonymous" makes the chunk request a CORS request without credentials. A host that serves the chunk from another origin than the page must send Access-Control-Allow-Origin. The Checkpoint-hosted file and public CDNs such as jsDelivr do. Your script-src rules don't change.
The check covers the lazy chunk only. It doesn't cover beacon.min.js itself, which you pin with an integrity attribute on your own tag (see Script tag), or the worker file, because a Worker constructor has no integrity option.
Trusted Types
A page that enforces Trusted Types (require-trusted-types-for 'script') must allow the beacon's policy by name, next to its own policies:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types checkpoint-beacon;From 1.4.3 the beacon creates one policy, checkpoint-beacon, once per page. It passes only its own inputs through it: an http: or https: URL naming beacon.worker.js or beacon-lazy-signals.min.js on the page's own origin (in the directory the beacon's script was loaded from, or at a workerUrl or lazySignalsUrl you configured), and the fixed, script-free document a fingerprint probe renders in a hidden iframe. Every copy of the beacon shares that one policy, so the directive needs no 'allow-duplicates'.
Inside a cross-site iframe
Install the script tag in the framed document, not the page that embeds it. The beacon then works as it would on a top-level page of the frame's origin: it loads its worker and lazy chunk from next to the frame's beacon.min.js, and every batch goes out with the frame's Origin. Posture is issued for that origin too, so list the frame's origin, not the embedding page's, as the project's domain or under Additional site origins.
Chromium refuses the beacon's SameSite=Lax cookies in a cross-site frame, and other browsers block or partition third-party cookies. Without them, each load of the frame starts a new session and no posture cookie is written. Read both from onReceipt instead: store browserSessionId and pass it back as the browserSessionId option, and send the latest posture.token to your server in the KYA-Posture header (see Verifying the posture token).
Browser compatibility
The package's stated support is Chrome 90+, Firefox 88+, Safari 14+, and Edge 90+. The browser files (beacon.min.js, beacon.worker.js, beacon-lazy-signals.min.js, and beacon.modern.mjs) are built for exactly that floor, so only syntax those browsers lack is lowered. The npm ESM entry targets ES2020.
CI checks syntax esbuild can lower (nothing needed lowering at Chrome 90, Firefox 88, Safari 14, and Edge 90), plus a denylist of runtime APIs it cannot lower (.at(), Object.hasOwn, structuredClone, AbortSignal.timeout, findLast, toSorted, groupBy, Promise.withResolvers, crypto.randomUUID, Error cause, Array.fromAsync) and regex lookbehind.
The one hard requirement is crypto.getRandomValues, which mints session ids. Everything else degrades:
A hanging or failing IndexedDB costs only the holder-of-key binding, never delivery:
Bundle size
beacon.min.js is budgeted at 28 KB gzipped, and the lazy chunk is budgeted at 10 KB gzipped. The measurement is Node 22.23.1 at gzip level 9. The npm package is marked side-effect free, so bundlers drop the exports you don't import.
