API Documentation
Base URL: https://api.zipcheckup.com/v1
Authentication
Direct requests to standard /v1 endpoints require an API key in the X-API-Key header. Anonymous access is limited to requests made by ZipCheckup pages and the deliberately narrow /v1/public/zip/:zip widget route.
curl -H "X-API-Key: YOUR_API_KEY" \
https://api.zipcheckup.com/v1/zip/90210
Get a free API key at /api/pricing/. No credit card required.
Rate Limiting
Free tier: 100 requests per day, counted per key when you send one and per IP address when you do not. The daily limit resets at midnight UTC.
| Plan | Daily Limit |
|---|---|
| Free | 100 requests/day |
| Pro ($49/mo) | 10,000 requests/day |
| Enterprise | Custom |
Rate limit headers are included in every API response:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per day (e.g., 100) |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the limit resets (midnight UTC) |
Example headers:
X-RateLimit-Limit: 100 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 1711929600
When you exceed the limit, you'll receive a 429 Too Many Requests response with an upgrade URL:
{
"error": { "message": "Rate limit exceeded. Max 100 requests/day.", "status": 429 },
"limit": 100,
"reset": "2026-03-26T00:00:00.000Z",
"upgrade": "https://zipcheckup.com/api/pricing/"
}
Need more than 100 requests/day? Request higher limits.
Endpoints
Get Water Quality Data by ZIP
Returns a full Home Safety Profile for a U.S. ZIP code: versioned multi-source score and coverage, plus separately typed water, contaminant, violation, and contextual evidence.
| Parameter | Type | Description |
|---|---|---|
zip | string | 5-digit U.S. ZIP code (path parameter) |
Example response:
{
"data": {
"zip": "90210",
"city": "Beverly Hills",
"stateAbbr": "CA",
"homeSafetyScore": 74,
"homeSafetyGrade": "B",
"coverage": "covered",
"contaminants": [
{
"code": "2456",
"name": "Total Trihalomethanes (TTHM)",
"category": "Disinfection Byproducts",
"mcl": 0.08,
"unit": "mg/L",
"violationCount": 1,
"healthBased": false
}
],
"recentViolations": [...],
"systems": [...],
"sourceCoverage": { ... }
},
"meta": {
"schema_version": "2.1.0",
"source_current_at": null,
"materialized_at": "2026-07-27T08:56:13.000Z",
"freshness": { "state": "unknown", "reason": "source_vintage_absent" }
}
}
contaminants lists the contaminants a violation was recorded for, not measured concentrations. mcl is the EPA limit and violationCount is how many violations reference that contaminant. Measured values, where a report was parsed, live separately in ccrMeasurements.
Get Safety Score Only
Returns only the safety score, grade, and risk level. Lighter response for dashboards and widgets.
Example response:
{
"data": {
"zip": "90210",
"score": 74,
"grade": "B",
"coverage": "covered",
"series": "home-safety-v4-cross-vertical",
"modelVersion": "4.0.0",
"componentScores": { "water-compliance": 96, "radon-zone": 50 },
"componentStatus": { "water-compliance": "observed" }
},
"meta": { "schema_version": "2.1.0", "freshness": { "state": "unknown" } }
}
Get State Summary
Returns a summary for a U.S. state: average score, number of ZIPs, top violations, and worst-scoring ZIPs.
| Parameter | Type | Description |
|---|---|---|
state | string | 2-letter state abbreviation (e.g., CA, NY, TX) |
Example response:
{
"data": {
"state": "CA",
"stateName": "California",
"avgScore": 68,
"totalZips": 1769,
"scoreKnownZipCount": 1769,
"violationKnownZipCount": 1602,
"violationUnknownZipCount": 167,
"topViolations": [...]
},
"meta": { "schema_version": "2.1.0" }
}
The *KnownZipCount and *UnknownZipCount fields are the denominators behind every state average. A ZIP counted as unknown was not measured; it is not a ZIP measured as zero.
Get National Rankings
Returns a ranked list of ZIP codes by water quality score.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of results (max 500) |
order | string | desc | desc = best first, asc = worst first |
state | string | all | Filter by 2-letter state code |
Get Contaminant Reference
Returns reference information about a contaminant: name, EPA MCL, health effects, and common sources.
| Parameter | Type | Description |
|---|---|---|
code | string | Contaminant code (from ZIP endpoint response) |
Bulk ZIP Lookup Pro+
A ZIP the API cannot answer for is returned in unavailable with a typed status rather than as an empty record in results: no_profile, contract_failed and upstream_unavailable are different facts. returned counts what is in results. Quota is charged one request per ZIP, and a batch spanning several data vintages reports freshness.state: "unknown" rather than one timestamp true of none of them.
Query up to 100 ZIP codes in a single request. Returns full versioned Home Safety Profile objects with separately typed evidence. Pro and Enterprise only.
| Body Parameter | Type | Description |
|---|---|---|
zips | string[] | Array of 5-digit ZIP codes (max 100) |
fields | string[] | Optional. Fields to include (e.g., ["score","grade","contaminants"]) |
Example request:
curl -X POST https://api.zipcheckup.com/v1/bulk/zip \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"zips": ["90210","10001","60601"]}'
Example response:
{
"data": {
"requested": 3,
"returned": 2,
"results": [ { "zip": "90210", "data": { ... } } ],
"unavailable": [
{
"zip": "00000",
"status": "no_profile",
"reason": "No ZipCheckup profile exists for this ZIP code."
}
]
},
"meta": { "freshness": { "state": "unknown", "reason": "bulk_mixed_vintage" } }
}
Response Format
All responses are JSON with the following envelope:
// Success { "data": { ... }, "meta": { "source": "ZipCheckup multi-source civic data; see data.sourceCoverage", "schema_version": "2.1.0", "source_current_at": null, "materialized_at": "2026-07-27T08:56:13.000Z", "freshness": { "state": "unknown", "reason": "source_vintage_absent" } } } // Error { "error": { "message": "No data found for ZIP 00000", "status": 404 } }
There is no ok field. A successful response has data and meta; an error has error with message and status. meta.source_current_at is null when the upstream source publishes no vintage, which is not the same as the data being current.
Pro and Enterprise plans can add ?format=csv to the ZIP, score, state, rankings, contaminant and county endpoints. A null renders as the literal #N/A rather than an empty cell, so a value that was never measured cannot be summed as if it were zero; a measured zero stays 0. Schema version and the null token travel in X-ZipCheckup-* headers, since CSV has no envelope.
Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | BAD_REQUEST | Invalid parameters (e.g., malformed ZIP code) |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 403 | FORBIDDEN | Endpoint requires a higher plan (e.g., bulk on Free tier) |
| 404 | NOT_FOUND | No data for the requested resource |
| 429 | RATE_LIMITED | Rate limit exceeded. Check X-RateLimit-Reset header |
| 500 | INTERNAL_ERROR | Something went wrong on our end |
Versioning
The API is versioned via the URL path (/v1/). We will not make breaking changes within a version. New fields may be added to responses at any time - your code should handle unknown fields gracefully.
When a new version is released, the previous version will be supported for at least 12 months.