Error handling

Understand error responses and implement robust retry logic for your API integration.

All Core API errors follow a consistent format. This guide explains each error type, what causes it, and how to handle it in your integration.

Error response format

When a request fails, the API returns an HTTP error status code and a JSON response body:

{
  "errors": [
    {
      "message": "external_id must be supplied"
    }
  ]
}

Standard structure:

  • errors — Array of error objects
  • message — Human-readable description of what went wrong

Some errors may include additional fields with context (e.g., which field failed validation).


HTTP status codes

400 Bad Request

Meaning: Your request is malformed or missing required data.

Example:

{
  "errors": [{
    "message": "external_id must be supplied"
  }]
}

Common causes:

  • Missing required fields
  • Invalid data format (e.g., string instead of number)
  • Malformed JSON in request body

How to fix:

  1. Review the error message — it tells you what's wrong
  2. Check the endpoint reference for required fields
  3. Validate your JSON syntax
  4. Ensure correct Content-Type header (application/json)

Retryable? ❌ No — Fix your request first, then retry.


401 Unauthorized

Meaning: Authentication failed or is missing.

Example:

{
  "errors": [{
    "message": "Unauthorized"
  }]
}

Common causes:

  • Missing X-Yotpo-Token header
  • Invalid or expired access token
  • Incorrect Store ID in URL path

How to fix:

  1. Verify X-Yotpo-Token header is present
  2. Check token value is correct (no extra spaces)
  3. Generate a new token: Authentication guide
  4. Verify Store ID in URL matches your token

Retryable? ❌ No — Fix authentication first.


404 Not Found

Meaning: The requested resource doesn't exist.

Example:

{
  "errors": [{
    "message": "Product not found"
  }]
}

Common causes:

  • Incorrect resource ID (typo or wrong ID)
  • Resource was deleted
  • Incorrect endpoint URL

How to fix:

  1. Verify the ID is correct
  2. Check the resource exists using a list/search endpoint
  3. Review the endpoint URL for typos

Retryable? ❌ No — The resource doesn't exist. Check your data.


422 Unprocessable Entity

Meaning: Request is well-formed, but validation failed.

Example:

{
  "errors": [{
    "message": "Currency must be valid ISO code of 3 characters"
  }]
}

Common causes:

  • Data violates business rules (e.g., invalid currency code)
  • Field value outside allowed range
  • Dependency not satisfied (e.g., creating a variant without a product)

How to fix:

  1. Read the error message for specific validation failure
  2. Review guidelines and conventions for data formats
  3. Check the endpoint documentation for field constraints
  4. Verify referenced resources exist (e.g., product exists before creating variant)

Retryable? ❌ No — Fix the data first.


429 Too Many Requests

Meaning: You've exceeded the rate limit (5 requests/second per store).

Example:

{
  "errors": [{
    "message": "Too Many Requests"
  }]
}

How to fix:

  1. Wait 1 second
  2. Retry the request
  3. Implement rate limit handling: Rate limiting guide

Retryable? ✅ Yes — Wait, then retry. Always retryable.


500 Internal Server Error

Meaning: Something went wrong on Yotpo's side.

Example:

{
  "errors": [{
    "message": "Internal server error"
  }]
}

How to handle:

  1. Wait a few seconds
  2. Retry with exponential backoff (see below)
  3. If problem persists, contact Yotpo support

Retryable? ✅ Yes — Transient error, safe to retry.


502 Bad Gateway / 503 Service Unavailable

Meaning: Yotpo's API is temporarily unavailable (maintenance, deployment, or overload).

How to handle:

  1. Wait 5-10 seconds
  2. Retry with exponential backoff
  3. If problem persists, check Yotpo status page

Retryable? ✅ Yes — Temporary issue, retry with backoff.


Retry strategies

Which errors should you retry?

Status CodeRetry?Strategy
400❌ NoFix request data
401❌ NoFix authentication
404❌ NoResource doesn't exist
422❌ NoFix validation errors
429✅ YesWait 1 second, then retry
500✅ YesExponential backoff
502✅ YesExponential backoff
503✅ YesExponential backoff

Exponential backoff

For server errors (500, 502, 503), use exponential backoff to avoid overwhelming the API:

Wait times: 1s → 2s → 4s → 8s → give up

import time
import requests

def make_request_with_retry(url, headers, max_retries=4):
    for attempt in range(max_retries):
        try:
            response = requests.get(url, headers=headers)
            
            # Success
            if response.status_code == 200:
                return response
            
            # Client errors - don't retry
            if 400 <= response.status_code < 500 and response.status_code != 429:
                raise Exception(f"Client error: {response.status_code}")
            
            # Rate limit - fixed 1s wait
            if response.status_code == 429:
                time.sleep(1)
                continue
            
            # Server error - exponential backoff
            if response.status_code >= 500:
                if attempt < max_retries - 1:
                    wait_time = 2 ** attempt
                    time.sleep(wait_time)
                    continue
                else:
                    raise Exception(f"Server error after {max_retries} retries")
        
        except requests.exceptions.RequestException as e:
            # Network error - retry with backoff
            if attempt < max_retries - 1:
                wait_time = 2 ** attempt
                time.sleep(wait_time)
            else:
                raise
    
    raise Exception("Max retries exceeded")

Idempotency and retries

Safe to retry:

  • GET requests — Read operations are always safe to retry
  • PUT requests — Updates with same payload are idempotent
  • DELETE requests — Deleting same resource twice is safe (2nd returns 404)

Retry with caution:

  • POST requests — May create duplicates if retried

See Idempotency guide for handling POST retries safely.


Best practices

1. Log errors for debugging

Capture full context when errors occur:

import logging

logger = logging.getLogger(__name__)

try:
    response = requests.post(url, json=data, headers=headers)
    response.raise_for_status()
except requests.exceptions.HTTPError as e:
    logger.error(
        f"API error: {e.response.status_code}",
        extra={
            "url": url,
            "status_code": e.response.status_code,
            "response_body": e.response.text,
            "request_data": data
        }
    )
    raise

2. Handle errors gracefully

Don't fail completely on transient errors:

def sync_products():
    try:
        products = fetch_products()
        return products
    except RateLimitError:
        logger.warning("Rate limited, using cached data")
        return load_from_cache()
    except ServerError:
        logger.error("Server error, will retry later")
        return None

3. Validate before sending

Catch errors before making API calls:

def create_product(product_data):
    # Validate required fields
    if not product_data.get("external_id"):
        raise ValueError("external_id is required")
    
    if not product_data.get("name"):
        raise ValueError("name is required")
    
    # Validate data formats
    if len(product_data.get("currency", "")) != 3:
        raise ValueError("currency must be 3-character ISO code")
    
    # Make API call
    return api_client.post("/products", data=product_data)

4. Set request timeouts

Prevent hanging requests:

try:
    response = requests.post(
        url,
        json=data,
        headers=headers,
        timeout=30  # 30 second timeout
    )
except requests.exceptions.Timeout:
    logger.error("Request timed out")
    # Retry or handle accordingly

5. Monitor error rates

Track error patterns to identify integration issues:

from collections import Counter

error_counter = Counter()

def track_error(status_code):
    error_counter[status_code] += 1
    
    # Alert if error rate is high
    if error_counter[status_code] > 100:
        alert("High error rate for status code: " + str(status_code))

Common scenarios

Handling validation errors (422)

def create_order(order_data):
    try:
        response = api_client.post("/orders", data=order_data)
        return response.json()
    except ValidationError as e:
        # Extract specific validation errors
        errors = e.response.json().get("errors", [])
        
        for error in errors:
            logger.warning(f"Validation error: {error['message']}")
        
        # Fix data and retry if possible
        if "currency" in errors[0]["message"]:
            order_data["currency"] = "USD"  # Fix
            return create_order(order_data)  # Retry
        
        raise  # Can't fix automatically

Recovering from authentication errors (401)

class APIClient:
    def __init__(self, store_id, secret):
        self.store_id = store_id
        self.secret = secret
        self.token = None
    
    def authenticate(self):
        response = requests.post(
            f"https://api.yotpo.com/core/v3/stores/{self.store_id}/access_tokens",
            json={"secret": self.secret}
        )
        self.token = response.json()["access_token"]
    
    def make_request(self, method, endpoint, **kwargs):
        if not self.token:
            self.authenticate()
        
        headers = {"X-Yotpo-Token": self.token}
        response = requests.request(method, endpoint, headers=headers, **kwargs)
        
        # Re-authenticate if token expired
        if response.status_code == 401:
            self.authenticate()
            headers = {"X-Yotpo-Token": self.token}
            response = requests.request(method, endpoint, headers=headers, **kwargs)
        
        return response

Handling server errors with retry queue

from queue import Queue
import time

retry_queue = Queue()

def process_with_retry():
    while True:
        # Get next item from queue
        if retry_queue.empty():
            time.sleep(1)
            continue
        
        item = retry_queue.get()
        
        try:
            response = make_request(item["url"], item["data"])
            
            if response.status_code >= 500:
                # Server error - requeue with backoff
                item["retry_count"] = item.get("retry_count", 0) + 1
                
                if item["retry_count"] < 4:
                    time.sleep(2 ** item["retry_count"])
                    retry_queue.put(item)
                else:
                    logger.error(f"Max retries exceeded for {item['url']}")
        
        except Exception as e:
            logger.error(f"Unhandled error: {e}")

Troubleshooting

Error message is generic

Problem: Error says "Bad Request" but doesn't specify what's wrong.

Solution:

  1. Check your JSON syntax (use a validator)
  2. Review endpoint documentation for required fields
  3. Compare your payload to working examples
  4. Enable debug logging to see full request/response

Getting 422 for valid data

Problem: Your data looks correct but validation fails.

Solution:

  1. Check data types (string vs. number, etc.)
  2. Verify ISO codes (currency, country, language)
  3. Check field length limits (see endpoint docs)
  4. Ensure referenced IDs exist (products, customers)

Random 500 errors

Problem: Same request sometimes works, sometimes returns 500.

Solution:

  1. Implement retry with exponential backoff
  2. Check if errors correlate with high traffic times
  3. If persistent, contact Yotpo support with request IDs

Summary

Key takeaways:

  • All errors follow {"errors": [{"message": "..."}]} format
  • Client errors (4xx) — Fix your request, don't retry
  • Server errors (5xx) — Retry with exponential backoff
  • 429 rate limit — Wait 1 second, then retry
  • Validate data before sending to catch errors early

Related


Did this page help you?