Skip to main content

Error Handling

Understanding and handling errors is crucial for building robust integrations with the Billbora API. This guide covers all error types, response formats, and best practices.

Error Response Format

All Billbora API errors follow a consistent JSON structure:
{
  "error": {
    "code": "validation_error",
    "message": "Invalid input data",
    "details": {
      "email": ["Must be a valid email address"],
      "amount": ["Must be a positive number"]
    }
  },
  "meta": {
    "request_id": "req_abc123xyz",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

HTTP Status Codes

  • 200 OK: Request successful
  • 201 Created: Resource created successfully
  • 204 No Content: Resource deleted successfully
  • 400 Bad Request: Invalid request parameters
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Insufficient permissions
  • 404 Not Found: Resource not found
  • 409 Conflict: Resource conflict (duplicate data)
  • 422 Unprocessable Entity: Validation failed
  • 429 Too Many Requests: Rate limit exceeded
  • 500 Internal Server Error: Unexpected server error
  • 502 Bad Gateway: Upstream service error
  • 503 Service Unavailable: Service temporarily unavailable
  • 504 Gateway Timeout: Request timeout

Common Error Codes

Authentication Errors

{
  "error": {
    "code": "missing_token",
    "message": "Authentication token is required"
  }
}

Validation Errors

{
  "error": {
    "code": "validation_error",
    "message": "Invalid input data",
    "details": {
      "email": ["Must be a valid email address"],
      "line_items": ["At least one line item is required"]
    }
  }
}

Resource Errors

{
  "error": {
    "code": "not_found",
    "message": "The requested resource was not found",
    "details": {
      "resource": "invoice",
      "id": "inv_1234567890"
    }
  }
}

Rate Limiting

rate_limit_exceeded
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Please try again later.",
    "details": {
      "limit": 1000,
      "reset_time": "2025-01-15T11:00:00Z",
      "retry_after": 3600
    }
  }
}

Error Handling Best Practices

1. Always Check Status Codes

const response = await fetch('https://api.billbora.com/api/v1/invoices/', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(invoiceData)
})

if (!response.ok) {
  const error = await response.json()
  throw new Error(`API Error: ${error.error.message}`)
}

const invoice = await response.json()

2. Handle Specific Error Types

enum BillboraErrorCode {
  VALIDATION_ERROR = 'validation_error',
  NOT_FOUND = 'not_found',
  RATE_LIMITED = 'rate_limit_exceeded',
  INSUFFICIENT_PERMISSIONS = 'insufficient_permissions'
}

class BillboraError extends Error {
  constructor(
    public code: BillboraErrorCode,
    public message: string,
    public details?: Record<string, string[]>,
    public statusCode?: number
  ) {
    super(message)
    this.name = 'BillboraError'
  }
}

const handleApiError = (error: any): never => {
  switch (error.error.code) {
    case BillboraErrorCode.VALIDATION_ERROR:
      // Show field-specific validation errors
      const fieldErrors = error.error.details
      throw new BillboraError(
        BillboraErrorCode.VALIDATION_ERROR,
        'Please fix the validation errors',
        fieldErrors,
        400
      )
      
    case BillboraErrorCode.NOT_FOUND:
      throw new BillboraError(
        BillboraErrorCode.NOT_FOUND,
        'The requested resource was not found',
        undefined,
        404
      )
      
    case BillboraErrorCode.RATE_LIMITED:
      const retryAfter = error.error.details?.retry_after || 3600
      throw new BillboraError(
        BillboraErrorCode.RATE_LIMITED,
        `Rate limit exceeded. Retry after ${retryAfter} seconds`,
        undefined,
        429
      )
      
    default:
      throw new BillboraError(
        error.error.code,
        error.error.message,
        error.error.details,
        error.status
      )
  }
}

3. Implement Retry Logic

async function apiCallWithRetry(apiCall, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await apiCall()
    } catch (error) {
      if (error.status === 429) {
        // Rate limited - wait and retry
        const retryAfter = error.details?.retry_after || Math.pow(2, attempt) * 1000
        await new Promise(resolve => setTimeout(resolve, retryAfter))
        continue
      }
      
      if (error.status >= 500 && attempt < maxRetries) {
        // Server error - retry with exponential backoff
        const delay = Math.pow(2, attempt) * 1000
        await new Promise(resolve => setTimeout(resolve, delay))
        continue
      }
      
      // Don't retry client errors (4xx)
      throw error
    }
  }
}

// Usage
const invoice = await apiCallWithRetry(() => 
  createInvoice(invoiceData)
)

Rate Limiting

Understanding Rate Limits

Billbora implements rate limiting to ensure fair usage:
  • General API: 1,000 requests per hour per user
  • Authentication: 10 requests per minute per IP
  • Bulk operations: 100 requests per hour per user

Rate Limit Headers

Every API response includes rate limit information:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1642694400
X-RateLimit-Reset-After: 3600

Handling Rate Limits

const response = await fetch('https://api.billbora.com/api/v1/invoices/')

const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'))
const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'))

if (remaining < 10) {
  console.warn('Rate limit nearly exceeded')
  // Consider slowing down requests
}

if (response.status === 429) {
  const retryAfter = response.headers.get('Retry-After')
  console.log(`Rate limited. Retry after ${retryAfter} seconds`)
}

Debugging API Errors

1. Use Request IDs

Every API response includes a request ID for debugging:
{
  "meta": {
    "request_id": "req_abc123xyz",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}
When contacting support, always include the request ID.

2. Enable Request Logging

const originalFetch = fetch

window.fetch = async (...args) => {
  const [url, options] = args
  const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
  
  console.log(`[${requestId}] Request:`, {
    url,
    method: options?.method || 'GET',
    headers: options?.headers,
    body: options?.body
  })
  
  try {
    const response = await originalFetch(...args)
    const clonedResponse = response.clone()
    const responseData = await clonedResponse.json()
    
    console.log(`[${requestId}] Response:`, {
      status: response.status,
      headers: Object.fromEntries(response.headers.entries()),
      data: responseData
    })
    
    return response
  } catch (error) {
    console.error(`[${requestId}] Error:`, error)
    throw error
  }
}

3. Validation Error Details

For validation errors, the API provides detailed field-level error messages:
{
  "error": {
    "code": "validation_error",
    "message": "Invalid input data",
    "details": {
      "line_items": [
        "At least one line item is required"
      ],
      "line_items.0.unit_price": [
        "Must be a valid monetary amount"
      ],
      "client": [
        "This field is required"
      ]
    }
  }
}
Use these details to show specific error messages to users.

Error Recovery Strategies

1. Graceful Degradation

async function createInvoiceWithFallback(invoiceData) {
  try {
    return await billboraAPI.invoices.create(invoiceData)
  } catch (error) {
    if (error.code === 'validation_error') {
      // Show validation errors to user
      showValidationErrors(error.details)
      return null
    }
    
    if (error.code === 'rate_limit_exceeded') {
      // Queue for later processing
      queueForLater(invoiceData)
      showMessage('Invoice queued for processing')
      return null
    }
    
    // For other errors, show generic message and log details
    logError(error)
    showMessage('Something went wrong. Please try again.')
    return null
  }
}

2. Offline Support

class OfflineInvoiceQueue {
  constructor() {
    this.queue = JSON.parse(localStorage.getItem('invoice_queue') || '[]')
  }
  
  add(invoiceData) {
    this.queue.push({
      id: Date.now(),
      data: invoiceData,
      timestamp: new Date().toISOString()
    })
    localStorage.setItem('invoice_queue', JSON.stringify(this.queue))
  }
  
  async processQueue() {
    const items = [...this.queue]
    this.queue = []
    localStorage.setItem('invoice_queue', JSON.stringify(this.queue))
    
    for (const item of items) {
      try {
        await billboraAPI.invoices.create(item.data)
        console.log(`Processed queued invoice ${item.id}`)
      } catch (error) {
        // Re-queue if still failing
        this.add(item.data)
        console.error(`Failed to process queued invoice ${item.id}:`, error)
      }
    }
  }
}

Getting Help

When you encounter errors that you can’t resolve:
Pro Tip: Always include the request ID when reporting issues. It helps our support team diagnose problems much faster.