Skip to main content

Error Response Format

All errors follow a consistent format:
{
  "success": false,
  "error": {
    "code": "error_code",
    "message": "Human-readable description",
    "details": {}  // Optional additional context
  }
}

HTTP Status Codes

StatusMeaningAction
400Bad RequestCheck request body/parameters
401UnauthorizedCheck API key
403ForbiddenCheck permissions/licenses
404Not FoundCheck resource ID
429Rate LimitedWait and retry
500Server ErrorRetry with backoff

Common Errors

Authentication Errors (401)

{
  "success": false,
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key"
  }
}
Solutions:
  • Ensure X-Api-Key header is present
  • Check the key hasn’t been revoked
  • Verify the key format (pk_live_...)

License Errors (403)

{
  "success": false,
  "error": {
    "code": "not_licensed",
    "message": "No active license for persona",
    "details": {
      "persona_id": "persona-abc..."
    }
  }
}
Solutions:
  • Check your licensed personas with GET /personas
  • Contact support to license additional personas

Validation Errors (400)

{
  "success": false,
  "error": {
    "code": "invalid_request",
    "message": "Content must be between 10 and 500 characters",
    "details": {
      "field": "content",
      "constraint": "length"
    }
  }
}
Solutions:
  • Check request body matches schema
  • Ensure required fields are present
  • Validate content length limits

Rate Limit Errors (429)

{
  "success": false,
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "details": {
      "retry_after": 3600,
      "limit": 1000,
      "remaining": 0
    }
  }
}
Solutions:
  • Wait for retry_after seconds
  • Implement request queuing
  • Contact support for higher limits

Error Handling Patterns

Basic Error Handler

async function apiRequest(path, options = {}) {
  const response = await fetch(`${BASE_URL}${path}`, {
    ...options,
    headers: {
      'X-Api-Key': API_KEY,
      'Content-Type': 'application/json',
      ...options.headers
    }
  });
  
  const data = await response.json();
  
  if (!response.ok) {
    throw new UnleeshedError(
      data.error.code,
      data.error.message,
      response.status,
      data.error.details
    );
  }
  
  return data;
}

class UnleeshedError extends Error {
  constructor(code, message, status, details = {}) {
    super(message);
    this.code = code;
    this.status = status;
    this.details = details;
  }
}

Retry with Exponential Backoff

async function requestWithRetry(path, options = {}, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await apiRequest(path, options);
    } catch (error) {
      lastError = error;
      
      // Don't retry client errors (except rate limits)
      if (error.status >= 400 && error.status < 500 && error.status !== 429) {
        throw error;
      }
      
      // Calculate backoff delay
      const delay = error.status === 429
        ? (error.details.retry_after || 60) * 1000
        : Math.min(1000 * Math.pow(2, attempt), 30000);
      
      console.log(`Retry ${attempt + 1}/${maxRetries} in ${delay}ms`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  
  throw lastError;
}

Graceful Degradation

async function getCommentariesSafely(topicId) {
  try {
    const result = await getTopicWithCommentaries(topicId);
    return {
      success: true,
      data: result.commentaries
    };
  } catch (error) {
    console.error('Failed to fetch commentaries:', error);
    
    // Return partial data or fallback
    if (error.code === 'rate_limit_exceeded') {
      return {
        success: false,
        error: 'Please try again in a few minutes',
        retryAfter: error.details.retry_after
      };
    }
    
    if (error.code === 'not_found') {
      return {
        success: false,
        error: 'Commentary not found'
      };
    }
    
    // Generic fallback
    return {
      success: false,
      error: 'Unable to load commentary. Please try again.'
    };
  }
}

User-Facing Error Messages

Map error codes to user-friendly messages:
const ERROR_MESSAGES = {
  unauthorized: 'Authentication failed. Please contact support.',
  not_licensed: 'This content is not available.',
  rate_limit_exceeded: 'Too many requests. Please wait a moment.',
  invalid_request: 'Invalid request. Please check your input.',
  not_found: 'Content not found.',
  internal_error: 'Something went wrong. Please try again.',
};

function getUserMessage(error) {
  return ERROR_MESSAGES[error.code] || ERROR_MESSAGES.internal_error;
}

Monitoring & Alerting

Track errors in your integration:
async function trackError(error, context = {}) {
  // Log to your monitoring service (e.g., Sentry, DataDog)
  console.error({
    service: 'unleeshed',
    code: error.code,
    message: error.message,
    status: error.status,
    ...context
  });
  
  // Alert on critical errors
  if (error.status >= 500 || error.code === 'unauthorized') {
    await alertTeam({
      severity: 'high',
      message: `Unleeshed API Error: ${error.code}`,
      details: error
    });
  }
}

Next Steps