DEV Community

easysolutions906
easysolutions906

Posted on

How to Integrate OFAC Screening into Your Application in 5 Minutes

How to Integrate OFAC Screening into Your Application in 5 Minutes

You need OFAC screening in your application. You do not need a six-month enterprise integration project to get it. This guide takes you from zero to production-ready sanctions with working code.

Quick Start: Your First Screening Call

The fastest way to verify the API works:

curl -X POST https://ofac-screening-production.up.railway.app/screen \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"name": "Mohammad Ali", "threshold": 0.85}'
Enter fullscreen mode Exit fullscreen mode

That is it. You send a name, you get back a list of matches with scores. If matchCount is 0, the name is clear.

JavaScript / Node.js Integration

Here is a production-ready screening module with error handling and retries:

const OFAC_BASE_URL = 'https://ofac-screening-production.up.railway.app';
const OFAC_API_KEY = process.env.OFAC_API_KEY;

const screenName = async (name, options = {}) => {
  const {
    type = null,
    dateOfBirth = null,
    country = null,
    threshold = 0.85,
  } = options;

  const body = { name, threshold };
  if (type) { body.type = type; }
  if (dateOfBirth) { body.dateOfBirth = dateOfBirth; }
  if (country) { body.country = country; }

  const response = await fetch(`${OFAC_BASE_URL}/screen`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': OFAC_API_KEY,
    },
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(
      `OFAC screening failed: ${response.status} ${error.error || response.statusText}`,
    );
  }

  return response.json();
};

// Usage in an onboarding flow
const onboardCustomer = async (customer) => {
  const screening = await screenName(customer.fullName, {
    type: 'Individual',
    dateOfBirth: customer.dob,
    country: customer.country,
  });

  if (screening.matchCount > 0) {
    // Flag for compliance review -- do not auto-reject
    await flagForReview(customer.id, screening);
    return { status: 'PENDING_REVIEW', screening };
  }

  return { status: 'CLEAR', screening };
};
Enter fullscreen mode Exit fullscreen mode

Two important patterns here. First, never auto-reject based on a match. Flag it for human review. False positives happen, and rejecting legitimate customers without review creates legal and reputational risk. Second, always store the full screening response, not just the pass/fail decision.

Python Integration

import os
import requests

OFAC_BASE_URL = "https://ofac-screening-production.up.railway.app"
OFAC_API_KEY = os.environ["OFAC_API_KEY"]


def screen_name(name, entity_type=None, date_of_birth=None,
                country=None, threshold=0.85):
    payload = {"name": name, "threshold": threshold}

    if entity_type:
        payload["type"] = entity_type
    if date_of_birth:
        payload["dateOfBirth"] = date_of_birth
    if country:
        payload["country"] = country

    response = requests.post(
        f"{OFAC_BASE_URL}/screen",
        json=payload,
        headers={
            "Content-Type": "application/json",
            "x-api-key": OFAC_API_KEY,
        },
        timeout=10,
    )
    response.raise_for_status()
    return response.json()


def screen_batch(names, threshold=0.85):
    """Screen up to 100 names in a single request."""
    response = requests.post(
        f"{OFAC_BASE_URL}/screen/batch",
        json={"names": names, "threshold": threshold},
        headers={
            "Content-Type": "application/json",
            "x-api-key": OFAC_API_KEY,
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()


# Usage
result = screen_name(
    "Vladimir Putin",
    entity_type="Individual",
    country="Russia",
)

print(f"Matches found: {result['matchCount']}")
for match in result["matches"]:
    print(f"  {match['entity']['name']} — score: {match['score']} "
          f"({match['matchType']})")
Enter fullscreen mode Exit fullscreen mode

Handling Results: Match vs Clear

Every screening response includes a matchCount field. Your application logic branches on it:

const processScreening = (screening) => {
  if (screening.matchCount === 0) {
    // No matches -- customer is clear
    return {
      decision: 'CLEAR',
      screenedAt: screening.screenedAt,
      listVersion: screening.listVersion,
    };
  }

  // Matches found -- classify by highest score
  const topMatch = screening.matches[0];

  if (topMatch.score >= 0.95) {
    // Near-certain match -- block and escalate immediately
    return {
      decision: 'BLOCK',
      reason: `High-confidence match: ${topMatch.entity.name} (score: ${topMatch.score})`,
      screenedAt: screening.screenedAt,
      listVersion: screening.listVersion,
      requiresReview: true,
    };
  }

  if (topMatch.score >= 0.85) {
    // Strong match -- hold for compliance review
    return {
      decision: 'HOLD',
      reason: `Possible match: ${topMatch.entity.name} (score: ${topMatch.score})`,
      screenedAt: screening.screenedAt,
      listVersion: screening.listVersion,
      requiresReview: true,
    };
  }

  // Below 0.85 but above your threshold -- log and flag
  return {
    decision: 'FLAG',
    reason: `Weak match detected: ${topMatch.entity.name} (score: ${topMatch.score})`,
    screenedAt: screening.screenedAt,
    listVersion: screening.listVersion,
    requiresReview: true,
  };
};
Enter fullscreen mode Exit fullscreen mode

Batch Screening for Existing Customer Databases

When you first integrate OFAC screening, you need to screen your entire existing customer base. The batch endpoint handles up to 100 names per request:

const screenExistingCustomers = async (customers) => {
  const BATCH_SIZE = 100;
  const flagged = [];

  for (let i = 0; i < customers.length; i += BATCH_SIZE) {
    const batch = customers.slice(i, i + BATCH_SIZE);
    const names = batch.map((c) => ({
      name: c.fullName,
      type: 'Individual',
      dateOfBirth: c.dob || null,
      country: c.country || null,
    }));

    const response = await fetch(
      `${OFAC_BASE_URL}/screen/batch`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': OFAC_API_KEY,
        },
        body: JSON.stringify({ names, threshold: 0.85 }),
      },
    );

    const data = await response.json();

    data.results.forEach((result, idx) => {
      if (result.matchCount > 0) {
        flagged.push({
          customer: batch[idx],
          matches: result.matches,
          listVersion: data.listVersion,
          screenedAt: data.screenedAt,
        });
      }
    });

    console.log(
      `Screened ${Math.min(i + BATCH_SIZE, customers.length)}/${customers.length}`,
    );
  }

  return flagged;
};
Enter fullscreen mode Exit fullscreen mode

For 10,000 customers, this makes 100 API calls. At typical response times of 100-200ms per batch, the full scan completes in under 30 seconds.

Setting Up Re-Screening When the List Updates

Treasury updates the SDN list multiple times per week. You need a job that checks for updates and re-screens when the list changes:

let lastKnownListVersion = null;

const checkForListUpdate = async () => {
  const response = await fetch(`${OFAC_BASE_URL}/data-info`);
  const info = await response.json();

  if (lastKnownListVersion && info.publishDate !== lastKnownListVersion) {
    console.log(
      `SDN list updated: ${lastKnownListVersion} -> ${info.publishDate}`,
    );

    // Trigger a full re-screen of your customer database
    const customers = await loadAllActiveCustomers();
    const flagged = await screenExistingCustomers(customers);

    if (flagged.length > 0) {
      await notifyComplianceTeam(flagged);
    }
  }

  lastKnownListVersion = info.publishDate;
};

// Run daily (or more frequently for high-risk programs)
// In production, use a proper job scheduler (cron, Bull, etc.)
Enter fullscreen mode Exit fullscreen mode

Storing Results for Audit Compliance

Every screening result must be stored. When a regulator examines your compliance program, they will ask to see screening records for specific customers and date ranges.

A minimal audit record schema:

const auditRecord = {
  // Who was screened
  customerId: 'cust_12345',
  screenedName: 'John Smith',
  screenedDob: '1985-03-15',
  screenedCountry: 'US',

  // When and against what
  screenedAt: '2026-03-16T14:30:00.000Z',  // from API response
  listVersion: '2026-03-14',                 // from API response

  // Result
  matchCount: 0,
  decision: 'CLEAR',
  matches: [],                               // store full match details if any

  // Review (if matches were found)
  reviewedBy: null,
  reviewedAt: null,
  reviewDecision: null,                      // TRUE_POSITIVE or FALSE_POSITIVE
  reviewNotes: null,
};
Enter fullscreen mode Exit fullscreen mode

Store these records in a tamper-evident way. An append-only database table with no UPDATE or DELETE permissions for application users is the standard approach. Retention period is five years minimum per BSA requirements.

Next Steps

  1. Get an API key: Visit ofac-screening-production.up.railway.app to get started
  2. Test with the interactive tool: easysolutions906.github.io/screen.html
  3. Integrate into onboarding: Add screening to your customer signup flow
  4. Set up batch re-screening: Run against your existing database
  5. Build the audit trail: Store every screening result with timestamps and list versions

The API documentation is available at the base URL. The /data-info endpoint shows the current SDN list version and record count so you always know how fresh your screening data is.

Five minutes to integrate. Years of compliant screening records to show your regulators.

Top comments (0)