Appearance
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
- HTTP Status Codes
- Error Categories
- Common Error Codes
- Validation Errors
- Business Logic Errors
- System Errors
- Rate Limiting Errors
- Error Resolution Strategies
- Retry Strategies
- Logging and Debugging
- Error Handling Examples
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
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable error message |
details | array | Field-specific error details (optional) |
requestId | string | Unique request identifier for support |
timestamp | string | ISO 8601 timestamp of error |
documentation | string | Link to error documentation (optional) |
HTTP Status Codes
The API uses standard HTTP status codes to indicate success or failure:
Success Codes (2xx)
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 202 | Accepted | Request accepted for processing |
| 204 | No Content | Request succeeded with no response body |
Client Error Codes (4xx)
| Code | Status | Description | Common Causes |
|---|---|---|---|
| 400 | Bad Request | Invalid request syntax | Malformed JSON, missing required headers |
| 401 | Unauthorized | Authentication failed | Invalid/expired token, missing credentials |
| 403 | Forbidden | Access denied | Insufficient permissions, IP not whitelisted |
| 404 | Not Found | Resource not found | Invalid ID, deleted resource |
| 405 | Method Not Allowed | HTTP method not supported | Wrong HTTP verb for endpoint |
| 409 | Conflict | Resource conflict | Duplicate creation, concurrent modification |
| 410 | Gone | Resource permanently deleted | Accessing deleted resource |
| 422 | Unprocessable Entity | Validation failed | Business rule violations |
| 429 | Too Many Requests | Rate limit exceeded | Too many requests in time window |
Server Error Codes (5xx)
| Code | Status | Description | Action Required |
|---|---|---|---|
| 500 | Internal Server Error | Unexpected server error | Retry with exponential backoff |
| 502 | Bad Gateway | Invalid upstream response | Retry after brief delay |
| 503 | Service Unavailable | Service temporarily down | Check status page, retry later |
| 504 | Gateway Timeout | Request timeout | Retry 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 secretINVALID_TOKEN- Token is malformed or invalidTOKEN_EXPIRED- Access token has expiredINSUFFICIENT_SCOPE- Token lacks required permissionsTENANT_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 missingINVALID_FORMAT- Field format is incorrectINVALID_LENGTH- String length violationINVALID_RANGE- Numeric value out of rangeINVALID_ENUM- Value not in allowed listINVALID_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 existsINVOICE_ALREADY_PAID- Cannot modify paid invoiceINSUFFICIENT_FUNDS- Insufficient balance or creditPAYMENT_DECLINED- Payment gateway declinedSUBSCRIPTION_EXPIRED- Subscription has expiredQUOTA_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 errorDATABASE_ERROR- Database operation failedGATEWAY_ERROR- Payment gateway errorTIMEOUT_ERROR- Operation timed outDEPENDENCY_ERROR- External service error
Common Error Codes
Complete Error Code Reference
| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | 422 | Request data validation failed | Check field requirements |
INVALID_CREDENTIALS | 401 | Authentication credentials invalid | Verify client ID/secret |
TOKEN_EXPIRED | 401 | Access token has expired | Refresh token |
INSUFFICIENT_PERMISSIONS | 403 | User lacks required permissions | Check user roles |
RESOURCE_NOT_FOUND | 404 | Requested resource doesn't exist | Verify resource ID |
DUPLICATE_RESOURCE | 409 | Resource already exists | Use different identifier |
RATE_LIMIT_EXCEEDED | 429 | Too many requests | Implement backoff |
PAYMENT_DECLINED | 422 | Payment was declined | Check payment details |
INVOICE_LOCKED | 422 | Invoice cannot be modified | Invoice is finalized |
INSUFFICIENT_BALANCE | 422 | Insufficient account balance | Add funds or credit |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable | Retry 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: 300Error Resolution Strategies
Validation Error Resolution
- Check Required Fields: Ensure all required fields are present
- Validate Data Types: Confirm correct data types (string, number, boolean)
- Check Format: Verify date formats, email formats, etc.
- Validate Ranges: Ensure numeric values are within allowed ranges
- Check Enum Values: Verify values are from allowed lists
Authentication Error Resolution
- Token Expiry: Implement automatic token refresh
- Invalid Credentials: Verify client ID and secret
- Permission Issues: Check user roles and permissions
- Tenant Mismatch: Ensure correct tenant code
Business Logic Error Resolution
- Read Error Details: Understand the specific business rule violated
- Check Prerequisites: Ensure all preconditions are met
- Verify State: Check resource is in correct state for operation
- 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_codesLogging 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-idError 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.
