Error Handling
Error response format, common status codes, and retry strategies.
Error Response Format
All error responses follow a consistent JSON structure:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description of what went wrong.",
"details": {}
}
}
The details field is optional and may contain additional context depending on the error type (for example, validation errors include field-level details).
Common HTTP Status Codes
| Code | Meaning | When You'll See It |
|------|---------|-------------------|
| 400 | Bad Request | Invalid query parameters, malformed request body |
| 401 | Unauthorized | Missing or invalid X-API-Key header |
| 403 | Forbidden | Account suspended, key revoked, or insufficient tier |
| 404 | Not Found | Parcel ID, county, or resource does not exist |
| 409 | Conflict | Duplicate resource (e.g., alert already exists for threshold) |
| 422 | Unprocessable Entity | Request is syntactically valid but semantically wrong |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Something went wrong on our end |
Error Codes
Common error codes returned in the error.code field:
UNAUTHORIZED-- No API key or invalid keyFORBIDDEN-- Valid key but insufficient permissionsNOT_FOUND-- Resource does not existVALIDATION_ERROR-- Request parameters failed validationRATE_LIMITED-- Too many requests; slow downQUOTA_EXCEEDED-- Monthly quota exhausted (overage applies on paid plans)INTERNAL_ERROR-- Server-side error; retry later
Rate Limiting
When you exceed your rate limit, the API responds with 429 Too Many Requests and includes retry information in the headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711893600
Retry-After: 45
| Header | Description |
|--------|-------------|
| X-RateLimit-Limit | Maximum requests per minute for your tier |
| X-RateLimit-Remaining | Requests remaining in the current window |
| X-RateLimit-Reset | Unix timestamp when the rate limit resets |
| Retry-After | Seconds to wait before retrying |
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: { "X-API-Key": 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
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");
}
Validation Errors
When request parameters fail validation, the details field includes per-field errors:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": {
"fields": {
"limit": "Must be between 1 and 100",
"county_id": "Must be an integer between 1 and 92"
}
}
}
}