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:
| Code | Meaning | When You See It |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
400 | Bad Request | Invalid request body or parameters |
401 | Unauthorized | Missing or invalid API key / JWT token |
403 | Forbidden | Insufficient permissions for this resource |
404 | Not Found | Resource does not exist |
409 | Conflict | Resource already exists or state conflict |
422 | Unprocessable Entity | Request body failed validation |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Server-side error (retry may help) |
502 | Bad Gateway | Upstream service unavailable |
503 | Service Unavailable | SALLY 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"
}
]
}| Field | Type | Description |
|---|---|---|
statusCode | number | HTTP status code |
message | string | Human-readable error description |
error | string | HTTP status text |
details | array | Field-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
AuthorizationorX-API-Keyheader - 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:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per hour |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix 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 Code | Retry? | Strategy |
|---|---|---|
400, 401, 403, 404, 409, 422 | No | Fix the request first |
429 | Yes | Wait for X-RateLimit-Reset |
500 | Yes | Exponential backoff |
502, 503 | Yes | Exponential backoff |
| Network error / timeout | Yes | Exponential 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 Type | Rate Limit | Window |
|---|---|---|
Staging (sk_staging_) | 1,000 requests | Per hour |
Production (sk_live_) | 10,000 requests | Per 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-Remainingheader
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 Type | Timeout | Notes |
|---|---|---|
| Standard CRUD | 10 seconds | List, get, create, update operations |
| Route planning | 30 seconds | Complex optimization may take longer |
| Integration test | 15 seconds | Depends on external vendor response time |
| Integration sync | 60 seconds | Large 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
- API Keys — Set up authentication
- Webhooks — Event-driven notifications (reduce polling)
- Integrations Overview — All supported integrations