Error Handling Guide
Complete reference for all HTTP status codes, error responses, and handling patterns for the Sticky Calls API.
400Bad Request - Fix request format401Unauthorized - Check API key402Payment Required - Upgrade plan (monthly quota exhausted)409Conflict - Use unique call_id429Too Many Requests - Wait and retry (rate limit exceeded)500Internal 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:
- Check request body matches the API schema
- Verify all required fields are present
- Ensure data types are correct
- Review the
details.issuesarray for specific problems - 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
Authorizationheader - 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:
- Verify API key exists in your configuration
- Check the Authorization header format:
Authorization: Bearer YOUR_API_KEY - Confirm key hasn't been revoked in dashboard
- Generate a new API key if necessary
- 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:
- Upgrade tier: Visit billing page to upgrade
- Wait for renewal: Quota renews on your billing date
- 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_idused twice for/v1/calls/start - Calling
/v1/calls/endtwice with samecall_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:
- Generate unique call_ids: Use UUIDs, timestamps, or session IDs
- Check idempotency: Use
X-Idempotency-Keyheader for safe retries - Track call state: Maintain local state to avoid duplicate ends
- 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 retryingcurrentUsage: Your request count in the last 60 secondslimit: Your tier's base rate limit (not including grace period)resetAt: ISO timestamp when the rate limit window resets
Rate Limits by Tier:
| Tier | Base Limit | Grace Period (+10%) | Hard Block At |
|---|---|---|---|
| Free | 100/min | 110/min | 111+/min |
| Starter | 600/min | 660/min | 661+/min |
| Growth | 3,000/min | 3,300/min | 3,301+/min |
| Scale | 10,000/min | 11,000/min | 11,001+/min |
What To Do:
- Wait: Honor the
retryAftervalue (seconds to wait) - Implement backoff: Use exponential backoff for retries
- Monitor headers: Check
X-RateLimit-Remainingbefore each request - Upgrade tier: If you consistently hit limits, upgrade for higher throughput
- 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:
- Retry: Implement exponential backoff (may be transient)
- Log details: Capture full error for investigation
- Contact support: If persistent, email nate@bananaintelligence.ai with:
- Timestamp
- Request ID (if available)
- Endpoint called
- Request body (sanitize PII)
- 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);
Related Documentation
- Understanding Limits - Credits vs rate limits explained
- API Reference - Complete endpoint documentation
- Best Practices - Production integration patterns
- Advanced Topics - Edge cases and design decisions
Need Help?
Persistent errors? Contact support with:
- Error status code
- Full error response
- Timestamp (ISO format)
- Request details (sanitize PII)
Email: nate@bananaintelligence.ai