Error Handling & Best Practices
This guide covers error handling, rate limits, and best practices for integrating with the ISAO API.
📋 Table of Contents
- Error Response Format
- HTTP Status Codes
- Error Types
- Rate Limiting
- Best Practices
- Retry Strategies
- Monitoring & Debugging
❌ Error Response Format
All API errors follow a consistent JSON format:
{
"success": false,
"error": "Short error description",
"message": "Detailed human-readable error message",
"code": "ERROR_CODE", // Optional
"details": {}, // Optional, additional error context
"timestamp": "2024-11-08T10:30:00.000Z" // Optional
}
Example Error Response
{
"success": false,
"error": "Permission denied: contacts:write required",
"message": "This API key does not have the required permission: contacts:write",
"requiredPermission": "contacts:write",
"availablePermissions": ["contacts:read", "webhooks:write"]
}
🔢 HTTP Status Codes
| Status Code | Description | Common Causes |
|---|---|---|
200 | Success | Request completed successfully |
201 | Created | Resource created successfully |
400 | Bad Request | Invalid request syntax or data |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Insufficient permissions |
404 | Not Found | Resource doesn't exist |
409 | Conflict | Resource already exists |
422 | Unprocessable Entity | Validation errors |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Server-side error |
503 | Service Unavailable | Temporary service outage |
🚨 Error Types
Authentication Errors (401)
Missing API Key
{
"success": false,
"error": "API key required in Authorization header",
"message": "Please provide a valid API key in the format: Authorization: Bearer YOUR_API_KEY"
}
Solution: Add the Authorization header with your API key.
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://staging.isao.io/api/external/test
Invalid API Key Format
{
"success": false,
"error": "Invalid API key format",
"message": "API key appears to be invalid or too short"
}
Solution: Verify your API key is correctly formatted and not truncated.
Expired/Invalid API Key
{
"success": false,
"error": "Invalid API key",
"message": "The provided API key is invalid, expired, or inactive"
}
Solution: Check your API key status in the dashboard and regenerate if necessary.
Permission Errors (403)
Insufficient Permissions
{
"success": false,
"error": "Permission denied: contacts:write required",
"message": "This API key does not have the required permission: contacts:write",
"requiredPermission": "contacts:write",
"availablePermissions": ["contacts:read"]
}
Solution: Update your API key permissions in the dashboard.
Validation Errors (422)
Field Validation
{
"success": false,
"error": "Validation failed",
"details": {
"name": "Name is required and must be between 1-255 characters",
"phoneNumber": "Phone number is required",
"email": "Invalid email format"
}
}
Solution: Fix the invalid fields according to the details provided.
Required Fields Missing
{
"success": false,
"error": "Missing required fields",
"details": {
"missingFields": ["name", "phoneNumber"]
}
}
Resource Errors (404, 409)
Resource Not Found
{
"success": false,
"error": "Contact not found"
}
Solution: Verify the resource ID exists and belongs to your company.
Duplicate Resource
{
"success": false,
"error": "Contact with this phone number already exists",
"existingContactId": "contact_123"
}
Solution: Use the existing resource ID or update instead of creating.
Rate Limiting Errors (429)
{
"success": false,
"error": "Rate limit exceeded",
"message": "Too many requests. Please slow down.",
"retryAfter": 60,
"limits": {
"hourly": 1000,
"daily": 10000
}
}
Solution: Implement exponential backoff and respect the retryAfter value.
⚡ Rate Limiting
Default Limits
| Limit Type | Default Value | Description |
|---|---|---|
| Per Hour | 1000 requests | Resets every hour |
| Per Day | 10000 requests | Resets at midnight UTC |
| Concurrent | 10 requests | Maximum simultaneous requests |
Rate Limit Headers
X-RateLimit-Hourly-Limit: 1000
X-RateLimit-Hourly-Remaining: 999
X-RateLimit-Hourly-Reset: 1699459200
X-RateLimit-Daily-Limit: 10000
X-RateLimit-Daily-Remaining: 9999
X-RateLimit-Daily-Reset: 1699459200
Handling Rate Limits
async function makeApiRequest(url, options) {
try {
const response = await fetch(url, options);
// Check rate limit headers
const hourlyRemaining = response.headers.get('X-RateLimit-Hourly-Remaining');
if (hourlyRemaining && parseInt(hourlyRemaining) < 10) {
console.warn('Approaching hourly rate limit');
}
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited, retrying after ${retryAfter} seconds`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return makeApiRequest(url, options); // Retry
}
return response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
✅ Best Practices
1. Implement Proper Error Handling
class IsaoAPIClient {
async makeRequest(endpoint, options = {}) {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new IsaoAPIError(data, response.status);
}
return data;
} catch (error) {
if (error instanceof IsaoAPIError) {
throw error;
}
throw new IsaoAPIError({ error: 'Network error', message: error.message }, 0);
}
}
}
class IsaoAPIError extends Error {
constructor(errorData, statusCode) {
super(errorData.message || errorData.error);
this.name = 'IsaoAPIError';
this.statusCode = statusCode;
this.errorData = errorData;
}
isAuthError() {
return this.statusCode === 401;
}
isPermissionError() {
return this.statusCode === 403;
}
isRateLimitError() {
return this.statusCode === 429;
}
isValidationError() {
return this.statusCode === 422;
}
}
2. Use Pagination for Large Datasets
async function getAllContacts(apiClient) {
const allContacts = [];
let page = 1;
let hasMore = true;
while (hasMore) {
try {
const response = await apiClient.makeRequest('/contacts', {
method: 'GET',
params: { page, limit: 200 } // Use maximum page size
});
allContacts.push(...response.data);
hasMore = response.meta.page < response.meta.totalPages;
page++;
// Small delay to avoid rate limits
if (hasMore) {
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
if (error.isRateLimitError()) {
// Wait and retry
await new Promise(resolve => setTimeout(resolve, 60000));
continue;
}
throw error;
}
}
return allContacts;
}
3. Validate Data Before Sending
function validateContact(contact) {
const errors = {};
if (!contact.name || contact.name.length < 1 || contact.name.length > 255) {
errors.name = 'Name is required and must be between 1-255 characters';
}
if (!contact.phoneNumber) {
errors.phoneNumber = 'Phone number is required';
}
if (contact.email && !isValidEmail(contact.email)) {
errors.email = 'Invalid email format';
}
if (Object.keys(errors).length > 0) {
throw new ValidationError('Contact validation failed', errors);
}
return true;
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
4. Implement Idempotency
class IdempotentClient {
constructor(apiClient) {
this.apiClient = apiClient;
this.requestCache = new Map();
}
async createContact(contactData, idempotencyKey = null) {
// Generate idempotency key if not provided
if (!idempotencyKey) {
idempotencyKey = this.generateIdempotencyKey(contactData);
}
// Check if we've already made this request
if (this.requestCache.has(idempotencyKey)) {
return this.requestCache.get(idempotencyKey);
}
try {
const result = await this.apiClient.makeRequest('/contacts', {
method: 'POST',
body: JSON.stringify(contactData),
headers: {
'Idempotency-Key': idempotencyKey
}
});
// Cache successful result
this.requestCache.set(idempotencyKey, result);
// Clean up cache after 1 hour
setTimeout(() => {
this.requestCache.delete(idempotencyKey);
}, 3600000);
return result;
} catch (error) {
if (error.statusCode === 409) {
// Conflict - resource might already exist
console.log('Contact already exists, fetching existing contact');
return this.getContactByPhone(contactData.phoneNumber);
}
throw error;
}
}
generateIdempotencyKey(data) {
const crypto = require('crypto');
return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
}
}
🔄 Retry Strategies
Exponential Backoff
class RetryableClient {
async makeRequestWithRetry(endpoint, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await this.makeRequest(endpoint, options);
} catch (error) {
if (this.shouldRetry(error, attempt, maxRetries)) {
const delay = this.calculateBackoff(attempt);
console.log(`Request failed, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
shouldRetry(error, attempt, maxRetries) {
// Don't retry if we've exceeded max attempts
if (attempt >= maxRetries) {
return false;
}
// Retry on rate limits and server errors
return error.isRateLimitError() || error.statusCode >= 500;
}
calculateBackoff(attempt) {
// Exponential backoff: 1s, 2s, 4s, 8s...
const baseDelay = 1000;
const maxDelay = 30000; // Cap at 30 seconds
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
// Add jitter to avoid thundering herd
return delay + Math.random() * 1000;
}
}
Circuit Breaker Pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
📊 Monitoring & Debugging
Request Logging
class LoggingClient {
constructor(apiKey, options = {}) {
this.apiKey = apiKey;
this.logger = options.logger || console;
this.baseUrl = 'https://staging.isao.io/api/external';
}
async makeRequest(endpoint, options = {}) {
const requestId = this.generateRequestId();
const startTime = Date.now();
this.logger.info('API Request', {
requestId,
method: options.method || 'GET',
endpoint,
timestamp: new Date().toISOString()
});
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'X-Request-ID': requestId,
...options.headers
},
...options
});
const duration = Date.now() - startTime;
const data = await response.json();
this.logger.info('API Response', {
requestId,
statusCode: response.status,
duration,
success: response.ok
});
if (!response.ok) {
this.logger.error('API Error', {
requestId,
statusCode: response.status,
error: data.error,
message: data.message
});
throw new IsaoAPIError(data, response.status);
}
return data;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('API Request Failed', {
requestId,
duration,
error: error.message,
stack: error.stack
});
throw error;
}
}
generateRequestId() {
return Math.random().toString(36).substring(7);
}
}
Health Check Implementation
class HealthChecker {
constructor(apiClient) {
this.apiClient = apiClient;
this.isHealthy = true;
this.lastCheck = null;
this.checkInterval = 30000; // 30 seconds
}
async checkHealth() {
try {
const response = await this.apiClient.makeRequest('/test');
this.isHealthy = true;
this.lastCheck = Date.now();
return { healthy: true, timestamp: new Date().toISOString() };
} catch (error) {
this.isHealthy = false;
this.lastCheck = Date.now();
return {
healthy: false,
timestamp: new Date().toISOString(),
error: error.message
};
}
}
startHealthCheck() {
setInterval(() => {
this.checkHealth().then(result => {
if (result.healthy) {
console.log('✅ API health check passed');
} else {
console.error('❌ API health check failed:', result.error);
}
});
}, this.checkInterval);
}
getStatus() {
return {
healthy: this.isHealthy,
lastCheck: this.lastCheck ? new Date(this.lastCheck).toISOString() : null
};
}
}
Error Reporting
class ErrorReporter {
constructor(webhookUrl) {
this.webhookUrl = webhookUrl;
}
async reportError(error, context = {}) {
const errorReport = {
timestamp: new Date().toISOString(),
error: {
name: error.name,
message: error.message,
stack: error.stack,
statusCode: error.statusCode
},
context,
environment: process.env.NODE_ENV || 'development'
};
try {
await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorReport)
});
} catch (reportingError) {
console.error('Failed to report error:', reportingError);
}
}
}
// Usage
const errorReporter = new ErrorReporter('https://your-error-tracking-service.com/webhook');
try {
await apiClient.createContact(contactData);
} catch (error) {
await errorReporter.reportError(error, {
operation: 'createContact',
contactData: { ...contactData, phoneNumber: '[REDACTED]' } // Don't log sensitive data
});
throw error;
}
🔧 Development Tools
API Response Validator
class ResponseValidator {
validateContact(contact) {
const requiredFields = ['id', 'name', 'phoneNumber', 'createdAt', 'updatedAt'];
const missingFields = requiredFields.filter(field => !contact[field]);
if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}
if (contact.email && !this.isValidEmail(contact.email)) {
throw new Error('Invalid email format in response');
}
return true;
}
validateDeal(deal) {
const requiredFields = ['id', 'title', 'value', 'status', 'createdAt', 'updatedAt'];
const missingFields = requiredFields.filter(field => deal[field] === undefined);
if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}
if (typeof deal.value !== 'number' || deal.value < 0) {
throw new Error('Deal value must be a non-negative number');
}
return true;
}
isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
🔗 Related Documentation
- Authentication - API key management and security
- Contacts API - Contact-specific error handling
- Deals API - Deal-specific error handling
- Webhooks - Webhook error handling and retry logic
📈 Performance Tips
- Batch Operations: Group multiple operations when possible
- Use Pagination: Don't fetch all data at once
- Cache Responses: Cache frequently accessed data
- Parallel Requests: Make independent requests in parallel
- Connection Pooling: Reuse HTTP connections
- Compress Requests: Use gzip compression for large payloads
- Monitor Performance: Track response times and error rates