Error Handling
Handle API errors gracefully with structured error responses and retry logic.
The EUlabel API returns structured error responses for all failure cases. This guide covers the error format, common error codes, and recommended retry strategies.
At a glance
- Every error is structured:
error,message,suggestion,status. - Don’t retry 4xx: fix the request; retry only transient failures (429/5xx).
- Design for observability: log
errorcodes andstatusfor support.
Error response format
All errors follow a consistent JSON structure:
{
"error": "validation_error",
"message": "GTIN check digit is invalid. Expected 0, got 5.",
"suggestion": "Verify the GTIN using the check digit algorithm at https://eulabel.eu/docs/guides/gtin-validation",
"status": 422
}| Field | Type | Description |
|---|---|---|
error | string | Machine-readable error code |
message | string | Human-readable explanation |
suggestion | string | Actionable guidance (when available) |
status | number | HTTP status code |
Error codes reference
Client errors (4xx)
| Code | Error | When it occurs |
|---|---|---|
| 400 | bad_request | Malformed JSON, missing required fields |
| 401 | unauthorized | Missing or invalid API key |
| 403 | forbidden | Valid key but insufficient scopes |
| 404 | not_found | Resource does not exist |
| 409 | conflict | Duplicate GTIN or resource already exists |
| 422 | validation_error | Data fails validation (invalid GTIN, missing nutrition, etc.) |
| 429 | rate_limited | Too many requests |
Server errors (5xx)
| Code | Error | When it occurs |
|---|---|---|
| 500 | internal_error | Unexpected server failure |
| 503 | service_unavailable | Temporary maintenance or overload |
Handling errors in code
const API_KEY = process.env.EULABEL_API_KEY;
async function createProduct(data) {
const response = await fetch('https://api.eulabel.eu/v1/products', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
switch (error.error) {
case 'validation_error':
console.error(`Validation failed: ${error.message}`);
if (error.suggestion) console.log(`Tip: ${error.suggestion}`);
break;
case 'rate_limited':
const retryAfter = response.headers.get('Retry-After');
console.log(`Rate limited. Retry after ${retryAfter}s`);
break;
case 'unauthorized':
throw new Error('Invalid API key. Check your credentials.');
default:
throw new Error(`API error: ${error.message}`);
}
return null;
}
return response.json();
}import requests
import time
API_KEY = "sk_test_..."
def create_product(data: dict) -> dict | None:
response = requests.post(
"https://api.eulabel.eu/v1/products",
headers={"Authorization": f"Bearer {API_KEY}"},
json=data,
)
if not response.ok:
error = response.json()
if error["error"] == "validation_error":
print(f"Validation failed: {error['message']}")
if "suggestion" in error:
print(f"Tip: {error['suggestion']}")
elif error["error"] == "rate_limited":
retry_after = int(response.headers.get("Retry-After", 60))
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
elif error["error"] == "unauthorized":
raise Exception("Invalid API key.")
else:
raise Exception(f"API error: {error['message']}")
return None
return response.json()Retry strategy
Only retry transient errors (429, 500, 503). Retrying 4xx errors like validation_error or unauthorized will always produce the same result and waste your rate limit budget.
For transient errors (429, 500, 503), use exponential backoff:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.ok) return response.json();
if (response.status === 429 || response.status >= 500) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(1000 * 2 ** attempt, 30000);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
const error = await response.json();
throw new Error(`${error.error}: ${error.message}`);
}
}import time
import requests
def fetch_with_retry(url: str, max_retries: int = 3, **kwargs) -> dict:
for attempt in range(max_retries + 1):
response = requests.request(**kwargs, url=url)
if response.ok:
return response.json()
if response.status_code in (429, 500, 503):
retry_after = response.headers.get("Retry-After")
delay = int(retry_after) if retry_after else min(2 ** attempt, 30)
if attempt < max_retries:
time.sleep(delay)
continue
error = response.json()
raise Exception(f"{error['error']}: {error['message']}")Rate limits
| Plan | Requests/minute | Burst |
|---|---|---|
| Free | 60 | 10 |
| Pro | 600 | 100 |
| Enterprise | Custom | Custom |
When rate limited, the response includes a Retry-After header indicating how many seconds to wait before retrying.