Skip to main content

Overview

The InstaView API uses standard HTTP status codes and returns structured error responses to help you diagnose and handle issues in your integration.

Response Structure

All API responses follow a consistent format:
{
  "success": boolean,
  "data": object | array | null,
  "error": {
    "code": string,
    "message": string,
    "details": object
  } | null,
  "timestamp": string
}

Success Response

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Senior Engineer"
  },
  "error": null,
  "timestamp": "2024-01-15T10:30:00Z"
}

Error Response

{
  "success": false,
  "data": null,
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Job with ID 550e8400-e29b-41d4-a716-446655440000 not found",
    "details": {
      "resourceType": "Job",
      "resourceId": "550e8400-e29b-41d4-a716-446655440000"
    }
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

HTTP Status Codes

CodeMeaningUsage
200OKSuccessful GET, PATCH, DELETE requests
201CreatedSuccessful POST request creating a resource

Error Codes

Authentication Errors

INVALID_API_KEY
401
The provided API key is invalid, revoked, or expired
{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "The provided API key is invalid or has been revoked"
  }
}
Solutions:
  • Verify API key format
  • Check if key was revoked in dashboard
  • Ensure key hasn’t expired
API_KEY_SUSPENDED
401
The API key has been temporarily suspended
{
  "error": {
    "code": "API_KEY_SUSPENDED",
    "message": "This API key has been suspended",
    "details": {
      "suspendedAt": "2024-01-10T00:00:00Z",
      "reason": "Unusual activity detected"
    }
  }
}
Solution: Contact support or check dashboard for details

Permission Errors

INSUFFICIENT_PERMISSIONS
403
API key lacks required scope
{
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "This API key does not have the required scope: write:jobs",
    "details": {
      "required": "write:jobs",
      "available": ["read:jobs", "read:candidates"]
    }
  }
}
Solution: Add required scope to API key or create new key
RESOURCE_ACCESS_DENIED
403
Resource belongs to different company
{
  "error": {
    "code": "RESOURCE_ACCESS_DENIED",
    "message": "Access denied to this resource"
  }
}
Solutions:
  • Verify resource belongs to your company
  • Check if using correct companyId (ATS keys)
  • Ensure resource hasn’t been deleted

Validation Errors

VALIDATION_ERROR
400
Request validation failed
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": {
      "fields": {
        "email": "Invalid email format",
        "phoneNumber": "Phone number is required",
        "jobId": "Must be a valid UUID"
      }
    }
  }
}
Solution: Fix validation errors in request body
MISSING_REQUIRED_FIELD
400
Required field not provided
{
  "error": {
    "code": "MISSING_REQUIRED_FIELD",
    "message": "Missing required field: jobId",
    "details": {
      "field": "jobId",
      "type": "string (UUID)"
    }
  }
}

Resource Errors

RESOURCE_NOT_FOUND
404
Requested resource doesn’t exist
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Job not found",
    "details": {
      "resourceType": "Job",
      "resourceId": "550e8400-e29b-41d4-a716-446655440000"
    }
  }
}
RESOURCE_CONFLICT
409
Resource already exists or state conflict
{
  "error": {
    "code": "RESOURCE_CONFLICT",
    "message": "A candidate with this email already exists for this job",
    "details": {
      "conflictingField": "email",
      "existingResourceId": "candidate-uuid"
    }
  }
}

Rate Limiting

RATE_LIMIT_EXCEEDED
429
Too many requests
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Please retry after 45 seconds",
    "details": {
      "retryAfter": 45,
      "limit": 300,
      "window": "minute"
    }
  }
}
Solution: Wait for retryAfter seconds, implement exponential backoff

Business Logic Errors

BILLING_LIMIT_EXCEEDED
403
Company billing limit reached
{
  "error": {
    "code": "BILLING_LIMIT_EXCEEDED",
    "message": "Monthly interview limit exceeded",
    "details": {
      "limit": 100,
      "used": 100,
      "resetDate": "2024-02-01T00:00:00Z"
    }
  }
}
Solution: Upgrade plan or wait for monthly reset
INVALID_STATE_TRANSITION
422
Invalid resource state change
{
  "error": {
    "code": "INVALID_STATE_TRANSITION",
    "message": "Cannot delete job with active candidates",
    "details": {
      "currentState": "active",
      "attemptedAction": "delete",
      "reason": "Job has 5 active candidates"
    }
  }
}

Error Handling Patterns

Basic Error Handling

async function createJob(jobData) {
  try {
    const response = await fetch('https://api.instaview.sk/jobs', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.INSTAVIEW_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(jobData)
    });
    
    const result = await response.json();
    
    if (!result.success) {
      throw new InstaViewError(result.error);
    }
    
    return result.data;
  } catch (error) {
    console.error('Failed to create job:', error.message);
    throw error;
  }
}

class InstaViewError extends Error {
  constructor(errorData) {
    super(errorData.message);
    this.code = errorData.code;
    this.details = errorData.details;
    this.name = 'InstaViewError';
  }
}

Comprehensive Error Handler

class InstaViewClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseURL = 'https://api.instaview.sk/v1';
  }
  
  async request(method, path, data = null) {
    try {
      const response = await fetch(`${this.baseURL}${path}`, {
        method,
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json'
        },
        body: data ? JSON.stringify(data) : null
      });
      
      const result = await response.json();
      
      if (!result.success) {
        return this.handleError(result.error, response.status);
      }
      
      return result.data;
    } catch (error) {
      console.error('Network error:', error);
      throw error;
    }
  }
  
  handleError(error, statusCode) {
    switch (error.code) {
      case 'INVALID_API_KEY':
      case 'API_KEY_SUSPENDED':
        console.error('Authentication error:', error.message);
        // Notify admin, stop operations
        throw new AuthenticationError(error);
        
      case 'INSUFFICIENT_PERMISSIONS':
        console.error('Permission denied:', error.message);
        console.error('Required:', error.details.required);
        console.error('Available:', error.details.available);
        throw new PermissionError(error);
        
      case 'VALIDATION_ERROR':
        console.error('Validation failed:', error.details.fields);
        // Show user-friendly messages
        throw new ValidationError(error);
        
      case 'RATE_LIMIT_EXCEEDED':
        console.log(`Rate limited. Retry after ${error.details.retryAfter}s`);
        // Implement retry logic
        throw new RateLimitError(error);
        
      case 'RESOURCE_NOT_FOUND':
        console.warn('Resource not found:', error.details);
        return null; // Handle gracefully
        
      case 'BILLING_LIMIT_EXCEEDED':
        console.error('Billing limit exceeded:', error.message);
        // Notify admin, show upgrade prompt
        throw new BillingError(error);
        
      default:
        console.error('Unknown error:', error);
        throw new InstaViewError(error);
    }
  }
}

Retry with Exponential Backoff

async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      // Don't retry client errors (except rate limits)
      if (error.statusCode >= 400 && error.statusCode < 500) {
        if (error.code === 'RATE_LIMIT_EXCEEDED') {
          const delay = error.details.retryAfter * 1000 || baseDelay;
          await sleep(delay);
          continue;
        }
        throw error; // Don't retry other client errors
      }
      
      // Retry server errors with exponential backoff
      if (error.statusCode >= 500) {
        const delay = baseDelay * Math.pow(2, attempt);
        console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
        await sleep(delay);
        continue;
      }
      
      throw error;
    }
  }
  
  throw lastError;
}

// Usage
const job = await retryWithBackoff(async () => {
  return await client.createJob(jobData);
});

Validation Error Handling

Handle field-level validation errors:
async function createCandidateWithValidation(candidateData) {
  try {
    return await client.createCandidate(candidateData);
  } catch (error) {
    if (error.code === 'VALIDATION_ERROR') {
      const fieldErrors = error.details.fields;
      
      // Display user-friendly errors
      for (const [field, message] of Object.entries(fieldErrors)) {
        showFieldError(field, message);
      }
      
      // Or collect all errors
      const errors = Object.entries(fieldErrors).map(([field, msg]) => ({
        field,
        message: msg
      }));
      
      return { success: false, errors };
    }
    
    throw error;
  }
}

function showFieldError(field, message) {
  // Display error next to form field
  document.getElementById(`${field}-error`).textContent = message;
}

Idempotency

Implement idempotency for safe retries:
async function createJobIdempotent(jobData, idempotencyKey) {
  try {
    return await fetch('https://api.instaview.sk/jobs', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey // Use same key for retries
      },
      body: JSON.stringify(jobData)
    });
  } catch (error) {
    // Safe to retry with same idempotency key
    return await fetch(/* same request */);
  }
}

// Generate idempotency key
const idempotencyKey = `job-create-${Date.now()}-${Math.random()}`;
await createJobIdempotent(jobData, idempotencyKey);

Logging and Monitoring

Log errors for debugging and monitoring:
class ErrorLogger {
  static log(error, context = {}) {
    const errorLog = {
      timestamp: new Date().toISOString(),
      code: error.code,
      message: error.message,
      details: error.details,
      statusCode: error.statusCode,
      context: context,
      stack: error.stack
    };
    
    // Send to logging service
    console.error(JSON.stringify(errorLog));
    
    // Send to error tracking (Sentry, etc.)
    if (process.env.NODE_ENV === 'production') {
      Sentry.captureException(error, {
        extra: errorLog
      });
    }
  }
}

// Usage
try {
  await createJob(jobData);
} catch (error) {
  ErrorLogger.log(error, {
    operation: 'createJob',
    jobData: jobData,
    userId: currentUser.id
  });
  throw error;
}

Best Practices

const response = await fetch(url);
const result = await response.json();

if (!result.success) {
  // Handle error
  throw new Error(result.error.message);
}

return result.data;
class DetailedError extends Error {
  constructor(error, context) {
    super(error.message);
    this.code = error.code;
    this.details = error.details;
    this.context = context; // Add operation context
  }
}

throw new DetailedError(apiError, {
  operation: 'createCandidate',
  candidateEmail: data.email,
  jobId: data.jobId
});
// ❌ Don't expose sensitive data in logs
console.error('Failed:', error, apiKey);

// ✅ Log safely
console.error('Failed:', error.code, error.message);
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED';
    this.nextAttempt = Date.now();
  }
  
  async execute(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;
    }
  }
}

Next Steps