HR & Recruiting Fraud Prevention
Detect fake job postings, fraudulent references, and applicant identity fraud.
Hiring fraud is growing fast. The FTC reports a 118% increase in job scam complaints since 2020. Fake employment references using burner phones, fraudulent job postings designed to harvest personal information, and applicant identity fraud all exploit weaknesses in traditional verification processes. The ScamVerify™ API adds an automated intelligence layer to catch these threats.
The Threat Landscape
Fake Reference Numbers
The most common hiring fraud tactic is simple: an applicant provides a friend's phone number as a "former supervisor." More sophisticated versions use disposable VoIP numbers routed to a call-answering service that confirms whatever the caller asks. These numbers are often registered within days of the application and discarded afterward.
Fraudulent Job Postings
Scammers post fake job listings on Indeed, LinkedIn, and Craigslist to collect Social Security numbers, bank routing numbers, and personal details from applicants. The listings link to phishing sites that mimic real company career pages.
Staffing Agency Fraud
Bad actors impersonate staffing agencies using VoIP numbers and hastily created websites. They collect placement fees from candidates, personal information for identity theft, or both.
How ScamVerify™ Helps
- Phone lookup on reference numbers reveals VoIP burners, recently activated numbers, and numbers with fraud histories
- URL verification on job posting links catches phishing domains and newly registered lookalike sites
- Text analysis scans job offer emails and messages for social engineering patterns
Code Example: Reference Check Verification
class ReferenceVerifier {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://scamverify.ai/api/v1';
}
async callApi(endpoint, body) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json();
}
async verifyReference(reference) {
const { name, title, company, phone, email } = reference;
const findings = { phone: null, companyUrl: null, flags: [] };
// Step 1: Verify the reference phone number
const phoneCheck = await this.callApi('/phone/lookup', {
phone_number: phone,
});
findings.phone = {
riskScore: phoneCheck.risk_score,
lineType: phoneCheck.signals.line_type,
carrier: phoneCheck.signals.carrier,
verdict: phoneCheck.verdict,
};
// Disposable VoIP for a "corporate reference" is a red flag
if (phoneCheck.signals.line_type === 'voip') {
findings.flags.push({
severity: 'medium',
signal: 'voip_reference_number',
detail: `Reference phone is VoIP (${phoneCheck.signals.carrier}). Corporate references typically use landline or mobile numbers.`,
});
}
if (phoneCheck.risk_score >= 40) {
findings.flags.push({
severity: 'high',
signal: 'high_risk_reference_phone',
detail: `Risk score ${phoneCheck.risk_score}/100. ${phoneCheck.explanation}`,
});
}
if (phoneCheck.signals.ftc_complaints > 0) {
findings.flags.push({
severity: 'critical',
signal: 'ftc_complaints_on_reference',
detail: `${phoneCheck.signals.ftc_complaints} FTC complaint(s) filed against this number.`,
});
}
// Step 2: If a company URL was provided, verify it
if (company) {
try {
const companyUrl = `https://${company.toLowerCase().replace(/\s+/g, '')}.com`;
const urlCheck = await this.callApi('/url/lookup', { url: companyUrl });
findings.companyUrl = {
url: companyUrl,
riskScore: urlCheck.risk_score,
verdict: urlCheck.verdict,
};
if (urlCheck.risk_score >= 30) {
findings.flags.push({
severity: 'medium',
signal: 'suspicious_company_domain',
detail: `Company domain flagged: ${urlCheck.explanation}`,
});
}
} catch (e) {
// Domain lookup failed, not necessarily suspicious
}
}
// Determine overall assessment
const hasCritical = findings.flags.some(f => f.severity === 'critical');
const hasHigh = findings.flags.some(f => f.severity === 'high');
findings.assessment = hasCritical
? 'REJECT'
: hasHigh
? 'REQUIRES_MANUAL_VERIFICATION'
: findings.flags.length > 0
? 'PROCEED_WITH_CAUTION'
: 'VERIFIED';
return findings;
}
async verifyAllReferences(references) {
const results = [];
for (const ref of references) {
const result = await this.verifyReference(ref);
results.push({ reference: ref, ...result });
}
const anyRejected = results.some(r => r.assessment === 'REJECT');
const anyManualReview = results.some(
r => r.assessment === 'REQUIRES_MANUAL_VERIFICATION'
);
return {
overallAssessment: anyRejected
? 'REFERENCES_FAILED'
: anyManualReview
? 'MANUAL_REVIEW_REQUIRED'
: 'ALL_VERIFIED',
references: results,
};
}
}
// Usage in your ATS integration
const verifier = new ReferenceVerifier(process.env.SCAMVERIFY_API_KEY);
app.post('/api/candidates/:id/verify-references', async (req, res) => {
const { references } = req.body;
const verification = await verifier.verifyAllReferences(references);
// Store results in the ATS
await updateCandidateRecord(req.params.id, {
referenceVerification: verification,
verifiedAt: new Date().toISOString(),
});
return res.json(verification);
});import requests
import os
class ReferenceVerifier:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://scamverify.ai/api/v1"
def _call_api(self, endpoint: str, body: dict) -> dict:
response = requests.post(
f"{self.base_url}{endpoint}",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
json=body,
)
response.raise_for_status()
return response.json()
def verify_reference(self, phone: str, company: str = None) -> dict:
flags = []
phone_check = self._call_api("/phone/lookup", {"phone_number": phone})
if phone_check["signals"]["line_type"] == "voip":
flags.append({
"severity": "medium",
"signal": "voip_reference_number",
"detail": f"VoIP carrier: {phone_check['signals']['carrier']}",
})
if phone_check["risk_score"] >= 40:
flags.append({
"severity": "high",
"signal": "high_risk_reference_phone",
"detail": phone_check["explanation"],
})
if phone_check["signals"]["ftc_complaints"] > 0:
flags.append({
"severity": "critical",
"signal": "ftc_complaints_on_reference",
"detail": f"{phone_check['signals']['ftc_complaints']} FTC complaints",
})
has_critical = any(f["severity"] == "critical" for f in flags)
has_high = any(f["severity"] == "high" for f in flags)
assessment = (
"REJECT" if has_critical
else "REQUIRES_MANUAL_VERIFICATION" if has_high
else "PROCEED_WITH_CAUTION" if flags
else "VERIFIED"
)
return {
"phone_risk_score": phone_check["risk_score"],
"line_type": phone_check["signals"]["line_type"],
"flags": flags,
"assessment": assessment,
}Reference Verification Signal Guide
| Signal | Interpretation | Action |
|---|---|---|
line_type: "landline" | Matches typical corporate phone systems. | Low risk. Proceed with the reference call. |
line_type: "mobile" | Common for small businesses and startups. Acceptable. | Low risk. Proceed normally. |
line_type: "voip" + reputable carrier | Could be a company using RingCentral, Vonage, or similar. | Low risk but worth noting. Verify the company independently. |
line_type: "voip" + high-risk carrier | Disposable number likely purchased for the reference check. | High risk. Verify the reference through LinkedIn or the company's official directory. |
ftc_complaints > 0 | The reference phone number has been reported for fraud. | Critical. Do not call. Disqualify the reference. |
robocall_detected: true | The reference number is associated with automated calling. | Critical. This is not a real reference. |
Job Posting Verification
For job boards and career platforms, scan posted URLs before displaying listings.
async function verifyJobPosting(posting) {
const flags = [];
// Check the application URL
if (posting.applicationUrl) {
const urlCheck = await verifier.callApi('/url/lookup', {
url: posting.applicationUrl,
});
if (urlCheck.risk_score >= 30) {
flags.push({
type: 'suspicious_application_url',
url: posting.applicationUrl,
riskScore: urlCheck.risk_score,
verdict: urlCheck.verdict,
});
}
}
// Check the company's contact number
if (posting.contactPhone) {
const phoneCheck = await verifier.callApi('/phone/lookup', {
phone_number: posting.contactPhone,
});
if (phoneCheck.risk_score >= 40 || phoneCheck.signals.robocall_detected) {
flags.push({
type: 'suspicious_contact_phone',
phone: posting.contactPhone,
riskScore: phoneCheck.risk_score,
lineType: phoneCheck.signals.line_type,
});
}
}
return {
approved: flags.length === 0,
flags,
recommendation: flags.length > 0
? 'Hold for manual review before publishing'
: 'Safe to publish',
};
}Fake job posting red flags beyond API signals. Combine ScamVerify™ data with your own checks: Does the posting request upfront fees? Does it ask for SSN or bank details before an interview? Is the salary unusually high for the role? These behavioral patterns plus phone and URL intelligence create the strongest detection.
Best Practices
- Check references before calling them. A 2-second API call can save 30 minutes on a fake reference call and prevent a bad hire.
- Verify job posting URLs at submission time. Do not wait for user reports. Scan application URLs and company websites before listings go live.
- Use company directory lookups as a second factor. When a reference phone comes back as VoIP, look up the company's main number independently and call through the switchboard.
- Store verification results in your ATS. Attach ScamVerify™ findings to candidate records for audit trails and compliance documentation.
- Re-check periodically. Phone numbers that were clean six months ago can develop complaint histories. Run quarterly re-verification on your active reference database.
Getting Started
- Create an API key and set up your account
- Follow the Quickstart guide to make your first lookup
- Review the Phone Lookup API and URL Verification API reference docs
- Start by integrating reference phone verification into your ATS workflow