Skip to main content

Overview

SmartLead applies rate limits to protect the platform and ensure fair access for all users. This guide explains the rate limit structure, how to detect when you’re being throttled, and strategies for building efficient integrations.

Rate Limit Structure

SmartLead enforces rate limits on a per-API-key basis:
TierRequests per MinuteRequests per HourBurst Limit
Standard601,00010 requests/second
Pro1203,00020 requests/second
EnterpriseCustomCustomCustom
Rate limits apply to your API key across all endpoints combined. A mix of campaign, lead, and analytics requests all count toward the same limit.

Detecting Rate Limits

When you exceed the limit, the API returns a 429 Too Many Requests response:
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please retry after 30 seconds.",
    "retry_after": 30
  }
}

Rate Limit Headers

Check response headers to monitor your usage:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait before retrying (only on 429)
Python
import requests
import os

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

response = requests.get(
    f"{BASE_URL}/campaigns/",
    params={"api_key": API_KEY}
)

# Check rate limit status
limit = response.headers.get("X-RateLimit-Limit")
remaining = response.headers.get("X-RateLimit-Remaining")
reset = response.headers.get("X-RateLimit-Reset")

print(f"Limit: {limit} | Remaining: {remaining} | Resets at: {reset}")

Backoff Strategies

Exponential Backoff with Jitter

The recommended approach for handling rate limits:
Python
import time
import random

def request_with_backoff(method, endpoint, payload=None, max_retries=5):
    """Make request with exponential backoff on rate limits."""
    for attempt in range(max_retries + 1):
        response = requests.request(
            method,
            f"{BASE_URL}/{endpoint}",
            params={"api_key": API_KEY},
            json=payload,
            timeout=30
        )

        if response.status_code == 429:
            if attempt == max_retries:
                raise Exception("Rate limit exceeded after max retries")

            # Use Retry-After header if available
            retry_after = response.headers.get("Retry-After")
            if retry_after:
                delay = float(retry_after)
            else:
                # Exponential backoff: 1s, 2s, 4s, 8s, 16s
                delay = (2 ** attempt) + random.uniform(0, 1)

            print(f"Rate limited. Waiting {delay:.1f}s (attempt {attempt + 1})")
            time.sleep(delay)
            continue

        response.raise_for_status()
        return response.json()

Proactive Rate Limiting

Instead of waiting for 429s, track your usage and throttle proactively:
Python
import time
from collections import deque

class RateLimiter:
    """Client-side rate limiter to avoid 429 errors."""

    def __init__(self, max_per_minute=50):
        self.max_per_minute = max_per_minute
        self.requests = deque()

    def wait_if_needed(self):
        """Block until it's safe to make another request."""
        now = time.time()

        # Remove requests older than 60 seconds
        while self.requests and self.requests[0] < now - 60:
            self.requests.popleft()

        if len(self.requests) >= self.max_per_minute:
            # Wait until the oldest request expires
            wait_time = 60 - (now - self.requests[0])
            if wait_time > 0:
                print(f"Throttling: waiting {wait_time:.1f}s")
                time.sleep(wait_time)

        self.requests.append(time.time())

# Usage
limiter = RateLimiter(max_per_minute=50)  # Stay under the 60/min limit

for campaign_id in campaign_ids:
    limiter.wait_if_needed()
    data = make_request("GET", f"campaigns/{campaign_id}/analytics")
Set your client-side limit to 80% of the actual limit (e.g., 50 requests/minute when the limit is 60). This buffer accounts for timing differences and prevents edge-case throttling.

Optimizing Request Patterns

Batch Operations

Instead of making individual requests per lead, use batch endpoints:
Python
# Bad: 100 individual requests
for lead in leads:
    requests.post(f"{BASE_URL}/campaigns/{cid}/leads",
                  params={"api_key": API_KEY},
                  json={"lead_list": [lead]})

# Good: 1 batch request for up to 400 leads
requests.post(f"{BASE_URL}/campaigns/{cid}/leads",
              params={"api_key": API_KEY},
              json={"lead_list": leads[:400]})

Cache Responses

Cache data that doesn’t change often to reduce API calls:
Python
import functools
import time

_cache = {}

def cached_request(endpoint, ttl_seconds=300):
    """Cache GET requests for a specified TTL."""
    now = time.time()

    if endpoint in _cache:
        data, cached_at = _cache[endpoint]
        if now - cached_at < ttl_seconds:
            return data

    data = make_request("GET", endpoint)
    _cache[endpoint] = (data, now)
    return data

# Campaigns list doesn't change often — cache for 5 minutes
campaigns = cached_request("campaigns/", ttl_seconds=300)

# Analytics change frequently — cache for 1 minute
analytics = cached_request(f"campaigns/{cid}/analytics", ttl_seconds=60)

Use Webhooks Instead of Polling

Instead of polling for new replies every few seconds:
Python
# Bad: Polling every 30 seconds (2 requests/minute per campaign)
while True:
    response = requests.get(f"{BASE_URL}/campaigns/{cid}/leads",
                           params={"api_key": API_KEY, "status": "INTERESTED"})
    time.sleep(30)
Set up a webhook to receive events in real time with zero API calls:
Python
# Good: Register a webhook once, receive events instantly
requests.post(f"{BASE_URL}/webhooks",
              params={"api_key": API_KEY},
              json={
                  "webhook_url": "https://yourapp.com/hooks/smartlead",
                  "event_types": ["EMAIL_REPLIED"]
              })
See the Webhook Integration Guide for full details.

Parallelize with Rate Awareness

When you need to make many requests, use controlled concurrency:
Python
import concurrent.futures
import time

limiter = RateLimiter(max_per_minute=50)

def fetch_campaign_data(campaign_id):
    limiter.wait_if_needed()
    return make_request("GET", f"campaigns/{campaign_id}/analytics")

# Process campaigns with controlled parallelism
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    futures = {
        executor.submit(fetch_campaign_data, cid): cid
        for cid in campaign_ids
    }

    for future in concurrent.futures.as_completed(futures):
        cid = futures[future]
        try:
            data = future.result()
            print(f"Campaign {cid}: {data.get('total_sent', 0)} sent")
        except Exception as e:
            print(f"Campaign {cid} failed: {e}")

Troubleshooting

Check if another integration or script is using the same API key. Rate limits are per-key, not per-client. Consider using separate API keys for different integrations.
Review your request patterns — are you polling when you could use webhooks? Are you making individual requests when batch endpoints are available? If you genuinely need higher limits, contact SmartLead support about Enterprise plans.
Default to exponential backoff starting at 1 second. Most rate limit windows reset within 60 seconds.

What’s Next?