Fetch the complete documentation index at: https://eulabel.eu/docs/llms.txt Use this file to discover all available pages before exploring further. Full content: https://eulabel.eu/docs/llms-full.txt Append .md to any page URL for markdown, or send Accept: text/markdown.

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 error codes and status for 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
}
FieldTypeDescription
errorstringMachine-readable error code
messagestringHuman-readable explanation
suggestionstringActionable guidance (when available)
statusnumberHTTP status code

Error codes reference

Client errors (4xx)

CodeErrorWhen it occurs
400bad_requestMalformed JSON, missing required fields
401unauthorizedMissing or invalid API key
403forbiddenValid key but insufficient scopes
404not_foundResource does not exist
409conflictDuplicate GTIN or resource already exists
422validation_errorData fails validation (invalid GTIN, missing nutrition, etc.)
429rate_limitedToo many requests

Server errors (5xx)

CodeErrorWhen it occurs
500internal_errorUnexpected server failure
503service_unavailableTemporary 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

PlanRequests/minuteBurst
Free6010
Pro600100
EnterpriseCustomCustom

When rate limited, the response includes a Retry-After header indicating how many seconds to wait before retrying.

Next steps

On this page