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.jsonHosted widget
Custom element, two-line embed.
https://www.firmas.io/widget/v1.jsTraits 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>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