Error Handling

Overview

The Kira API uses conventional HTTP response codes to indicate the success or failure of requests. This guide covers common errors, their causes, and how to handle them in your integration.

HTTP Status Codes

Status CodeMeaningWhen It Occurs
200 OKSuccessRequest completed successfully
201 CreatedCreatedResource successfully created
400 Bad RequestClient ErrorInvalid request parameters or validation error
401 UnauthorizedAuth ErrorMissing or invalid authentication token
404 Not FoundNot FoundResource doesn't exist
409 ConflictConflictIdempotency key reused (user creation only)
500 Internal Server ErrorServer ErrorUnexpected server error

Error Response Format

There are two error response formats depending on where the error occurs:

Business Logic Errors

Most API errors follow this simple format:

{
  "code": "error_code",
  "message": "Human-readable error description"
}

Fields:

  • code - Machine-readable error code (e.g., validation_error, unauthorized, not_found)
  • message - Human-readable error message for logging or display

Request Schema Validation Errors

Errors from request validation return a structured format with details:

{
  "error": "Invalid request data",
  "details": [
    {
      "path": "field.name",
      "message": "Error description",
      "code": "error_type"
    }
  ]
}

Fields:

  • error - Always "Invalid request data" for schema validation errors
  • details - Array of specific validation errors with field path, message, and error code

Common Errors

Authentication Errors

Missing Authorization Token

Status: 401 Unauthorized

{
  "code": "unauthorized",
  "message": "No authorization token provided"
}

Cause: Request missing Authorization header

Solution:

const headers = {
  'Authorization': `Bearer ${accessToken}`,  // Add this!
  'Content-Type': 'application/json'
};

Invalid or Expired Token

Status: 401 Unauthorized

{
  "code": "unauthorized",
  "message": "Token expired"
}

Cause: JWT token has expired or is invalid

Solution: Re-authenticate to get a new token

async function requestWithAuth(url, options) {
  try {
    return await fetch(url, {
      ...options,
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        ...options.headers
      }
    });
  } catch (error) {
    if (error.status === 401) {
      // Token expired, re-authenticate
      accessToken = await authenticate();
      // Retry request
      return await fetch(url, {
        ...options,
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          ...options.headers
        }
      });
    }
    throw error;
  }
}

No Client ID in Claims

Status: 401 Unauthorized

{
  "code": "unauthorized",
  "message": "No client ID found in claims"
}

Cause: JWT token doesn't contain required client ID claim

Solution: Ensure you're authenticating with valid credentials and API key

Validation Errors

There are two types of validation errors depending on where validation fails:

Schema Validation Errors (Request Format)

Status: 400 Bad Request

Format: Structured error with details array

{
  "error": "Invalid request data",
  "details": [
    {
      "path": "destination.currency",
      "message": "Required",
      "code": "invalid_type"
    }
  ]
}

Cause: Request body doesn't match the expected schema (missing fields, wrong types, invalid formats)

Solution: Validate request structure before sending

Business Logic Validation Errors

Status: 400 Bad Request

Format: Simple error with code and message

{
  "code": "validation_error",
  "message": "Customer not found: 550e8400-e29b-41d4-a716-446655440000"
}

Cause: Request is well-formed but violates business rules

Solution: Ensure business logic requirements are met

Examples:

// Validate request structure before sending
function validateUserData(userData) {
  const errors = [];

  if (!userData.type) errors.push('type is required');
  if (!userData.email) errors.push('email is required');
  if (!userData.first_name) errors.push('first_name is required');

  if (errors.length > 0) {
    throw new Error(`Validation failed: ${errors.join(', ')}`);
  }
}

// Check business logic requirements
async function createVirtualAccountSafe(userId, accountData) {
  // Verify user exists and is verified first
  const user = await getUser(userId);

  if (!user) {
    throw new Error('User not found');
  }

  if (user.verification_status !== 'verified') {
    throw new Error('User must be verified before creating virtual accounts');
  }

  return await createVirtualAccount(userId, accountData);
}

Resource Errors

Resource Not Found

Status: 404 Not Found

{
  "code": "not_found",
  "message": "User with ID 550e8400-e29b-41d4-a716-446655440000 not found"
}

Cause: Resource doesn't exist or user doesn't have access

Solution: Verify resource ID and user permissions

async function getUser(userId) {
  try {
    const response = await fetch(`/v1/users/${userId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });

    if (response.status === 404) {
      console.error(`User ${userId} not found`);
      return null;
    }

    return await response.json();
  } catch (error) {
    console.error('Error fetching user:', error);
    throw error;
  }
}

Idempotency Errors

Idempotency Key Missing

Status: 400 Bad Request

{
  "code": "validation_error",
  "message": "Idempotency key is required"
}

Solution: Always include idempotency key for create operations

const idempotencyKey = `user-${Date.now()}-${Math.random()}`;

await fetch('/v1/users', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
    'idempotency-key': idempotencyKey  // Always include!
  },
  body: JSON.stringify(userData)
});

Idempotency Key Reused

User Creation:

Status: 409 Conflict

{
  "code": "idempotency_key_reused",
  "message": "Idempotency key has already been used with different request data"
}

Virtual Account Creation:

Status: 400 Bad Request

{
  "code": "validation_error",
  "message": "Idempotency key has already been used with different request data"
}

Cause: Same idempotency key used with different request body

Solution: Use unique keys for each request

async function createUserWithRetry(userData, maxRetries = 3) {
  const idempotencyKey = `user-${userData.email}-${Date.now()}`;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await createUser(userData, idempotencyKey);
    } catch (error) {
      if (error.code === 'idempotency_key_reused') {
        // Same key with same data - just return success
        return await getUserByEmail(userData.email);
      }
      if (attempt === maxRetries - 1) throw error;
      await sleep(1000 * (attempt + 1)); // Exponential backoff
    }
  }
}

Business Logic Errors

User Not Verified

Status: 400 Bad Request

{
  "code": "validation_error",
  "message": "User must complete identity verification before creating virtual accounts"
}

Solution: Check verification status before creating virtual accounts

async function createVirtualAccountSafe(userId, accountData) {
  // Check user verification status
  const user = await getUser(userId);

  if (user.verification_status !== 'verified') {
    console.error('User not verified. Verification required before creating virtual accounts.');
    return {
      success: false,
      error: 'Please complete identity verification first'
    };
  }

  // User is verified, proceed
  return await createVirtualAccount(userId, accountData);
}

Customer Not Found or Not Active

Customer Not Found:

Status: 400 Bad Request

{
  "code": "validation_error",
  "message": "Customer not found: 550e8400-e29b-41d4-a716-446655440000"
}

Customer Not Active:

Status: 400 Bad Request

{
  "code": "validation_error",
  "message": "Customer is not active: 550e8400-e29b-41d4-a716-446655440000. Please activate the customer first."
}

Solution: Ensure user has completed identity verification before creating virtual accounts

Cannot Update Verified User

Status: 400 Bad Request

{
  "code": "validation_error",
  "message": "Cannot update user information for verified users"
}

Cause: Attempting to update personal information after verification

Solution: Only update allowed fields or create new verification

async function updateUser(userId, updates) {
  const user = await getUser(userId);

  if (user.verification_status === 'verified') {
    // Filter out fields that can't be updated
    const allowedFields = ['metadata', 'notification_preferences'];
    const safeUpdates = Object.keys(updates)
      .filter(key => allowedFields.includes(key))
      .reduce((obj, key) => {
        obj[key] = updates[key];
        return obj;
      }, {});

    if (Object.keys(safeUpdates).length === 0) {
      console.warn('No updates allowed for verified user');
      return user;
    }

    return await patchUser(userId, safeUpdates);
  }

  // User not verified, can update everything
  return await patchUser(userId, updates);
}

Server Errors

Internal Server Error

Status: 500 Internal Server Error

{
  "code": "internal_error",
  "message": "An unexpected error occurred"
}

Cause: Unexpected server error during request processing

Solution: Retry with exponential backoff, contact support if persists

async function requestWithRetry(requestFn, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await requestFn();
    } catch (error) {
      lastError = error;

      if (error.code === 'internal_error') {
        const waitTime = Math.pow(2, attempt) * 2000; // 2s, 4s, 8s
        console.log(`Server error. Retrying in ${waitTime}ms...`);
        await sleep(waitTime);
        continue;
      }

      // For other errors, don't retry
      throw error;
    }
  }

  // All retries failed
  console.error('Failed after all retries:', lastError);
  throw new Error('Service temporarily unavailable. Please try again later.');
}

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

Error Handling Best Practices

1. Implement Proper Error Handling

async function handleApiRequest(requestFn) {
  try {
    const response = await requestFn();
    return { success: true, data: response };
  } catch (error) {
    console.error('API Error:', {
      code: error.code,
      message: error.message,
      status: error.status
    });

    return {
      success: false,
      error: {
        code: error.code,
        message: error.message,
        userMessage: getUserFriendlyMessage(error)
      }
    };
  }
}

function getUserFriendlyMessage(error) {
  const messages = {
    'validation_error': 'Please check your input and try again.',
    'unauthorized': 'Your session has expired. Please log in again.',
    'not_found': 'The requested resource was not found.',
    'idempotency_key_reused': 'This request was already processed with different data.',
    'internal_error': 'An unexpected error occurred. Please try again later.'
  };

  return messages[error.code] || 'An error occurred. Please try again.';
}

2. Log Errors for Debugging

function logError(context, error, additionalData = {}) {
  console.error({
    timestamp: new Date().toISOString(),
    context,
    errorCode: error.code,
    errorMessage: error.message,
    statusCode: error.status,
    ...additionalData
  });

  // Send to error tracking service
  if (process.env.NODE_ENV === 'production') {
    Sentry.captureException(error, {
      extra: {
        context,
        ...additionalData
      }
    });
  }
}

3. Validate Before Sending

function validateUserData(userData) {
  const errors = [];

  // Required fields
  if (!userData.email) errors.push('Email is required');
  if (!userData.phone_number) errors.push('Phone number is required');

  // Format validation
  if (userData.email && !isValidEmail(userData.email)) {
    errors.push('Invalid email format');
  }

  if (userData.phone_number && !isValidE164(userData.phone_number)) {
    errors.push('Phone number must be in E.164 format');
  }

  if (errors.length > 0) {
    throw new ValidationError(errors.join(', '));
  }

  return true;
}

// Use before API call
try {
  validateUserData(userData);
  const user = await createUser(userData);
} catch (error) {
  if (error instanceof ValidationError) {
    // Handle validation error locally
    console.error('Validation failed:', error.message);
  }
}

4. Implement Circuit Breaker

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 execute(request) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }

    try {
      const response = await request();
      this.onSuccess();
      return response;
    } 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();

async function createVirtualAccountSafe(userId, data) {
  try {
    return await breaker.execute(() =>
      createVirtualAccount(userId, data)
    );
  } catch (error) {
    console.error('Circuit breaker triggered:', error.message);
    throw error;
  }
}

Error Response Reference

Complete Error Code List

Error CodeHTTP StatusDescription
validation_error400Invalid request data or business logic validation failure
unauthorized401Missing or invalid authentication
not_found404Resource not found
idempotency_key_reused409Idempotency key used with different data (user creation only)
internal_error500Unexpected server error

Note: Virtual account creation returns validation_error (400) for idempotency key reuse instead of 409.

Debugging Tips

Enable Detailed Logging

const DEBUG = process.env.DEBUG === 'true';

async function apiCall(url, options) {
  if (DEBUG) {
    console.log('API Request:', {
      url,
      method: options.method,
      headers: options.headers,
      body: options.body
    });
  }

  const response = await fetch(url, options);

  if (DEBUG) {
    console.log('API Response:', {
      status: response.status,
      headers: Object.fromEntries(response.headers.entries())
    });
  }

  return response;
}

Test Error Scenarios

// Test error handling in development
if (process.env.NODE_ENV === 'development') {
  describe('Error Handling', () => {
    it('handles 404 errors', async () => {
      const result = await getUser('invalid-uuid');
      expect(result).toBeNull();
    });

    it('handles validation errors', async () => {
      await expect(createUser({})).rejects.toThrow('validation_error');
    });
  });
}

Getting Help

If you encounter persistent errors:

  1. Review Documentation - Ensure you're following the guides correctly
  2. Search Error Code - Look up the specific error code in this guide
  3. Contact Support - Email [email protected] with:
    • Error code and message
    • Request ID (if available)
    • Timestamp of the error
    • Steps to reproduce

Next Steps

  • Review all integration guides to ensure proper implementation
  • Set up monitoring and alerting for errors
  • Implement comprehensive error handling in your application
  • Test error scenarios in sandbox environment