OFAC Screening for Payroll Companies: Automate Compliance Before Every Pay Run
Payroll companies process millions of payments every cycle. Each one of those payments is a US financial transaction, and every US financial transaction falls under OFAC jurisdiction. That includes direct deposits to employees, payments to contractors, and disbursements to vendors. If any of those recipients appear on the Specially Designated Nationals (SDN) list, you are legally prohibited from sending them money -- and ignorance is not a defense. Penalties start at $356,579 per violation.
Most payroll platforms have robust tax calculation engines, time-tracking integrations, and ACH batch processing. What many lack is automated sanctions screening. This article covers why payroll companies are in scope, how to screen your roster programmatically, and how to build screening into your pay run pipeline.
Why payroll companies must screen
OFAC regulations apply to all US persons and entities engaging in financial transactions. "Financial transactions" is interpreted broadly -- it covers wire transfers, ACH payments, check disbursements, and yes, payroll deposits. The Treasury Department has made clear that the obligation extends to any company facilitating payments, not just banks.
Here is the practical risk for a payroll company:
- You process payroll for a client. That client has an employee or contractor whose name matches an SDN entry.
- You send the payment. The ACH transfer clears. The funds reach a sanctioned individual.
- OFAC investigates. They do not care that you are "just the payroll processor." You facilitated the transaction. You are liable.
Payroll processors sit in the same enforcement category as payment processors, money service businesses, and neobanks. If you move money, you screen. Period.
The entities you need to screen include:
- Employees on every client's payroll roster
- Independent contractors and 1099 recipients
- Vendors receiving payments through your platform (benefits providers, garnishment recipients, tax authorities are exempt but third-party vendors are not)
- Client companies themselves -- you should not process payroll for a sanctioned entity
Screening a single name
The OFAC screening API at https://ofac-screening-production.up.railway.app accepts a name and returns scored matches against the full SDN list. Here is a basic screen:
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": "Ali Hassan", "threshold": 0.85}'
The response includes match scores, confidence levels, and the sanctions programs involved:
{
"query": { "name": "Ali Hassan" },
"threshold": 0.85,
"matchCount": 2,
"matches": [
{
"entity": {
"name": "Ali HASSAN",
"sdnType": "Individual",
"programs": ["SDGT"]
},
"score": 0.97,
"matchType": "exact",
"matchedOn": "primary"
},
{
"entity": {
"name": "Hassan ALI",
"sdnType": "Individual",
"programs": ["IRAN"]
},
"score": 0.88,
"matchType": "strong",
"matchedOn": "alias"
}
],
"listVersion": "03/13/2026",
"screenedAt": "2026-03-16T14:30:00.000Z"
}
The listVersion and screenedAt fields are your audit trail. Regulators expect to see when you screened and which version of the SDN list you screened against.
Screening a payroll roster in JavaScript
In practice, you are not screening one name at a time. You have a roster of employees and contractors that needs to be screened before every pay run. The /screen/batch endpoint accepts up to 100 names per request.
const API_URL = 'https://ofac-screening-production.up.railway.app';
const API_KEY = process.env.OFAC_API_KEY;
const screenBatch = async (names) => {
const res = await fetch(`${API_URL}/screen/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({ names, threshold: 0.85 }),
});
if (!res.ok) {
throw new Error(`Screening failed: ${res.status} ${await res.text()}`);
}
return res.json();
};
// Chunk an array into groups of a given size
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
const screenPayrollRoster = async (employees) => {
const names = employees.map((emp) => ({
name: `${emp.firstName} ${emp.lastName}`,
country: emp.country || null,
}));
const batches = chunk(names, 100);
const allResults = [];
for (const batch of batches) {
const data = await screenBatch(batch);
allResults.push(...data.results);
}
const flagged = allResults.filter((r) => r.matchCount > 0);
const clear = allResults.filter((r) => r.matchCount === 0);
return { flagged, clear, total: allResults.length };
};
For a payroll company with 500 employees on a client's roster, this sends 5 batch requests. Each batch returns in under a second. The entire roster screens in a few seconds.
Integrating screening into your pay run pipeline
Screening should happen at two points in your workflow:
- Employee onboarding. When a new employee or contractor is added to any client's roster, screen them immediately. Do not wait for the first pay run.
- Before every pay run. The SDN list updates frequently. Someone who was clear last month may be sanctioned today. Screen the full roster before you submit the ACH batch.
Here is how that looks in a pay run pipeline:
const processPayRun = async (clientId, payPeriod) => {
// 1. Pull the roster
const employees = await getActiveEmployees(clientId);
// 2. Screen everyone against OFAC
const screening = await screenPayrollRoster(employees);
// 3. If anyone is flagged, halt the pay run for review
if (screening.flagged.length > 0) {
await flagPayRunForReview(clientId, payPeriod, screening.flagged);
await notifyComplianceTeam(clientId, screening.flagged);
console.log(
`PAY RUN HELD: ${screening.flagged.length} potential OFAC match(es) for client ${clientId}`
);
screening.flagged.forEach((result) => {
result.matches.forEach((m) => {
console.log(
` ${result.query.name} -> ${m.entity.name} (score: ${m.score}, program: ${m.entity.programs.join(', ')})`
);
});
});
return { status: 'held', reason: 'ofac_review_required' };
}
// 4. Log the clear screening for audit purposes
await saveScreeningRecord(clientId, payPeriod, {
screenedAt: new Date().toISOString(),
totalScreened: screening.total,
matchCount: 0,
});
// 5. Proceed with ACH batch submission
const achResult = await submitAchBatch(clientId, payPeriod, employees);
return { status: 'submitted', achResult };
};
The key design decision here is that a flagged result halts the pay run. You do not skip flagged employees and pay everyone else. You hold the entire batch until compliance reviews the matches. This is the conservative approach and the one regulators expect.
Automating screening on a schedule
Beyond the per-pay-run screen, you should also run screening on a regular schedule to catch SDN list updates between pay cycles. Many payroll companies run biweekly pay runs, but the SDN list can update multiple times per week.
// Run nightly at 2 AM — catches SDN updates between pay runs
const nightlyScreening = async () => {
const clients = await getAllActiveClients();
for (const client of clients) {
const employees = await getActiveEmployees(client.id);
const screening = await screenPayrollRoster(employees);
if (screening.flagged.length > 0) {
await notifyComplianceTeam(client.id, screening.flagged);
await markClientForReview(client.id);
}
// Always log the screening, even when clear
await saveScreeningRecord(client.id, 'nightly', {
screenedAt: new Date().toISOString(),
totalScreened: screening.total,
matchCount: screening.flagged.length,
});
}
};
For a cron-based setup, trigger this with your task scheduler. If you are on Railway, Render, or a similar platform, use their cron job feature. On AWS, a Lambda triggered by EventBridge on a schedule works well.
The frequency depends on your risk profile:
- Daily: recommended for payroll companies processing payments for financial services clients
- Weekly: acceptable for most payroll companies
- Per pay run: the minimum. Never skip this one.
What to do when you get a match
A match does not mean you have a sanctioned individual on your payroll. The SDN list contains roughly 18,000 entries, and common names produce false positives. "Ali Hassan," "Mohammed Ali," and "Carlos Garcia" will all generate potential matches. Here is the process:
1. Do not process the payment
The moment a screening returns a match above your threshold, freeze the payment for that individual. Do not release funds until the match is resolved. This is not optional -- processing a payment to a confirmed SDN match is a federal violation.
2. Escalate to compliance
Your compliance team (or compliance officer, or if you are a smaller shop, whoever owns compliance) reviews the match. They compare the matched SDN entry's identifying details -- date of birth, nationality, address, aliases -- against your employee record. Most matches resolve as false positives at this stage.
3. Document the resolution
Whether the match is a true positive or a false positive, document the review. Record who reviewed it, when, what data points they compared, and what they concluded. This documentation is your evidence of a functioning compliance program.
const resolveScreeningMatch = async (matchId, resolution) => {
const record = {
matchId,
resolvedBy: resolution.reviewerEmail,
resolvedAt: new Date().toISOString(),
determination: resolution.isTruePositive ? 'true_positive' : 'false_positive',
notes: resolution.notes,
sdnFieldsCompared: resolution.fieldsCompared, // e.g., ['dob', 'nationality', 'address']
};
await saveMatchResolution(record);
if (resolution.isTruePositive) {
await blockEmployee(resolution.employeeId);
await fileSuspiciousActivityReport(resolution);
await notifyLegalCounsel(resolution);
} else {
await releasePayment(resolution.employeeId, resolution.payPeriod);
}
};
4. True positive: block and report
If the match is confirmed, you must:
- Block the payment permanently. Do not release funds.
- File a Suspicious Activity Report (SAR) with FinCEN. You have 30 days from the date you determine the activity is suspicious.
- File a blocking report with OFAC within 10 business days if you have blocked property or funds.
- Notify legal counsel. A confirmed SDN match is a serious matter that may involve law enforcement coordination.
- Do not notify the sanctioned individual. Tipping off an SDN match is itself a violation.
5. False positive: release and whitelist
If your compliance team determines the match is a false positive, release the held payment and add the employee to an internal whitelist so the same false positive does not halt future pay runs. The whitelist should be re-reviewed periodically -- quarterly is standard practice.
Building an audit trail
Regulators do not just want to know that you screen. They want to see proof. Every screening event should produce a record that includes:
- The date and time of the screening
- The version of the SDN list used (the API returns this as
listVersion) - The names screened
- The threshold used
- The results (matches and clears)
- For matches: the resolution and who resolved it
The API's response already includes listVersion and screenedAt in every response. Store these alongside your payroll records. When an examiner asks "How do you verify your employees are not on the SDN list?", you hand them the screening log.
Cost and scaling
For a payroll company processing 10,000 employees across all clients, a nightly screen plus per-pay-run screens for biweekly payroll works out to roughly 40,000 screens per month. The Pro plan at $4.99/month covers 5,000 screens, and the Ultra plan at $29.99/month covers 25,000. For larger volumes, the Mega plan at $99.99/month includes 100,000 screens -- still a fraction of what enterprise compliance platforms charge.
Compare that to Dow Jones Risk & Compliance or LexisNexis, which start at $10,000/year and go up from there. For a payroll company that needs sanctions screening and nothing else, a purpose-built API at $30-100/month is the right tool.
Next steps
-
REST API:
POST /screenfor single names,POST /screen/batchfor up to 100 names per request athttps://ofac-screening-production.up.railway.app -
MCP server: Run
npx @easysolutions906/mcp-ofacfor conversational screening in Claude Desktop or Cursor -
Data freshness: Hit
/data-infoto verify the SDN list version and publish date before each pay run
Sanctions screening is not a feature you add after launch. It is a requirement before you process your first payment. The good news is that with a batch screening endpoint and a few lines of integration code, you can screen your entire payroll roster in seconds -- every single pay run, automatically, with a full audit trail.
Top comments (0)