> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instaview.sk/llms.txt
> Use this file to discover all available pages before exploring further.

# Candidates

> Manage candidate profiles and applications with the Candidates API

## 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

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "jobId": "987e6543-e21b-12d3-a456-426614174000",
  "jobIds": ["987e6543-e21b-12d3-a456-426614174000"],
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.com",
  "phoneNumber": "+421915123456",
  "status": "APPLIED",
  "gender": "female",
  "gdprExpiryDate": "2026-11-16",
  "anonymizedCvText": "[NAME]\nSoftware Engineer\nExperience: ...",
  "overallRating": 85,
  "metadata": {
    "source": "LinkedIn",
    "externalId": "CAND-12345",
    "resumeUrl": "https://storage.example.com/resumes/jane-doe.pdf",
    "linkedinUrl": "https://linkedin.com/in/janedoe"
  },
  "workHistory": [
    {
      "id": "work-history-uuid",
      "companyName": "Tech Corp",
      "candidatePosition": "Software Engineer",
      "referenceName": "Jane Smith",
      "referencePhone": "+1234567890",
      "startDate": "2020-01-01",
      "endDate": "2022-12-31"
    }
  ],
  "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"
}
```

<Info>
  **Custom Fields**: Additional candidate information (resume URL, LinkedIn
  profile, skills, etc.) can be stored in the `metadata` field as key-value
  pairs.
</Info>

## Required Scopes

| Operation           | Required Scope      | Additional Scopes           |
| ------------------- | ------------------- | --------------------------- |
| List candidates     | `read:candidates`   | `read:jobs` (for filtering) |
| Get candidate by ID | `read:candidates`   |                             |
| Create candidate    | `write:candidates`  | `read:jobs` (validation)    |
| Update candidate    | `write:candidates`  |                             |
| Delete candidate    | `delete: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.

<CodeGroup>
  ```bash cURL (with job) theme={null}
  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": "jane.doe@example.com",
      "phoneNumber": "+421915123456",
      "gdprExpiryDate": "2026-11-16"
    }'
  ```

  ```bash cURL (without job) theme={null}
  curl -X POST https://api.instaview.sk/candidates \
    -H "Authorization: Bearer sk_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane.doe@example.com",
      "phoneNumber": "+421915123456",
      "gdprExpiryDate": "2026-11-16"
    }'
  ```

  ```javascript Node.js (with job) theme={null}
  const candidate = await fetch("https://api.instaview.sk/candidates", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.INSTAVIEW_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      jobId: "job-uuid",
      firstName: "Jane",
      lastName: "Doe",
      email: "jane.doe@example.com",
      phoneNumber: "+421915123456",
      gdprExpiryDate: "2026-11-16",
    }),
  });

  const result = await candidate.json();
  console.log("Candidate created:", result.data.id);
  ```

  ```javascript Node.js (without job) theme={null}
  const candidate = await fetch("https://api.instaview.sk/candidates", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.INSTAVIEW_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      firstName: "Jane",
      lastName: "Doe",
      email: "jane.doe@example.com",
      phoneNumber: "+421915123456",
      gdprExpiryDate: "2026-11-16",
      metadata: {
        source: "Career Fair",
        skills: ["Python", "JavaScript"],
      },
    }),
  });

  const result = await candidate.json();
  console.log("Candidate created:", result.data.id);
  ```

  ```python Python (with job) theme={null}
  response = requests.post(
      'https://api.instaview.sk/candidates',
      headers={
          'Authorization': f'Bearer {os.environ["INSTAVIEW_API_KEY"]}',
          'Content-Type': 'application/json'
      },
      json={
          'jobId': 'job-uuid',
          'firstName': 'Jane',
          'lastName': 'Doe',
          'email': 'jane.doe@example.com',
          'phoneNumber': '+421915123456',
          'gdprExpiryDate': '2026-11-16'
      }
  )

  candidate = response.json()['data']
  print(f'Candidate created: {candidate["id"]}')
  ```

  ```python Python (without job) theme={null}
  response = requests.post(
      'https://api.instaview.sk/candidates',
      headers={
          'Authorization': f'Bearer {os.environ["INSTAVIEW_API_KEY"]}',
          'Content-Type': 'application/json'
      },
      json={
          'firstName': 'Jane',
          'lastName': 'Doe',
          'email': 'jane.doe@example.com',
          'phoneNumber': '+421915123456',
          'gdprExpiryDate': '2026-11-16'
      }
  )

  candidate = response.json()['data']
  print(f'Candidate created: {candidate["id"]}')
  ```
</CodeGroup>

### Comprehensive Candidate Profile

```javascript theme={null}
const comprehensiveCandidate = {
  // Required fields
  firstName: "Jane",
  lastName: "Doe",
  email: "jane.doe@example.com",
  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

```javascript theme={null}
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

```javascript theme={null}
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

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

  return await response.json();
}
```

### Search Candidates

```javascript theme={null}
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");
```

<Info>
  **Best Practice**: For production integrations, always implement request timeouts.

  ```javascript theme={null}
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout

  try {
  const response = await fetch(url, {
  headers: { Authorization: `Bearer ${apiKey}` },
  signal: controller.signal,
  });
  } finally {
  clearTimeout(timeoutId);
  }

  ```
</Info>

## Updating Candidates

### Partial Update

```javascript theme={null}
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: "jane.newemail@example.com",
phoneNumber: "+1987654321",
});
```

### Update Status

```javascript theme={null}
// Move candidate through pipeline
await updateCandidate("candidate-uuid", { status: "IN_PROCESS" });
await updateCandidate("candidate-uuid", { status: "ACCEPTED" });
```

### Update Job Assignments

```javascript theme={null}
// 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

<Tabs>
  <Tab title="APPLIED">
    Initial application status - Candidate has applied - Awaiting initial review

    * Default status for new candidates
  </Tab>

  <Tab title="IN_PROCESS">
    Candidate is in the recruitment process - Resume screening in progress -
    Interviews scheduled or completed - Active evaluation and assessment
  </Tab>

  <Tab title="REJECTED">
    Not selected for position - Did not meet requirements - Unsuccessful interview

    * Position filled by another candidate
  </Tab>

  <Tab title="ACCEPTED">
    Candidate accepted the offer - Offer accepted by candidate - Process
    completed successfully - Candidate will start soon/has started
  </Tab>
</Tabs>

### Status Workflow

```javascript theme={null}
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.

```javascript theme={null}
// Create candidate and assign to multiple jobs at once
const candidate = await createCandidate({
  firstName: "Jane",
  lastName: "Doe",
  email: "jane@example.com",
  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"],
//   ...
// }
```

<Info>
  **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.
</Info>

## Deleting Candidates

### Soft Delete

```javascript theme={null}
async function deleteCandidate(candidateId) {
  const response = await fetch(
    `https://api.instaview.sk/candidates/${candidateId}`,
    {
      method: "DELETE",
      headers: { Authorization: `Bearer ${apiKey}` },
    },
  );

  return await response.json();
}
```

<Warning>
  **GDPR Compliance**: Deleted candidates are soft-deleted and retained for
  audit purposes. For permanent deletion (GDPR "right to be forgotten"), contact
  support.
</Warning>

### Bulk Delete

```javascript theme={null}
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

```javascript theme={null}
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,
        metadata: {
          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

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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, {
    metadata: {
      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

<ResponseField name="firstName" type="string" required>
  Candidate's first name (1-100 characters)
</ResponseField>

<ResponseField name="lastName" type="string" required>
  Candidate's last name (1-100 characters)
</ResponseField>

<ResponseField name="gdprExpiryDate" type="string" required>
  GDPR expiry date in ISO 8601 format (must be in the future, e.g.,
  "2026-11-16")
</ResponseField>

<ResponseField name="email" type="string">
  Valid email address (at least email or phoneNumber required)
</ResponseField>

<ResponseField name="phoneNumber" type="string">
  Phone number in E.164 format (+1234567890) (at least email or phoneNumber
  required)
</ResponseField>

<ResponseField name="jobId" type="string">
  UUID of the job to associate with (optional - must exist and belong to your
  company if provided)
</ResponseField>

<ResponseField name="jobIds" type="array">
  Array of job UUIDs for multi-job assignments (used in update operations)
</ResponseField>

<ResponseField name="metadata" type="object">
  Custom key-value pairs for extensibility (max 10KB, max 5 levels deep, max 50
  keys). Store additional fields like resumeUrl, skills, education, etc.
</ResponseField>

<ResponseField name="gender" type="string">
  Candidate's gender (`"male"` or `"female"`). Used for gender-aware addressing
  in interviews (e.g., Slovak/Czech formal titles). If not provided, gender is
  auto-detected from the candidate's name.
</ResponseField>

<ResponseField name="anonymizedCvText" type="string">
  The candidate's anonymized CV in plain text. When provided in a create or update
  request via the `cvText` field, it is automatically anonymized using Google
  Gemini.
</ResponseField>

<ResponseField name="workHistory" type="array">
  Array of candidate's professional work history items:

  * **id** (string, optional): UUID of the work history record
  * **companyName** (string, required): Name of the company/employer
  * **candidatePosition** (string, required): Candidate's role/job title
  * **referenceName** (string, optional): Name of the reference contact
  * **referencePhone** (string, required): Phone number of reference contact in E.164 format (e.g. `+1234567890`)
  * **startDate** (string, optional): Job start date in calendar format (`yyyy-MM-dd`)
  * **endDate** (string, optional): Job end date in calendar format (`yyyy-MM-dd`)
</ResponseField>

## Error Scenarios

<AccordionGroup>
  <Accordion title="Invalid Job ID" icon="circle-xmark">
    ```json theme={null}
    {
      "error": {
        "code": "RESOURCE_NOT_FOUND",
        "message": "Job not found",
        "details": {
          "field": "jobId",
          "provided": "invalid-uuid"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Duplicate Email" icon="copy">
    ```json theme={null}
    {
      "error": {
        "code": "RESOURCE_CONFLICT",
        "message": "A candidate with this email already exists for this job",
        "details": {
          "field": "email",
          "existingCandidateId": "existing-uuid"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Invalid Phone Number" icon="phone">
    ```json theme={null}
    {
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid phone number format",
        "details": {
          "field": "phoneNumber",
          "expected": "E.164 format (e.g., +1234567890)"
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Best Practices

<CardGroup cols={2}>
  <Card title="Validate Data" icon="check">
    Validate email and phone formats before submission
  </Card>

  <Card title="Handle Duplicates" icon="copy">
    Implement duplicate detection logic
  </Card>

  <Card title="Update Status" icon="arrows-rotate">
    Keep candidate status current throughout pipeline
  </Card>

  <Card title="Secure Resume URLs" icon="lock">
    Use signed URLs with expiration for resume access
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Interviews" icon="microphone" href="/guides/resources/interviews">
    Schedule interviews for candidates
  </Card>

  <Card title="Jobs" icon="briefcase" href="/guides/resources/jobs">
    Learn about job management
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference">
    View complete Candidates API reference
  </Card>

  <Card title="Best Practices" icon="star" href="/guides/best-practices">
    Production integration patterns
  </Card>
</CardGroup>
