Skip to main content
The ShingleAI API uses conventional HTTP status codes and returns detailed error information in JSON format to help you handle errors gracefully.

Error Response Format

All error responses follow a consistent structure:
{
    "error": {
        "code": "ERROR_CODE",
        "message": "A human-readable description of the error",
        "details": {
            // Additional context (optional)
        }
    }
}
FieldTypeDescription
codestringA machine-readable error code for programmatic handling
messagestringA human-readable description of what went wrong
detailsobjectAdditional context about the error (optional)

HTTP Status Codes

StatusMeaningWhen It Occurs
200OKRequest succeeded
201CreatedResource was created successfully
204No ContentRequest succeeded with no response body (e.g., DELETE)
400Bad RequestInvalid request format or parameters
401UnauthorizedMissing or invalid API key
403ForbiddenValid API key but insufficient permissions
404Not FoundResource doesn’t exist
429Too Many RequestsRate limit exceeded
500Internal Server ErrorSomething went wrong on our end
503Service UnavailableTemporary service outage

Error Codes Reference

Authentication Errors

CodeStatusDescription
UNAUTHORIZED401Invalid or missing API key
INSUFFICIENT_PERMISSIONS403API key lacks required permissions
{
    "error": {
        "code": "UNAUTHORIZED",
        "message": "Invalid or missing API key"
    }
}

Validation Errors

CodeStatusDescription
VALIDATION_ERROR400Request body failed validation
INVALID_INPUT400Invalid query parameters or input
MISSING_QUERY400Required query parameter is missing
Validation errors include details about which fields failed:
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid request body",
        "details": {
            "issues": [
                {
                    "path": ["email"],
                    "message": "Invalid email format"
                },
                {
                    "path": ["name"],
                    "message": "Required field is missing"
                }
            ]
        }
    }
}

Resource Errors

CodeStatusDescription
NOT_FOUND404Requested resource doesn’t exist
INVALID_MESSAGE_ID400Invalid message ID format
INVALID_CONTACT_ID400Invalid contact ID format
INVALID_BUSINESS_ID400Invalid business ID format
{
    "error": {
        "code": "NOT_FOUND",
        "message": "Contact not found"
    }
}

Rate Limiting Errors

CodeStatusDescription
RATE_LIMIT_EXCEEDED429Too many requests
{
    "error": {
        "code": "RATE_LIMIT_EXCEEDED",
        "message": "Rate limit exceeded. Please retry after 60 seconds",
        "details": {
            "retryAfter": 60
        }
    }
}

Server Errors

CodeStatusDescription
INTERNAL_ERROR500Unexpected server error
SERVICE_UNAVAILABLE503Service temporarily unavailable
{
    "error": {
        "code": "INTERNAL_ERROR",
        "message": "An unexpected error occurred"
    }
}

Handling Errors

Basic Error Handling

async function makeRequest() {
    const response = await fetch('https://api.shingleai.com/v1/contacts', {
        headers: {
            'Authorization': `Bearer ${apiKey}`,
        },
    });

    if (!response.ok) {
        const { error } = await response.json();

        switch (response.status) {
            case 401:
                throw new Error('Invalid API key');
            case 403:
                throw new Error(`Permission denied: ${error.message}`);
            case 404:
                throw new Error('Resource not found');
            case 429:
                throw new Error('Rate limited - retry later');
            default:
                throw new Error(error.message || 'Request failed');
        }
    }

    return response.json();
}

Retry with Exponential Backoff

For transient errors (429, 500, 503), implement retry logic with exponential backoff:
async function fetchWithRetry(
    url: string,
    options: RequestInit,
    maxRetries = 3
): Promise<Response> {
    let lastError: Error;

    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            const response = await fetch(url, options);

            // Don't retry client errors (except rate limiting)
            if (response.ok || (response.status >= 400 && response.status < 500 && response.status !== 429)) {
                return response;
            }

            // For rate limiting, use Retry-After header if available
            if (response.status === 429) {
                const retryAfter = response.headers.get('Retry-After');
                const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
                await new Promise(resolve => setTimeout(resolve, delay));
                continue;
            }

            // For server errors, use exponential backoff
            if (response.status >= 500) {
                const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
                await new Promise(resolve => setTimeout(resolve, delay));
                continue;
            }

            return response;
        } catch (error) {
            lastError = error as Error;
            const delay = Math.pow(2, attempt) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }

    throw lastError!;
}

Best Practices

Use the code field for programmatic error handling, not the message. Error messages may change, but error codes remain stable.
Include the request ID from response headers (X-Request-ID) when logging errors to help with debugging and support requests.
Parse the details.issues array to show specific field errors to your users rather than generic error messages.