Error Handling
Learn how to handle errors gracefully in your SpatialFlow integrations with proper retry strategies, error recovery patterns, and troubleshooting workflows.
Error Response Format
All SpatialFlow API errors return a consistent JSON structure:
{
"detail": "Invalid geometry format",
"error_code": "VALIDATION_ERROR"
}
Fields:
detail: Human-readable error description (always present)error_code: (Optional) Machine-readable error codedetails: (Optional) Additional context about the error
Validation errors return detail as an array of objects:
{
"detail": [
{
"type": "missing",
"loc": ["body", "geometry"],
"msg": "Field required"
}
]
}
HTTP Status Codes
2xx Success
| Code | Meaning | When It Occurs |
|---|---|---|
| 200 | OK | Successful GET, PATCH, DELETE request |
| 201 | Created | Successful POST request creating a resource |
| 204 | No Content | Successful DELETE with no response body |
4xx Client Errors
| Code | Meaning | Common Causes | Solution |
|---|---|---|---|
| 400 | Bad Request | Invalid JSON, malformed data | Check request body format |
| 401 | Unauthorized | Missing or invalid API key/token | Verify authentication credentials |
| 403 | Forbidden | Insufficient permissions | Check API key scopes |
| 404 | Not Found | Resource doesn't exist | Verify resource ID |
| 409 | Conflict | Resource already exists | Check for duplicates |
| 422 | Unprocessable Entity | Validation failed | Fix validation errors in details |
| 429 | Too Many Requests | Rate limit exceeded | Implement exponential backoff |
5xx Server Errors
| Code | Meaning | What It Means | Action |
|---|---|---|---|
| 500 | Internal Server Error | Unexpected server error | Retry with backoff, contact support if persists |
| 502 | Bad Gateway | Upstream service error | Temporary issue, retry |
| 503 | Service Unavailable | Maintenance or overload | Check status page, retry later |
| 504 | Gateway Timeout | Request took too long | Retry with backoff |
Common Errors by Feature
Authentication Errors
Invalid API Key
{
"detail": "Unauthorized"
}
Solution: Verify your API key is correct and hasn't been revoked.
Token Expired
{
"detail": "Token has expired",
"error_code": "TOKEN_EXPIRED"
}
Solution: Refresh your JWT token using the /auth/refresh endpoint.
Geofence Errors
Invalid Geometry
{
"detail": [
{
"type": "value_error",
"loc": ["body", "geometry"],
"msg": "Polygon must be closed (first and last points must match)"
}
]
}
Solution: Ensure first and last coordinates in polygon are identical.
Too Many Vertices
{
"detail": "Too many vertices in polygon. Maximum is 500, got 523.",
"error_code": "VALIDATION_ERROR"
}
Solution: Simplify polygon to 500 vertices or fewer.
Workflow Errors
Invalid Action Type
{
"detail": [
{
"type": "value_error",
"loc": ["body", "actions", 0, "type"],
"msg": "Invalid action type"
}
]
}
Solution: Use only supported action types from the list.
Geofence Not Found
{
"detail": "Geofence not found"
}
Solution: Verify geofence exists and belongs to your workspace.
Webhook Errors
Invalid Signature
{
"detail": "Webhook signature verification failed",
"error_code": "INVALID_SIGNATURE"
}
Solution: Verify you're using the correct webhook secret for signature validation.
Rate Limit Errors
{
"detail": "Too many requests. Rate limit exceeded.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
Response Headers:
Retry-After: 60
Solution: Implement exponential backoff (see Retry Strategies below).
Retry Strategies
Exponential Backoff
Use exponential backoff for transient errors (429, 5xx):
Python Example:
import time
import random
import requests
def make_request_with_retry(url, headers, max_retries=5):
"""Make API request with exponential backoff retry logic."""
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers, timeout=30)
# Success
if response.status_code == 200:
return response.json()
# Client error - don't retry
if 400 <= response.status_code < 500 and response.status_code != 429:
raise Exception(f"Client error: {response.status_code}")
# Rate limited - use Retry-After header if available
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
print(f"Rate limited. Retrying after {retry_after} seconds...")
time.sleep(retry_after)
continue
# Server error or timeout - exponential backoff
if response.status_code >= 500:
wait_time = (2 ** attempt) + (random.randint(0, 1000) / 1000)
print(f"Server error. Retrying in {wait_time:.2f} seconds...")
time.sleep(wait_time)
continue
except requests.exceptions.Timeout:
wait_time = (2 ** attempt) + (random.randint(0, 1000) / 1000)
print(f"Request timeout. Retrying in {wait_time:.2f} seconds...")
time.sleep(wait_time)
continue
raise Exception(f"Max retries ({max_retries}) exceeded")
# Usage
try:
data = make_request_with_retry(
"https://api.spatialflow.io/api/v1/geofences",
headers={"Authorization": f"Bearer {API_KEY}"}
)
print(f"Success: {data}")
except Exception as e:
print(f"Failed after retries: {e}")
Node.js Example (requires Node.js 18+ or browser with fetch API):
async function makeRequestWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
// Success
if (response.ok) {
return await response.json();
}
// Client error - don't retry (except 429)
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
throw new Error(`Client error: ${response.status}`);
}
// Rate limited - use Retry-After header
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
console.log(`Rate limited. Retrying after ${retryAfter} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
// Server error - exponential backoff
if (response.status >= 500) {
const waitTime = (2 ** attempt) * 1000 + Math.random() * 1000;
console.log(`Server error. Retrying in ${waitTime/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
const waitTime = (2 ** attempt) * 1000 + Math.random() * 1000;
console.log(`Request timeout. Retrying in ${waitTime/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
throw error;
}
}
throw new Error(`Max retries (${maxRetries}) exceeded`);
}
// Usage
try {
const data = await makeRequestWithRetry(
'https://api.spatialflow.io/api/v1/geofences',
{
headers: { 'Authorization': `Bearer ${API_KEY}` }
}
);
console.log('Success:', data);
} catch (error) {
console.error('Failed after retries:', error);
}
Retry Best Practices
Do:
- ✅ Retry on 429 (rate limit), 500, 502, 503, 504
- ✅ Use exponential backoff with jitter
- ✅ Respect
Retry-Afterheader for 429 responses - ✅ Set reasonable max retries (3-5 attempts)
- ✅ Add timeout to all requests (30 seconds recommended)
- ✅ Log retry attempts for debugging
Don't:
- ❌ Retry on 4xx errors (except 429)
- ❌ Retry immediately without delay
- ❌ Retry indefinitely
- ❌ Ignore
Retry-Afterheader - ❌ Make requests without timeout
Rate Limit Handling
Proactive Rate Limiting
Implement client-side rate limiting to avoid 429 errors:
Python Example (with Token Bucket):
import time
from threading import Lock
class RateLimiter:
"""Token bucket rate limiter for API requests."""
def __init__(self, max_per_hour=1000):
"""
Args:
max_per_hour: Maximum requests per hour (default: 1000, matching API limit)
"""
rate = max_per_hour / 3600 # Convert to per-second
burst = min(max_per_hour, 50) # Allow short bursts
self.rate = rate
self.burst = burst
self.tokens = burst
self.last_update = time.time()
self.lock = Lock()
def wait_if_needed(self):
"""Wait if rate limit would be exceeded."""
with self.lock:
now = time.time()
elapsed = now - self.last_update
# Refill tokens based on elapsed time
self.tokens = min(
self.burst,
self.tokens + (elapsed * self.rate)
)
self.last_update = now
# Wait if no tokens available
if self.tokens < 1:
wait_time = (1 - self.tokens) / self.rate
time.sleep(wait_time)
# Refill tokens after waiting
self.tokens = min(
self.burst,
self.tokens + (wait_time * self.rate)
)
# Consume one token
self.tokens -= 1
# Usage
limiter = RateLimiter(rate=200, burst=1000)
for i in range(1000):
limiter.wait_if_needed()
response = requests.get(
"https://api.spatialflow.io/api/v1/geofences",
headers={"Authorization": f"Bearer {API_KEY}"}
)
Monitoring Rate Limits
Track your API usage to stay under limits:
def make_tracked_request(url, headers):
"""Make request and track rate limit usage."""
response = requests.get(url, headers=headers)
# Log current rate limit status
print(f"Status: {response.status_code}")
if response.status_code == 429:
retry_after = response.headers.get('Retry-After', 'unknown')
print(f"⚠️ Rate limited! Retry after: {retry_after}s")
return response
Troubleshooting Workflows
Debug Checklist
When encountering errors, work through this checklist:
1. Verify Authentication
# Test API key
curl -X GET https://api.spatialflow.io/api/v1/auth/test-api-key \
-H "Authorization: Bearer YOUR_API_KEY"
# Test JWT token
curl -X GET https://api.spatialflow.io/api/v1/auth/me \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
2. Check Resource Existence
# Verify geofence exists
curl -X GET https://api.spatialflow.io/api/v1/geofences/{geofence_id} \
-H "Authorization: Bearer YOUR_API_KEY"
# Verify workflow exists
curl -X GET https://api.spatialflow.io/api/v1/workflows/{workflow_id} \
-H "Authorization: Bearer YOUR_API_KEY"
3. Validate Request Body
- Use a JSON validator to check syntax
- Verify all required fields are present
- Check data types match API expectations
- Ensure GeoJSON geometry is valid
4. Check Rate Limits
- Review request frequency
- Implement rate limiting if making bulk requests
- Add delays between requests if needed
5. Review Error Details
- Read the
detailsfield for specific validation errors - Check field names and allowed values
- Verify data formats (dates, UUIDs, etc.)
Common Validation Issues
Geofence Validation:
# Test if coordinates are in correct [lng, lat] order
# Incorrect: [37.7749, -122.4194] (lat, lng)
# Correct: [-122.4194, 37.7749] (lng, lat)
# Verify polygon is closed
# First and last coordinates must be identical
Workflow Validation:
# Ensure action types are supported
# Valid: "webhook", "slack", "email", "sms", "lambda", "sns", "teams", "discord", "database"
# Verify trigger geofence exists
# GET /api/v1/geofences/{geofence_id} should return 200
Error Recovery Patterns
Graceful Degradation
Handle API errors gracefully in your application:
def get_geofences_with_fallback(api_key):
"""Fetch geofences with fallback to cached data."""
try:
response = requests.get(
"https://api.spatialflow.io/api/v1/geofences",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10
)
if response.status_code == 200:
geofences = response.json()
# Cache successful response
cache.set('geofences', geofences, ttl=300)
return geofences
else:
# Fall back to cache on error
print(f"API error {response.status_code}, using cached data")
return cache.get('geofences', default=[])
except requests.exceptions.Timeout:
print("API timeout, using cached data")
return cache.get('geofences', default=[])
Circuit Breaker
Prevent cascading failures with circuit breaker pattern:
class CircuitBreaker:
"""Simple circuit breaker for API calls."""
def __init__(self, failure_threshold=5, timeout=60):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.timeout = timeout
self.last_failure_time = None
self.state = 'closed' # closed, open, half-open
def call(self, func):
"""Execute function with circuit breaker protection."""
# Check if circuit is open
if self.state == 'open':
if time.time() - self.last_failure_time > self.timeout:
self.state = 'half-open'
else:
raise Exception("Circuit breaker is OPEN")
try:
result = func()
# Success - reset circuit
self.failure_count = 0
if self.state == 'half-open':
self.state = 'closed'
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = 'open'
print(f"Circuit breaker opened after {self.failure_count} failures")
raise e
# Usage
breaker = CircuitBreaker(failure_threshold=5, timeout=60)
def make_api_call():
return requests.get(
"https://api.spatialflow.io/api/v1/geofences",
headers={"Authorization": f"Bearer {API_KEY}"}
).json()
try:
data = breaker.call(make_api_call)
except Exception as e:
print(f"Request failed: {e}")
Getting Help
If you're still experiencing issues after troubleshooting:
- Check the status page: status.spatialflow.io
- Review API documentation: Ensure you're using the correct endpoints and formats
- Contact support: support@spatialflow.io
- Include your request ID (from error response headers)
- Provide example request and response
- Specify your API client (language, library versions)
Next Steps
- API Reference - Complete endpoint documentation
- Authentication - API keys and JWT tokens
- Webhook Integration - Handle webhook errors
- Rate Limiting - Understand rate limits