Developer Documentation
Add proof-of-personhood to any website in three steps: get an API key, add the widget, verify server-side.
1. Get Your API Key
Sign up at verena.dev and create an API key from your dashboard. You'll get two keys:
| Key | Format | Where to use |
|---|---|---|
| Site key | site_... |
Frontend — passed to Verena.init(). Safe to expose in client-side code. |
| Secret key | sk_... |
Backend only — used to verify proof tokens. Never expose in client-side code. |
2. Add the Widget
Option A — Script tag (CDN)
Drop this into your HTML. The Verena global is available immediately.
<script src="https://cdn.verena.dev/v1/verena.js"></script>
Option B — npm package
npm install @verena/js
Then import in your code:
import { Verena } from "@verena/js";
Initialize with your site key and either mount the inline widget or use the modal flow:
Inline widget (recommended)
Mount a checkbox-style widget into a container element. The widget handles the entire verification flow.
<div id="verena"></div>
<script>
Verena.init({
siteKey: "site_your_key",
onVerified: async (proof) => {
// Send the token to your backend for verification
const res = await fetch("/api/protected", {
headers: { "X-Verena-Token": proof.token }
});
const data = await res.json();
// Use the protected data
}
});
Verena.mount("#verena");
</script>
Modal flow
Open a verification modal programmatically. Useful for gating actions like form submissions.
async function handleSubmit() {
try {
const proof = await Verena.verify();
// Send form data + proof token to your backend
await fetch("/api/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Verena-Token": proof.token
},
body: JSON.stringify(formData)
});
} catch (err) {
// User dismissed the modal or verification failed
}
}
3. Server-Side Verification
After the user verifies in the browser, send the proof token to your backend. Your backend validates it with the Verena API using your secret key before serving protected content.
Flow
onVerified or Verena.verify().
X-Verena-Token) or in the request body.
https://api.verena.dev/v1/verify with your secret key.
Node.js / Express
app.get("/api/protected", async (req, res) => {
const token = req.headers["x-verena-token"];
if (!token) return res.status(403).json({ error: "Missing token" });
// Verify the token with Verena's API
const check = await fetch("https://api.verena.dev/v1/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.VERENA_SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ token })
});
const { valid } = await check.json();
if (!valid) return res.status(403).json({ error: "Not verified" });
// Token is valid — serve protected content
const article = await db.articles.findById(req.params.id);
res.json({ html: article.fullContent });
});
Python / Flask
@app.route("/api/protected")
def protected():
token = request.headers.get("X-Verena-Token")
if not token:
return jsonify(error="Missing token"), 403
check = requests.post("https://api.verena.dev/v1/verify",
headers={"Authorization": f"Bearer {VERENA_SECRET_KEY}"},
json={"token": token}
)
if not check.json().get("valid"):
return jsonify(error="Not verified"), 403
return jsonify(html=get_article_content())
cURL
curl -X POST https://api.verena.dev/v1/verify \
-H "Authorization: Bearer sk_your_key" \
-H "Content-Type: application/json" \
-d '{"token": "eyJhbGciOiJFUzI1NiIs..."}'
# Response: { "valid": true, "scope": "personhood" }
{ "valid": true, "scope": "personhood" } for valid tokens. Invalid or expired tokens return { "valid": false, "error": "..." }.
Verena.init(config)
Initialize the library. Must be called once before any other method.
Verena.init({
siteKey: "site_your_key", // required
theme: "light", // "light" | "dark" | "auto"
onVerified: (proof) => { ... }, // called on successful verification
onError: (err) => { ... }, // called on failure
onDismiss: () => { ... }, // called when user closes the modal
externalStyles: false, // true to use your own CSS
});
config options| Option | Type | Description |
|---|---|---|
| siteKey | string | Required. Your public site key. |
| theme | string | Modal theme: "light", "dark", or "auto". Defaults to "light". |
| onVerified | function | Callback with { token, expiresAt } on success. Useful for the callback-based flow. |
| onError | function | Callback with an Error on failure. |
| onDismiss | function | Callback when the user closes the modal without verifying. |
| externalStyles | boolean | When true, the inline widget won't inject its own styles — use your own CSS. Defaults to false. |
Verena.verify()
Opens the verification modal and runs the proof-of-personhood flow. Returns a promise that resolves with a proof token on success.
const proof = await Verena.verify();
// proof.token → signed JWT proof token
// proof.expiresAt → unix timestamp (seconds)
The promise rejects if the user closes the modal or if verification fails. Use a try/catch to handle both cases:
try {
const proof = await Verena.verify();
// Send proof.token to your backend
} catch (err) {
if (err.message === "User dismissed verification") {
// User closed the modal — not an error, just cancelled
} else {
// Actual verification failure
console.error(err);
}
}
Return value
| Field | Type | Description |
|---|---|---|
| token | string | Signed proof token (JWT). Send this to your backend for verification. |
| expiresAt | number | Unix timestamp (seconds) when the token expires. Default: 1 hour from issuance. |
Verena.mount(container)
Mounts an inline checkbox-style widget into the specified container element. The widget handles the full verification flow including the passkey form, loading animation, and success state.
<div id="verena"></div>
<script>
Verena.init({ siteKey: "site_your_key" });
Verena.mount("#verena"); // CSS selector or DOM element
</script>
Use this instead of Verena.verify() when you want a persistent, visible widget on the page (like a CAPTCHA checkbox). Configure externalStyles: true in init() if you want to style the widget with your own CSS instead of the built-in styles.
Verena.status()
Check whether the current user already has a valid (non-expired) proof stored locally. Useful for skipping verification if they've already passed.
const status = Verena.status();
if (status.verified) {
console.log("Already verified, expires at:", status.expiresAt);
} else {
// Trigger verification
await Verena.verify();
}
Return value
| Field | Type | Description |
|---|---|---|
| verified | boolean | true if a valid, non-expired proof exists. |
| expiresAt | number? | Unix timestamp of token expiry. Only present when verified is true. |
Proofs are stored in sessionStorage by default — they survive page navigations but are cleared when the tab is closed.
Verena.reset()
Clears the locally stored proof token. Call this on user logout or whenever you want to require re-verification.
// e.g. on logout
logoutButton.addEventListener("click", () => {
Verena.reset();
window.location.href = "/";
});
How Verification Works
When Verena.verify() is called, the library runs the following flow:
The entire flow typically completes in under 2 seconds for returning users.
Mobile QR Fallback
If the user's device doesn't have a platform authenticator, or they prefer to use their phone, the modal shows a "Use your phone instead" option. This starts the QR fallback flow:
Mobile sessions expire after 5 minutes. The user can switch back to desktop biometrics at any time.
Configuration
All configuration is passed to Verena.init(). There are no environment variables or external config files.
Callback-based flow
If you prefer callbacks over async/await, use the onVerified callback. This is useful when you want to handle verification globally rather than at each call site:
Verena.init({
siteKey: "site_your_key",
onVerified: async (proof) => {
// Send token to your backend for server-side verification
const res = await fetch("/api/protected", {
headers: { "X-Verena-Token": proof.token }
});
const data = await res.json();
document.getElementById("content").innerHTML = data.html;
},
onError: (err) => {
console.error("Verification failed:", err.message);
}
});
// Mount inline widget or use verify() for modal
Verena.mount("#verena");
Proof Tokens
After successful verification, the library returns a signed JWT proof token:
{
"token": "eyJhbGciOiJFUzI1NiIs...",
"expiresAt": 1708003600
}
The token contains the following claims:
| Claim | Description |
|---|---|
| iss | verena.dev — token issuer |
| aud | Your site key — prevents tokens from being used on other sites |
| exp | Expiration timestamp (default: 1 hour) |
| scope | personhood — confirms biometric verification occurred |
| auth | webauthn — authentication method |
Tokens are short-lived (1 hour by default) and audience-scoped to your site. They cannot be reused on other sites. There is no revocation mechanism — just let them expire.
Security Model
WebAuthn / FIDO2
Verena requires platform authenticators only (Face ID, Touch ID, Windows Hello). This ensures verification happens on a real device with hardware-backed biometrics. Software tokens and password managers are not accepted.
Challenge-Response
Every verification generates a unique cryptographic challenge that is verified server-side. This prevents replay attacks — captured responses cannot be reused.
Site Isolation
Proof tokens are scoped to your site key. A token issued for site_abc123 cannot be used with any other site key. Your secret key is never exposed to the browser.
Shadow DOM Isolation
The verification modal is rendered inside a closed Shadow DOM. This prevents CSS conflicts with your page and protects the modal from being manipulated by page scripts.
Privacy
- No personal data is collected or stored
- Biometric data never leaves the user's device
- No cross-site tracking or fingerprinting
- Attestation is set to
none— no device fingerprinting - GDPR, CCPA, and HIPAA friendly
localhost works for development, but production deployments must use HTTPS.