Skip to main content

Overview

The SmartLead API uses standard HTTP status codes and structured error responses to communicate what went wrong. This guide covers how to interpret errors, implement robust retry logic, and debug the most common issues.

HTTP Status Codes

CodeMeaningAction
200SuccessRequest completed successfully
201CreatedResource created successfully
400Bad RequestFix the request payload — check required fields and data types
401UnauthorizedCheck your API key
403ForbiddenYou don’t have access to this resource
404Not FoundThe resource (campaign, lead, etc.) doesn’t exist
409ConflictDuplicate resource — the lead or campaign already exists
422Unprocessable EntityValidation failed — check field values
429Too Many RequestsRate limited — slow down and retry after the delay
500Internal Server ErrorSmartLead server issue — retry with exponential backoff
503Service UnavailableTemporary outage — retry after a short delay

Error Response Format

Error responses follow a consistent JSON structure:
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "email field is required for all leads in lead_list",
    "details": {
      "field": "lead_list[2].email",
      "constraint": "required"
    }
  }
}

Implementing Error Handling

Basic Error Handler

import requests
import os

API_KEY = os.getenv("SMARTLEAD_API_KEY")
BASE_URL = "https://server.smartlead.ai/api/v1"

def make_request(method, endpoint, payload=None):
    """Make an API request with comprehensive error handling."""
    url = f"{BASE_URL}/{endpoint}"
    params = {"api_key": API_KEY}

    try:
        if method == "GET":
            response = requests.get(url, params=params, timeout=30)
        elif method == "POST":
            response = requests.post(url, params=params, json=payload, timeout=30)
        elif method == "PATCH":
            response = requests.patch(url, params=params, json=payload, timeout=30)
        elif method == "DELETE":
            response = requests.delete(url, params=params, timeout=30)

        # Raise for HTTP errors
        response.raise_for_status()
        return response.json()

    except requests.exceptions.HTTPError as e:
        status = e.response.status_code
        error_body = e.response.json() if e.response.content else {}

        if status == 400:
            print(f"Bad request: {error_body.get('error', {}).get('message', 'Unknown')}")
        elif status == 401:
            print("Invalid API key. Check your SMARTLEAD_API_KEY.")
        elif status == 404:
            print(f"Resource not found: {endpoint}")
        elif status == 429:
            print("Rate limited. Retry after a delay.")
        elif status >= 500:
            print(f"Server error ({status}). Retry later.")
        else:
            print(f"HTTP {status}: {error_body}")

        raise

    except requests.exceptions.ConnectionError:
        print("Connection failed. Check your network.")
        raise

    except requests.exceptions.Timeout:
        print("Request timed out. Try again.")
        raise

Retry with Exponential Backoff

For transient errors (429, 500, 503), implement automatic retries:
Python
import time
import random

def make_request_with_retry(method, endpoint, payload=None, max_retries=3):
    """Make an API request with exponential backoff retry."""
    for attempt in range(max_retries + 1):
        try:
            return make_request(method, endpoint, payload)

        except requests.exceptions.HTTPError as e:
            status = e.response.status_code

            # Only retry on transient errors
            if status not in [429, 500, 502, 503]:
                raise

            if attempt == max_retries:
                print(f"Failed after {max_retries} retries")
                raise

            # Exponential backoff with jitter
            delay = (2 ** attempt) + random.uniform(0, 1)

            # Respect Retry-After header if present
            retry_after = e.response.headers.get("Retry-After")
            if retry_after:
                delay = max(delay, float(retry_after))

            print(f"Retry {attempt + 1}/{max_retries} in {delay:.1f}s...")
            time.sleep(delay)

        except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
            if attempt == max_retries:
                raise

            delay = (2 ** attempt) + random.uniform(0, 1)
            print(f"Connection issue. Retry {attempt + 1}/{max_retries} in {delay:.1f}s...")
            time.sleep(delay)
Always add random jitter to your backoff delay. Without jitter, multiple clients retrying at the same intervals will create “thundering herd” problems that amplify the load on the server.

Common Errors and Solutions

Cause: The api_key parameter is missing, expired, or incorrect.Fix: Verify your API key in SmartLead Settings → API Keys. Regenerate if needed. Ensure the key is passed as a query parameter: ?api_key=YOUR_KEY.
Cause: One or more leads in your lead_list are missing the email field.Fix: Validate your data before sending. Every lead object must have an email field with a valid email address.
# Validate before import
valid_leads = [lead for lead in leads if lead.get("email")]
invalid_leads = [lead for lead in leads if not lead.get("email")]
if invalid_leads:
    print(f"Skipping {len(invalid_leads)} leads without email")
Cause: You’re trying to import more than 400 leads in a single request.Fix: Batch your imports into groups of 400 or fewer. See the Lead Management Guide for a batching example.
Cause: The campaign ID doesn’t exist or belongs to a different account.Fix: Verify the campaign ID by listing your campaigns first:
campaigns = make_request("GET", "campaigns/")
for c in campaigns.get("campaigns", []):
    print(f"ID: {c['id']}{c['name']}")
Cause: A lead with the same email address is already in this campaign.Fix: This isn’t necessarily an error — SmartLead prevents duplicate sends. Check the skipped_leads array in the import response for details.
Cause: Too many requests in a short time period.Fix: Implement exponential backoff (see above). Check the Retry-After header for the recommended wait time. See the Rate Limits Guide for details.
Cause: An unexpected error on SmartLead’s servers.Fix: Retry with exponential backoff. If the error persists, check the SmartLead status page and contact support with the request details.

Debugging Tips

Log Every Request and Response

Python
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("smartlead")

def make_request_debug(method, endpoint, payload=None):
    url = f"{BASE_URL}/{endpoint}"
    logger.debug(f"Request: {method} {url}")
    if payload:
        logger.debug(f"Payload: {payload}")

    response = requests.request(
        method, url,
        params={"api_key": API_KEY},
        json=payload,
        timeout=30
    )

    logger.debug(f"Response: {response.status_code}")
    logger.debug(f"Body: {response.text[:500]}")

    response.raise_for_status()
    return response.json()

Validate Data Before Sending

Python
def validate_lead(lead):
    """Validate a lead object before import."""
    errors = []

    if not lead.get("email"):
        errors.append("Missing email")
    elif "@" not in lead["email"]:
        errors.append(f"Invalid email format: {lead['email']}")

    if not lead.get("first_name"):
        errors.append("Missing first_name (will affect personalization)")

    return errors

# Validate batch before import
for i, lead in enumerate(lead_list):
    errors = validate_lead(lead)
    if errors:
        print(f"Lead {i}: {', '.join(errors)}")

What’s Next?