Skip to main content

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 code
  • details: (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

CodeMeaningWhen It Occurs
200OKSuccessful GET, PATCH, DELETE request
201CreatedSuccessful POST request creating a resource
204No ContentSuccessful DELETE with no response body

4xx Client Errors

CodeMeaningCommon CausesSolution
400Bad RequestInvalid JSON, malformed dataCheck request body format
401UnauthorizedMissing or invalid API key/tokenVerify authentication credentials
403ForbiddenInsufficient permissionsCheck API key scopes
404Not FoundResource doesn't existVerify resource ID
409ConflictResource already existsCheck for duplicates
422Unprocessable EntityValidation failedFix validation errors in details
429Too Many RequestsRate limit exceededImplement exponential backoff

5xx Server Errors

CodeMeaningWhat It MeansAction
500Internal Server ErrorUnexpected server errorRetry with backoff, contact support if persists
502Bad GatewayUpstream service errorTemporary issue, retry
503Service UnavailableMaintenance or overloadCheck status page, retry later
504Gateway TimeoutRequest took too longRetry 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-After header 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-After header
  • ❌ 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 details field 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:

  1. Check the status page: status.spatialflow.io
  2. Review API documentation: Ensure you're using the correct endpoints and formats
  3. 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