The Node.js SDK Is Here.
Zero Dependencies, Full TypeScript.
Last week we shipped the Python SDK. Today it has a sibling. The FaceVault Node.js SDK gives TypeScript and JavaScript developers the same experience — typed models, webhook verification, HTTPS enforcement — with zero runtime dependencies. Just npm install facevault and go. Both SDKs are now at v1.0.0 with full feature parity — trust engine, proof of address, identity credentials, sanctions screening, and a security audit before going open source.
Why a Node.js SDK
The Python SDK was built for ourselves — our Telegram bot needed a clean way to talk to the FaceVault API. But not everyone writes Python. Express servers, Next.js backends, Cloudflare Workers, Telegram bots on grammY — the Node.js ecosystem is where a lot of verification integrations will live.
We wanted the Node SDK to feel native to TypeScript developers. Not a port. Not a wrapper around axios with a few type annotations bolted on. A proper, idiomatic package that uses the language's strengths: interfaces instead of classes for data, native fetch instead of HTTP libraries, async/await everywhere because that's what JavaScript already is.
Zero Dependencies
The Python SDK depends on httpx. We needed it for sync+async support, and it's a reasonable single dependency. But Node 18+ has globalThis.fetch built in, and node:crypto has everything we need for HMAC-SHA256. So the Node SDK has zero runtime dependencies.
Zero dependencies means:
No supply chain risk
Your node_modules doesn't grow. No transitive dependencies. No npm audit noise from some library three levels deep.
Works everywhere Node 18+ runs
AWS Lambda, Docker, bare metal, Deno, Bun — if it has fetch and crypto, it works.
Tiny install
The published package is just the compiled dist/ folder. ESM, CJS, and .d.ts type declarations. Nothing else.
Design Decisions
1 Interfaces, not classes
In Python, we used dataclasses for Session, SessionStatus, and WebhookEvent. In TypeScript, the equivalent is interfaces. Plain objects with type annotations. No new Session(), no constructors, no prototype chain. The API returns JSON, we type it, you use it.
This means session.sessionId works exactly like you'd expect. Your editor autocompletes it. TypeScript catches typos at compile time. And the runtime cost is zero — it's just an object.
2 Native fetch, not axios
Node 18 stabilised globalThis.fetch. It's fast, it's standards-compliant, and it's already there. Adding axios or got would give us retry logic and interceptors we don't need, plus a dependency tree we don't want. We use AbortSignal.timeout() for request timeouts — also built in since Node 18. Zero dependencies means zero supply chain risk.
3 ESM + CJS dual package
The package ships as ESM (.js) with a CJS fallback (.cjs). import { FaceVaultClient } from "facevault" works. const { FaceVaultClient } = require("facevault") also works. TypeScript declarations ship alongside both. Built with tsup, which handles the dual-format bundling without any conditional export gymnastics.
4 camelCase everywhere
The FaceVault API uses snake_case (session_id, face_match_passed). The SDK maps these to camelCase (sessionId, faceMatchPassed) because that's what JavaScript developers expect. The mapping happens inside the SDK — you never touch snake_case.
What It Looks Like
Install it:
npm install facevault Create a verification session:
import { FaceVaultClient } from "facevault";
const client = new FaceVaultClient({ apiKey: "fv_live_your_api_key" });
// Create a verification session
const session = await client.createSession("user-123");
console.log(session.webappUrl); // send this to your user
// Or require proof of address for this session
const session2 = await client.createSession("user-456", {
requirePoa: true,
});
// Check results — trust engine gives you a single score
const status = await client.getSession(session.sessionId);
console.log(status.trustScore); // 0-100
console.log(status.trustDecision); // "accept", "review", "reject"
console.log(status.faceMatchPassed); Handle errors:
import { FaceVaultClient, AuthError, NotFoundError, RateLimitError } from "facevault";
const client = new FaceVaultClient({ apiKey: "fv_live_your_api_key" });
try {
const status = await client.getSession("nonexistent");
} catch (err) {
if (err instanceof AuthError) console.log("Invalid API key");
if (err instanceof NotFoundError) console.log("Session not found");
if (err instanceof RateLimitError) console.log("Too many requests");
} That's it. Same surface area as the Python SDK: one client, three models, four exceptions, two webhook helpers.
Webhook Verification
Webhook payloads are signed with HMAC-SHA256. The SDK re-canonicalises the JSON (sorted keys, compact format) before computing the HMAC, matching what the server does. Signature comparison uses crypto.timingSafeEqual() to prevent timing attacks.
import express from "express";
import { verifySignature, parseEvent } from "facevault";
const app = express();
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-signature"] as string;
if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const event = parseEvent(req.body);
// Trust engine decision — one field, no guesswork
if (event.trustDecision === "accept") {
console.log(`User ${event.externalUserId} verified! Score: ${event.trustScore}`);
} else if (event.trustDecision === "review") {
console.log(`Manual review needed — score: ${event.trustScore}`);
} else {
console.log(`Rejected — sanctions hit: ${event.sanctionsHit}`);
}
res.sendStatus(200);
}); express.raw() to get the raw body as a Buffer, not express.json(). If Express parses the JSON first and you re-stringify it, whitespace and key order may differ from the original, and the signature won't match.
Parity with Python
Both SDKs share the same API surface. If you've used one, you know the other.
| Feature | Python | Node.js |
|---|---|---|
| Create session | create_session() | createSession() |
| Get status | get_session() | getSession() |
| Verify webhook | verify_signature() | verifySignature() |
| Parse event | parse_event() | parseEvent() |
| Require PoA | require_poa=True | { requirePoa: true } |
| Trust score | status.trust_score | status.trustScore |
| Auth error | AuthError | AuthError |
| Version | 1.0.0 | 1.0.0 |
| Dependencies | httpx | None |
| HTTPS enforced | Yes | Yes |
| Key validation | Yes | Yes |
| Secret redaction | __repr__ | #private + toJSON |
| Tests | 46 | 43 |
The naming difference is just convention — snake_case for Python, camelCase for JavaScript. The behaviour is identical. Same endpoints, same error mapping, same HMAC canonicalisation, same security guarantees.
What's New in 1.0.0
v0.1 shipped the essentials. v1.0.0 ships everything. The SDK now exposes every signal the FaceVault engine produces — and we security-audited the whole thing before flipping the repos to public.
Trust engine
status.trustScore (0–100) and status.trustDecision ("accept", "review", "reject"). One number, one decision. The engine weighs face match, anti-spoofing, and document fraud so you don't have to.
Proof of address
createSession("user", { requirePoa: true }) enables per-session proof of address. The status.poa object gives you extraction results, name matching, and fraud scoring.
Identity credentials & sanctions
status.credential for reusable identity credentials. event.sanctionsHit on webhooks. Full anti-spoofing breakdown in status.antiSpoofing.
Hardened for open source
ES2022 # private fields make the API key truly inaccessible at runtime — not just TypeScript private, which compiles away. toJSON() prevents accidental key leakage via JSON.stringify. Full security audit: zero secrets in git history, HMAC timing-safe, HTTPS enforced.
The upgrade is non-breaking. Every new field is optional with a sensible default. Your existing code keeps working — you just get richer data when you're ready for it.
Get Started
The SDK is live on npm at v1.0.0 and the source is on GitHub. Install it, read it, break it, improve it. Whether you're building an Express webhook handler, a Next.js API route, or a grammY Telegram bot — this is the fastest way to integrate FaceVault in JavaScript.
Links
facevault-node on GitHub — source code, MIT license
facevault on npm — npm install facevault
Python SDK blog post — the companion SDK for Python developers
FaceVault API Docs — quickstart guide and API reference