Skip to main content

Error Handling Guide

Complete reference for all HTTP status codes, error responses, and handling patterns for the Sticky Calls API.

Quick Reference
  • 400 Bad Request - Fix request format
  • 401 Unauthorized - Check API key
  • 402 Payment Required - Upgrade plan (monthly quota exhausted)
  • 409 Conflict - Use unique call_id
  • 429 Too Many Requests - Wait and retry (rate limit exceeded)
  • 500 Internal Server Error - Retry with backoff

Complete Error Reference

400 Bad Request

Cause: Invalid request format, missing required fields, or validation errors

When it happens:

  • Missing required fields (identity_hints, call_id)
  • Invalid data types (string where number expected)
  • Malformed JSON body
  • Invalid enum values

Example Response:

HTTP 400 Bad Request

{
"error": "Bad Request",
"message": "Validation failed",
"details": {
"issues": [
{
"path": ["identity_hints"],
"message": "identity_hints must include ani, external_ids, or customer_ref"
}
]
}
}

Another Example (missing required field):

HTTP 400 Bad Request

{
"error": "Bad Request",
"message": "Validation failed",
"details": {
"issues": [
{
"path": ["call_id"],
"message": "Required field 'call_id' is missing"
}
]
}
}

What To Do:

  1. Check request body matches the API schema
  2. Verify all required fields are present
  3. Ensure data types are correct
  4. Review the details.issues array for specific problems
  5. Consult API Reference for correct request format

Code Example:

try {
const response = await stickyCallsAPI.start(requestBody);
} catch (error) {
if (error.status === 400) {
console.error('Invalid request:', error.data.details.issues);
// Log specific validation errors
error.data.details.issues.forEach(issue => {
console.error(`Field ${issue.path.join('.')}: ${issue.message}`);
});
// Fix request and don't retry
throw new Error('Fix request format');
}
}

401 Unauthorized

Cause: Invalid, missing, or revoked API key

When it happens:

  • API key is missing from Authorization header
  • API key format is incorrect (must be Bearer YOUR_KEY)
  • API key has been revoked in dashboard
  • API key doesn't exist or was deleted

Example Response:

HTTP 401 Unauthorized

{
"error": "Unauthorized",
"message": "Invalid or missing API key"
}

Another Example (revoked key):

HTTP 401 Unauthorized

{
"error": "Unauthorized",
"message": "API key has been revoked"
}

What To Do:

  1. Verify API key exists in your configuration
  2. Check the Authorization header format: Authorization: Bearer YOUR_API_KEY
  3. Confirm key hasn't been revoked in dashboard
  4. Generate a new API key if necessary
  5. Update your environment variables/secrets

Code Example:

try {
const response = await fetch('https://api.stickycalls.com/v1/calls/start', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STICKY_CALLS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
} catch (error) {
if (error.status === 401) {
// Critical error - stop processing and alert
logger.critical('Sticky Calls API key invalid!', error);
alertTeam('Invalid API key - check configuration immediately');
throw error; // Don't retry with bad credentials
}
}

402 Payment Required (Monthly Quota Exhausted)

Cause: Monthly quota has been exhausted

When it happens:

  • You've used all your monthly API calls
  • No quota remains for this billing cycle
  • Account has not been upgraded or renewed

Important: This is about monthly budget, not rate limiting. See Understanding Limits for the difference.

Example Response:

HTTP 402 Payment Required

{
"error": "Payment Required",
"message": "Monthly quota exhausted. Your account has 0 API calls remaining.",
"quota_remaining": 0,
"billing_account_id": "ba_1a2b3c4d5e6f..."
}

Response Fields:

  • quota_remaining: Current quota balance (will be 0)
  • billing_account_id: Your billing account identifier

What To Do:

  1. Upgrade tier: Visit billing page to upgrade
  2. Wait for renewal: Quota renews on your billing date
  3. Alert billing team: Set up monitoring to alert before exhaustion

Code Example:

try {
const response = await stickyCallsAPI.start(requestBody);
} catch (error) {
if (error.status === 402) {
// Credits exhausted - alert billing
logger.error('Sticky Calls monthly quota exhausted', {
quotaRemaining: error.data.quota_remaining,
billingAccountId: error.data.billing_account_id
});

// Send alert to billing team
await alertBillingTeam({
service: 'Sticky Calls',
action: 'Upgrade plan',
urgency: 'high'
});

// Gracefully degrade: continue without caller context
return handleWithoutStickyCall();
}
}

Prevention:

Monitor credits proactively using response headers:

const response = await stickyCallsAPI.start(requestBody);
const quotaRemaining = response.headers.get('X-Quota-Remaining');

if (parseInt(quotaRemaining) < 100) {
alertBillingTeam('Low credits - add more before exhaustion');
}

409 Conflict

Cause: Duplicate call_id or attempting to end a call that's already ended

When it happens:

  • Same call_id used twice for /v1/calls/start
  • Calling /v1/calls/end twice with same call_id
  • Call has already been marked as ended

Example Response (duplicate start):

HTTP 409 Conflict

{
"error": "Conflict",
"message": "Call with call_id 'call_12345' already exists"
}

Example Response (already ended):

HTTP 409 Conflict

{
"error": "Conflict",
"message": "Call 'call_12345' has already been ended"
}

What To Do:

  1. Generate unique call_ids: Use UUIDs, timestamps, or session IDs
  2. Check idempotency: Use X-Idempotency-Key header for safe retries
  3. Track call state: Maintain local state to avoid duplicate ends
  4. Log conflicts: May indicate duplicate webhooks or race conditions

Code Example:

import { v4 as uuidv4 } from 'uuid';

// GOOD: Generate unique call_id
const callId = `call_${uuidv4()}`;

try {
const response = await stickyCallsAPI.start({
call_id: callId,
// ... other fields
});
} catch (error) {
if (error.status === 409) {
// Likely a duplicate call_id - log and investigate
logger.warn('Duplicate call_id detected', { callId });

// If using idempotency keys, this might be a legitimate retry
if (hasIdempotencyKey) {
// Safe to ignore - original request succeeded
return cachedResponse;
} else {
// Real duplicate - generate new call_id
return retryWithNewCallId();
}
}
}

Prevention:

Use idempotency keys for safe retries:

const idempotencyKey = uuidv4();

await fetch('https://api.stickycalls.com/v1/calls/end', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'X-Idempotency-Key': idempotencyKey
},
body: JSON.stringify(requestBody)
});

429 Too Many Requests (Rate Limit Exceeded)

Cause: Per-minute rate limit exceeded (after 10% grace period)

When it happens:

  • Exceeded your tier's requests/minute limit
  • Used up the 10% grace period
  • Burst traffic too high for your tier

Important: This is about per-minute throttling, not monthly quota. See Understanding Limits for the difference.

Example Response:

HTTP 429 Too Many Requests

{
"error": "Too Many Requests",
"message": "Rate limit exceeded for your tier. Limit: 100 requests/minute.",
"retryAfter": 42,
"currentUsage": 111,
"limit": 100,
"resetAt": "2026-02-08T12:01:00.000Z"
}

Response Fields:

  • retryAfter: Seconds to wait before retrying
  • currentUsage: Your request count in the last 60 seconds
  • limit: Your tier's base rate limit (not including grace period)
  • resetAt: ISO timestamp when the rate limit window resets

Rate Limits by Tier:

TierBase LimitGrace Period (+10%)Hard Block At
Free100/min110/min111+/min
Starter600/min660/min661+/min
Growth3,000/min3,300/min3,301+/min
Scale10,000/min11,000/min11,001+/min

What To Do:

  1. Wait: Honor the retryAfter value (seconds to wait)
  2. Implement backoff: Use exponential backoff for retries
  3. Monitor headers: Check X-RateLimit-Remaining before each request
  4. Upgrade tier: If you consistently hit limits, upgrade for higher throughput
  5. Spread traffic: Add small delays between requests

Code Example (with exponential backoff):

async function callWithRateLimitHandling(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.status === 429) {
if (i === maxRetries - 1) throw error; // Last retry failed

// Use retryAfter from response, or exponential backoff
const retryAfter = error.data.retryAfter || Math.pow(2, i);
logger.warn(`Rate limited. Waiting ${retryAfter}s before retry ${i + 1}/${maxRetries}`);

await sleep(retryAfter * 1000);
continue;
}
throw error;
}
}
}

// Usage
await callWithRateLimitHandling(async () => {
return await stickyCallsAPI.start(requestBody);
});

Prevention:

Monitor rate limit headers in every response:

const response = await stickyCallsAPI.start(requestBody);

const rateLimit = parseInt(response.headers.get('X-RateLimit-Limit'));
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const percentUsed = ((rateLimit - remaining) / rateLimit) * 100;

if (percentUsed > 90) {
// Approaching limit - slow down
logger.warn('In rate limit grace period - backing off');
await sleep(1000); // Add 1 second delay
}

500 Internal Server Error

Cause: Unexpected server-side error

When it happens:

  • Database connection failures
  • Unhandled exceptions in server code
  • Infrastructure issues
  • Third-party service failures

Example Response:

HTTP 500 Internal Server Error

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

What To Do:

  1. Retry: Implement exponential backoff (may be transient)
  2. Log details: Capture full error for investigation
  3. Contact support: If persistent, email nate@bananaintelligence.ai with:
    • Timestamp
    • Request ID (if available)
    • Endpoint called
    • Request body (sanitize PII)
  4. Graceful degradation: Continue workflow without caller context

Code Example:

async function callWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.status === 500) {
if (i === maxRetries - 1) {
// All retries exhausted
logger.error('Sticky Calls 500 error after retries', {
attempt: i + 1,
error: error.message
});
// Contact support if persistent
throw error;
}

// Retry with exponential backoff
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
logger.warn(`Server error. Retrying in ${delay}ms (${i + 1}/${maxRetries})`);
await sleep(delay);
continue;
}
throw error;
}
}
}

Error Handling Patterns

Complete Error Handler

Comprehensive error handling for all status codes:

async function handleStickyCallsError(error) {
const status = error.response?.status;
const data = error.response?.data;

switch (status) {
case 400:
// Bad request - log validation errors, don't retry
logger.error('Invalid Sticky Calls request', {
issues: data.details?.issues
});
throw new Error('Fix request format');

case 401:
// Invalid API key - CRITICAL, alert immediately
logger.critical('Sticky Calls API key invalid!', error);
await alertTeam('Invalid API key - check configuration');
throw error;

case 402:
// Credits exhausted - alert billing team
logger.error('Sticky Calls monthly quota exhausted', {
quotaRemaining: data.quota_remaining
});
await alertBillingTeam('Add credits to Sticky Calls account');
// Gracefully degrade
return handleWithoutContext();

case 409:
// Conflict - log and investigate
logger.warn('Duplicate call_id detected', { error });
throw new Error('Duplicate call_id - check idempotency');

case 429:
// Rate limited - wait and retry
const retryAfter = data.retryAfter || 60;
logger.warn(`Rate limited. Retry in ${retryAfter}s`);
return { shouldRetry: true, waitSeconds: retryAfter };

case 500:
// Server error - retry with backoff
logger.error('Sticky Calls server error', error);
return { shouldRetry: true, waitSeconds: 2 };

default:
// Unknown error
logger.error('Unexpected Sticky Calls error', { status, error });
throw error;
}
}

Retry with Exponential Backoff

Standard retry pattern for transient errors (429, 500):

async function callWithBackoff(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 60000
} = options;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const shouldRetry = error.status === 429 || error.status === 500;

if (!shouldRetry || attempt === maxRetries - 1) {
throw error;
}

// Calculate delay (exponential with jitter)
const exponentialDelay = Math.min(
baseDelay * Math.pow(2, attempt),
maxDelay
);
const jitter = Math.random() * 1000;
const delay = exponentialDelay + jitter;

logger.info(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await sleep(delay);
}
}
}

Graceful Degradation

Handle API failures without breaking your workflow:

async function getCallerContext(identityHints) {
try {
const response = await stickyCallsAPI.start({
call_id: generateCallId(),
identity_hints: identityHints
});

return {
hasContext: true,
customer_ref: response.customer_ref,
identity: response.identity,
variables: response.variables,
open_intents: response.open_intents
};
} catch (error) {
// Log error but don't fail the call
logger.error('Failed to fetch caller context', { error });

// Return empty context - call continues without history
return {
hasContext: false,
customer_ref: null,
identity: { confidence: 0, level: 'none' },
variables: {},
open_intents: []
};
}
}

// Usage in call flow
const context = await getCallerContext(identityHints);

if (context.hasContext && context.identity.confidence >= 0.5) {
// Use caller history
greeting = `Welcome back! ${context.variables.name || 'valued customer'}`;
} else {
// Standard greeting
greeting = "Welcome! How can we help you today?";
}

Proactive Monitoring

Monitor headers to prevent errors before they occur:

class StickyCallsClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.metrics = {
quotaRemaining: null,
rateLimitRemaining: null
};
}

async start(requestBody) {
// Check if we're low on resources
this.checkThresholds();

const response = await fetch('https://api.stickycalls.com/v1/calls/start', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});

// Update metrics from headers
this.updateMetrics(response.headers);

return response.json();
}

updateMetrics(headers) {
this.metrics.quotaRemaining = parseInt(headers.get('X-Quota-Remaining'));
this.metrics.rateLimitRemaining = parseInt(headers.get('X-RateLimit-Remaining'));
}

checkThresholds() {
// Alert if credits low
if (this.metrics.quotaRemaining !== null &&
this.metrics.quotaRemaining < 100) {
logger.warn('Low credits', { remaining: this.metrics.quotaRemaining });
}

// Slow down if approaching rate limit
if (this.metrics.rateLimitRemaining !== null &&
this.metrics.rateLimitRemaining < 10) {
logger.warn('Approaching rate limit', {
remaining: this.metrics.rateLimitRemaining
});
// Add artificial delay
return sleep(1000);
}
}
}

Error Decision Tree

┌─────────────────┐
│ API Call Failed │
└────────┬────────┘


Check Status

┌────┴────┬────────┬────────┬────────┬────────┐
│ │ │ │ │ │
400 401 402 409 429 500
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Fix & Alert Add Use Wait & Retry
Don't Team Credits Unique Retry with
Retry or call_id Backoff
Upgrade

Testing Error Scenarios

Local Testing

Test error handling without impacting production:

# Test 401 (invalid key)
curl -X POST https://api.stickycalls.com/v1/calls/start \
-H "Authorization: Bearer invalid_key" \
-H "Content-Type: application/json" \
-d '{"call_id": "test_123", "identity_hints": {"ani": "+14155551234"}}'

# Test 400 (missing field)
curl -X POST https://api.stickycalls.com/v1/calls/start \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"call_id": "test_123"}' # Missing identity_hints

# Test 409 (duplicate call_id)
# Make same call twice with same call_id

Mock Error Responses

Test your error handling logic:

// Mock 402 error
const mock402 = {
status: 402,
data: {
error: 'Payment Required',
message: 'Insufficient credits. Your account has 0 credits remaining.',
quota_remaining: 0,
billing_account_id: 'ba_test'
}
};

// Mock 429 error
const mock429 = {
status: 429,
data: {
error: 'Too Many Requests',
message: 'Rate limit exceeded for your tier. Limit: 100 requests/minute.',
retryAfter: 30,
currentUsage: 111,
limit: 100,
resetAt: new Date(Date.now() + 30000).toISOString()
}
};

// Test your handler
await handleStickyCallsError(mock402);
await handleStickyCallsError(mock429);


Need Help?

Persistent errors? Contact support with:

  • Error status code
  • Full error response
  • Timestamp (ISO format)
  • Request details (sanitize PII)

Email: nate@bananaintelligence.ai