Set up browser posture

Set up browser posture with the kya-os CLI, serve the beacon from your own domain, verify the signed token on your server, and check the result on a live page

Browser posture gives your server a signed copy of Vouched KYA's verdict on a browser session, so a protected request can carry it. This page takes the CLI path to a verified token on a live page.

A verified token shows that Vouched KYA issued it for this page and this session, and it carries Vouched KYA's verdict. It doesn't identify the visitor. A server that finds no token, or a refused one, should treat the request as having no posture, never as permission.

Prerequisites

  • kya-os 2.5.1 or later, signed in with kya-os login. Posture calls use your management sign-in. Changing origins needs write access to the project.
  • The full @kya-os/checkpoint-beacon package for a JavaScript build. Server-rendered templates without a JavaScript build use a pinned script.
  • Pages served over HTTPS. The beacon doesn't store posture tokens on other pages.
  • A server or edge you control that can read two request headers and verify an ES256 signature.

The caret package install and build-time asset synchronization described below need CLI 2.5.1 or later. Reload persistence needs @kya-os/checkpoint-beacon 1.4.4 or later. Java verification requires SDK 0.2.2 or later.

Add the beacon and set its origins

Every project has a posture key, so there is nothing to turn on. Add the beacon and set the page origins in one command:

kya-os setup --surface beacon --origins https://www.example.com,https://shop.example.com

kya-os detect install takes the same flags: --origins and --proxy. The flag tables list them and how the CLI normalizes each origin.

--origins replaces the project's additional origins, so an origin you leave out is removed. On setup, --origin (singular) is the Vouched KYA address you sign in to. Page origins are --origins (plural).

Vouched KYA issues a token only to pages on the project's domain and the origins you list. Changes to the domain or origins take effect for token issuance within 10 minutes, because ingest instances cache the project's settings for that long. Wildcards, the apex rule, and the limit of 20 are in Verifying the posture token.

Without a terminal, the CLI asks nothing and leaves the origins as they are unless you pass --origins. Signed out, detect install finishes the install, notes that browser posture was not configured, and exits non-zero until you run kya-os login and re-run. The reference lists the terminal prompt and every other case.

The CLI then checks the key and prints key: confirmed at the keys endpoint or key: not confirmed, with a missing: line that says why. A key that isn't confirmed makes the command exit non-zero; re-run it once the keys endpoint serves the key. --json output carries keyConfirmed. The reference has the retry rules. Confirm a live receipt after setup: key confirmation alone does not prove browser collection, issuance, or API forwarding.

Check what the CLI wrote

JavaScript builds use @kya-os/checkpoint-beacon. The CLI adds a caret dependency and generates an asset synchronization step that runs before the application's build, dev, and typecheck commands, which the CLI keeps as checkpoint:build, checkpoint:dev, and checkpoint:typecheck. The root checkpoint-beacon-assets.cjs script reads the installed package, copies its worker and lazy signals files, and writes checkpoint-beacon-assets.json beside the client module. Initialization imports those URLs. Template pages without a JavaScript build retain a pinned script. The reference lists the behavior by stack.

Next.js App Router gets a mounted client component, and Vite HTML gets a module tag. Other Node apps get a root checkpoint-detect.js module and a printed import for you to add to the browser entry. The application shell starts one beacon in browser-only code. Micro-frontends use that instance or the shell's exported header getter. The generated initialization supplies workerUrl and lazySignalsUrl for the companion files.

Read beacon.getPostureHeaders() in your shared API transport when dispatching each request to an API you control. Installation does not rewrite existing API calls, and host-only cookies do not reach an API on another host:

await fetch('https://api.example.com/generate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    ...beacon.getPostureHeaders(),
  },
  body: JSON.stringify({ prompt }),
});

getPostureHeaders() is synchronous. It reads the available session and token without collection, refresh, or a network request. Dispatch immediately; do not wait inside the getter or add a fixed delay. A call that must carry a token on the page's first request can await prepareForAction instead (see below). The returned token must be unexpired and belong to the current project and session. Replace any previous posture headers when reusing a request configuration, and never save the result as client-wide defaults. The generated checkpointPostureHeaders() helper delegates to this getter and returns an empty object before initialization.

A fresh visit needs the first ingest response before a signed token exists. An HTTPS reload can restore an unexpired token for the same project and session from session storage. Background renewal replaces the 120-second token without extending the session's idle deadline. After session expiry, neither header is sent until real activity starts a session again. Missing posture stays unknown; insufficient browser evidence does not establish human traffic.

For cross-origin requests, add KYA-Posture, KYA-Posture-Session, and, if used, DPoP to the API's existing Access-Control-Allow-Headers. Preserve its existing methods and headers, allow the exact page origin, and expose DPoP-Nonce and WWW-Authenticate for proof flows. Browser preflight can add a network exchange, so check the API's CORS behavior in staging.

A caret range allows compatible dependency updates when the package manager resolves them. A lockfile update, asset synchronization, rebuild, and application deployment are still required to deliver a new beacon version.

Serve the beacon through your own domain

A strict Content-Security-Policy may not allow connect-src https://kya.vouched.id or a script from a CDN. --proxy moves both onto your origin under a path you choose:

kya-os detect install --surface beacon --proxy /_cp

The beacon's endpoint becomes /_cp/api/v1/beacon, which your domain forwards to https://kya.vouched.id/api/v1/beacon. The forwarding uses Forward edge evidence: your edge sets X-API-Key and KYA-Forwarded-IP on the proxied request, and Vouched KYA scores it with the visitor's address instead of your proxy's.

The proxy key has the proxy purpose, which carries only the forward permission: it authenticates forwarded evidence and nothing else. It's shown once, and minting needs a project owner or admin. Signed in, the CLI stores it as CHECKPOINT_PROXY_KEY in a gitignored env file and never prints it. Signed out, it tells you to sign in and re-run, or to set the variable yourself.

For Next.js, the CLI writes a helper, checkpoint-beacon-proxy.ts, and one entry file. For nginx, Cloudflare, and Akamai, it prints the configuration for you to add and applies nothing. Vouched KYA hasn't verified the printed Akamai rule against a live property, so test it in staging. The reference lists each target.

Every generated proxy removes the visitor's Cookie and Authorization headers. The Next.js proxy and the Cloudflare Worker also remove every KYA-Forwarded-* header the visitor sent. The nginx block and the Akamai rule clear only three KYA-Forwarded-* headers by name (JA4, challenge, challenge-id), so delete any other KYA-Forwarded-* header yourself.

On Vercel and other hosts, .env.local isn't deployed. Set CHECKPOINT_PROXY_KEY in the host's environment too.

With the proxy in place, connect-src needs only 'self', and script-src needs only 'self' when the script is self-hosted. A nonce-based policy still needs the response nonce and 'strict-dynamic'; 'self' alone does not authorize scripts under that policy. The CLI prints the directives for your install. The reference lists what each one needs, including when to add base-uri 'none' and trusted-types checkpoint-beacon.

Verify the token on your server

Your server reads the KYA-Posture and KYA-Posture-Session headers, or the matching cookies on the same host. Verifying the posture token has the precedence, cookie names, and token format.

The server SDKs verify for you. They need the project's UUID (the lowercase UUID, not the Project ID slug the rest of the dashboard shows, for now), the key, and the page origin. The CLI's configuration output already uses the UUID; to copy it yourself, open All settings โ†’ Projects in the user menu (/dashboard/{orgId}/account/projects), where it is listed as Project UUID. Get the key id and public JWK from any of these:

  • The server configuration output from setup and detect install, when it includes the public key. No block prints without a usable page origin.
  • posture.state.keyId and posture.state.publicJwk in --json output.
  • The Installations card in step 6.

The Java dependency and verifier snippets are manual steps: the CLI does not edit your Maven or Gradle files or apply server configuration. The examples read configuration from environment variables. For a server that answers for several origins, list the extras next to the primary:

middleware.ts
export default withCheckpoint({
  apiKey: process.env.CHECKPOINT_API_KEY,
  projectId: '<lowercase project UUID>',
  browserPosture: {
    origin: 'https://www.shop.example',
    origins: ['https://checkout.shop.example', 'https://*.shop.example'],
    keyId: process.env.CHECKPOINT_POSTURE_KEY_ID!,
    publicKey: JSON.parse(process.env.CHECKPOINT_POSTURE_PUBLIC_JWK!),
  },
});
SettingRule
originThe primary, and required except for Java's remoteKeysAndOrigins. Java and .NET refuse one that isn't canonical: https, a lowercase host, no default port, and no path.
originsAdditional to origin, not a replacement, so a config without origins behaves as before. It takes the same entries the CLI accepts, up to 20.
WildcardsThe verifiers refuse wildcards over shared public suffixes. List only page origins you control; exact origins are easiest to audit.
The keyYou supply it, pinned or (in Java) fetched from the keys URL. The verifiers never take it from the token.

SDK verifiers compare the token's signed origin claim with origin and origins (Java's remoteKeysAndOrigins compares it with the origins the keys endpoint lists), then check that aud names the same origin. They ignore the request's Host and Origin headers. The gateway needs no configuration: it matches the request's origin against the project's domain and additional origins, then checks the token against the match.

PackageMinimum version
@kya-os/checkpoint-shared (used by @kya-os/checkpoint-express and @kya-os/checkpoint-nextjs)1.5.2
Java ai.knowthat.checkpoint0.2.2
.NET KyaOs.Checkpoint1.11.9
@kya-os/checkpoint-wasm-runtime (Next.js and Express postureOutcome reporting)1.10.1

A refused token doesn't stop the request, grant an allow, or override a block. The request proceeds as if it carried no posture. The only trace is telemetry: the SDK sends postureOutcome with status of verified, absent, or rejected, and a reason on every rejected, to POST /api/v1/log-detection.

ReasonWhat it means
malformedNot a well-formed token, or its header is unusable.
unknown-keyThe token's kid isn't the key you configured.
bad-signatureThe signature doesn't verify against your key.
invalid-claimsA claim is missing or the wrong shape, the issuer is wrong, or the lifetime is over 120 seconds.
project-mismatchproject_id isn't the project you configured.
origin-mismatchThe token's origin is neither origin nor one of origins (with Java's remoteKeysAndOrigins, not one of the origins the keys endpoint lists).
audience-mismatchaud doesn't name that origin.
expiredThe token's lifetime has ended.
not-yet-validiat or nbf is in the future.
binding-mismatchbinding isn't the SHA-256 of the session id you were sent.
session-missingA token arrived with no session id.

Forward posture to Verdict

A backend that calls POST /api/v1/verdict instead of running a server SDK can pass the browser's posture headers in the request's headers field. Verdict checks the token against your project's key and uses the browser session's verdict.

{
  "sessionId": "<your session id>",
  "method": "POST",
  "url": "https://api.example.com/generate",
  "ipAddress": "203.0.113.7",
  "headers": {
    "KYA-Posture": "<token from the browser's request>",
    "KYA-Posture-Session": "<session id from the browser's request>",
    "DPoP": "<proof, only if the browser sent one>"
  }
}

Copy the header values without change. The header names are not case-sensitive. Send url and method as the browser called your backend, because a DPoP proof is signed for that URL and method. Send traceparent (or X-KYA-Correlation-Id) as an HTTP header on your call to Verdict, so you can join the detection to your own traces and logs by that id. The response returns the id as correlationId.

The response and the stored detection (metadata.posture) carry the outcome in posture:

statusMeaning
validThe token passed every check above. session is the token's session id. Its verdict joins the detection (see below).
invalidVerdict ignored the token. reason is one of the reasons above, proof-<reason> when a DPoP proof failed, or verification-error. The call still succeeds with the verdict it gets without a token.
absentThe request had no KYA-Posture header.

When a DPoP proof fails, reason is proof- plus one of these:

Proof reasonWhat it means
malformedThe DPoP header is not a usable proof.
bad-signatureThe proof's signature does not verify against the key it carries.
invalid-claimsA claim is missing or the wrong shape.
jkt-mismatchThe proof's key is not the key in the token's cnf.
htu-mismatchThe url or method you sent differs from what the browser signed.
stale-iatThe proof is more than 60 seconds old.

verification-error means Vouched KYA failed while checking the token. Verdict ignored the token, and the call still succeeded. It is Vouched KYA's fault, not the visitor's, so it is not counted in Installations.

Verdict records its outcome the way a server SDK does. A valid token counts as Verified, absent as No token sent, and invalid as Rejected with the same reason, including the proof- reasons. The detection's postureOutcome carries that outcome, so the detections postureOutcome filter and the activity log find Verdict calls too.

A valid token changes a human or incomplete-data read in four cases. The token names an agent or bot: the read becomes that agent or bot. The token reports disclosed automation: the read becomes a bot. The token's verdict is agent-driven: the read becomes a browser agent. The token reports corroborated automation: the read becomes incomplete data. In every other case nothing changes, which includes every other valid human token. Verdict never lowers suspicion, and never replaces a signed agent or a known bot that detection already found.

When the token carries a key binding (cnf) and the request carries DPoP, Verdict also checks that the key signed the proof for this method and URL. A token with a key binding and no DPoP header is accepted, as it is by the server SDKs. Verdict does not track whether it saw a token or proof before: a page reuses its token for 120 seconds, so reuse is normal.

Report API calls that send no token

A script that calls your API directly runs no page, so it never has a token. Its outcome is absent. Express and Next.js report every request. The Java and .NET SDKs do not report it by default, because they report only requests that were detected, rejected or sampled. Your project can list the request paths on which the Java and .NET SDKs report an absent or rejected outcome even when nothing else flagged the request. The list is the project setting postureReportPaths. Vouched KYA serves it to the SDK, so changing it needs no code change on your servers. With no paths, nothing changes. The dashboard has no field for the list: to set or change it, email support with your Project ID and the paths.

The Java and .NET SDKs apply this list. See Java for the path rules and for how to read the reported requests. To find the requests, call the project detections endpoint with view=extended, postureOutcome=absent and a startDate. This reports only. It never changes whether a request is allowed or blocked.

A call to a listed path before the page's first token reports absent unless the page awaits checkpoint.prepareForAction({ action }) or uses checkpointFetch. On a first visit the token arrives only after the first beacon response. Awaiting prepareForAction waits for it, up to 3 seconds by default. checkpointFetch waits the same way before the page's first token and never again after that, whether or not a token arrived.

Read the outcome in your handler

In Java, .NET and Express the middleware or filter has already verified the token, so your handler can read the result instead of verifying it a second time. In Next.js the accessor verifies the token itself. Every runtime gives you the same three statuses (verified with the verified claims, absent, or rejected with a reason). verified means the token's signature and claims are valid, not that the visitor is human: check the verdict in the claims. In the Java, .NET and Express middleware a missing result means the request was not checked, which is not the same as absent: treat it as unknown before you gate an action on it. The Next.js accessor verifies the token itself, so it always returns a status.

RuntimeHow to read it
Next.jsawait getCheckpointPostureOutcome(request, { browserPosture, projectId }) from @kya-os/checkpoint-nextjs. It verifies the token itself, so it always returns a status. See Next.js.
ExpressgetCheckpointPostureOutcome(req) from @kya-os/checkpoint-express. See Express.
JavaThe request attribute PostureOutcome.REQUEST_ATTRIBUTE (ai.knowthat.checkpoint.postureOutcome) with CheckpointServletFilter, or CheckpointFilter.postureOutcome(exchange) with CheckpointFilter.
ASP.NET Core and .NET FrameworkHttpContext.GetCheckpointPostureOutcome(). See .NET.
PostureOutcome posture = (PostureOutcome) request.getAttribute(PostureOutcome.REQUEST_ATTRIBUTE);
if (posture != null && posture.status() == PostureOutcome.Status.VERIFIED) {
    BrowserPosture claims = posture.posture().orElseThrow();
    // check claims.verdict() before treating the visitor as human
}

Check the page with detect doctor

kya-os detect doctor fetches a live page and reports whether the beacon and its posture path can work there:

kya-os detect doctor --url https://www.example.com

It reads the page's HTML, then probes the files and headers the beacon needs. Each check reports ok, info, warn, or fail, and the command exits 1 if any check fails. The reference lists every check and the scan limits. These catch most posture problems:

CheckWhat it confirms
Beacon filesThe script, the lazy-signals chunk, and the worker each answer 200 and are served as JavaScript. A pinned tag's bytes match its integrity hash.
Content-Security-Policyscript-src, worker-src, and connect-src allow the beacon's assets and endpoint. Trusted Types must allow checkpoint-beacon.
Proxy preflightA proxied endpoint on another origin answers a browser's OPTIONS request.
Posture, when signed inThe page's origin is allowed to carry posture, the keys endpoint serves the project's key, and the page's beacon uses the project you compared.

A failing run:

Checked https://staging.example.com/
  [ok  ] page: https://staging.example.com/ answered 200
  [ok  ] beacon script integrity: the served bytes match the page's integrity hash
  [FAIL] posture origin: https://staging.example.com is not among the allowed origins (https://www.example.com, https://shop.example.com)
         fix: Run: kya-os detect install --surface beacon --project <project uuid> --origins 'https://shop.example.com,https://staging.example.com' (--origins replaces the whole list, so this keeps the current additional origins)
1 problem(s) found.

The doctor probes configuration; it does not run your application. In a real browser, confirm that a fresh request proceeds before posture arrives, a later request carries the current token and session, CORS permits both, and Java reports verified. Repeat after a reload and a session change. A green doctor result cannot establish these runtime checks or load capacity.

Watch it in Installations

Open Installations in the project's dashboard, at /dashboard/<org>/<project>/installations. The Browser posture card shows:

PartWhat it shows
Key id, Primary origin, Keys URLThe primary origin comes from the project's Site URL, and the card says to set one if it's empty. The key id and keys URL have copy buttons.
Additional site originsThe same list --origins sets, with the same validation and a limit of 20. Save origins saves without touching the rest of the project.
Public keyThe JWK for your server's configuration, with a copy button.
Posture healthThe last 24 hours of beacon responses, by status and page origin.
Server-side verificationVerified, No token sent, and Rejected counts, with the reason for each rejection, once your servers report postureOutcome.

Anyone who can read the project sees the health view. Editing origins needs write access.

Posture health shows the share of responses issued a token, then a row per status and origin: Issued, Origin not allowed, Site URL not set, Signer unavailable, or State unavailable.

For ordinary visitors, Verified and No token sent (outside your report paths) appear only after you set telemetryUndetectedSampleRate. Dashboard reporting lists what the Java SDK reports.

Counts are approximate: Vouched KYA records issuance and verification outcomes separately from stored detections and batches repeated outcomes. Each row's origin is what the browser reported, and it isn't verified. Each hour keeps 50 named origins per status and sums the rest as other origins, not listed. Use these counts to diagnose delivery and verification, not to prove exact traffic reconciliation or a throughput guarantee.

Common problems

Next steps

A token can also carry the beacon's own agent or bot verdict in an optional detection claim, which the server SDKs use to reclassify a request the server had called human. Verifying the posture token has the token's claims, for a stack the SDKs don't cover. Forward edge evidence covers the proxy headers and key checks. The CLI reference lists every flag, file, and JSON field.