Error Handling
Handle API errors gracefully with consistent error responses and proper retry logic.
Overview​
Rynko API uses conventional HTTP response codes and returns structured error responses in JSON format. All errors follow a consistent schema to make error handling predictable and straightforward.
Error Response Format​
All errors return this consistent structure:
{
"statusCode": 400,
"code": "ERR_VALID_001",
"message": "Validation failed",
"timestamp": "2025-11-07T10:30:00.000Z",
"path": "/api/v1/documents/generate",
"relatedInfo": {
"errors": [
{
"field": "templateId",
"message": "Template ID is required"
}
]
}
}
Response Fields​
| Field | Type | Description |
|---|---|---|
statusCode | number | HTTP status code |
code | string | Error code (e.g., ERR_VALID_001) |
message | string | Human-readable error message |
timestamp | string | ISO 8601 timestamp |
path | string | API endpoint that generated error |
relatedInfo | object | Additional context (optional) |
HTTP Status Codes​
Success Codes (2xx)​
| Code | Description |
|---|---|
200 OK | Request successful |
201 Created | Resource created successfully |
204 No Content | Request successful, no content to return |
Client Error Codes (4xx)​
| Code | Description | When to Expect |
|---|---|---|
400 Bad Request | Invalid request data | Validation errors, malformed JSON |
401 Unauthorized | Authentication required | Missing or invalid API key |
403 Forbidden | Insufficient permissions | API key lacks required permission |
404 Not Found | Resource doesn't exist | Template, document, or endpoint not found |
409 Conflict | Resource conflict | Duplicate resource, version mismatch |
422 Unprocessable Entity | Valid syntax but semantic errors | Business logic violation |
429 Too Many Requests | Rate limit exceeded | Too many requests in time window |
Server Error Codes (5xx)​
| Code | Description | What to Do |
|---|---|---|
500 Internal Server Error | Unexpected server error | Retry with exponential backoff |
502 Bad Gateway | Upstream service error | Retry after delay |
503 Service Unavailable | Service temporarily down | Check status page, retry later |
504 Gateway Timeout | Request timeout | Retry with longer timeout |
Error Codes​
Authentication Errors (ERR_AUTH_xxx)​
{
"statusCode": 401,
"code": "ERR_AUTH_001",
"message": "You are not authorized to access this resource"
}
| Code | Message | Solution |
|---|---|---|
ERR_AUTH_001 | You are not authorized to access this resource | Check API key is correct |
ERR_AUTH_002 | Invalid email or password | Check credentials |
ERR_AUTH_003 | Your session has expired | Refresh your authentication token |
Validation Errors (ERR_VALID_xxx)​
{
"statusCode": 400,
"code": "ERR_VALID_001",
"message": "Component validation failed",
"relatedInfo": {
"errors": [
{ "field": "templateId", "message": "Template ID is required" }
]
}
}
| Code | Message |
|---|---|
ERR_VALID_001 | Component validation failed |
ERR_VALID_002 | Variable validation failed |
ERR_VALID_003 | Formula validation failed |
Template Errors (ERR_TMPL_xxx)​
{
"statusCode": 404,
"code": "ERR_TMPL_002",
"message": "Document template not found",
"relatedInfo": {
"templateId": "tmpl_invalid"
}
}
| Code | Message |
|---|---|
ERR_TMPL_001 | Template not found |
ERR_TMPL_002 | Document template not found |
ERR_TMPL_003 | Attachment template not found |
ERR_TMPL_008 | Template schema validation failed |
Quota Errors (ERR_QUOTA_xxx)​
{
"statusCode": 429,
"code": "ERR_QUOTA_008",
"message": "Monthly document generation quota exceeded",
"relatedInfo": {
"limit": 10000,
"used": 10000,
"resetsAt": "2025-12-01T00:00:00Z"
}
}
| Code | Message |
|---|---|
ERR_QUOTA_003 | Rate limit exceeded |
ERR_QUOTA_008 | Monthly document generation quota exceeded |
ERR_QUOTA_011 | Insufficient document credits |
Implementing Error Handling​
JavaScript/TypeScript​
interface ApiError {
statusCode: number;
code: string;
message: string;
timestamp: string;
path: string;
relatedInfo?: Record<string, any>;
}
async function generateDocument(data: any) {
try {
const response = await fetch('https://api.rynko.dev/api/v1/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error: ApiError = await response.json();
throw new RynkoError(error);
}
return await response.json();
} catch (error) {
if (error instanceof RynkoError) {
handleRynkoError(error);
} else {
// Network error
console.error('Network error:', error);
}
throw error;
}
}
class RynkoError extends Error {
statusCode: number;
code: string;
relatedInfo?: Record<string, any>;
constructor(apiError: ApiError) {
super(apiError.message);
this.name = 'RynkoError';
this.statusCode = apiError.statusCode;
this.code = apiError.code;
this.relatedInfo = apiError.relatedInfo;
}
}
function handleRynkoError(error: RynkoError) {
switch (error.code) {
case 'ERR_AUTH_001':
// Invalid API key
console.error('Invalid API key. Please check your credentials.');
break;
case 'ERR_QUOTA_008':
// Quota exceeded
const { limit, resetsAt } = error.relatedInfo!;
console.error(`Monthly quota of ${limit} exceeded. Resets at ${resetsAt}`);
break;
case 'ERR_VALID_001':
// Validation errors
const { errors } = error.relatedInfo!;
errors.forEach((err: any) => {
console.error(`${err.field}: ${err.message}`);
});
break;
case 'ERR_TMPL_002':
// Template not found
console.error(`Template not found: ${error.relatedInfo!.templateId}`);
break;
default:
console.error('API error:', error.message);
}
}
Python​
from typing import Dict, Any, Optional
import requests
class RynkoError(Exception):
def __init__(self, status_code: int, code: str, message: str, related_info: Optional[Dict] = None):
self.status_code = status_code
self.code = code
self.message = message
self.related_info = related_info or {}
super().__init__(self.message)
def generate_document(data: Dict[str, Any], api_key: str):
try:
response = requests.post(
'https://api.rynko.dev/api/v1/documents/generate',
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
},
json=data
)
if not response.ok:
error_data = response.json()
raise RynkoError(
status_code=error_data['statusCode'],
code=error_data['code'],
message=error_data['message'],
related_info=error_data.get('relatedInfo')
)
return response.json()
except RynkoError as e:
handle_rynko_error(e)
raise
except requests.RequestException as e:
print(f"Network error: {e}")
raise
def handle_rynko_error(error: RynkoError):
if error.code == 'ERR_AUTH_001':
print('Invalid API key. Please check your credentials.')
elif error.code == 'ERR_QUOTA_008':
limit = error.related_info.get('limit')
resets_at = error.related_info.get('resetsAt')
print(f'Monthly quota of {limit} exceeded. Resets at {resets_at}')
elif error.code == 'ERR_VALID_001':
errors = error.related_info.get('errors', [])
for err in errors:
print(f"{err['field']}: {err['message']}")
elif error.code == 'ERR_TMPL_002':
template_id = error.related_info.get('templateId')
print(f'Template not found: {template_id}')
else:
print(f'API error: {error.message}')
Retry Strategies​
Exponential Backoff​
async function generateDocumentWithRetry(data, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await generateDocument(data);
} catch (error) {
lastError = error;
// Don't retry client errors (4xx except 429)
if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
throw error;
}
// Calculate delay with exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
Retry Conditions​
Retry these errors:
500 Internal Server Error502 Bad Gateway503 Service Unavailable504 Gateway Timeout429 Too Many Requests(with backoff fromRetry-Afterheader)- Network timeouts
- Connection errors
Don't retry these:
400 Bad Request- Fix request data401 Unauthorized- Fix authentication403 Forbidden- Fix permissions404 Not Found- Resource doesn't exist409 Conflict- Fix conflict422 Unprocessable Entity- Fix business logic
Rate Limit Headers​
Rate limit responses include headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699363200
Retry-After: 60
Using Rate Limit Headers​
async function generateWithRateLimit(data) {
try {
const response = await fetch('https://api.rynko.dev/api/v1/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
// Check rate limit headers
const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '0');
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0');
const reset = parseInt(response.headers.get('X-RateLimit-Reset') || '0');
console.log(`Rate limit: ${remaining}/${limit} remaining. Resets at ${new Date(reset * 1000)}`);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
throw new Error('Rate limited');
}
if (!response.ok) {
const error = await response.json();
throw new RynkoError(error);
}
return await response.json();
} catch (error) {
console.error('Error generating document:', error);
throw error;
}
}
Best Practices​
1. Log All Errors​
function logError(error, context) {
console.error({
timestamp: new Date().toISOString(),
errorCode: error.code,
statusCode: error.statusCode,
message: error.message,
context,
relatedInfo: error.relatedInfo
});
// Send to error tracking service
errorTracker.captureException(error, { extra: context });
}
2. Provide User-Friendly Messages​
function getUserMessage(error) {
const messages = {
'ERR_AUTH_001': 'Authentication failed. Please check your API key.',
'ERR_QUOTA_008': 'You have reached your monthly document limit. Please upgrade your plan or wait until next month.',
'ERR_VALID_001': 'Please check your input and try again.',
'ERR_TMPL_002': 'The requested template was not found.',
};
return messages[error.code] || 'An unexpected error occurred. Please try again later.';
}
3. Implement Circuit Breaker​
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'closed'; // closed, open, half-open
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'open') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is open');
}
this.state = 'half-open';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'closed';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'open';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
const breaker = new CircuitBreaker();
async function generateDocumentSafely(data) {
return breaker.execute(() => generateDocument(data));
}
4. Monitor Error Rates​
const errorMetrics = {
total: 0,
byCode: {},
byStatusCode: {}
};
function trackError(error) {
errorMetrics.total++;
errorMetrics.byCode[error.code] = (errorMetrics.byCode[error.code] || 0) + 1;
errorMetrics.byStatusCode[error.statusCode] = (errorMetrics.byStatusCode[error.statusCode] || 0) + 1;
// Alert if error rate is high
if (errorMetrics.total > 100 && errorMetrics.byStatusCode[500] / errorMetrics.total > 0.1) {
alertAdmin('High server error rate');
}
}
Common Errors and Solutions​
| Error | Cause | Solution |
|---|---|---|
ERR_AUTH_001 | Invalid API key | Check API key in dashboard |
ERR_AUTH_003 | Session expired | Refresh authentication token |
ERR_AUTH_005 | Insufficient permissions | Update API key permissions |
ERR_VALID_001 | Validation failed | Check request body against API docs |
ERR_TMPL_001 | Template not found | Verify template ID exists |
ERR_TMPL_002 | Document template not found | Check template type and ID |
ERR_QUOTA_003 | Rate limit exceeded | Implement backoff strategy |
ERR_QUOTA_008 | Document quota exceeded | Upgrade plan or wait for reset |
ERR_QUOTA_011 | Insufficient credits | Purchase more credits |
500 | Server error | Retry with exponential backoff |
503 | Service unavailable | Check status page, retry later |
Related: Authentication | Rate Limiting