Skip to main content

Overview

Robust error handling ensures your integration remains reliable and provides a great user experience even when things go wrong.

Error Response Format

All Sticker API errors follow a consistent format:
{
  "error": "Human-readable error message",
  "code": "MACHINE_READABLE_CODE",
  "details": "Additional context (optional)",
  "field": "problematic_field (for validation errors)"
}

HTTP Status Codes

4xx Client Errors

Errors caused by the requestUsually don’t retry

5xx Server Errors

Errors on Sticker’s sideSafe to retry with backoff

Common Error Codes

Authentication Errors (401, 403)

{
  "error": "Invalid API key",
  "code": "INVALID_API_KEY"
}
How to handle:
  • Verify your API key is correct
  • Check signature generation logic
  • Ensure key hasn’t been revoked
  • Don’t retry without fixing the issue

Validation Errors (400)

{
  "error": "Missing required field: organization.email",
  "code": "INVALID_REQUEST",
  "field": "organization.email"
}
How to handle:
  • Validate input before sending
  • Show field-specific error messages
  • Don’t retry without fixing validation issues

Resource Errors (404, 409)

{
  "error": "No profile found with partner_org_id: acme_123",
  "code": "ORGANIZATION_NOT_FOUND",
  "partner_org_id": "acme_123"
}
How to handle:
  • For 404: Ensure setup was completed first
  • For 409: Use existing resource ID
  • Check your data consistency

Rate Limiting (429)

{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMIT_EXCEEDED",
  "retry_after": 60
}
How to handle:
  • Implement exponential backoff
  • Respect retry_after header
  • Cache organization data to reduce calls
  • Consider request throttling

Server Errors (500, 503)

{
  "error": "Internal server error",
  "code": "INTERNAL_ERROR",
  "details": "Database connection timeout"
}
How to handle:
  • Retry with exponential backoff
  • Log error for investigation
  • Show user-friendly message
  • Alert on persistent failures

Retry Logic

Exponential Backoff

Implement smart retries for transient failures:
async function callStickerWithRetry(
  endpoint,
  options,
  maxRetries = 3
) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(endpoint, options);
      
      // Don't retry client errors (except 429)
      if (response.status >= 400 && response.status < 500) {
        if (response.status === 429) {
          // Handle rate limiting
          const retryAfter = response.headers.get('Retry-After') || 60;
          await sleep(parseInt(retryAfter) * 1000);
          continue;
        }
        // Other 4xx errors - don't retry
        throw new Error(`Client error: ${response.status}`);
      }
      
      // Server error - will retry
      if (!response.ok) {
        throw new Error(`Server error: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      lastError = error;
      
      // Don't retry on last attempt
      if (attempt === maxRetries - 1) {
        break;
      }
      
      // Exponential backoff: 1s, 2s, 4s
      const delay = Math.pow(2, attempt) * 1000;
      console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
      await sleep(delay);
    }
  }
  
  throw lastError;
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Retry Decision Tree

Error Occurred
    ├─ Network Error? → Retry with backoff
    ├─ Timeout? → Retry with backoff
    ├─ 429 Rate Limit? → Wait retry_after, then retry
    ├─ 5xx Server Error? → Retry with backoff
    ├─ 401/403 Auth Error? → DON'T retry, fix credentials
    ├─ 400 Validation Error? → DON'T retry, fix request
    └─ 404/409 Resource Error? → DON'T retry, fix data

User-Friendly Error Messages

Transform technical errors into helpful messages:
function getUserMessage(error) {
  const messages = {
    // Authentication
    'INVALID_API_KEY': 'Connection error. Please contact support.',
    'INVALID_SIGNATURE': 'Authentication failed. Please try again.',
    
    // Resources
    'ORGANIZATION_NOT_FOUND': 'Your organization needs to be set up. Please contact your administrator.',
    'USER_NOT_FOUND': 'You don\'t have access to supplies. Please contact your administrator.',
    
    // Validation
    'INVALID_EMAIL': 'Please check your email address.',
    'INVALID_REQUEST': 'Some information is missing. Please check and try again.',
    
    // System
    'RATE_LIMIT_EXCEEDED': 'Too many requests. Please wait a moment.',
    'INTERNAL_ERROR': 'Something went wrong on our end. Please try again.',
    'SERVICE_UNAVAILABLE': 'Service temporarily unavailable. Please try again in a few minutes.',
    
    // Session
    'SESSION_EXPIRED': 'Your session expired. Refreshing...',
    'SESSION_INVALID': 'Invalid session. Please reload the page.',
  };
  
  return messages[error.code] || 'An error occurred. Please try again or contact support.';
}

Error Handling Patterns

Organization Setup

async function setupOrganization(orgData) {
  try {
    const result = await callStickerAPI(
      '/api/partner/organization-setup',
      orgData
    );
    
    // Success
    return {
      success: true,
      profileId: result.profile.id
    };
    
  } catch (error) {
    // Log for debugging
    console.error('Organization setup failed:', error);
    
    // Handle specific errors
    if (error.code === 'DUPLICATE_ORGANIZATION') {
      // Organization already exists, that's okay
      return {
        success: true,
        profileId: error.existing_profile_id,
        message: 'Organization already exists'
      };
    }
    
    if (error.code === 'INVALID_EMAIL') {
      return {
        success: false,
        error: 'Invalid email address',
        field: error.field
      };
    }
    
    if (error.code === 'INVALID_REQUEST') {
      return {
        success: false,
        error: `Missing required information: ${error.field}`,
        field: error.field
      };
    }
    
    // Unknown error
    return {
      success: false,
      error: 'Failed to set up organization. Please try again.',
      canRetry: true
    };
  }
}

User Handshake

async function authenticateUser(orgId, user) {
  try {
    const result = await callStickerAPI(
      '/api/partner/handshake',
      { partner_org_id: orgId, user }
    );
    
    return {
      success: true,
      sessionKey: result.session_key,
      iframeUrl: result.iframe_url
    };
    
  } catch (error) {
    console.error('Handshake failed:', error);
    
    // Organization not found
    if (error.code === 'ORGANIZATION_NOT_FOUND') {
      // Try to set up organization automatically
      await setupOrganization(orgData);
      // Retry handshake
      return authenticateUser(orgId, user);
    }
    
    // User not authorized
    if (error.code === 'USER_NOT_FOUND') {
      return {
        success: false,
        error: 'You don\'t have access. Contact your admin.',
        requiresSetup: true
      };
    }
    
    // Session error
    if (error.code === 'SESSION_EXPIRED') {
      return {
        success: false,
        error: 'Session expired. Refreshing...',
        shouldRetry: true
      };
    }
    
    // Rate limited
    if (error.code === 'RATE_LIMIT_EXCEEDED') {
      return {
        success: false,
        error: 'Too many requests. Please wait.',
        retryAfter: error.retry_after
      };
    }
    
    // Generic error
    return {
      success: false,
      error: getUserMessage(error),
      canRetry: true
    };
  }
}

iframe Loading

function StickerEmbed({ orgId, user }) {
  const [state, setState] = useState('loading');
  const [error, setError] = useState(null);
  const [sessionKey, setSessionKey] = useState(null);
  const [retryCount, setRetryCount] = useState(0);

  const loadSupplies = async () => {
    setState('loading');
    setError(null);
    
    try {
      const result = await authenticateUser(orgId, user);
      
      if (!result.success) {
        setError(result.error);
        setState('error');
        
        // Auto-retry for certain errors
        if (result.shouldRetry && retryCount < 3) {
          setTimeout(() => {
            setRetryCount(retryCount + 1);
            loadSupplies();
          }, 2000 * (retryCount + 1)); // Exponential backoff
        }
        return;
      }
      
      setSessionKey(result.sessionKey);
      setState('ready');
      
    } catch (error) {
      console.error('Failed to load supplies:', error);
      setError('Failed to load supplies. Please try again.');
      setState('error');
    }
  };

  useEffect(() => {
    loadSupplies();
  }, []);

  if (state === 'loading') {
    return (
      <div className="loading-state">
        <Spinner />
        <p>Loading supplies...</p>
      </div>
    );
  }

  if (state === 'error') {
    return (
      <div className="error-state">
        <ErrorIcon />
        <p>{error}</p>
        <button onClick={() => {
          setRetryCount(0);
          loadSupplies();
        }}>
          Try Again
        </button>
      </div>
    );
  }

  return (
    <iframe
      src={`https://app.sticker.com/embed?session_key=${sessionKey}`}
      width="100%"
      height="800px"
    />
  );
}

Error Logging

Structured Logging

Log errors with context for debugging:
function logError(context, error, additionalInfo = {}) {
  const errorLog = {
    timestamp: new Date().toISOString(),
    context: context,
    error: {
      message: error.message,
      code: error.code,
      stack: error.stack
    },
    api: {
      endpoint: additionalInfo.endpoint,
      method: additionalInfo.method,
      status: additionalInfo.status
    },
    user: {
      org_id: additionalInfo.orgId,
      email: additionalInfo.userEmail
    },
    ...additionalInfo
  };
  
  // Send to your logging service
  logger.error('Sticker Integration Error', errorLog);
  
  // Alert on critical errors
  if (error.code === 'INTERNAL_ERROR' || error.status >= 500) {
    alertOps('Sticker API error', errorLog);
  }
}

Error Metrics

Track error rates and patterns:
const errorMetrics = {
  total: 0,
  byCode: {},
  byEndpoint: {}
};

function trackError(error, endpoint) {
  errorMetrics.total++;
  errorMetrics.byCode[error.code] = (errorMetrics.byCode[error.code] || 0) + 1;
  errorMetrics.byEndpoint[endpoint] = (errorMetrics.byEndpoint[endpoint] || 0) + 1;
  
  // Alert if error rate is high
  const errorRate = errorMetrics.total / totalRequests;
  if (errorRate > 0.05) { // 5% error rate
    alertOps('High error rate in Sticker integration', {
      error_rate: errorRate,
      total_errors: errorMetrics.total,
      by_code: errorMetrics.byCode
    });
  }
}

Fallback Strategies

Graceful Degradation

Provide alternatives when integration fails:
function SuppliesSection({ orgId, user }) {
  const [integrationHealthy, setIntegrationHealthy] = useState(true);
  const [consecutiveFailures, setConsecutiveFailures] = useState(0);

  const checkHealth = async () => {
    try {
      await fetch('https://api.sticker.com/health');
      setConsecutiveFailures(0);
    } catch (error) {
      setConsecutiveFailures(prev => prev + 1);
      
      // After 3 failures, show fallback
      if (consecutiveFailures >= 3) {
        setIntegrationHealthy(false);
      }
    }
  };

  if (!integrationHealthy) {
    return (
      <div className="fallback-ui">
        <h3>Supplies Currently Unavailable</h3>
        <p>Our supplies system is temporarily unavailable.</p>
        <div className="alternatives">
          <button onClick={() => window.location.href = 'tel:18005551234'}>
            Call to Order: 1-800-555-1234
          </button>
          <button onClick={() => window.location.href = 'mailto:orders@sticker.com'}>
            Email Your Order
          </button>
          <button onClick={() => setIntegrationHealthy(true)}>
            Try Again
          </button>
        </div>
      </div>
    );
  }

  return <StickerEmbed orgId={orgId} user={user} />;
}

Circuit Breaker

Prevent cascading failures:
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = Date.now();
  }

  async call(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

// Usage
const breaker = new CircuitBreaker();

try {
  const result = await breaker.call(() =>
    callStickerAPI('/api/partner/handshake', data)
  );
} catch (error) {
  if (error.message === 'Circuit breaker is OPEN') {
    showFallbackUI();
  }
}

Testing Error Handling

Test how your integration handles errors:
describe('Error Handling', () => {
  it('should retry on 500 errors', async () => {
    mockAPI.fail(500, 2); // Fail twice with 500
    
    const result = await callStickerWithRetry(endpoint, options);
    
    expect(result).toBeDefined();
    expect(mockAPI.callCount).toBe(3);
  });

  it('should not retry on 400 errors', async () => {
    mockAPI.fail(400);
    
    await expect(callStickerWithRetry(endpoint, options))
      .rejects.toThrow();
    
    expect(mockAPI.callCount).toBe(1);
  });

  it('should show user-friendly messages', () => {
    const error = { code: 'ORGANIZATION_NOT_FOUND' };
    const message = getUserMessage(error);
    
    expect(message).not.toContain('ORGANIZATION_NOT_FOUND');
    expect(message).toContain('organization');
  });
});

Next Steps