Errors & Rate Limits

This page explains how BuyerCaddy API reports errors, how to handle 429 (throttling), and how to build robust retries with backoff and jitter.

Audience: developers/integrators.
Applies to all endpoints under https://api.salescaddy.ai/api.


Error model

BuyerCaddy returns standard HTTP status codes. Error payloads commonly follow this shape:

[
  { "message": "Description of error" }
]

Notes

  • Some endpoints can return a single object instead of an array; always check Content-Type and status.
  • For 401/403 you might get a minimal message from the gateway/IdP.

Typical HTTP status codes

CodeMeaningWhat to do
400Bad RequestValidate required params/body schema; check types and ranges.
401UnauthorizedObtain/refresh OAuth token; include Authorization: Bearer <token>.
403ForbiddenYour client is authenticated but not allowed; verify account configuration.
404Not FoundIdentifier (e.g., companyDomain, productId) is wrong or data is unavailable.
409ConflictThe requested operation conflicts with the current state (rare).
415Unsupported MediaCheck Content-Type (e.g., application/json).
422Unprocessable EntityBody parsed but invalid (semantic validation failed).
429Rate Limit ExceededBack off and retry after a delay (see below).
500Internal ErrorRetry with backoff; contact support if persistent.
503Service UnavailableTransient; retry with backoff.

Example payloads (trimmed)

401 Unauthorized

{ "status": 401, "error": "Unauthorized", "message": "Missing or invalid access token" }

404 Not Found

[ { "message": "Company 'acme.invalid' was not found" } ]

429 Rate Limit

[ { "message": "Rate limit exceeded. Please retry later." } ]

Rate limits

  • BuyerCaddy enforces per‑client throttling to ensure stability.
  • On 429, you must retry with exponential backoff.
  • When present, honor the Retry-After header (seconds).
  • Prefer pagination & batching over large single requests.
  • Keep concurrency reasonable; increase gradually and observe 429s.

If you consistently hit 429 even with backoff, reduce request rate or contact support to discuss plan limits.


Retry strategies (backoff + jitter)

Below are production‑ready snippets to handle transient errors (429, 500, 503, timeouts).
They implement exponential backoff with jitter and respect Retry-After when present.

Example Retry (GET) — Vendors

# bash (curl) — simple loop with sleep and Retry-After support via grep/sed
URL="https://api.salescaddy.ai/api/vendors?page=0&size=10"
HDR="Authorization: Bearer $TOKEN"

for i in 0 1 2 3 4; do
  RESP_HEADERS=$(mktemp); RESP_BODY=$(mktemp)

  http_code=$(curl -sS -D "$RESP_HEADERS" -w "%{http_code}" -o "$RESP_BODY"     -H "$HDR" "$URL")

  if [ "$http_code" -lt 400 ] || [ "$http_code" -eq 404 ]; then
    cat "$RESP_BODY"
    rm -f "$RESP_HEADERS" "$RESP_BODY"
    break
  fi

  # Extract Retry-After if present
  ra=$(grep -i "^Retry-After:" "$RESP_HEADERS" | sed -E 's/Retry-After:\s*([0-9]+)/\1/i')
  delay=${ra:-$(( 1 << i ))}
  sleep "$delay"
done
// node (fetch) — exponential backoff + jitter + Retry-After
import fetch from "node-fetch";

const URL = "https://api.salescaddy.ai/api/vendors?page=0&size=10";
const headers = { Authorization: `Bearer ${process.env.TOKEN}` };

async function getWithRetry(url, max=5) {
  let attempt = 0;
  while (true) {
    const res = await fetch(url, { headers });
    if (res.status < 400 || res.status === 404) return res.json();

    const retryAfter = parseInt(res.headers.get("retry-after") || "0", 10);
    if (attempt >= max) throw new Error(`Failed after ${max} attempts: ${res.status}`);

    const backoff = Math.min(60, Math.pow(2, attempt)) + Math.random(); // jitter
    const delay = retryAfter > 0 ? retryAfter : backoff;
    await new Promise(r => setTimeout(r, delay * 1000));
    attempt++;
  }
}

console.log(await getWithRetry(URL));
# python (requests) — exponential backoff + jitter + Retry-After
import os, time, random, requests

URL = "https://api.salescaddy.ai/api/vendors"
HEADERS = {"Authorization": f"Bearer {os.environ['TOKEN']}"}

def get_with_retry(url, params=None, max_attempts=5):
    for attempt in range(max_attempts):
        r = requests.get(url, params=params, headers=HEADERS, timeout=30)
        if r.status_code < 400 or r.status_code == 404:
            return r.json()
        retry_after = r.headers.get("Retry-After")
        if attempt == max_attempts - 1:
            r.raise_for_status()
        backoff = min(60, 2 ** attempt) + random.random()
        delay = int(retry_after) if retry_after and retry_after.isdigit() else backoff
        time.sleep(delay)

print(get_with_retry(URL, params={"page":0,"size":10}))
// csharp (HttpClientFactory recommended) — backoff + jitter + Retry-After
using System.Net.Http.Headers;

static async Task<string> GetWithRetry(HttpClient http, string url, string token, int max=5) {
  http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
  var rand = new Random();
  for (int i=0; i<max; i++) {
    var res = await http.GetAsync(url);
    if ((int)res.StatusCode < 400 || res.StatusCode == System.Net.HttpStatusCode.NotFound)
      return await res.Content.ReadAsStringAsync();

    if (i == max - 1) throw new HttpRequestException($"Failed after {max} attempts: {(int)res.StatusCode}");

    var retryAfter = 0;
    if (res.Headers.TryGetValues("Retry-After", out var values) && int.TryParse(values.FirstOrDefault(), out var v))
      retryAfter = v;

    var backoff = Math.Min(60, Math.Pow(2, i)) + rand.NextDouble(); // jitter
    var delay = retryAfter > 0 ? retryAfter : backoff;
    await Task.Delay(TimeSpan.FromSeconds(delay));
  }
  throw new Exception("unreachable");
}

var json = await GetWithRetry(new HttpClient(), "https://api.salescaddy.ai/api/vendors?page=0&size=10", TOKEN);
Console.WriteLine(json);

Example Retry (POST) — Products In Use

⚠️

When retrying POST requests, ensure your operation is idempotent on the client side (e.g., deduplicate by request hash) to avoid duplicates on transient failures.

curl -sS -X POST "https://api.salescaddy.ai/api/companies/hilton.com/products-in-use"   -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"   -d '{ "productIds": ["prod_office365","prod_slack"] }'
const res = await fetch("https://api.salescaddy.ai/api/companies/hilton.com/products-in-use", {
  method: "POST",
  headers: { "Authorization": `Bearer ${process.env.TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ productIds: ["prod_office365","prod_slack"] })
});
console.log(await res.json());
r = requests.post("https://api.salescaddy.ai/api/companies/hilton.com/products-in-use",
                  headers={"Authorization": f"Bearer {TOKEN}", "Content-Type":"application/json"},
                  json={"productIds":["prod_office365","prod_slack"]})
print(r.json())
var req = new StringContent("{"productIds":["prod_office365","prod_slack"]}", System.Text.Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.salescaddy.ai/api/companies/hilton.com/products-in-use", req);
Console.WriteLine(await res.Content.ReadAsStringAsync());

Validation & pagination hints

  • Validate required fields (e.g., companyDomain, productId) and types (page/size integers, size within limits).
  • Pagination defaults: page=0, size=20 (upper bounds vary by endpoint).
  • Avoid requesting very large size; prefer multiple pages to reduce timeouts.
  • For CSV exports, include X-On-Behalf-Of-User when required by the endpoint.

Networking & timeouts

  • Use reasonable timeouts (e.g., 30s) and treat timeouts like other transient errors (retry with backoff).
  • Prefer persistent HTTP clients (reuse connection pools) for performance.
  • Log request IDs/contexts (if provided in headers) to correlate failures.

Monitoring

  • Track 4xx/5xx rates, latency, and retry counts.
  • Alert on sustained spikes in 429/5xx despite backoff (possible misconfiguration or plan limits).