Deploy

Wire Firmas verification into your site or app.

Four integration shapes. Pick whichever fits your stack — they all return the same {trait, traitValue, expiresAt} verdict against the same public JWKS.

Quick reference

Verifier endpoint

GET request, returns JSON. No auth required.

https://www.firmas.io/api/vouch/verify?id=…

Public JWKS

ECDSA P-256 keys, RFC 7517 keyset.

/.well-known/firmas-vouch-jwks.json

Hosted widget

Custom element, two-line embed.

https://www.firmas.io/widget/v1.js

Traits available

`human`, `over_18`, `residency` (optionally with country code, e.g. `residency:ES`).

For AI agents and coding assistants

If you're an automation reading this page to wire up Firmas verification: the recommended path is the hosted widget (two lines), and the canonical machine-readable description of the verifier API lives at /api/openapi.json. Trait values follow OpenID-Connect-style naming (`over_18`, `human`, `residency`). The verdict shape is stable.

Integration patterns

Recommended

Hosted widget — recommended

Drop in our hosted custom element. Renders a styled button, opens a camera modal, returns the verdict via DOM event. Shadow DOM keeps your styles clean.

<!-- 1. Two-line embed: anywhere on your page -->
<script src="https://www.firmas.io/widget/v1.js"></script>
<firmas-verify trait="over_18"></firmas-verify>

<!-- Other traits:
       trait="human"           — confirms a real person
       trait="residency"       — any country
       trait="residency:ES"    — must be resident of Spain
       trait="residency:US-CA" — must be resident of California
-->

<!-- 2. Listen for the verdict (or cancellation) -->
<script>
  document.querySelector('firmas-verify').addEventListener(
    'firmas-verify:verdict',
    (e) => {
      if (e.detail.ok) {
        // unlock your gated content here
        document.body.classList.add('verified');
      } else {
        // show retry UI; e.detail.error has the reason
        console.warn('Firmas verify failed:', e.detail.error);
      }
    },
  );
</script>
Try the live demo →

Browser-only — full control

Roll your own button + camera UI. Useful when you want full control over visuals or already use a different QR-scanner library.

<!-- 1. Add the button + a small result element -->
<button id="firmas-verify">Verify your age</button>
<p id="firmas-result" hidden></p>

<!-- 2. Wire it to the device camera + Firmas verifier -->
<script type="module">
  import QrScanner from 'https://esm.sh/qr-scanner@1.4.2';

  document.getElementById('firmas-verify').addEventListener('click', async () => {
    const video = document.createElement('video');
    document.body.appendChild(video);
    const scanner = new QrScanner(video, async (res) => {
      // Firmas QR encodes https://www.firmas.io/verify/<short_id>
      const match = res.data.match(/\/verify\/([A-Za-z0-9_-]+)/);
      if (!match) return;
      scanner.stop(); video.remove();

      const r = await fetch(`https://www.firmas.io/api/vouch/verify?id=${match[1]}`);
      const verdict = await r.json();

      const out = document.getElementById('firmas-result');
      out.hidden = false;
      if (verdict.valid && verdict.trait === 'over_18' && verdict.traitValue === true) {
        out.textContent = '✓ Verified adult';
        // unlock your gated content here
      } else {
        out.textContent = 'Verification failed — please try again';
      }
    }, { returnDetailedScanResult: true });
    scanner.start();
  });
</script>

Server-side — production-grade

Verify on your own backend. Works with any HTTP client; below is curl + Node + Python.

curl

# 1. The visitor's QR contains a URL like
#    https://www.firmas.io/verify/abc123
#    Extract the short_id and ask the verifier:

curl "https://www.firmas.io/api/vouch/verify?id=abc123"

# Response on success:
# {
#   "valid": true,
#   "trait": "over_18",
#   "traitValue": true,
#   "confidence": 87,
#   "issuer": "https://www.firmas.io",
#   "expiresAt": 1745721600,
#   ...
# }

Node

// Node 18+ (built-in fetch)
async function verifyFirmasShortId(shortId) {
  const r = await fetch(`https://www.firmas.io/api/vouch/verify?id=${shortId}`);
  const v = await r.json();
  if (!v.valid) return { ok: false, reason: v.error };
  if (v.trait !== 'over_18' || v.traitValue !== true) {
    return { ok: false, reason: 'wrong_trait' };
  }
  if (Date.now() / 1000 > v.expiresAt) {
    return { ok: false, reason: 'expired' };
  }
  return { ok: true, confidence: v.confidence };
}

Python

# Python 3.8+
import time, requests

def verify_firmas_short_id(short_id: str) -> dict:
    r = requests.get(f"https://www.firmas.io/api/vouch/verify",
                     params={"id": short_id}, timeout=5)
    v = r.json()
    if not v.get("valid"):
        return {"ok": False, "reason": v.get("error")}
    if v.get("trait") != "over_18" or v.get("traitValue") is not True:
        return {"ok": False, "reason": "wrong_trait"}
    if time.time() > v["expiresAt"]:
        return {"ok": False, "reason": "expired"}
    return {"ok": True, "confidence": v["confidence"]}

Self-verifying — paranoid mode

Fetch our JWKS, verify the device + issuer signatures yourself. No round-trip to firmas.io. Useful for high-stakes gating.

// Self-verifying — fetch our JWKS once, verify presentations locally.
// Uses 'jose' (npm install jose). No round-trip to firmas.io per check.

import { jwtVerify, createRemoteJWKSet, decodeJwt } from 'jose';

// Cache this — public keys rotate rarely. The JWKS endpoint serves
// proper Cache-Control + stale-while-revalidate.
const FIRMAS_JWKS = createRemoteJWKSet(
  new URL('https://www.firmas.io/.well-known/firmas-vouch-jwks.json'),
);

export async function verifyPresentation(presentationJwt) {
  // 1. Outer JWT is the presentation, signed by the user's device key.
  //    The device key is embedded in the presentation header (kid +
  //    public JWK). Decode without verifying first to get the jwk.
  const presHeader = JSON.parse(
    Buffer.from(presentationJwt.split('.')[0], 'base64url').toString(),
  );
  const devicePub = presHeader.jwk;
  await jwtVerify(presentationJwt, devicePub);

  // 2. The presentation payload contains the credential JWT, signed
  //    by Firmas's issuer key — verify against the JWKS.
  const presPayload = decodeJwt(presentationJwt);
  const credJwt = presPayload.credential;
  const { payload: credential } = await jwtVerify(credJwt, FIRMAS_JWKS);

  // 3. Bind the credential to the device that just signed: the
  //    credential's "sub" must equal sha256(devicePub).
  // ... (see firmas.io's open-source verifier reference for full chain)

  return { trait: credential.trait, traitValue: credential.trait_value };
}

Need something else?

Custom flows, volume pricing, hosted dashboards, or an MCP server for AI integrations — we're happy to talk.

Email us: firmasfb@rindogatan.com