· 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.
| Intent | The question | Surface |
|---|---|---|
presence | Is a human here? | The checkbox widget, where a captcha would sit |
login | Which human is it? | A "Continue with veyns" button; the pairwise sub is the account key |
action | Did 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
| Attribute | Required | Meaning |
|---|---|---|
data-client-id | Yes | Your client identifier. The token's aud claim must match it, and the pairwise sub is scoped to it. |
data-client-name | No | Name shown in the popup ("Trying to sign in to …?"). Defaults to the page title. |
data-callback | No | Global function called with { widgetId, token, claims } on success. |
data-expired-callback | No | Called when the short-lived token expires and the widget resets. |
data-error-callback | No | Called with an error code, for example popup_blocked or bad_signature. |
data-authorize-url | No | Override the Veyns authorization page. Defaults to authorize.html next to veyns.js. |
JavaScript API
| Call | Meaning |
|---|---|
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:
| Endpoint | Meaning |
|---|---|
GET /.well-known/openid-configuration | Discovery document: issuer, endpoints, supported algorithms. |
GET /jwks.json | Issuer keys. ML-DSA-44 keys use the AKP key type from the JOSE post-quantum draft, with the public key in pub. |
GET /authorize | The authorization popup the widget opens. |
POST /v1/clients | Register a relying party: { name, origins[], client_id? }. Challenges and tokens are only issued for registered, active clients on their declared origins. |
POST /v1/challenge | Issues a one-time challenge bound to client_id and nonce. |
POST /v1/token | Takes 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:
| Claim | Meaning |
|---|---|
iss | http://127.0.0.1:3004 in this sandbox; always matches the discovery document. |
aud | Your client id. |
sub | Pairwise person ID, stable for your client only. Two sites can never correlate the same person. |
nonce | Fresh per request; the widget checks it against the one it issued. |
iat / exp | Issued and expires; tokens live for 2 minutes. |
veyns_presence | true when a person approved. |
veyns_intent | presence, login, or action. Always check it matches what you asked for. |
veyns_action | Approvals only: { statement, digest }. The digest is SHA-256 over the canonical JSON of { statement, details } (keys sorted, no whitespace). |
veyns_level | Assurance level. VA0 in this sandbox. |
veyns_mode | wallet (software) or palm (hardware, future). |
cnf.veyns_wallet_pk | Confirmation 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.