Error Handling

This guide covers how to handle errors from the SALLY API, implement effective retry strategies, and stay within rate limits. Building resilient integrations means planning for failures from the start.

HTTP Status Codes

SALLY uses standard HTTP status codes:

CodeMeaningWhen You See It
200OKRequest succeeded
201CreatedResource created successfully
400Bad RequestInvalid request body or parameters
401UnauthorizedMissing or invalid API key / JWT token
403ForbiddenInsufficient permissions for this resource
404Not FoundResource does not exist
409ConflictResource already exists or state conflict
422Unprocessable EntityRequest body failed validation
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error (retry may help)
502Bad GatewayUpstream service unavailable
503Service UnavailableSALLY is temporarily down (retry)

Error Response Format

All error responses follow a consistent JSON structure:

{
  "statusCode": 400,
  "message": "Validation failed: licenseNumber is required",
  "error": "Bad Request",
  "details": [
    {
      "field": "licenseNumber",
      "constraint": "isNotEmpty",
      "message": "licenseNumber should not be empty"
    }
  ]
}
FieldTypeDescription
statusCodenumberHTTP status code
messagestringHuman-readable error description
errorstringHTTP status text
detailsarrayField-level validation errors (present on 400/422)

Common Error Patterns

Validation Errors (400/422)

These indicate a problem with your request. Fix the request body before retrying.

Missing required field:

{
  "statusCode": 400,
  "message": "Validation failed",
  "error": "Bad Request",
  "details": [
    {
      "field": "firstName",
      "constraint": "isNotEmpty",
      "message": "firstName should not be empty"
    }
  ]
}

Invalid field value:

{
  "statusCode": 422,
  "message": "Validation failed: status must be one of ACTIVE, INACTIVE, PENDING",
  "error": "Unprocessable Entity",
  "details": [
    {
      "field": "status",
      "constraint": "isEnum",
      "message": "status must be one of: ACTIVE, INACTIVE, PENDING"
    }
  ]
}

Invalid state transition:

{
  "statusCode": 400,
  "message": "Cannot activate route: route is already ACTIVE",
  "error": "Bad Request"
}

Authentication Errors (401)

The request lacks valid credentials. Do not retry without fixing the auth.

{
  "statusCode": 401,
  "message": "Invalid or expired API key",
  "error": "Unauthorized"
}

Common causes:

  • API key was revoked
  • JWT token has expired (refresh it)
  • Missing Authorization or X-API-Key header
  • Typo in the key (check for extra whitespace)

Authorization Errors (403)

You are authenticated but lack permission for this operation.

{
  "statusCode": 403,
  "message": "Insufficient permissions. DISPATCHER role cannot delete drivers.",
  "error": "Forbidden"
}

Rate Limiting (429)

You have exceeded the allowed request rate.

{
  "statusCode": 429,
  "message": "Rate limit exceeded. Try again in 42 seconds.",
  "error": "Too Many Requests"
}

Rate limit headers on every response:

HeaderDescription
X-RateLimit-LimitMaximum requests per hour
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the limit resets

Server Errors (500/502/503)

These indicate a problem on SALLY’s side. Retry with backoff.

{
  "statusCode": 500,
  "message": "An unexpected error occurred. Please try again later.",
  "error": "Internal Server Error"
}

Implementing Retry Logic

Which Errors to Retry

Status CodeRetry?Strategy
400, 401, 403, 404, 409, 422NoFix the request first
429YesWait for X-RateLimit-Reset
500YesExponential backoff
502, 503YesExponential backoff
Network error / timeoutYesExponential backoff

Exponential Backoff with Jitter

The recommended retry strategy uses exponential backoff with jitter to avoid thundering herd problems:

async function fetchWithRetry(url, options, maxRetries = 5) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
 
      // Do not retry client errors (except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        const error = await response.json();
        throw new SallyApiError(response.status, error.message, error.details);
      }
 
      // Rate limited -- wait for reset
      if (response.status === 429) {
        const resetTimestamp = response.headers.get("X-RateLimit-Reset");
        const waitMs = resetTimestamp
          ? parseInt(resetTimestamp) * 1000 - Date.now()
          : 60000;
        console.log(`Rate limited. Waiting ${Math.ceil(waitMs / 1000)}s...`);
        await sleep(Math.max(waitMs, 1000));
        continue;
      }
 
      // Server error -- retry with backoff
      if (response.status >= 500) {
        if (attempt === maxRetries) {
          const error = await response.json();
          throw new SallyApiError(response.status, error.message);
        }
        const delay = calculateBackoff(attempt);
        console.log(`Server error ${response.status}. Retrying in ${delay}ms...`);
        await sleep(delay);
        continue;
      }
 
      return response;
    } catch (error) {
      if (error instanceof SallyApiError) throw error;
 
      // Network error -- retry with backoff
      if (attempt === maxRetries) throw error;
      const delay = calculateBackoff(attempt);
      console.log(`Network error. Retrying in ${delay}ms...`);
      await sleep(delay);
    }
  }
}
 
function calculateBackoff(attempt) {
  const baseDelay = 1000; // 1 second
  const maxDelay = 30000; // 30 seconds
  const exponentialDelay = baseDelay * Math.pow(2, attempt);
  const jitter = Math.random() * 1000; // 0-1s random jitter
  return Math.min(exponentialDelay + jitter, maxDelay);
}
 
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
class SallyApiError extends Error {
  constructor(statusCode, message, details) {
    super(message);
    this.statusCode = statusCode;
    this.details = details;
  }
}

Python Retry Example

import time
import random
import requests
 
class SallyClient:
    def __init__(self, api_key, base_url="https://sally-api.apps.appshore.in/api/v1"):
        self.api_key = api_key
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({"X-API-Key": api_key})
 
    def request(self, method, path, max_retries=5, **kwargs):
        url = f"{self.base_url}{path}"
 
        for attempt in range(max_retries + 1):
            try:
                response = self.session.request(method, url, **kwargs)
 
                # Success
                if response.status_code < 400:
                    return response.json()
 
                # Client error (do not retry, except 429)
                if 400 <= response.status_code < 500 and response.status_code != 429:
                    raise SallyApiError(response.status_code, response.json())
 
                # Rate limited
                if response.status_code == 429:
                    reset = response.headers.get("X-RateLimit-Reset")
                    wait = int(reset) - int(time.time()) if reset else 60
                    print(f"Rate limited. Waiting {wait}s...")
                    time.sleep(max(wait, 1))
                    continue
 
                # Server error
                if attempt == max_retries:
                    raise SallyApiError(response.status_code, response.json())
 
                delay = min(2 ** attempt + random.random(), 30)
                print(f"Server error {response.status_code}. Retrying in {delay:.1f}s...")
                time.sleep(delay)
 
            except requests.ConnectionError:
                if attempt == max_retries:
                    raise
                delay = min(2 ** attempt + random.random(), 30)
                print(f"Connection error. Retrying in {delay:.1f}s...")
                time.sleep(delay)

Rate Limits

Key TypeRate LimitWindow
Staging (sk_staging_)1,000 requestsPer hour
Production (sk_live_)10,000 requestsPer hour

Staying Within Limits

  • Cache responses when the data does not change frequently (e.g., vehicle list, vendor registry)
  • Use webhooks or SSE instead of polling for real-time updates
  • Batch operations where available (e.g., bulk acknowledge alerts)
  • Monitor remaining quota via the X-RateLimit-Remaining header

Rate Limit Response

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1707580800
Content-Type: application/json
 
{
  "statusCode": 429,
  "message": "Rate limit exceeded. Try again in 42 seconds.",
  "error": "Too Many Requests"
}

Timeout Handling

SALLY API endpoints have the following timeout characteristics:

Endpoint TypeTimeoutNotes
Standard CRUD10 secondsList, get, create, update operations
Route planning30 secondsComplex optimization may take longer
Integration test15 secondsDepends on external vendor response time
Integration sync60 secondsLarge data volumes take longer

Set your HTTP client timeout accordingly:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000); // 30s for route planning
 
try {
  const response = await fetch(`${BASE_URL}/routes/plan`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify(routePlanRequest),
    signal: controller.signal,
  });
  clearTimeout(timeout);
  return await response.json();
} catch (error) {
  clearTimeout(timeout);
  if (error.name === "AbortError") {
    console.error("Route planning request timed out after 30s");
  }
  throw error;
}

Building a Resilient Client

Here is a complete, production-ready JavaScript client that handles all error cases:

class SallyClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = "https://sally-api.apps.appshore.in/api/v1";
  }
 
  async request(method, path, { body, params, timeout = 10000, maxRetries = 3 } = {}) {
    let url = `${this.baseUrl}${path}`;
    if (params) {
      url += "?" + new URLSearchParams(params).toString();
    }
 
    const options = {
      method,
      headers: {
        "X-API-Key": this.apiKey,
        "Content-Type": "application/json",
      },
    };
 
    if (body) {
      options.body = JSON.stringify(body);
    }
 
    return this._fetchWithRetry(url, options, timeout, maxRetries);
  }
 
  // Convenience methods
  async get(path, params) {
    return this.request("GET", path, { params });
  }
 
  async post(path, body, options = {}) {
    return this.request("POST", path, { body, ...options });
  }
 
  async put(path, body) {
    return this.request("PUT", path, { body });
  }
 
  async delete(path) {
    return this.request("DELETE", path);
  }
 
  // Usage examples:
  // const client = new SallyClient(process.env.SALLY_API_KEY);
  // const drivers = await client.get("/drivers", { status: "ACTIVE" });
  // const route = await client.post("/routes/plan", routeData, { timeout: 30000 });
}

Next Steps