Error Handling

Error response format, common status codes, and retry strategies.

Error Response Format

All error responses follow a consistent JSON structure:

Code
{
  "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 key
  • FORBIDDEN -- Valid key but insufficient permissions
  • NOT_FOUND -- Resource does not exist
  • VALIDATION_ERROR -- Request parameters failed validation
  • RATE_LIMITED -- Too many requests; slow down
  • QUOTA_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:

Code
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:

Code
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:

Code
{
  "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"
      }
    }
  }
}