Skip to content

Error Handling Guide

Comprehensive guide to understanding and handling errors from the Engagifii Revenue API. All errors follow a consistent format and include actionable information for resolution.

Table of Contents

Error Response Format

All API errors return a consistent JSON structure:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid data",
    "details": [
      {
        "field": "amount",
        "message": "Amount must be greater than 0",
        "code": "INVALID_AMOUNT"
      },
      {
        "field": "dueDate",
        "message": "Due date cannot be in the past",
        "code": "INVALID_DATE"
      }
    ],
    "requestId": "req-123e4567-e89b-12d3-a456-426614174000",
    "timestamp": "2024-01-20T10:30:45Z",
    "documentation": "https://docs.engagifii.com/errors/VALIDATION_ERROR"
  }
}

Error Object Fields

FieldTypeDescription
codestringMachine-readable error code
messagestringHuman-readable error message
detailsarrayField-specific error details (optional)
requestIdstringUnique request identifier for support
timestampstringISO 8601 timestamp of error
documentationstringLink to error documentation (optional)

HTTP Status Codes

The API uses standard HTTP status codes to indicate success or failure:

Success Codes (2xx)

CodeStatusDescription
200OKRequest succeeded
201CreatedResource created successfully
202AcceptedRequest accepted for processing
204No ContentRequest succeeded with no response body

Client Error Codes (4xx)

CodeStatusDescriptionCommon Causes
400Bad RequestInvalid request syntaxMalformed JSON, missing required headers
401UnauthorizedAuthentication failedInvalid/expired token, missing credentials
403ForbiddenAccess deniedInsufficient permissions, IP not whitelisted
404Not FoundResource not foundInvalid ID, deleted resource
405Method Not AllowedHTTP method not supportedWrong HTTP verb for endpoint
409ConflictResource conflictDuplicate creation, concurrent modification
410GoneResource permanently deletedAccessing deleted resource
422Unprocessable EntityValidation failedBusiness rule violations
429Too Many RequestsRate limit exceededToo many requests in time window

Server Error Codes (5xx)

CodeStatusDescriptionAction Required
500Internal Server ErrorUnexpected server errorRetry with exponential backoff
502Bad GatewayInvalid upstream responseRetry after brief delay
503Service UnavailableService temporarily downCheck status page, retry later
504Gateway TimeoutRequest timeoutRetry with smaller payload

Error Categories

Authentication Errors

Errors related to authentication and authorization.

json
{
  "error": {
    "code": "INVALID_TOKEN",
    "message": "The access token is invalid or has expired",
    "details": [
      {
        "message": "Token expired at 2024-01-20T09:30:00Z",
        "code": "TOKEN_EXPIRED"
      }
    ]
  }
}

Common Authentication Error Codes:

  • INVALID_CREDENTIALS - Invalid client ID or secret
  • INVALID_TOKEN - Token is malformed or invalid
  • TOKEN_EXPIRED - Access token has expired
  • INSUFFICIENT_SCOPE - Token lacks required permissions
  • TENANT_MISMATCH - Token tenant doesn't match request

Validation Errors

Errors when request data fails validation.

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Email address is not valid",
        "code": "INVALID_EMAIL"
      },
      {
        "field": "items[0].quantity",
        "message": "Quantity must be greater than 0",
        "code": "INVALID_QUANTITY"
      }
    ]
  }
}

Common Validation Error Codes:

  • REQUIRED_FIELD - Required field is missing
  • INVALID_FORMAT - Field format is incorrect
  • INVALID_LENGTH - String length violation
  • INVALID_RANGE - Numeric value out of range
  • INVALID_ENUM - Value not in allowed list
  • INVALID_DATE - Date format or range error

Business Logic Errors

Errors due to business rule violations.

json
{
  "error": {
    "code": "INSUFFICIENT_FUNDS",
    "message": "Customer has insufficient credit balance",
    "details": [
      {
        "field": "amount",
        "message": "Requested amount: $500.00, Available credit: $250.00",
        "code": "CREDIT_EXCEEDED"
      }
    ]
  }
}

Common Business Error Codes:

  • DUPLICATE_INVOICE - Invoice number already exists
  • INVOICE_ALREADY_PAID - Cannot modify paid invoice
  • INSUFFICIENT_FUNDS - Insufficient balance or credit
  • PAYMENT_DECLINED - Payment gateway declined
  • SUBSCRIPTION_EXPIRED - Subscription has expired
  • QUOTA_EXCEEDED - Usage limit exceeded

System Errors

Errors due to system issues.

json
{
  "error": {
    "code": "SERVICE_ERROR",
    "message": "A system error occurred. Please try again later",
    "requestId": "req-123e4567-e89b-12d3-a456-426614174000"
  }
}

Common System Error Codes:

  • SERVICE_ERROR - Generic service error
  • DATABASE_ERROR - Database operation failed
  • GATEWAY_ERROR - Payment gateway error
  • TIMEOUT_ERROR - Operation timed out
  • DEPENDENCY_ERROR - External service error

Common Error Codes

Complete Error Code Reference

Error CodeHTTP StatusDescriptionResolution
VALIDATION_ERROR422Request data validation failedCheck field requirements
INVALID_CREDENTIALS401Authentication credentials invalidVerify client ID/secret
TOKEN_EXPIRED401Access token has expiredRefresh token
INSUFFICIENT_PERMISSIONS403User lacks required permissionsCheck user roles
RESOURCE_NOT_FOUND404Requested resource doesn't existVerify resource ID
DUPLICATE_RESOURCE409Resource already existsUse different identifier
RATE_LIMIT_EXCEEDED429Too many requestsImplement backoff
PAYMENT_DECLINED422Payment was declinedCheck payment details
INVOICE_LOCKED422Invoice cannot be modifiedInvoice is finalized
INSUFFICIENT_BALANCE422Insufficient account balanceAdd funds or credit
SERVICE_UNAVAILABLE503Service temporarily unavailableRetry later

Validation Errors

Field Validation Rules

Each field has specific validation rules that generate errors when violated:

Amount Fields

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "details": [
      {
        "field": "amount",
        "message": "Amount must be between 0.01 and 999999.99",
        "code": "INVALID_RANGE",
        "min": 0.01,
        "max": 999999.99
      }
    ]
  }
}

Date Fields

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "details": [
      {
        "field": "dueDate",
        "message": "Date must be in ISO 8601 format (YYYY-MM-DD)",
        "code": "INVALID_DATE_FORMAT",
        "example": "2024-01-20"
      }
    ]
  }
}

Email Fields

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "details": [
      {
        "field": "email",
        "message": "Email address is not valid",
        "code": "INVALID_EMAIL",
        "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
      }
    ]
  }
}

Business Logic Errors

Invoice State Errors

json
{
  "error": {
    "code": "INVALID_INVOICE_STATE",
    "message": "Cannot perform this action on invoice in current state",
    "details": [
      {
        "currentState": "Paid",
        "allowedStates": ["Draft", "Sent"],
        "action": "modify"
      }
    ]
  }
}

Payment Errors

json
{
  "error": {
    "code": "PAYMENT_PROCESSING_ERROR",
    "message": "Payment could not be processed",
    "details": [
      {
        "gatewayCode": "card_declined",
        "gatewayMessage": "Your card was declined",
        "suggestion": "Please use a different payment method"
      }
    ]
  }
}

Subscription Errors

json
{
  "error": {
    "code": "SUBSCRIPTION_ERROR",
    "message": "Cannot modify subscription",
    "details": [
      {
        "reason": "Subscription is cancelled",
        "suggestion": "Create a new subscription instead"
      }
    ]
  }
}

System Errors

Database Errors

json
{
  "error": {
    "code": "DATABASE_ERROR",
    "message": "A database error occurred",
    "requestId": "req-123e4567-e89b-12d3-a456-426614174000",
    "suggestion": "Please retry the request"
  }
}

Timeout Errors

json
{
  "error": {
    "code": "TIMEOUT_ERROR",
    "message": "The request timed out",
    "details": [
      {
        "timeout": 30000,
        "suggestion": "Try with a smaller payload or retry later"
      }
    ]
  }
}

Rate Limiting Errors

Rate Limit Response

json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "API rate limit exceeded",
    "details": [
      {
        "limit": 60,
        "remaining": 0,
        "resetTime": "2024-01-20T11:00:00Z",
        "resetAfter": 300
      }
    ]
  }
}

Rate Limit Headers

http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1609459200
X-RateLimit-Reset-After: 300
Retry-After: 300

Error Resolution Strategies

Validation Error Resolution

  1. Check Required Fields: Ensure all required fields are present
  2. Validate Data Types: Confirm correct data types (string, number, boolean)
  3. Check Format: Verify date formats, email formats, etc.
  4. Validate Ranges: Ensure numeric values are within allowed ranges
  5. Check Enum Values: Verify values are from allowed lists

Authentication Error Resolution

  1. Token Expiry: Implement automatic token refresh
  2. Invalid Credentials: Verify client ID and secret
  3. Permission Issues: Check user roles and permissions
  4. Tenant Mismatch: Ensure correct tenant code

Business Logic Error Resolution

  1. Read Error Details: Understand the specific business rule violated
  2. Check Prerequisites: Ensure all preconditions are met
  3. Verify State: Check resource is in correct state for operation
  4. Review Documentation: Consult API docs for business rules

Retry Strategies

Exponential Backoff

Implement exponential backoff for transient errors:

javascript
async function apiCallWithRetry(url, options, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      
      if (response.ok) {
        return response.json();
      }
      
      const error = await response.json();
      
      // Don't retry client errors (except rate limits)
      if (response.status >= 400 && response.status < 500) {
        if (response.status !== 429) {
          throw error;
        }
      }
      
      // Calculate backoff
      const backoff = Math.min(
        1000 * Math.pow(2, attempt) + Math.random() * 1000,
        30000
      );
      
      console.log(`Retry ${attempt + 1}/${maxRetries} after ${backoff}ms`);
      await new Promise(resolve => setTimeout(resolve, backoff));
      
      lastError = error;
    } catch (err) {
      lastError = err;
      
      // Network errors - retry
      if (attempt < maxRetries - 1) {
        const backoff = 1000 * Math.pow(2, attempt);
        await new Promise(resolve => setTimeout(resolve, backoff));
      }
    }
  }
  
  throw lastError;
}

Retry Configuration

python
import time
import random
from typing import Optional, Callable

class RetryConfig:
    def __init__(
        self,
        max_retries: int = 3,
        base_delay: float = 1.0,
        max_delay: float = 60.0,
        exponential_base: float = 2.0,
        jitter: bool = True
    ):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.exponential_base = exponential_base
        self.jitter = jitter
    
    def get_delay(self, attempt: int) -> float:
        """Calculate delay for retry attempt."""
        delay = min(
            self.base_delay * (self.exponential_base ** attempt),
            self.max_delay
        )
        
        if self.jitter:
            delay *= (0.5 + random.random())
        
        return delay

def should_retry(status_code: int, error_code: Optional[str]) -> bool:
    """Determine if request should be retried."""
    # Retry server errors
    if status_code >= 500:
        return True
    
    # Retry rate limits
    if status_code == 429:
        return True
    
    # Retry specific error codes
    retryable_codes = [
        'TIMEOUT_ERROR',
        'SERVICE_UNAVAILABLE',
        'GATEWAY_ERROR'
    ]
    
    return error_code in retryable_codes

Logging and Debugging

Structured Error Logging

javascript
class ErrorLogger {
  logError(error, context) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level: 'ERROR',
      errorCode: error.error?.code,
      errorMessage: error.error?.message,
      requestId: error.error?.requestId,
      httpStatus: context.statusCode,
      endpoint: context.endpoint,
      method: context.method,
      userId: context.userId,
      tenantId: context.tenantId,
      details: error.error?.details,
      stack: error.stack
    };
    
    // Log to structured logging service
    console.error(JSON.stringify(logEntry));
    
    // Send to error tracking service
    this.sendToErrorTracking(logEntry);
    
    // Store for debugging
    this.storeErrorForDebugging(logEntry);
  }
  
  sendToErrorTracking(logEntry) {
    // Send to Sentry, Rollbar, etc.
    if (this.isUserError(logEntry.errorCode)) {
      // Lower priority for user errors
      trackingService.logInfo(logEntry);
    } else {
      // High priority for system errors
      trackingService.logError(logEntry);
    }
  }
  
  storeErrorForDebugging(logEntry) {
    // Store recent errors for debugging
    const key = `error:${logEntry.requestId}`;
    cache.set(key, logEntry, 3600); // Store for 1 hour
  }
  
  isUserError(errorCode) {
    const userErrors = [
      'VALIDATION_ERROR',
      'INVALID_CREDENTIALS',
      'INSUFFICIENT_PERMISSIONS'
    ];
    return userErrors.includes(errorCode);
  }
}

Debug Headers

Include debug headers in requests for better troubleshooting:

http
X-Request-ID: unique-request-id
X-Debug-Mode: true
X-Correlation-ID: trace-id

Error Handling Examples

JavaScript/TypeScript

typescript
class RevenueAPIClient {
  async handleAPIError(response: Response): Promise<never> {
    const errorBody = await response.json().catch(() => ({}));
    
    const error = new APIError(
      errorBody.error?.message || 'An error occurred',
      errorBody.error?.code || 'UNKNOWN_ERROR',
      response.status,
      errorBody.error?.details,
      errorBody.error?.requestId
    );
    
    // Log error
    console.error(`API Error [${error.code}]: ${error.message}`, {
      status: error.status,
      requestId: error.requestId,
      details: error.details
    });
    
    // Handle specific errors
    switch (error.code) {
      case 'TOKEN_EXPIRED':
        await this.refreshToken();
        throw new RetryableError(error);
        
      case 'RATE_LIMIT_EXCEEDED':
        const resetAfter = errorBody.error?.details?.[0]?.resetAfter || 60;
        await this.waitForRateLimit(resetAfter);
        throw new RetryableError(error);
        
      case 'VALIDATION_ERROR':
        throw new ValidationError(error);
        
      default:
        throw error;
    }
  }
  
  async makeRequest(endpoint: string, options: RequestInit): Promise<any> {
    try {
      const response = await fetch(endpoint, options);
      
      if (!response.ok) {
        await this.handleAPIError(response);
      }
      
      return response.json();
    } catch (error) {
      if (error instanceof RetryableError) {
        // Retry logic
        return this.makeRequest(endpoint, options);
      }
      
      throw error;
    }
  }
}

class APIError extends Error {
  constructor(
    message: string,
    public code: string,
    public status: number,
    public details?: any[],
    public requestId?: string
  ) {
    super(message);
    this.name = 'APIError';
  }
}

class RetryableError extends APIError {
  constructor(error: APIError) {
    super(error.message, error.code, error.status, error.details, error.requestId);
    this.name = 'RetryableError';
  }
}

class ValidationError extends APIError {
  constructor(error: APIError) {
    super(error.message, error.code, error.status, error.details, error.requestId);
    this.name = 'ValidationError';
  }
}

Python

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

logger = logging.getLogger(__name__)

class APIError(Exception):
    def __init__(self, message: str, code: str, status: int, 
                 details: Optional[list] = None, 
                 request_id: Optional[str] = None):
        super().__init__(message)
        self.code = code
        self.status = status
        self.details = details or []
        self.request_id = request_id

class ValidationError(APIError):
    pass

class AuthenticationError(APIError):
    pass

class RateLimitError(APIError):
    def __init__(self, *args, reset_after: int = 60, **kwargs):
        super().__init__(*args, **kwargs)
        self.reset_after = reset_after

class RevenueAPIClient:
    def handle_error_response(self, response: requests.Response):
        """Parse and handle error responses."""
        try:
            error_body = response.json()
            error_info = error_body.get('error', {})
        except ValueError:
            error_info = {}
        
        message = error_info.get('message', 'An error occurred')
        code = error_info.get('code', 'UNKNOWN_ERROR')
        details = error_info.get('details', [])
        request_id = error_info.get('requestId')
        
        # Log the error
        logger.error(
            f"API Error [{code}]: {message}",
            extra={
                'status': response.status_code,
                'request_id': request_id,
                'details': details
            }
        )
        
        # Raise specific error types
        if response.status_code == 401:
            raise AuthenticationError(message, code, response.status_code, details, request_id)
        elif response.status_code == 422:
            raise ValidationError(message, code, response.status_code, details, request_id)
        elif response.status_code == 429:
            reset_after = int(response.headers.get('X-RateLimit-Reset-After', 60))
            raise RateLimitError(message, code, response.status_code, details, request_id, reset_after=reset_after)
        else:
            raise APIError(message, code, response.status_code, details, request_id)
    
    def make_request(self, method: str, endpoint: str, 
                    max_retries: int = 3, **kwargs) -> Dict[str, Any]:
        """Make API request with error handling and retries."""
        
        for attempt in range(max_retries):
            try:
                response = requests.request(method, endpoint, **kwargs)
                response.raise_for_status()
                return response.json()
                
            except requests.HTTPError:
                self.handle_error_response(response)
                
            except AuthenticationError:
                # Refresh token and retry
                self.refresh_authentication()
                if attempt < max_retries - 1:
                    continue
                raise
                
            except RateLimitError as e:
                # Wait for rate limit reset
                if attempt < max_retries - 1:
                    logger.info(f"Rate limited, waiting {e.reset_after} seconds")
                    time.sleep(e.reset_after)
                    continue
                raise
                
            except ValidationError:
                # Don't retry validation errors
                raise
                
            except APIError as e:
                # Retry server errors
                if e.status >= 500 and attempt < max_retries - 1:
                    delay = 2 ** attempt
                    logger.info(f"Server error, retrying after {delay} seconds")
                    time.sleep(delay)
                    continue
                raise
                
            except requests.RequestException as e:
                # Network errors - retry
                if attempt < max_retries - 1:
                    delay = 2 ** attempt
                    logger.warning(f"Network error, retrying after {delay} seconds: {e}")
                    time.sleep(delay)
                    continue
                raise
        
        raise Exception(f"Max retries ({max_retries}) exceeded")

C#/.NET

csharp
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Polly;
using Polly.Extensions.Http;

public class APIError : Exception
{
    public string Code { get; }
    public int StatusCode { get; }
    public object[] Details { get; }
    public string RequestId { get; }
    
    public APIError(string message, string code, int statusCode, 
                   object[] details = null, string requestId = null) 
        : base(message)
    {
        Code = code;
        StatusCode = statusCode;
        Details = details ?? new object[0];
        RequestId = requestId;
    }
}

public class RevenueAPIClient
{
    private readonly HttpClient httpClient;
    private readonly IAsyncPolicy<HttpResponseMessage> retryPolicy;
    
    public RevenueAPIClient()
    {
        httpClient = new HttpClient();
        
        // Configure retry policy with exponential backoff
        retryPolicy = HttpPolicyExtensions
            .HandleTransientHttpError()
            .OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests)
            .WaitAndRetryAsync(
                3,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    var msg = $"Retry {retryCount} after {timespan} seconds";
                    Console.WriteLine(msg);
                });
    }
    
    private async Task HandleErrorResponse(HttpResponseMessage response)
    {
        var content = await response.Content.ReadAsStringAsync();
        dynamic errorBody = JsonConvert.DeserializeObject(content);
        
        var error = errorBody?.error;
        var message = error?.message ?? "An error occurred";
        var code = error?.code ?? "UNKNOWN_ERROR";
        var details = error?.details?.ToObject<object[]>() ?? new object[0];
        var requestId = error?.requestId?.ToString();
        
        throw new APIError(message, code, (int)response.StatusCode, details, requestId);
    }
    
    public async Task<T> MakeRequest<T>(HttpMethod method, string endpoint, object data = null)
    {
        var request = new HttpRequestMessage(method, endpoint);
        
        if (data != null)
        {
            request.Content = new StringContent(
                JsonConvert.SerializeObject(data),
                System.Text.Encoding.UTF8,
                "application/json");
        }
        
        try
        {
            var response = await retryPolicy.ExecuteAsync(
                async () => await httpClient.SendAsync(request));
            
            if (!response.IsSuccessStatusCode)
            {
                await HandleErrorResponse(response);
            }
            
            var responseContent = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<T>(responseContent);
        }
        catch (APIError ex)
        {
            // Handle specific error codes
            switch (ex.Code)
            {
                case "VALIDATION_ERROR":
                    Console.WriteLine($"Validation failed: {ex.Message}");
                    foreach (var detail in ex.Details)
                    {
                        Console.WriteLine($"  - {detail}");
                    }
                    break;
                    
                case "TOKEN_EXPIRED":
                    await RefreshToken();
                    // Retry the request
                    return await MakeRequest<T>(method, endpoint, data);
                    
                default:
                    Console.WriteLine($"API Error [{ex.Code}]: {ex.Message}");
                    break;
            }
            
            throw;
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Network error: {ex.Message}");
            throw;
        }
    }
}

Testing Error Handling

Test Error Scenarios

javascript
// Test suite for error handling
describe('Error Handling', () => {
  it('should handle validation errors', async () => {
    const response = await api.createInvoice({
      amount: -100  // Invalid amount
    });
    
    expect(response.error.code).toBe('VALIDATION_ERROR');
    expect(response.error.details[0].field).toBe('amount');
  });
  
  it('should retry on rate limit', async () => {
    // Trigger rate limit
    const promises = Array(100).fill().map(() => 
      api.getInvoice('test-id')
    );
    
    const results = await Promise.allSettled(promises);
    const rateLimited = results.filter(r => 
      r.status === 'rejected' && 
      r.reason.code === 'RATE_LIMIT_EXCEEDED'
    );
    
    expect(rateLimited.length).toBeGreaterThan(0);
  });
  
  it('should handle network errors', async () => {
    // Simulate network error
    const api = new API({ timeout: 1 });
    
    await expect(api.getInvoice('test-id'))
      .rejects
      .toThrow('TIMEOUT_ERROR');
  });
});

For additional support with error handling, contact our technical support team with the request ID from the error response.