Back to Blog
Tutorial
March 23, 20268 min read

How to Add Sanctions Screening to Your Node.js App in 10 Minutes

If your application handles money, you are legally required to screen customers and counterparties against sanctions lists like the OFAC SDN list. Skipping this step can result in fines reaching $356,579 per violation, and OFAC enforces on a strict liability basis. That means it does not matter whether you knew about the sanctioned person or not. If you processed a transaction involving them, you are liable.

The good news is that adding sanctions screening to a Node.js application is straightforward. This tutorial walks you through the entire process, from integrating the REST API to building production-ready middleware, in about 10 minutes.

Why your app needs sanctions screening

Every fintech, payment processor, crypto exchange, neobank, and lending platform that touches U.S. dollars or serves U.S. customers must comply with OFAC regulations. This is not optional. The Office of Foreign Assets Control has been increasingly aggressive with enforcement actions, especially against technology companies that process payments without adequate screening.

In practical terms, compliance means checking every customer name against sanctions lists at three points: when they sign up, before you process a payment on their behalf, and periodically to catch people who get added to lists after they became your customer.

The cost of not screening is severe. OFAC fines can reach $356,579 per negligent violation and can reach $20 million for willful violations. Beyond fines, a sanctions violation can destroy banking relationships, trigger license revocations, and generate the kind of press coverage no startup wants. The top sanctions screening APIs make this easier to implement, and Verifex is designed for developers who need screening evidence quickly.

Step 1: Get your API key

Verifex exposes a REST API — no package installation required. Use Node.js built-in fetch (Node 18+) or any HTTP client. The API key is the only setup step.

Official SDKs are available for Node.js, Python, Go, and Rust. You can also use the REST API directly — it works identically in any HTTP client.

Grab your API key from the Verifex dashboard. The free tier gives you 50 screens per month, which is plenty for development and testing. Store the key as an environment variable in your .env file:

# .env
VERIFEX_API_KEY=vfx_your_api_key

Never hardcode API keys in your source code. If you are using dotenv, make sure .env is in your .gitignore file.

Step 2: Basic screening — your first API call

Here is the simplest possible screening. A single fetch call to POST /v1/screen with your API key and entity name is all it takes.

const API_KEY = process.env.VERIFEX_API_KEY;
const BASE    = "https://api.verifex.dev/v1";

// Screen a person — Node 18+ built-in fetch
const resp = await fetch(`${BASE}/screen`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Rosneft", type: "company" }),
});
const result = await resp.json();

console.log(result.risk_level);    // "critical"
console.log(result.matches);       // Array of matched sanctions entries
console.log(result.lists_checked); // ["OFAC", "UN", "EU", "UK"]

One POST call screens against OFAC SDN, UN Security Council, EU Consolidated List, and UK HM Treasury sanctions lists simultaneously. The response comes back in under 500 milliseconds.

Step 3: Handling results — risk levels and confidence scores

The screening response includes a risk level and an array of matches, each with a confidence score. Understanding these is critical for building the right compliance logic.

Risk levels map to confidence score ranges:

  • Critical (90-100%). Exact or near-exact match against a sanctions entry. This almost certainly refers to the same person or entity. You should block the transaction and escalate to your compliance team immediately.
  • High (75-89%). Strong fuzzy or phonetic match. Likely the same person, but minor variations exist. Flag for manual review before approving.
  • Medium (60-74%). Moderate similarity. Could be the same person or a common name collision. Worth investigating, especially if other data points like date of birth or nationality also match.
  • Low (below 60%). Weak match. Probably a different person. Most businesses auto-approve at this level unless other risk indicators are present.

Here is how to implement risk-based decision logic in your application:

async function handleScreeningResult(result, userId) {
  switch (result.risk_level) {
    case "critical":
      // Block immediately and alert compliance
      await db.user.update({
        where: { id: userId },
        data: { status: "blocked", screeningResult: JSON.stringify(result) },
      });
      await notifyComplianceTeam(userId, result);
      return { approved: false, reason: "sanctions_match" };

    case "high":
      // Flag for manual review
      await db.user.update({
        where: { id: userId },
        data: { status: "pending_review", screeningResult: JSON.stringify(result) },
      });
      return { approved: false, reason: "pending_review" };

    case "medium":
      // Auto-approve but log for audit trail
      await db.screeningLog.create({
        data: { userId, result: JSON.stringify(result), action: "auto_approved" },
      });
      return { approved: true };

    default:
      // Low risk or clear — approve
      return { approved: true };
  }
}

Step 4: Batch screening — screen multiple names at once

When you need to re-screen your entire customer base or process a list of counterparties, use the batch endpoint. The Verifex API accepts up to 100 entities per batch request, which is significantly faster than making individual calls.

// Fetch all active customers
const customers = await db.user.findMany({
  where: { status: "active" },
  select: { id: true, name: true },
});

// Process in batches of 100
for (let i = 0; i < customers.length; i += 100) {
  const batch = customers.slice(i, i + 100);

  const batchResp = await fetch(`${BASE}/screen/batch`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ entities: batch.map((c) => ({ name: c.name, type: "person" })) }),
  });
  const results = (await batchResp.json()).results;

  for (let j = 0; j < results.length; j++) {
    if (results[j].risk_level === "critical" || results[j].risk_level === "high") {
      await flagForReview(batch[j].id, results[j]);
    }
  }
}

console.log(`Screened ${customers.length} customers`);

Schedule this as a weekly or monthly cron job. Most compliance frameworks require at least monthly re-screening. If you are in a higher-risk sector like crypto, weekly re-screening is recommended.

Step 5: Express.js middleware for automatic screening

If your backend runs on Express.js, you can create middleware that automatically screens users during signup and before transactions. This ensures that screening happens consistently, without relying on individual route handlers to remember to call the API.

const API_KEY = process.env.VERIFEX_API_KEY;
const BASE    = "https://api.verifex.dev/v1";

async function screen(name, type = "person") {
  const resp = await fetch(`${BASE}/screen`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ name, type }),
  });
  if (!resp.ok) throw Object.assign(new Error("Screening failed"), { status: resp.status });
  return resp.json();
}

// Middleware: screen a name before proceeding
function sanctionsScreen(nameField = "name") {
  return async (req, res, next) => {
    const name = req.body[nameField];
    if (!name) return next();

    try {
      const result = await screen(name);

      // Attach result to request for downstream handlers
      req.screeningResult = result;

      if (result.risk_level === "critical") {
        return res.status(403).json({
          error: "This request has been blocked for compliance review.",
          reference: result.request_id,
        });
      }

      next();
    } catch (err) {
      // Log the error but do not block the user — queue for async screening
      console.error("Screening failed:", err.message);
      req.screeningResult = { error: true, queued: true };
      next();
    }
  };
}

// Usage in routes
app.post("/api/signup", sanctionsScreen("name"), async (req, res) => {
  const { name, email, password } = req.body;
  const status = req.screeningResult?.risk_level === "high"
    ? "pending_review"
    : "active";

  const user = await db.user.create({ data: { name, email, password, status } });
  res.json({ user, status });
});

app.post("/api/transfer", sanctionsScreen("recipientName"), async (req, res) => {
  if (req.screeningResult?.risk_level === "high") {
    return res.json({ status: "pending_review" });
  }

  const transfer = await processTransfer(req.body);
  res.json({ transfer });
});

This pattern centralizes your screening logic in one place. Every route that needs screening simply adds the middleware, and the screening result is available to the downstream handler on req.screeningResult.

Step 6: Going to production — error handling and retry logic

In production, you need to handle edge cases that do not come up during development. The three most important are network failures, rate limiting, and timeout handling.

Here is a production-ready wrapper with exponential backoff retry logic:

const API_KEY = process.env.VERIFEX_API_KEY;
const BASE    = "https://api.verifex.dev/v1";

async function screenWithRetry(name, type = "person", maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const resp = await fetch(`${BASE}/screen`, {
        method: "POST",
        headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
        body: JSON.stringify({ name, type }),
        signal: AbortSignal.timeout(5000),
      });
      if (!resp.ok) throw Object.assign(new Error("Screening failed"), { status: resp.status });
      return await resp.json();
    } catch (err) {
      const isRetryable =
        err.status === 429 ||    // Rate limited
        err.status === 502 ||    // Bad gateway
        err.status === 503 ||    // Service unavailable
        err.code === "ECONNRESET";

      if (!isRetryable || attempt === maxRetries) {
        throw err;
      }

      // Exponential backoff: 1s, 2s, 4s
      const delay = Math.pow(2, attempt - 1) * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

Beyond retry logic, here are additional production best practices:

  • Log every screening request. Store the request ID, name screened, risk level, and action taken. This creates the audit trail regulators require. Check the API documentation for details on the response format.
  • Set a timeout. Configure a 5-second timeout on screening requests. If the API does not respond within that window, queue the request for async processing rather than blocking the user indefinitely.
  • Monitor your screening volume. Set up alerts when you approach your plan limit so you can upgrade before screening stops working.
  • Never skip screening on error. If the API is unreachable, place the user in a pending state rather than auto-approving. You can process the queue once the service is back.

Testing your integration

Before going live, verify your integration handles each risk level correctly. You can use the OFAC screening API demo guide to find known sanctioned names for testing. A few reliable test cases:

  • Critical match: Screen "Rosneft" and verify your app blocks the transaction and notifies your compliance team.
  • Fuzzy match: Screen "Rosnefft" (note the extra "f") and verify it still returns a high-confidence match.
  • Clear result: Screen a common name like "John Smith" and verify your app auto-approves with a low risk level.
  • Error handling: Temporarily set an invalid API key and verify your app gracefully handles the 401 error without crashing.

Complete example: screening at signup

Here is a complete, copy-paste-ready example that puts everything together. This handles signup screening with proper error handling, audit logging, and risk-based decisions:

import express from "express";

const app = express();
app.use(express.json());

const API_KEY = process.env.VERIFEX_API_KEY;
const BASE    = "https://api.verifex.dev/v1";

async function screen(name, type = "person") {
  const resp = await fetch(`${BASE}/screen`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ name, type }),
    signal: AbortSignal.timeout(5000),
  });
  if (!resp.ok) throw Object.assign(new Error("Screening error"), { status: resp.status });
  return resp.json();
}

app.post("/api/signup", async (req, res) => {
  const { name, email, password } = req.body;

  try {
    // Screen the customer
    const screening = await screen(name);

    // Log the screening for audit trail
    await db.screeningLog.create({
      data: {
        name,
        requestId: screening.request_id,
        riskLevel: screening.risk_level,
        matchCount: screening.matches.length,
        listsChecked: screening.lists_checked,
        timestamp: new Date(),
      },
    });

    // Decide based on risk level
    if (screening.risk_level === "critical") {
      return res.status(403).json({
        message: "We are unable to process your registration at this time.",
        reference: screening.request_id,
      });
    }

    const status = screening.risk_level === "high" ? "pending_review" : "active";
    const user = await db.user.create({ data: { name, email, password, status } });

    res.json({ message: "Account created", status: user.status });
  } catch (err) {
    // Screening failed — do not auto-approve, queue for later
    const user = await db.user.create({
      data: { name, email, password, status: "pending_screening" },
    });

    res.json({ message: "Account created, verification in progress", status: user.status });
  }
});

app.listen(3000);

What to do next

You now have a working sanctions screening integration in your Node.js app. Here are the logical next steps to build out your compliance program:

  • Add transaction screening before outbound payments and transfers
  • Set up batch re-screening as a weekly or monthly cron job
  • Build a compliance dashboard to review flagged users
  • Read the full API documentation for advanced features like entity type filtering and webhook notifications
  • Check the benchmark results to see how Verifex performs against other screening providers

Sanctions screening is one of those compliance requirements that seems daunting until you actually implement it. With the Verifex REST API, the entire integration takes about 10 minutes and fewer than 50 lines of code. The hard part is not the technology. It is remembering to screen at every touchpoint and keeping your audit trail clean.

Frequently asked questions

Do I need sanctions screening if my app only serves U.S. customers?

Yes. OFAC regulations apply to all U.S. persons and any business operating within the United States. Even if your customers are exclusively domestic, you are still required to screen against the SDN list before processing transactions or onboarding users. Fines for non-compliance can reach $356,579 per violation.

How often should I re-screen existing customers in my Node.js app?

Most compliance frameworks recommend re-screening your entire customer base at least monthly. The OFAC SDN list is updated multiple times per week, so someone who was clear at signup could be added to the list later. Use the Verifex batch endpoint with a cron job to automate this.

Can I use the Verifex REST API in a serverless environment like AWS Lambda?

Yes. The Verifex REST API uses standard HTTPS — it works in any Node.js runtime including AWS Lambda, Vercel Functions, Cloudflare Workers, and Google Cloud Functions. Node 18+ includes native fetch; for older runtimes use node-fetch. Average response time is under 500ms, well within serverless timeout limits.

What happens if the Verifex API is down when my app tries to screen a user?

You should implement retry logic with exponential backoff, which this tutorial covers. For critical flows like onboarding, consider a fallback strategy: queue the screening request and place the user in a pending state until the screen completes. Never skip screening entirely due to a transient API error.

This guide is for technical and operational education. Verifex provides screening infrastructure and evidence records, not legal advice, transaction approval, or a replacement for your risk-based compliance program.

Get started with Verifex

Screen against OFAC, UN, EU & UK sanctions lists in one API call. Free tier available.

Start screening free