Skip to main content

Error Handling

Understanding how to handle errors effectively is crucial for building robust integrations with the OEMSTREAM Webhook API. This guide covers all possible error scenarios and best practices for handling them.

HTTP Status Codes

The OEMSTREAM API uses standard HTTP status codes to indicate the success or failure of requests:

Success Codes

200 OK

Request was successful and webhook was processed.

Client Error Codes

400 Bad Request

The request was invalid. Common causes:

  • Invalid JSON in request body
  • Missing required fields
  • Malformed data
401 Unauthorized

Authentication failed. Causes:

  • Missing API key
  • Invalid API key
  • Expired API key
403 Forbidden

Request was authenticated but not authorized. Causes:

  • Insufficient permissions
  • Company access restrictions
  • Integration access denied
404 Not Found

The requested resource was not found. Causes:

  • Invalid integration key
  • Integration not accessible to your account
422 Unprocessable Entity

The request was well-formed but contains semantic errors. Causes:

  • Validation failures
  • Business rule violations
429 Too Many Requests

Rate limit exceeded. The response includes a retry_after field indicating when to retry.

Server Error Codes

500 Internal Server Error

An unexpected error occurred on the server. These should be retried with exponential backoff.

502 Bad Gateway

The server received an invalid response from an upstream server.

503 Service Unavailable

The service is temporarily unavailable. Retry with exponential backoff.

504 Gateway Timeout

The server didn't receive a timely response from an upstream server.

Error Response Format

All error responses follow a consistent JSON format:

{
"error": "error_code",
"message": "Human-readable error description",
"details": {
"field": "Additional error details (optional)"
},
"retry_after": 60
}

Error Response Fields

FieldTypeDescription
errorstringMachine-readable error code
messagestringHuman-readable error description
detailsobjectAdditional error context (optional)
retry_afterintegerSeconds to wait before retrying (for rate limits)

Common Error Scenarios

Authentication Errors

Missing API Key

{
"error": "missing_api_key",
"message": "API key is required in X-API-Key header"
}

Solution: Include the X-API-Key header in your request.

Invalid API Key

{
"error": "invalid_api_key",
"message": "The provided API key is not valid or has been revoked"
}

Solution: Verify your API key is correct and hasn't been revoked.

Integration Errors

Integration Not Found

{
"error": "integration_not_found",
"message": "The specified integration key does not exist or is not accessible"
}

Solution: Verify the integration key and ensure you have access to it.

Integration Inactive

{
"error": "integration_inactive",
"message": "The integration is currently inactive"
}

Solution: Activate the integration in your admin panel.

Validation Errors

Invalid JSON

{
"error": "invalid_json",
"message": "The request body contains invalid JSON data",
"details": {
"line": 5,
"column": 12,
"syntax_error": "Unexpected token '}'"
}
}

Solution: Validate your JSON before sending the request.

Request Too Large

{
"error": "payload_too_large",
"message": "Request payload exceeds maximum size limit",
"details": {
"max_size": "1MB",
"received_size": "1.5MB"
}
}

Solution: Reduce the size of your request payload.

Rate Limiting Errors

{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again later.",
"retry_after": 60,
"details": {
"limit": 100,
"window": "1 minute",
"reset_time": "2025-01-10T23:00:00Z"
}
}

Solution: Wait for the specified retry_after period before retrying.

Error Handling Best Practices

1. Implement Proper Error Detection

Always check the HTTP status code and parse error responses:

async function sendWebhook(integrationKey, data) {
try {
const response = await fetch(`/api/webhooks/${integrationKey}`, {
method: 'POST',
headers: {
'X-API-Key': process.env.API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});

if (!response.ok) {
const errorData = await response.json();
throw new WebhookError(response.status, errorData);
}

return await response.json();
} catch (error) {
console.error('Webhook failed:', error);
throw error;
}
}

class WebhookError extends Error {
constructor(status, errorData) {
super(errorData.message);
this.name = 'WebhookError';
this.status = status;
this.code = errorData.error;
this.details = errorData.details;
this.retryAfter = errorData.retry_after;
}
}

2. Implement Retry Logic

Use exponential backoff for retryable errors:

import time
import random
from typing import Optional

class WebhookClient:
def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.base_url = base_url

def send_webhook_with_retry(
self,
integration_key: str,
data: dict,
max_retries: int = 3
) -> dict:
"""Send webhook with automatic retry logic"""

for attempt in range(max_retries + 1):
try:
return self._send_webhook(integration_key, data)

except WebhookError as e:
if not self._should_retry(e, attempt, max_retries):
raise

delay = self._calculate_delay(e, attempt)
print(f"Retrying in {delay} seconds (attempt {attempt + 1}/{max_retries + 1})")
time.sleep(delay)

raise Exception("Max retries exceeded")

def _should_retry(self, error: 'WebhookError', attempt: int, max_retries: int) -> bool:
"""Determine if the error should be retried"""

# Don't retry if we've exhausted attempts
if attempt >= max_retries:
return False

# Retry server errors (5xx)
if 500 <= error.status < 600:
return True

# Retry rate limits (429)
if error.status == 429:
return True

# Don't retry client errors (4xx)
return False

def _calculate_delay(self, error: 'WebhookError', attempt: int) -> float:
"""Calculate delay for retry with exponential backoff"""

# Use retry_after for rate limits
if error.status == 429 and error.retry_after:
return error.retry_after

# Exponential backoff with jitter
base_delay = 2 ** attempt
jitter = random.uniform(0, 1)
return base_delay + jitter

3. Handle Specific Error Types

Create specific handlers for different error scenarios:

<?php

class WebhookErrorHandler
{
public function handleWebhookError(\Exception $exception, string $integrationKey, array $data)
{
if ($exception instanceof WebhookError) {
switch ($exception->getCode()) {
case 401:
$this->handleAuthenticationError($exception);
break;

case 404:
$this->handleIntegrationNotFound($exception, $integrationKey);
break;

case 429:
$this->handleRateLimit($exception, $integrationKey, $data);
break;

case 500:
case 502:
case 503:
case 504:
$this->handleServerError($exception, $integrationKey, $data);
break;

default:
$this->handleGenericError($exception);
}
}
}

private function handleAuthenticationError(WebhookError $error)
{
// Log authentication failure
\Log::error('Webhook authentication failed', [
'error' => $error->getMessage(),
'suggestion' => 'Check API key configuration'
]);

// Notify administrators
$this->notifyAdministrators('API key authentication failed');
}

private function handleIntegrationNotFound(WebhookError $error, string $integrationKey)
{
\Log::error('Integration not found', [
'integration_key' => $integrationKey,
'error' => $error->getMessage()
]);

// Disable webhook sending for this integration
$this->disableIntegration($integrationKey);
}

private function handleRateLimit(WebhookError $error, string $integrationKey, array $data)
{
$retryAfter = $error->getRetryAfter() ?? 60;

\Log::warning('Rate limit exceeded', [
'integration_key' => $integrationKey,
'retry_after' => $retryAfter
]);

// Queue for retry
$this->queueForRetry($integrationKey, $data, $retryAfter);
}

private function handleServerError(WebhookError $error, string $integrationKey, array $data)
{
\Log::error('Server error occurred', [
'status' => $error->getStatus(),
'error' => $error->getMessage(),
'integration_key' => $integrationKey
]);

// Queue for retry with exponential backoff
$this->queueForRetryWithBackoff($integrationKey, $data);
}
}

4. Logging and Monitoring

Implement comprehensive logging for debugging and monitoring:

public class WebhookLogger
{
private readonly ILogger<WebhookLogger> _logger;

public WebhookLogger(ILogger<WebhookLogger> logger)
{
_logger = logger;
}

public void LogWebhookAttempt(string integrationKey, object data, int attempt = 1)
{
_logger.LogInformation("Sending webhook {IntegrationKey} (attempt {Attempt})",
integrationKey, attempt);
}

public void LogWebhookSuccess(string integrationKey, string uuid, TimeSpan responseTime)
{
_logger.LogInformation("Webhook sent successfully {IntegrationKey} {Uuid} in {ResponseTime}ms",
integrationKey, uuid, responseTime.TotalMilliseconds);
}

public void LogWebhookError(string integrationKey, Exception error, int attempt)
{
if (error is WebhookError webhookError)
{
_logger.LogError("Webhook failed {IntegrationKey} (attempt {Attempt}): {Status} {Error}",
integrationKey, attempt, webhookError.Status, webhookError.Message);
}
else
{
_logger.LogError(error, "Webhook failed {IntegrationKey} (attempt {Attempt})",
integrationKey, attempt);
}
}

public void LogRetryScheduled(string integrationKey, TimeSpan delay, int attempt)
{
_logger.LogWarning("Webhook retry scheduled {IntegrationKey} in {Delay}s (attempt {Attempt})",
integrationKey, delay.TotalSeconds, attempt);
}
}

5. Circuit Breaker Pattern

Implement circuit breaker to prevent cascading failures:

class WebhookCircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}

async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is 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++;
this.lastFailureTime = Date.now();

if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
}
}

// Usage
const circuitBreaker = new WebhookCircuitBreaker();

async function sendWebhookWithCircuitBreaker(integrationKey, data) {
return await circuitBreaker.call(async () => {
return await sendWebhook(integrationKey, data);
});
}

Error Recovery Strategies

1. Dead Letter Queue

Implement a dead letter queue for failed webhooks:

import json
from datetime import datetime, timedelta

class DeadLetterQueue:
def __init__(self, storage_backend):
self.storage = storage_backend

def add_failed_webhook(self, integration_key: str, data: dict, error: dict, max_retries: int = 3):
"""Add a failed webhook to the dead letter queue"""

dead_letter = {
'id': str(uuid.uuid4()),
'integration_key': integration_key,
'data': data,
'error': error,
'failed_at': datetime.utcnow().isoformat(),
'retry_count': 0,
'max_retries': max_retries,
'next_retry': (datetime.utcnow() + timedelta(minutes=5)).isoformat()
}

self.storage.save(dead_letter['id'], dead_letter)

def process_dead_letters(self):
"""Process items in the dead letter queue"""

now = datetime.utcnow()

for item in self.storage.get_ready_for_retry(now):
try:
# Attempt to resend
result = self.resend_webhook(item['integration_key'], item['data'])

# Success - remove from queue
self.storage.remove(item['id'])

except Exception as e:
# Failed again
item['retry_count'] += 1
item['last_error'] = str(e)

if item['retry_count'] >= item['max_retries']:
# Move to permanent failure
self.storage.move_to_permanent_failure(item['id'])
else:
# Schedule next retry with exponential backoff
delay = 2 ** item['retry_count'] * 5 # 5, 10, 20, 40 minutes
item['next_retry'] = (now + timedelta(minutes=delay)).isoformat()
self.storage.update(item['id'], item)

2. Health Checks

Implement health checks to monitor integration status:

package main

import (
"context"
"fmt"
"net/http"
"time"
)

type IntegrationHealthChecker struct {
client *http.Client
apiKey string
baseURL string
}

func NewIntegrationHealthChecker(apiKey, baseURL string) *IntegrationHealthChecker {
return &IntegrationHealthChecker{
client: &http.Client{
Timeout: 10 * time.Second,
},
apiKey: apiKey,
baseURL: baseURL,
}
}

func (h *IntegrationHealthChecker) CheckIntegration(ctx context.Context, integrationKey string) error {
// Send a test webhook to check if integration is healthy
testData := map[string]interface{}{
"event": "health_check",
"data": map[string]interface{}{
"timestamp": time.Now().UTC().Format(time.RFC3339),
"source": "health_checker",
},
}

req, err := h.createRequest(ctx, integrationKey, testData)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

resp, err := h.client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check failed with status %d", resp.StatusCode)
}

return nil
}

func (h *IntegrationHealthChecker) MonitorIntegrations(ctx context.Context, integrationKeys []string, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
for _, key := range integrationKeys {
if err := h.CheckIntegration(ctx, key); err != nil {
fmt.Printf("Integration %s is unhealthy: %v\n", key, err)
// Alert administrators
h.alertUnhealthyIntegration(key, err)
} else {
fmt.Printf("Integration %s is healthy\n", key)
}
}
}
}
}

Next Steps