Error Handling
Error response format, common status codes, and retry strategies.
Error Response Format
Error responses return an error field with a human-readable message. Some errors include additional context fields:
{
"error": "Invalid or expired API key."
}
Rate limit and quota errors include limit details alongside the message:
{
"error": "Rate limit exceeded.",
"limit": 10,
"retryAfterSeconds": 45
}
Validation errors (malformed parameters) return the failing issues from the request validator:
{
"success": false,
"error": {
"issues": [
{
"validation": "uuid",
"code": "invalid_string",
"message": "Invalid uuid",
"path": ["id"]
}
],
"name": "ZodError"
}
}
Common HTTP Status Codes
| Code | Meaning | When You'll See It |
|------|---------|-------------------|
| 400 | Bad Request | Invalid query parameters, malformed request body |
| 401 | Unauthorized | Missing, invalid, revoked, or expired API key |
| 404 | Not Found | Parcel ID, county, or resource does not exist |
| 429 | Too Many Requests | Per-minute rate limit or monthly quota exceeded |
| 500 | Internal Server Error | Something went wrong on our end |
Rate Limiting
When you exceed your per-minute rate limit, the API responds with 429 Too Many Requests and a Retry-After header (seconds to wait):
HTTP/1.1 429 Too Many Requests
Retry-After: 45
{
"error": "Rate limit exceeded.",
"limit": 10,
"retryAfterSeconds": 45
}
When you exhaust your monthly quota, the API also responds with 429:
{
"error": "Monthly request limit exceeded.",
"limit": 50,
"used": 50,
"resetDate": "2026-09-01T00:00:00.000Z"
}
Retry Strategy
For transient errors (429 and 5xx), use exponential backoff with jitter:
async function fetchWithRetry(url: string, apiKey: string, maxRetries = 3): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (res.ok) return res;
// Don't retry client errors (except 429)
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
throw new Error(`Client error: ${res.status}`);
}
if (attempt === maxRetries) {
throw new Error(`Failed after ${maxRetries} retries: ${res.status}`);
}
// Exponential backoff with jitter; honor Retry-After when present
const retryAfter = res.headers.get("Retry-After");
const baseDelay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 1000 * Math.pow(2, attempt);
const jitter = Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
}
throw new Error("Unreachable");
}
Note: a monthly-quota 429 (body contains resetDate) will not succeed on retry until the quota period resets -- treat it as terminal, not transient.