Error Handling

Error Handling Guide

This guide explains how to handle errors and edge cases when using the Spreetail Channel Integration API.

Overview

The API uses standard HTTP status codes and returns error responses in a consistent JSON format. Understanding these errors and implementing proper error handling is crucial for building robust integrations.

HTTP Status Codes

Status CodeMeaningDescription
200OKRequest successful
400Bad RequestInvalid request data or validation error
401UnauthorizedAuthentication failed or token expired
403ForbiddenInsufficient permissions
404Not FoundResource not found
409ConflictDuplicate resource (e.g., duplicate order reference)
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error
503Service UnavailableService temporarily unavailable

Error Response Format

All error responses follow this structure:

{
  "success": false,
  "error": "Error message describing what went wrong"
}

Some errors may include additional fields:

{
  "success": false,
  "error": "Validation failed",
  "details": {
    "field": "ReferenceNumber",
    "message": "ReferenceNumber is required"
  }
}

Common Errors

400 Bad Request

Invalid Request Data

{
  "success": false,
  "error": "ReferenceNumber is required"
}

Invalid Format

{
  "success": false,
  "error": "Invalid date format. Expected ISO 8601 format (YYYY-MM-DDTHH:mm:ssZ)"
}

Validation Error

{
  "success": false,
  "error": "OrderItems must contain at least one item"
}

401 Unauthorized

Missing Token

{
  "success": false,
  "error": "Authorization header is required"
}

Invalid Token

{
  "success": false,
  "error": "Invalid or expired token"
}

Invalid Credentials

{
  "success": false,
  "error": "Invalid credentials"
}

409 Conflict

Duplicate Reference Number

{
  "success": false,
  "error": "Order with ReferenceNumber 'ORD-2025-001' already exists"
}

429 Too Many Requests

Rate Limit Exceeded

{
  "success": false,
  "error": "Rate limit exceeded. Please retry after 60 seconds"
}

Error Handling Patterns

Python Example

import requests
import time
from typing import Optional, Dict, Any

class APIError(Exception):
    """Base exception for API errors"""
    def __init__(self, message: str, status_code: int = None, response: Dict = None):
        self.message = message
        self.status_code = status_code
        self.response = response
        super().__init__(self.message)

def make_request_with_retry(
    method: str,
    url: str,
    headers: Dict[str, str],
    max_retries: int = 3,
    retry_delay: int = 1,
    **kwargs
) -> Dict[str, Any]:
    """Make API request with automatic retry logic"""
    
    for attempt in range(max_retries):
        try:
            response = requests.request(method, url, headers=headers, **kwargs)
            
            # Handle rate limiting
            if response.status_code == 429:
                retry_after = int(response.headers.get('Retry-After', retry_delay * (attempt + 1)))
                if attempt < max_retries - 1:
                    time.sleep(retry_after)
                    continue
                else:
                    raise APIError(
                        "Rate limit exceeded. Please try again later.",
                        status_code=429,
                        response=response.json()
                    )
            
            # Handle authentication errors
            if response.status_code == 401:
                raise APIError(
                    "Authentication failed. Token may be expired.",
                    status_code=401,
                    response=response.json()
                )
            
            # Handle client errors (4xx)
            if 400 <= response.status_code < 500:
                error_data = response.json()
                raise APIError(
                    error_data.get('error', 'Client error'),
                    status_code=response.status_code,
                    response=error_data
                )
            
            # Handle server errors (5xx)
            if response.status_code >= 500:
                if attempt < max_retries - 1:
                    time.sleep(retry_delay * (attempt + 1))
                    continue
                else:
                    error_data = response.json() if response.content else {}
                    raise APIError(
                        "Server error. Please try again later.",
                        status_code=response.status_code,
                        response=error_data
                    )
            
            # Success
            response.raise_for_status()
            return response.json()
            
        except requests.exceptions.RequestException as e:
            if attempt < max_retries - 1:
                time.sleep(retry_delay * (attempt + 1))
                continue
            else:
                raise APIError(f"Request failed: {str(e)}")
    
    raise APIError("Max retries exceeded")

# Usage
try:
    result = make_request_with_retry(
        'POST',
        'https://api.spreetaileu.com/api/api/v1/outbound',
        headers={'Authorization': f'Bearer {token}'},
        json=order_data
    )
    print("Order created successfully:", result)
except APIError as e:
    if e.status_code == 409:
        print(f"Duplicate order: {e.message}")
    elif e.status_code == 400:
        print(f"Validation error: {e.message}")
    elif e.status_code == 401:
        print("Authentication failed - refreshing token...")
        # Refresh token logic here
    else:
        print(f"Error ({e.status_code}): {e.message}")

JavaScript/Node.js Example

class APIError extends Error {
  constructor(message, statusCode, response) {
    super(message);
    this.name = 'APIError';
    this.statusCode = statusCode;
    this.response = response;
  }
}

async function makeRequestWithRetry(
  url,
  options = {},
  maxRetries = 3,
  retryDelay = 1000
) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      
      // Handle rate limiting
      if (response.status === 429) {
        const retryAfter = parseInt(
          response.headers.get('Retry-After') || retryDelay * (attempt + 1)
        );
        if (attempt < maxRetries - 1) {
          await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
          continue;
        } else {
          const errorData = await response.json();
          throw new APIError(
            'Rate limit exceeded. Please try again later.',
            429,
            errorData
          );
        }
      }
      
      // Handle authentication errors
      if (response.status === 401) {
        const errorData = await response.json();
        throw new APIError(
          'Authentication failed. Token may be expired.',
          401,
          errorData
        );
      }
      
      // Handle client errors (4xx)
      if (response.status >= 400 && response.status < 500) {
        const errorData = await response.json();
        throw new APIError(
          errorData.error || 'Client error',
          response.status,
          errorData
        );
      }
      
      // Handle server errors (5xx)
      if (response.status >= 500) {
        if (attempt < maxRetries - 1) {
          await new Promise(resolve => 
            setTimeout(resolve, retryDelay * (attempt + 1))
          );
          continue;
        } else {
          const errorData = response.headers.get('content-type')?.includes('json')
            ? await response.json()
            : {};
          throw new APIError(
            'Server error. Please try again later.',
            response.status,
            errorData
          );
        }
      }
      
      // Success
      return await response.json();
      
    } catch (error) {
      if (error instanceof APIError) {
        throw error;
      }
      
      if (attempt < maxRetries - 1) {
        await new Promise(resolve => 
          setTimeout(resolve, retryDelay * (attempt + 1))
        );
        continue;
      } else {
        throw new APIError(`Request failed: ${error.message}`, 0, null);
      }
    }
  }
}

// Usage
try {
  const result = await makeRequestWithRetry(
    'https://api.spreetaileu.com/api/api/v1/outbound',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(orderData)
    }
  );
  console.log('Order created successfully:', result);
} catch (error) {
  if (error instanceof APIError) {
    if (error.statusCode === 409) {
      console.log(`Duplicate order: ${error.message}`);
    } else if (error.statusCode === 400) {
      console.log(`Validation error: ${error.message}`);
    } else if (error.statusCode === 401) {
      console.log('Authentication failed - refreshing token...');
      // Refresh token logic here
    } else {
      console.log(`Error (${error.statusCode}): ${error.message}`);
    }
  } else {
    console.log('Unexpected error:', error);
  }
}

Retry Strategies

Exponential Backoff

import time
import random

def exponential_backoff(attempt: int, base_delay: float = 1.0, max_delay: float = 60.0) -> float:
    """Calculate exponential backoff delay with jitter"""
    delay = min(base_delay * (2 ** attempt), max_delay)
    jitter = random.uniform(0, delay * 0.1)  # Add 10% jitter
    return delay + jitter

# Usage in retry loop
for attempt in range(max_retries):
    try:
        # Make request
        break
    except APIError as e:
        if e.status_code >= 500 and attempt < max_retries - 1:
            delay = exponential_backoff(attempt)
            time.sleep(delay)
            continue
        raise

Validation Before Requests

Prevent errors by validating data before sending:

from datetime import datetime
import re

def validate_order(order_data: Dict) -> tuple[bool, Optional[str]]:
    """Validate order data before sending to API"""
    
    # Required fields
    required_fields = [
        'ReferenceNumber', 'ReceivedDate', 'DispatchBy',
        'Status', 'Currency', 'PaymentStatus', 'ChannelBuyerName',
        'OrderItems', 'DeliveryAddress', 'BillingAddress'
    ]
    
    for field in required_fields:
        if field not in order_data:
            return False, f"{field} is required"
    
    # Validate date formats
    date_fields = ['ReceivedDate', 'DispatchBy']
    for field in date_fields:
        try:
            datetime.fromisoformat(order_data[field].replace('Z', '+00:00'))
        except (ValueError, AttributeError):
            return False, f"{field} must be in ISO 8601 format"
    
    # Validate currency code
    valid_currencies = ['EUR', 'GBP', 'USD']
    if order_data['Currency'] not in valid_currencies:
        return False, f"Currency must be one of: {', '.join(valid_currencies)}"
    
    # Validate order items
    if not order_data['OrderItems'] or len(order_data['OrderItems']) == 0:
        return False, "OrderItems must contain at least one item"
    
    for item in order_data['OrderItems']:
        if 'SKU' not in item or 'Qty' not in item:
            return False, "Each OrderItem must have SKU and Qty"
        if item['Qty'] <= 0:
            return False, "OrderItem Qty must be greater than 0"
    
    return True, None

# Usage
is_valid, error = validate_order(order_data)
if not is_valid:
    print(f"Validation failed: {error}")
else:
    # Proceed with API call
    result = create_order(order_data)

Best Practices

  1. Always Check Response Status: Don't assume requests succeed
  2. Implement Retry Logic: For transient errors (5xx, 429)
  3. Validate Before Sending: Catch validation errors early
  4. Log Errors: Log errors with context for debugging
  5. Handle Specific Errors: Different error types need different handling
  6. Respect Rate Limits: Implement backoff for 429 errors
  7. Refresh Tokens: Automatically refresh expired tokens

Support

For persistent errors or questions:

  • Email: [email protected]
  • Include: Error message, status code, request details, and timestamp