veyns· for developers

Prove a human. Sign them in. Get their approval.

Veyns is one integration with three intents. Behind every intent is the same ceremony: the person approves in a Veyns popup with their device wallet, the wallet proves possession of its key to the identity service, and the service mints an ID token signed with the issuer's ML-DSA-44 key. The SDK verifies the signature against the published JWKS, plus every claim, before your code sees a result.

IntentThe questionSurface
presenceIs a human here?The checkbox widget, where a captcha would sit
loginWhich human is it?A "Continue with veyns" button; the pairwise sub is the account key
actionDid they approve this exact action?veyns.approve(...) before anything sensitive runs, including what your AI agents do

See all three live on the sample site

1. Register your app

Register in the console to get a client id. You declare the exact origins your site runs on; the service refuses verification requests from anywhere else. (Programmatic registration: POST /v1/clients.)

2. Load the SDK

<script src="http://127.0.0.1:3004/veyns.js" defer></script>

3. Place the widget

<div class="veyns-widget"
     data-client-id="your_client_id"
     data-client-name="Your App"
     data-callback="onVeynsVerified"></div>

<script>
  function onVeynsVerified({ token, claims }) {
    // The person is verified. `claims.sub` is a stable ID,
    // unique to your client. Enable your form, or send the
    // token to your server with the request it protects.
  }
</script>

Inside a form, the widget also fills a hidden <input name="veyns_token">, so a plain HTML form submits the token with no JavaScript at all.

The other two intents

Sign-in is a drop-in button (or call veyns.signin(...) yourself):

<div class="veyns-signin"
     data-client-id="your_client_id"
     data-callback="onVeynsSignin"></div>

<script>
  function onVeynsSignin({ token, claims }) {
    // claims.sub is the account key: stable for your client,
    // meaningless to anyone else. Create your own session now.
  }
</script>

Approvals bind a signature to one exact action. The popup shows the statement verbatim; the token carries a digest of it (dynamic linking), and the SDK recomputes that digest itself before resolving:

const { claims } = await veyns.approve({
  statement: "Transfer $450.00 to Omar K.",
  details: { amount: "450.00", currency: "USD", to: "omar-k" }
}, { clientId: "your_client_id" });

// claims.veyns_action.digest is bound to exactly this action.
// Send the token with the request that executes it; verify
// server-side and compare the digest before running anything.

Widget attributes

AttributeRequiredMeaning
data-client-idYesYour client identifier. The token's aud claim must match it, and the pairwise sub is scoped to it.
data-client-nameNoName shown in the popup ("Trying to sign in to …?"). Defaults to the page title.
data-callbackNoGlobal function called with { widgetId, token, claims } on success.
data-expired-callbackNoCalled when the short-lived token expires and the widget resets.
data-error-callbackNoCalled with an error code, for example popup_blocked or bad_signature.
data-authorize-urlNoOverride the Veyns authorization page. Defaults to authorize.html next to veyns.js.

JavaScript API

CallMeaning
veyns.render(el, options)Render a widget into an element programmatically instead of auto-render. Returns a widget id. Options mirror the data attributes: clientId, clientName, onVerified, onError, onExpired, authorizeUrl.
veyns.getResponse(id?)The current token, or an empty string.
veyns.reset(id?)Reset one widget, or all of them when no id is given.

Widgets also dispatch a bubbling veyns:verified DOM event on their container.

The identity service

The sandbox issuer is a standard OpenID Connect surface at http://127.0.0.1:3004:

EndpointMeaning
GET /.well-known/openid-configurationDiscovery document: issuer, endpoints, supported algorithms.
GET /jwks.jsonIssuer keys. ML-DSA-44 keys use the AKP key type from the JOSE post-quantum draft, with the public key in pub.
GET /authorizeThe authorization popup the widget opens.
POST /v1/clientsRegister a relying party: { name, origins[], client_id? }. Challenges and tokens are only issued for registered, active clients on their declared origins.
POST /v1/challengeIssues a one-time challenge bound to client_id and nonce.
POST /v1/tokenTakes the wallet's signature over the challenge, verifies proof of possession, and returns the issuer-signed ID token. Challenges are single use and expire after 2 minutes.

The token

A JWT-shaped token signed by the Veyns issuer with ML-DSA-44 (NIST FIPS 204, post-quantum). Claims:

ClaimMeaning
isshttp://127.0.0.1:3004 in this sandbox; always matches the discovery document.
audYour client id.
subPairwise person ID, stable for your client only. Two sites can never correlate the same person.
nonceFresh per request; the widget checks it against the one it issued.
iat / expIssued and expires; tokens live for 2 minutes.
veyns_presencetrue when a person approved.
veyns_intentpresence, login, or action. Always check it matches what you asked for.
veyns_actionApprovals only: { statement, digest }. The digest is SHA-256 over the canonical JSON of { statement, details } (keys sorted, no whitespace).
veyns_levelAssurance level. VA0 in this sandbox.
veyns_modewallet (software) or palm (hardware, future).
cnf.veyns_wallet_pkConfirmation claim: the wallet key whose proof of possession the issuer verified before minting this token.

Verifying on your server

The widget verifies in the browser for UX. Before trusting a token, verify it server-side against the issuer's JWKS, the same way you would for any OpenID Connect provider:

// Node, using the same bundled verifier
import { ml_dsa44 } from "./vendor/veyns-pqc.js";

const ISSUER = "http://127.0.0.1:3004";
const { keys } = await fetch(`${ISSUER}/jwks.json`).then(r => r.json());

function verifyVeynsToken(token, expectedAud, expectedNonce) {
  const [head, body, sig] = token.split(".");
  const header = JSON.parse(Buffer.from(head, "base64url").toString());
  const claims = JSON.parse(Buffer.from(body, "base64url").toString());
  const key = keys.find(k => k.kid === header.kid && k.alg === "ML-DSA-44");
  const now = Math.floor(Date.now() / 1000);
  const ok =
    key &&
    ml_dsa44.verify(
      Buffer.from(sig, "base64url"),
      Buffer.from(`${head}.${body}`),
      Buffer.from(key.pub, "base64url")
    ) &&
    claims.iss === ISSUER &&
    claims.aud === expectedAud &&
    claims.nonce === expectedNonce &&
    claims.exp > now &&
    claims.veyns_presence === true;
  return ok ? claims : null;
}

Sandbox boundaries

This is the VA0 sandbox: the wallet key lives in browser storage, wallets are trusted on first use (no enrollment or attestation yet), challenges are held in service memory, and transport is plain HTTP on loopback. The production service adds wallet attestation, durable storage, TLS, higher assurance levels, and the hardware palm mode. The integration surface above is the intended production surface.