Your company serves lunch to 800 employees every weekday. The catering is handled by a third-party vendor operating out of a commissary kitchen across town. You have a signed contract, a certificate of insurance, and a ServSafe certificate on file. But here is the question your procurement team almost certainly cannot answer: when was that kitchen last inspected by the health department, and did it pass?
Food service vendor risk is a genuinely underappreciated category of corporate liability. A foodborne illness outbreak traced to a contracted caterer does not just create an HR headache - it creates legal exposure, reputational damage, and the kind of employee trust problem that takes years to repair. Yet most corporate procurement processes treat food vendor vetting the same way they treat office supply vendor vetting: check the contract, check the insurance, move on.
Health inspection data changes that equation. Every commissary kitchen, catering operation, and food service vendor operating in the United States is subject to periodic government inspection - and the results are public record. The FoodSafe Score API normalizes those records across 3,000+ jurisdictions into a single 0-100 score, making it possible to build an objective, automated vendor risk scoring process for the first time.
The Problem With Current Vendor Vetting
Standard food service vendor audits typically require vendors to self-report their compliance status. You ask for their most recent health inspection report. They provide it. You file it. This approach has three fundamental weaknesses:
- It is point-in-time. A clean inspection from 18 months ago tells you nothing about today's score. Kitchen conditions change when staff turns over, equipment ages, or volume increases.
- It relies on vendor honesty. Vendors have a strong incentive to share favorable reports and not volunteer unfavorable ones. Self-reported compliance is not the same as verified compliance.
- It creates no ongoing monitoring. Even if you receive a current report at contract signing, there is no mechanism to alert you when the vendor's score drops six months later.
Automated health inspection monitoring solves all three. You query the API at contract signing, during annual reviews, and continuously throughout the contract term. Score drops trigger alerts before they become incidents.
Building a Vendor Scorecard
A useful vendor scorecard combines three signals from the FoodSafe Score API into a composite risk rating. Here is the framework:
Signal 1 - Current Health Score (0-100)
The FoodSafe normalized score is the primary signal. It weights violations by severity: critical violations (temperature abuse, pest activity, bare-hand contact with ready-to-eat food) deduct 25 points each; non-critical violations deduct 5 points; corrected-on-site violations deduct only 2 points.
- Grade A (85-100): Low risk. Approve.
- Grade B (70-84): Moderate risk. Approve with monitoring.
- Grade C (50-69): Elevated risk. Require remediation plan before approval.
- Grade F (0-49): High risk. Reject or place on probation.
Signal 2 - Inspection History Trend
A vendor with a current score of 78 (Grade B) tells a very different story depending on their history. If they scored 92, 88, 83, 78 over their last four inspections, the trend is declining and the risk is higher than the current score suggests. If they scored 65, 70, 74, 78 - they are improving and the risk is lower. The API returns a trend field (improving, stable, or declining) and the full inspection history array so you can calculate the slope yourself.
Signal 3 - Violation Type Breakdown
Two vendors can both score 75 for very different reasons. Vendor A has fifteen minor non-critical violations (dusty shelves, missing labels). Vendor B has three critical violations related to temperature control. The raw scores are similar but the risk profiles are completely different. The violation breakdown in each inspection record lets you flag vendors whose score includes critical violations regardless of their overall number.
Node.js Implementation
The following Node.js code implements the full vendor scorecard calculation:
// vendor-scorecard.js
const FOODSAFE_API_KEY = process.env.FOODSAFE_API_KEY;
const BASE_URL = 'https://api.foodsafescore.com/v1';
async function fetchVendorScore(vendorName, address, city, state) {
const params = new URLSearchParams({ name: vendorName, address, city, state });
const res = await fetch(`${BASE_URL}/lookup?${params}`, {
headers: { 'X-Api-Key': FOODSAFE_API_KEY }
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `API error ${res.status}`);
}
return res.json();
}
function countCriticalViolations(inspectionHistory) {
// Count critical violations across the most recent 3 inspections
const recent = inspectionHistory.slice(0, 3);
return recent.reduce((total, inspection) => {
const critical = inspection.violations.filter(v => v.type === 'critical').length;
return total + critical;
}, 0);
}
function calculateTrendSlope(inspectionHistory) {
if (inspectionHistory.length < 2) return 0;
const recent = inspectionHistory.slice(0, 5);
// Simple linear regression on score over time (most recent = index 0)
const n = recent.length;
const scores = recent.map((r, i) => ({ x: i, y: r.score }));
const sumX = scores.reduce((s, p) => s + p.x, 0);
const sumY = scores.reduce((s, p) => s + p.y, 0);
const sumXY = scores.reduce((s, p) => s + p.x * p.y, 0);
const sumX2 = scores.reduce((s, p) => s + p.x * p.x, 0);
return (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
}
function scoreVendor(apiResult) {
const { score, grade, trend, inspection_history } = apiResult.result;
// Base score from current health inspection (0-100, weighted 60%)
const baseComponent = score * 0.6;
// Trend adjustment: slope is negative when recent scores are higher
// (index 0 = most recent, so a downward slope = declining scores)
const slope = calculateTrendSlope(inspection_history);
// Normalize slope to +/- 10 point adjustment
const trendAdjustment = Math.max(-10, Math.min(10, -slope * 5));
// Critical violation penalty: -5 points per critical violation
const criticalCount = countCriticalViolations(inspection_history);
const criticalPenalty = criticalCount * 5;
// Composite vendor risk score (0-100)
const vendorRiskScore = Math.max(0, Math.min(100,
baseComponent + (score * 0.4) + trendAdjustment - criticalPenalty
));
let riskLevel;
let recommendation;
if (vendorRiskScore >= 85) {
riskLevel = 'LOW';
recommendation = 'APPROVE';
} else if (vendorRiskScore >= 70) {
riskLevel = 'MODERATE';
recommendation = 'APPROVE_WITH_MONITORING';
} else if (vendorRiskScore >= 50) {
riskLevel = 'ELEVATED';
recommendation = 'REQUIRE_REMEDIATION_PLAN';
} else {
riskLevel = 'HIGH';
recommendation = 'REJECT';
}
return {
currentScore: score,
currentGrade: grade,
trend,
criticalViolationsRecent: criticalCount,
compositeRiskScore: Math.round(vendorRiskScore),
riskLevel,
recommendation,
lastInspected: apiResult.result.last_inspected,
};
}
// Example usage
async function evaluateVendor(vendor) {
console.log(`Evaluating vendor: ${vendor.name}`);
const apiResult = await fetchVendorScore(
vendor.name,
vendor.address,
vendor.city,
vendor.state
);
return {
vendor: vendor.name,
...scoreVendor(apiResult),
};
}
module.exports = { evaluateVendor };
Discovering All Licensed Food Businesses at a Vendor Address
One underappreciated capability of the geo search endpoint is vendor footprint discovery. A commissary kitchen may operate multiple licensed food businesses at the same physical address - a catering operation, a food prep facility, and a retail counter, each with a separate health inspection license. If you only look up the vendor by their DBA name, you may miss inspection records filed under the license name.
// geo-discovery.js
async function discoverVendorLicenses(latitude, longitude, radiusMeters = 100) {
// Use a tight radius (100m) to find all licensed food operations
// at or immediately adjacent to the vendor's address
const params = new URLSearchParams({
lat: String(latitude),
lng: String(longitude),
radius: String(radiusMeters),
});
const res = await fetch(`${BASE_URL}/geo?${params}`, {
headers: { 'X-Api-Key': FOODSAFE_API_KEY }
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `API error ${res.status}`);
}
const data = await res.json();
return data.results;
}
// For each discovered license, run the full vendor scorecard
async function fullVendorAudit(vendorName, lat, lng) {
const licenses = await discoverVendorLicenses(lat, lng);
console.log(`Found ${licenses.length} licensed food operations at vendor address`);
const scorecards = licenses.map(license => ({
licenseeName: license.name,
restaurantId: license.restaurant_id,
currentScore: license.score,
currentGrade: license.grade,
lastInspected: license.last_inspected,
}));
// The overall vendor risk is driven by the WORST license, not the average
const worstScore = Math.min(...scorecards.map(s => s.currentScore));
const avgScore = scorecards.reduce((sum, s) => sum + s.currentScore, 0) / scorecards.length;
return {
vendorName,
licensesFound: scorecards.length,
worstScore,
averageScore,
scorecards,
overallGrade: worstScore >= 85 ? 'A' : worstScore >= 70 ? 'B' : worstScore >= 50 ? 'C' : 'F',
};
}
Use the worst-case score across all licenses, not the average. A commissary kitchen that runs an A-grade catering operation but an F-grade prep facility is an F-grade vendor from a liability perspective. The pathogen contamination risk is in the failing kitchen regardless of the others.
Bulk Monitoring of Your Approved Vendor List
Once vendors are approved and active, continuous monitoring is what separates a compliance program from a compliance theater. Use the bulk zip endpoint to monitor your entire approved vendor list on a weekly schedule without burning through your per-lookup quota.
// vendor-monitoring.js
const cron = require('node-cron');
// Load approved vendors from your database
async function getApprovedVendors() {
// Returns array of { id, name, address, city, state, zip, approvedScore, alertThreshold }
return db.query('SELECT * FROM food_vendors WHERE status = ?', ['approved']);
}
async function bulkCheckByZip(zip) {
const res = await fetch(`${BASE_URL}/bulk?zip=${zip}`, {
headers: { 'X-Api-Key': FOODSAFE_API_KEY }
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `API error ${res.status}`);
}
return res.json(); // { results: [...] }
}
async function runWeeklyVendorMonitoring() {
const vendors = await getApprovedVendors();
// Group vendors by ZIP to minimize API calls
const byZip = vendors.reduce((acc, v) => {
(acc[v.zip] = acc[v.zip] || []).push(v);
return acc;
}, {});
for (const [zip, zipVendors] of Object.entries(byZip)) {
const { results } = await bulkCheckByZip(zip);
for (const vendor of zipVendors) {
// Match API result to vendor by name similarity
const match = results.find(r =>
r.name.toLowerCase().includes(vendor.name.toLowerCase().split(' ')[0])
);
if (!match) continue;
const previousScore = vendor.approvedScore;
const currentScore = match.score;
const threshold = vendor.alertThreshold || 70;
// Alert if score dropped below threshold
if (currentScore < threshold && previousScore >= threshold) {
await sendVendorAlert({
vendorId: vendor.id,
vendorName: vendor.name,
previousScore,
currentScore,
grade: match.grade,
lastInspected: match.last_inspected,
alertType: 'SCORE_BELOW_THRESHOLD',
});
}
// Alert on significant score drop (10+ points)
if (previousScore - currentScore >= 10) {
await sendVendorAlert({
vendorId: vendor.id,
vendorName: vendor.name,
previousScore,
currentScore,
grade: match.grade,
alertType: 'SIGNIFICANT_SCORE_DROP',
});
}
// Update stored score
await db.query(
'UPDATE food_vendors SET current_score = ?, last_checked = NOW() WHERE id = ?',
[currentScore, vendor.id]
);
}
// Delay between ZIP requests to be a good API citizen
await new Promise(resolve => setTimeout(resolve, 500));
}
}
// Run every Monday at 7 AM
cron.schedule('0 7 * * 1', runWeeklyVendorMonitoring);
Building the Supplier Approval Workflow
The health inspection score should be a hard gate in your vendor onboarding workflow, not an advisory signal. Here is how to structure that gate:
Stage 1 - Pre-qualification
When a new food service vendor submits an application, run the lookup immediately and return the score in the application acknowledgment. Vendors with a Grade F score are disqualified at this stage without human review required - the score is objective and the threshold is a policy decision, not a judgment call. This saves procurement staff time and removes any temptation to make exceptions based on price or relationship.
Stage 2 - Due Diligence
Vendors who pass the initial score check move to due diligence. At this stage, run the geo discovery search to confirm there are no subsidiary licenses at the same address with worse scores. Review the full inspection history trend. A vendor with a current B grade but a consistently declining trend over six inspections should be flagged for discussion even if they technically meet the threshold.
Stage 3 - Approval with Ongoing Conditions
Approved vendors are enrolled in the weekly monitoring job. The approval terms specify the minimum score threshold (e.g., "vendor must maintain a score of 75 or above") and the consequence of dropping below it (e.g., "automatic 30-day probationary period during which vendor must provide a remediation plan and evidence of corrective action").
// approval-workflow.js
async function processVendorApplication(applicationData) {
const { name, address, city, state, zip, lat, lng } = applicationData;
// Stage 1: Initial score check
const lookup = await fetchVendorScore(name, address, city, state);
if (!lookup.result) {
return { status: 'MANUAL_REVIEW', reason: 'No inspection record found for this address.' };
}
const { score, grade } = lookup.result;
if (score < 50) {
return {
status: 'DISQUALIFIED',
reason: `Current health inspection grade is ${grade} (${score}/100). Minimum required: Grade C (50+).`,
score,
grade,
};
}
// Stage 2: Full address audit
const audit = await fullVendorAudit(name, lat, lng);
if (audit.worstScore < 50) {
return {
status: 'DISQUALIFIED',
reason: `A co-located license at this address has a Grade F score (${audit.worstScore}/100).`,
auditDetails: audit,
};
}
// Stage 3: Trend check
const history = lookup.result.inspection_history;
const slope = calculateTrendSlope(history);
const declining = slope > 2; // Positive slope = getting worse (index 0 = most recent)
const approvalStatus = score >= 85 ? 'APPROVED' :
score >= 70 ? 'APPROVED_STANDARD' :
'APPROVED_CONDITIONAL';
return {
status: approvalStatus,
score,
grade,
trend: lookup.result.trend,
trendConcern: declining,
licensesFound: audit.licensesFound,
enrollInMonitoring: true,
alertThreshold: score >= 85 ? 75 : 70,
};
}
For organizations managing food service at many locations simultaneously - such as franchise networks - the monitoring approach scales identically. See Franchise QA: Automate Health Inspection Monitoring for the franchise-specific implementation pattern, including how to handle vendors who supply multiple franchise locations across different jurisdictions.
What to Do When a Vendor Score Drops
An automated alert is only as useful as the response protocol behind it. When the monitoring job fires an alert, the typical response workflow looks like this:
- Automatic email to vendor contact notifying them their score has triggered a review.
- Procurement team review within 48 hours - pull the full inspection report, review the specific violations, assess severity.
- Vendor response required within 7 days - vendor submits a written corrective action plan addressing each critical violation.
- Re-inspection monitoring - the system watches for a new inspection record. When the next score is posted, if it meets the threshold, the vendor is reinstated automatically.
- Service suspension option - if the score drops below 50 (Grade F) or a critical violation involves an imminent health hazard, service is suspended pending resolution.
The key insight is that automation handles the detection and escalation. Human judgment is reserved for the response - reviewing the actual violation details, evaluating the remediation plan, and making the call on service suspension. This is the right division of labor: machines are better at continuous monitoring, humans are better at judgment calls.
If your interest in food service risk extends to insurance coverage and underwriting decisions, Food Service Insurance Underwriting with Inspection Data covers how insurers use the same scores to price policies and set coverage terms.
Building the Dashboard
The vendor risk scorecard is most useful when visualized in a procurement dashboard that gives the team an at-a-glance view of the entire approved vendor list. Key metrics to surface:
- Count of vendors by grade (A, B, C, F) as a color-coded bar
- Vendors with declining trends over the last 3 inspections - flagged in amber
- Vendors with any critical violations in the last 12 months - flagged in red
- Vendors not yet checked this week - flagged as stale
- Vendors whose score dropped since last check - sorted by magnitude of drop
A simple weekly email digest of the above metrics, sent to the procurement lead and facilities manager every Monday morning alongside the monitoring job run, gives decision-makers visibility without requiring them to log into any system.