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:

KeyFormatWhere 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
  }
}
Always verify server-side Client-side verification is cosmetic — a bot can skip JavaScript entirely. Your backend must validate the proof token before serving protected content or processing actions. See step 3.

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

1
User verifies in browser The widget returns a signed proof token via onVerified or Verena.verify().
2
Frontend sends token to your backend Include the token as a header (X-Verena-Token) or in the request body.
3
Backend verifies with Verena API POST the token to https://api.verena.dev/v1/verify with your secret key.
4
Serve content or process action If valid, serve the protected resource. Otherwise return 403.

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" }
API response The verification endpoint returns { "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

FieldTypeDescription
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

FieldTypeDescription
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:

1
User clicks the checkbox The "Verify you're human" checkbox transitions to show passkey options: sign in with an existing passkey or create a new one.
2
User chooses a passkey Existing users sign in instantly. New users create a passkey — the browser stores it in the platform authenticator.
3
Device prompts biometric The browser triggers Face ID, Touch ID, or Windows Hello. Biometric data never leaves the device.
4
Proof token issued After cryptographic verification, Verena issues a signed proof token. Send this to your backend for server-side validation.

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:

1
QR code displayed The library creates a mobile verification session and renders a QR code in the modal.
2
User scans with phone The QR code opens a mobile verification page where the user completes the biometric flow.
3
Desktop detects completion The library polls for the mobile session status. When verification completes on the phone, the desktop modal resolves with the proof token.

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:

ClaimDescription
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

WebAuthn requires HTTPS WebAuthn only works in secure contexts. localhost works for development, but production deployments must use HTTPS.