All posts
Engineering 22 February 2026 · Updated 11 Mar · 7 min read

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.

The goal: A developer who's read the Python SDK README should be able to use the Node SDK without reading the Node README. Same methods, same models, same errors. Just camelCase instead of snake_case.

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:

shell
npm install facevault

Create a verification session:

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

TypeScript
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.

TypeScript
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);
});
Important: Use 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 sessioncreate_session()createSession()
Get statusget_session()getSession()
Verify webhookverify_signature()verifySignature()
Parse eventparse_event()parseEvent()
Require PoArequire_poa=True{ requirePoa: true }
Trust scorestatus.trust_scorestatus.trustScore
Auth errorAuthErrorAuthError
Version1.0.01.0.0
DependencieshttpxNone
HTTPS enforcedYesYes
Key validationYesYes
Secret redaction__repr__#private + toJSON
Tests4643

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