Whether you're building a food delivery app, a restaurant discovery platform, or a franchise monitoring tool, you'll eventually need to answer a simple question: what is this restaurant's current health inspection score? The challenge is that public health inspection data lives across thousands of county and city databases, each with its own format, grading scale, and update frequency.
This guide walks through the practical steps of looking up a restaurant's health score using the FoodSafe Score API - from crafting your first lookup request to handling edge cases like no-match results, ambiguous addresses, and when to fall back to a geographic search. We'll cover both JavaScript and Python implementations with working code samples you can drop straight into your project.
Understanding the Lookup Endpoint
The primary way to fetch a restaurant's health score is the name-and-address lookup endpoint. This takes a restaurant name, a street address, and a city, then returns a normalized 0-100 score along with the underlying inspection metadata pulled from the relevant jurisdiction's public records.
The endpoint signature looks like this:
GET https://api.foodsafescoreapi.com/v1/lookup
?name=Tacos El Primo
&address=1234 Mission St
&city=San Francisco
&state=CA
The name, address, and city parameters are all required. The state parameter is strongly recommended whenever you have it - it helps the API route to the correct jurisdiction authority and resolve faster for cities that share names across state lines.
Authentication
All requests require your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
You can get an API key by joining the waitlist for early access. During beta, keys are issued on a rolling basis as new jurisdictions come online.
JavaScript Implementation (Fetch API)
Here is a complete JavaScript function that performs a restaurant lookup and handles the response. This works in both browser and Node.js (v18+) environments since native fetch is available in both.
const FOODSAFE_API_KEY = process.env.FOODSAFE_API_KEY;
const BASE_URL = 'https://api.foodsafescoreapi.com/v1';
async function getRestaurantHealthScore({ name, address, city, state }) {
const params = new URLSearchParams({ name, address, city });
if (state) params.set('state', state);
const res = await fetch(`${BASE_URL}/lookup?${params}`, {
headers: {
'Authorization': `Bearer ${FOODSAFE_API_KEY}`,
'Accept': 'application/json'
}
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `Lookup failed (${res.status})`);
}
const data = await res.json();
if (data.match_status === 'not_found') {
return null; // Caller can fall back to geo search
}
return data;
}
// Usage
try {
const result = await getRestaurantHealthScore({
name: 'Tacos El Primo',
address: '1234 Mission St',
city: 'San Francisco',
state: 'CA'
});
if (result) {
console.log(`Score: ${result.score} (Grade ${result.grade})`);
console.log(`Last inspected: ${result.last_inspection_date}`);
console.log(`Violations: ${result.violation_count}`);
} else {
console.log('Restaurant not found - trying geo search');
}
} catch (err) {
console.error('Health score lookup failed:', err.message);
}
Notice the response.ok check before calling .json(). Skipping this is the single most common mistake when working with external APIs - without it, a 401 or 500 error response gets silently swallowed and you end up debugging phantom data issues instead of seeing the real error message.
Python Implementation (requests library)
The same lookup in Python using the requests library:
import os
import requests
FOODSAFE_API_KEY = os.environ['FOODSAFE_API_KEY']
BASE_URL = 'https://api.foodsafescoreapi.com/v1'
def get_restaurant_health_score(name, address, city, state=None):
params = {'name': name, 'address': address, 'city': city}
if state:
params['state'] = state
headers = {
'Authorization': f'Bearer {FOODSAFE_API_KEY}',
'Accept': 'application/json'
}
response = requests.get(f'{BASE_URL}/lookup', params=params, headers=headers)
response.raise_for_status() # Raises HTTPError for 4xx/5xx
data = response.json()
if data.get('match_status') == 'not_found':
return None
return data
# Usage
result = get_restaurant_health_score(
name='Tacos El Primo',
address='1234 Mission St',
city='San Francisco',
state='CA'
)
if result:
print(f"Score: {result['score']} (Grade {result['grade']})")
print(f"Last inspected: {result['last_inspection_date']}")
print(f"Critical violations: {result['critical_violation_count']}")
print(f"Non-critical violations: {result['non_critical_violation_count']}")
else:
print("Not found - fall back to geo search")
Understanding the Response Fields
A successful lookup returns a JSON object with the following key fields:
| Field | Type | Description |
|---|---|---|
| score | integer | Normalized 0-100 score. Higher is better. |
| grade | string | Letter grade: A (85-100), B (70-84), C (50-69), F (0-49) |
| last_inspection_date | string (ISO 8601) | Date of the most recent health inspection on record |
| violation_count | integer | Total violations found in the most recent inspection |
| critical_violation_count | integer | Number of critical violations (each deducts 25 points) |
| non_critical_violation_count | integer | Number of non-critical violations (each deducts 5 points) |
| corrected_on_site_count | integer | Violations corrected on site (each deducts only 2 points) |
| jurisdiction | string | The health authority source (e.g. "SF Department of Public Health") |
| match_status | string | "exact", "fuzzy", or "not_found" |
| source_url | string | Link to the original public record for attribution |
The match_status Field
Pay close attention to match_status. An exact match means the name and address resolved cleanly to a single record. A fuzzy match means the API found a close result but the name or address didn't match character-for-character - common with chains that have slightly different registered names ("McDonald's" vs "McDonalds") or addresses with suite numbers formatted differently. A not_found status means no record could be located for that combination.
When you get a fuzzy match, surface the matched_name and matched_address fields in your UI alongside the score so users can confirm it's the right establishment. This is especially important for high-stakes use cases like insurance underwriting or franchise auditing.
Handling No-Match Results
A not_found result doesn't necessarily mean the restaurant has no inspection records - it may mean the name or address you passed doesn't match the record in the jurisdiction's database closely enough. Before surfacing a "no data" message to your users, try these fallback strategies:
Strategy 1 - Try a shorter name
Chain restaurants often register under shortened names. "The Cheesecake Factory Restaurant" may be indexed as "Cheesecake Factory". Try stripping common prefixes ("The", "A", "An") and suffixes ("Restaurant", "Grill", "Bar & Grill") from the name.
function normalizeName(name) {
return name
.replace(/^(the|a|an)\s+/i, '')
.replace(/\s+(restaurant|grill|bar\s*&\s*grill|cafe|diner|kitchen)$/i, '')
.trim();
}
Strategy 2 - Fall back to geo search
If name normalization doesn't help, fall back to the geo search endpoint using a lat/lng coordinate derived from the address. This searches all inspection records within a given radius and returns the closest match by name similarity.
async function getScoreWithGeoFallback({ name, address, city, state, lat, lng }) {
// First try direct lookup
const direct = await getRestaurantHealthScore({ name, address, city, state })
.catch(() => null);
if (direct) return direct;
// Fall back to geo search if we have coordinates
if (lat && lng) {
const params = new URLSearchParams({
name,
lat: lat.toString(),
lng: lng.toString(),
radius_meters: '100'
});
const res = await fetch(`${BASE_URL}/geo-search?${params}`, {
headers: { 'Authorization': `Bearer ${FOODSAFE_API_KEY}` }
});
if (!res.ok) return null;
const data = await res.json();
return data.results?.[0] || null;
}
return null;
}
For a deeper dive into the geographic search endpoint and when to use it, see our guide on how to integrate a restaurant health inspection API end-to-end.
Displaying Health Scores in a Consumer UI
Once you have a score from the API, the presentation layer matters as much as the data itself. Research on food safety transparency suggests that users make faster trust decisions from letter grades than from numeric scores - so lead with the grade and use the score as supporting detail.
Recommended UI fields to display
- Grade badge - prominently styled A/B/C/F with color coding (green for A, yellow for B, orange for C, red for F)
- Numeric score - shown beneath or beside the grade badge as supporting detail
- Last inspection date - critical for freshness context; a score from 3 years ago means far less than one from last month
- Violation summary - "2 critical, 1 non-critical" is more informative than just the violation count
- Source attribution link - use the
source_urlfield to link back to the official government record
Grade badge component (JavaScript)
function renderGradeBadge(score, grade) {
const colors = {
A: { bg: '#16a34a', text: '#ffffff' },
B: { bg: '#d97706', text: '#ffffff' },
C: { bg: '#ea580c', text: '#ffffff' },
F: { bg: '#dc2626', text: '#ffffff' }
};
const color = colors[grade] || colors.F;
return `
<div class="health-grade-badge" style="
display: inline-flex;
flex-direction: column;
align-items: center;
background: ${color.bg};
color: ${color.text};
border-radius: 8px;
padding: 0.5rem 0.85rem;
font-weight: 800;
min-width: 56px;
">
<span style="font-size: 1.75rem; line-height: 1;">${grade}</span>
<span style="font-size: 0.65rem; opacity: 0.85;">${score}/100</span>
</div>
`;
}
Inspection date freshness indicator
Always display how old the inspection data is - a date alone isn't intuitive for most users. A simple "inspected 8 months ago" is more immediately understood than a raw ISO date.
function inspectionAgeText(isoDate) {
const inspectedAt = new Date(isoDate);
const now = new Date();
const months = Math.floor((now - inspectedAt) / (1000 * 60 * 60 * 24 * 30));
if (months < 1) return 'Inspected this month';
if (months === 1) return 'Inspected 1 month ago';
if (months < 12) return `Inspected ${months} months ago`;
const years = Math.floor(months / 12);
return years === 1 ? 'Inspected 1 year ago' : `Inspected ${years} years ago`;
}
Rate Limits and Pricing
Each successful lookup call costs $0.25 or draws from your monthly plan's included lookups. To avoid surprise charges, always cache results on your end - inspection data typically updates monthly, so a 24-hour cache TTL is a reasonable default for most consumer applications. For more active monitoring scenarios (like franchise QA), you may want a shorter TTL or a webhook-based approach for change alerts.
Store the last_inspection_date in your cache key logic. If the API returns a fresher inspection date than what you have cached, invalidate the cache immediately and update your stored record regardless of TTL.
Bulk Lookups
If you need to check scores for a large list of restaurants - say, all franchise locations in a city - use the bulk-by-zip endpoint rather than calling the lookup endpoint in a loop. It returns all inspection records in a given ZIP code in a single call, which you can then match against your location list client-side. See our post on how to normalize food safety scores across jurisdictions for the full bulk workflow.
Error Handling Reference
Here are the HTTP status codes you'll encounter and how to handle each:
- 200 - Success. Check
match_statusto distinguish found vs not-found. - 400 - Bad request. One or more required parameters are missing or malformed. Log the error body and fix your request.
- 401 - Invalid or missing API key. Check your
Authorizationheader. - 404 - The endpoint path is wrong. Double-check the URL.
- 429 - Rate limit exceeded. Implement exponential backoff with a minimum 1-second delay between retries.
- 503 - Jurisdiction data source temporarily unavailable. Retry after 30 seconds; surface a "data temporarily unavailable" message if retries exhaust.
Next Steps
With the lookup endpoint working, you have the foundation for a wide range of food safety features. Food delivery apps can surface inspection badges on restaurant cards. Franchise operators can build bulk monitoring pipelines. For coverage of those more advanced patterns - including how to structure a monitoring dashboard and how to set up score-change alerts - see our guides on integrating restaurant health scores into a food delivery platform and franchise health inspection monitoring.
The lookup endpoint is designed to be the simplest possible on-ramp. One HTTP call, one JSON response, one normalized score - regardless of which of the 3,000+ US jurisdictions the restaurant sits in. From there, you can layer in history, trends, and alerting as your use case demands.