Skip to main content

Overview

The Candidates API allows you to create, manage, and track candidates throughout your recruitment process. Candidates can optionally be associated with one or more jobs, or exist in your candidate pool without job assignments. They serve as the foundation for scheduling interviews.

Resource Structure

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "jobId": "987e6543-e21b-12d3-a456-426614174000",
  "jobIds": ["987e6543-e21b-12d3-a456-426614174000"],
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "[email protected]",
  "phoneNumber": "+421915123456",
  "status": "APPLIED",
  "gdprExpiryDate": "2026-11-16",
  "overallRating": 85,
  "metadata": {
    "source": "LinkedIn",
    "externalId": "CAND-12345",
    "resumeUrl": "https://storage.example.com/resumes/jane-doe.pdf",
    "linkedinUrl": "https://linkedin.com/in/janedoe"
  },
  "analysisCount": 2,
  "interviewCount": 3,
  "links": {
    "analyses": "/candidates/550e8400-e29b-41d4-a716-446655440000/analyses",
    "interviews": "/candidates/550e8400-e29b-41d4-a716-446655440000/interviews"
  },
  "createdAt": "2025-11-20T10:30:00Z",
  "updatedAt": "2025-11-20T10:30:00Z"
}
Custom Fields: Additional candidate information (resume URL, LinkedIn profile, skills, etc.) can be stored in the metadata field as key-value pairs.

Required Scopes

OperationRequired ScopeAdditional Scopes
List candidatesread:candidatesread:jobs (for filtering)
Get candidate by IDread:candidates
Create candidatewrite:candidatesread:jobs (validation)
Update candidatewrite:candidates
Delete candidatedelete:candidates

Creating Candidates

Basic Candidate Creation

Candidates can be created with or without an associated job. When created without a job, they exist in your candidate pool and can be assigned to jobs later.
curl -X POST https://api.instaview.sk/candidates \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "jobId": "job-uuid",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "[email protected]",
    "phoneNumber": "+421915123456",
    "gdprExpiryDate": "2026-11-16"
  }'

Comprehensive Candidate Profile

const comprehensiveCandidate = {
  // Required fields
  firstName: 'Jane',
  lastName: 'Doe',
  email: '[email protected]',
  phoneNumber: '+1234567890',
  gdprExpiryDate: '2026-11-16',
  
  // Optional: Associate with job(s)
  jobId: 'job-uuid', // Single job association
  
  // Store additional data in metadata (max 10KB)
  metadata: {
    // Professional information
    yearsOfExperience: 5,
    currentJobTitle: 'Senior Software Engineer',
    currentCompany: 'Tech Corp',
    skills: ['JavaScript', 'React', 'Node.js', 'TypeScript', 'AWS'],
    
    // Education
    educationLevel: 'bachelors',
    
    // Location
    location: {
      city: 'San Francisco',
      countryCode: 'US'
    },
    
    // Availability
    availability: {
      canStart: '2024-03-01',
      noticePeriod: 30 // days
    },
    
    // Links
    resumeUrl: 'https://storage.example.com/resumes/jane-doe.pdf',
    linkedinUrl: 'https://linkedin.com/in/janedoe',
    portfolioUrl: 'https://janedoe.dev',
    
    // Additional context
    notes: 'Strong technical background with excellent communication skills'
  }
};

const response = await createCandidate(comprehensiveCandidate);

Listing Candidates

List All Candidates

async function listCandidates(page = 1, limit = 20) {
  const response = await fetch(
    `https://api.instaview.sk/candidates?page=${page}&limit=${limit}`,
    {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    }
  );
  
  const result = await response.json();
  return result.data;
}

// Usage
const { items, pagination } = await listCandidates(1, 50);
console.log(`Found ${pagination.total} candidates`);

Filter by Job

async function getCandidatesForJob(jobId) {
  const response = await fetch(
    `https://api.instaview.sk/candidates?jobId=${jobId}&limit=100`,
    {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    }
  );
  
  return await response.json();
}

Filter by Status

async function getActiveCandidates() {
  const response = await fetch(
    'https://api.instaview.sk/candidates?status=APPLIED',
    {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    }
  );
  
  return await response.json();
}

Search Candidates

async function searchCandidates(query) {
  const response = await fetch(
    `https://api.instaview.sk/candidates?search=${encodeURIComponent(query)}`,
    {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    }
  );
  
  return await response.json();
}

// Search by name or email
const results = await searchCandidates('jane doe');

Updating Candidates

Partial Update

async function updateCandidate(candidateId, updates) {
  const response = await fetch(
    `https://api.instaview.sk/candidates/${candidateId}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(updates)
    }
  );
  
  return await response.json();
}

// Update contact information
await updateCandidate('candidate-uuid', {
  email: '[email protected]',
  phoneNumber: '+1987654321'
});

Update Status

// Move candidate through pipeline
await updateCandidate('candidate-uuid', { status: 'IN_PROCESS' });
await updateCandidate('candidate-uuid', { status: 'ACCEPTED' });

Update Job Assignments

// Assign candidate to multiple jobs
await updateCandidate('candidate-uuid', {
  jobIds: ['job-uuid-1', 'job-uuid-2', 'job-uuid-3']
});

// Add candidate to a specific job
const candidate = await getCandidate('candidate-uuid');
await updateCandidate('candidate-uuid', {
  jobIds: [...candidate.jobIds, 'new-job-uuid']
});

// Remove all job assignments (candidate pool)
await updateCandidate('candidate-uuid', {
  jobIds: []
});

Candidate Status Management

Available Statuses

Initial application status
  • Candidate has applied
  • Awaiting initial review
  • Default status for new candidates

Status Workflow

class CandidateWorkflow {
  async startProcess(candidateId) {
    return await updateCandidate(candidateId, {
      status: 'IN_PROCESS'
    });
  }
  
  async acceptCandidate(candidateId, startDate) {
    return await updateCandidate(candidateId, {
      status: 'ACCEPTED',
      metadata: {
        startDate: startDate
      }
    });
  }
  
  async rejectCandidate(candidateId, reason) {
    return await updateCandidate(candidateId, {
      status: 'REJECTED',
      metadata: {
        rejectionReason: reason
      }
    });
  }
}

Multiple Job Applications

Managing Multi-Job Candidates

Candidates can be associated with multiple jobs through the jobIds field, allowing you to track a single candidate across different positions.
// Create candidate and assign to multiple jobs at once
const candidate = await createCandidate({
  firstName: 'Jane',
  lastName: 'Doe',
  email: '[email protected]',
  phoneNumber: '+1234567890',
  gdprExpiryDate: '2026-11-16',
  jobId: 'job-1-uuid' // Initial job assignment
});

// Later, assign to additional jobs
await updateCandidate(candidate.id, {
  jobIds: ['job-1-uuid', 'job-2-uuid', 'job-3-uuid']
});

// Response includes both fields for compatibility
// {
//   "id": "candidate-uuid",
//   "jobId": "job-1-uuid",        // First job (backward compatible)
//   "jobIds": ["job-1-uuid", "job-2-uuid", "job-3-uuid"],
//   ...
// }
Job Associations: Use the jobIds array for managing multiple job assignments. The jobId field (singular) is maintained for backward compatibility and represents the first job.

Deleting Candidates

Soft Delete

async function deleteCandidate(candidateId) {
  const response = await fetch(
    `https://api.instaview.sk/candidates/${candidateId}`,
    {
      method: 'DELETE',
      headers: { 'Authorization': `Bearer ${apiKey}` }
    }
  );
  
  return await response.json();
}
GDPR Compliance: Deleted candidates are soft-deleted and retained for audit purposes. For permanent deletion (GDPR “right to be forgotten”), contact support.

Bulk Delete

async function bulkDeleteCandidates(candidateIds) {
  const results = [];
  
  for (const id of candidateIds) {
    try {
      await deleteCandidate(id);
      results.push({ id, success: true });
      await sleep(200); // Rate limiting
    } catch (error) {
      results.push({ id, success: false, error: error.message });
    }
  }
  
  return results;
}

Common Patterns

Candidate Import from ATS

async function importCandidatesFromATS(atsData, jobMapping) {
  const imported = [];
  const errors = [];
  
  for (const atsCandidate of atsData) {
    try {
      const instaviewJobId = jobMapping[atsCandidate.jobId];
      
      if (!instaviewJobId) {
        throw new Error(`No mapping for job ${atsCandidate.jobId}`);
      }
      
      const candidate = await createCandidate({
        jobId: instaviewJobId,
        firstName: atsCandidate.firstName,
        lastName: atsCandidate.lastName,
        email: atsCandidate.email,
        phoneNumber: atsCandidate.phone,
        resumeUrl: atsCandidate.resumeUrl,
        status: mapAtsStatus(atsCandidate.status)
      });
      
      imported.push(candidate);
      await sleep(200);
      
    } catch (error) {
      errors.push({
        candidate: atsCandidate,
        error: error.message
      });
    }
  }
  
  return { imported, errors };
}

function mapAtsStatus(atsStatus) {
  const statusMap = {
    'new': 'APPLIED',
    'in_review': 'IN_PROCESS',
    'interview': 'IN_PROCESS',
    'offer': 'IN_PROCESS',
    'hired': 'ACCEPTED',
    'rejected': 'REJECTED'
  };
  
  return statusMap[atsStatus] || 'APPLIED';
}

Duplicate Detection

async function findDuplicateCandidates(candidate) {
  // Search by email
  const byEmail = await searchCandidates(candidate.email);
  
  // Search by phone
  const byPhone = await searchCandidates(candidate.phoneNumber);
  
  // Combine and deduplicate
  const allMatches = [...byEmail.data.items, ...byPhone.data.items];
  const uniqueMatches = Array.from(
    new Map(allMatches.map(c => [c.id, c])).values()
  );
  
  return uniqueMatches.filter(c => c.id !== candidate.id);
}

async function createOrUpdateCandidate(candidateData) {
  const duplicates = await findDuplicateCandidates(candidateData);
  
  if (duplicates.length > 0) {
    console.log(`Found ${duplicates.length} potential duplicates`);
    // Update existing candidate
    return await updateCandidate(duplicates[0].id, candidateData);
  }
  
  // Create new candidate
  return await createCandidate(candidateData);
}

Candidate Analytics

async function getCandidateAnalytics(candidateId) {
  const [candidate, interviews] = await Promise.all([
    getCandidate(candidateId),
    listInterviews({ candidateId })
  ]);
  
  return {
    candidate,
    totalInterviews: interviews.pagination.total,
    completedInterviews: interviews.items.filter(
      i => i.status === 'completed'
    ).length,
    averageScore: calculateAverageScore(interviews.items),
    lastInterviewDate: getLastInterviewDate(interviews.items)
  };
}

function calculateAverageScore(interviews) {
  const scored = interviews.filter(i => i.analysis?.overallScore);
  if (scored.length === 0) return null;
  
  const sum = scored.reduce((acc, i) => acc + i.analysis.overallScore, 0);
  return sum / scored.length;
}

Resume Processing

async function uploadAndAttachResume(candidateId, resumeFile) {
  // 1. Upload resume to your storage
  const resumeUrl = await uploadToStorage(resumeFile);
  
  // 2. Update candidate with resume URL
  const updated = await updateCandidate(candidateId, {
    resumeUrl: resumeUrl
  });
  
  // 3. Optional: Trigger resume parsing
  await triggerResumeParser(candidateId, resumeUrl);
  
  return updated;
}

async function parseResumeData(resumeUrl) {
  // Use a resume parsing service
  const parsed = await resumeParsingService.parse(resumeUrl);
  
  return {
    skills: parsed.skills,
    yearsOfExperience: parsed.totalYears,
    educationLevel: parsed.highestDegree,
    currentJobTitle: parsed.currentPosition,
    currentCompany: parsed.currentEmployer
  };
}

Validation Rules

firstName
string
required
Candidate’s first name (1-100 characters)
lastName
string
required
Candidate’s last name (1-100 characters)
gdprExpiryDate
string
required
GDPR expiry date in ISO 8601 format (must be in the future, e.g., “2026-11-16”)
email
string
Valid email address (at least email or phoneNumber required)
phoneNumber
string
Phone number in E.164 format (+1234567890) (at least email or phoneNumber required)
jobId
string
UUID of the job to associate with (optional - must exist and belong to your company if provided)
jobIds
array
Array of job UUIDs for multi-job assignments (used in update operations)
metadata
object
Custom key-value pairs for extensibility (max 10KB, max 5 levels deep, max 50 keys). Store additional fields like resumeUrl, skills, education, etc.

Error Scenarios

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Job not found",
    "details": {
      "field": "jobId",
      "provided": "invalid-uuid"
    }
  }
}
{
  "error": {
    "code": "RESOURCE_CONFLICT",
    "message": "A candidate with this email already exists for this job",
    "details": {
      "field": "email",
      "existingCandidateId": "existing-uuid"
    }
  }
}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid phone number format",
    "details": {
      "field": "phoneNumber",
      "expected": "E.164 format (e.g., +1234567890)"
    }
  }
}

Best Practices

Validate Data

Validate email and phone formats before submission

Handle Duplicates

Implement duplicate detection logic

Update Status

Keep candidate status current throughout pipeline

Secure Resume URLs

Use signed URLs with expiration for resume access

Next Steps