---
title: "How to Match Candidates to Jobs Using ATS Data with"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_match_candidates_to_jobs_using_ats_data_with-icon.png
date: 2026-02-18T21:23:00.000Z
updated: 2026-10-21T11:23:24.762Z
readingTime: 4
tag: Product
description: "To match candidates to jobs with Unified.to's ATS API, read open jobs, candidates, applications and resumes, extract structured and text features, and score..."
url: "https://unified.to/blog/how_to_match_candidates_to_jobs_using_ats_data_with"
---

# How to Match Candidates to Jobs Using ATS Data with

To match candidates to jobs with [Unified.to](http://unified.to/)'s [ATS API](/ats), read open jobs, candidates, applications and resumes, extract structured and text features, and score them in your own system. This guide is for teams building matching, ranking or screening features. Matching sounds like an algorithm problem; in practice, it's a data-modelling problem first.


Before you can score candidates, rank applicants, or train historical models, you need consistent access to:

- Open job requirements
- Candidate qualifications
- Application relationships
- Outcome signals

Across ATS systems, those elements are structured differently — and not always fully available.


## What data does matching need?


Candidate-job matching depends on four data layers:

1. **Jobs (**`AtsJob`**)** – requirements and context
2. **Candidates (**`AtsCandidate`**)** – qualifications and profile data
3. **Applications (**`AtsApplication`**)** – pipeline relationships
4. **Documents (**`AtsDocument`**)** – resumes and attachments

Unified.to provides normalized models for each of these objects. However, field availability varies by integration and must be handled defensively.


## Step 1: How do you read open jobs?


Open jobs are your target set for matching.


### Endpoint


```plain text
GET /ats/{connection_id}/job
```


SDK example:


```typescript
const jobs = await sdk.ats.listAtsJobs({
  connectionId,
  status: 'OPEN',
  sort: 'updated_at',
  order: 'asc',
  limit: 100,
  offset: 0,
  fields: '',
  raw: '',
});
```


### Key Job Fields


From the normalized `AtsJob` model:

- `id`
- `name`
- `description`
- `status`
- `minimum_experience_years`
- `minimum_degree`
- `employment_type`
- `remote`
- `questions[]`
- `metadata[]`

### Important Caveat: `skills[]`


Although `skills[]` exists on the job model, few integrations return it for jobs. For candidates it's more common but still a minority, on integrations such as Bullhorn, JobAdder, Workable, iCIMS, Workday and Vincere.


**Implication:**


Do not rely on `skills[]` as your primary matching signal. Treat it as optional structured enrichment when available.


## Step 2: How do you read candidate profiles?


Candidates are ingested via:


```plain text
GET /ats/{connection_id}/candidate
```


SDK example:


```typescript
const candidates = await sdk.ats.listAtsCandidates({
  connectionId,
  sort: 'updated_at',
  order: 'asc',
  limit: 100,
  offset: 0,
  fields: '',
  raw: '',
});
```


### Key Candidate Fields


From `AtsCandidate`:

- `first_name`, `last_name`
- `title`
- `company_name`
- `experiences[]`
- `education[]`
- `skills[]` (rarely populated)
- `tags[]`
- `metadata[]`
- `link_urls[]`

### Skills Field Variability


The `skills[]` field exists in the normalized schema, but most integrations do not populate it. Integrations such as Bullhorn, Workable and Vincere return candidate skills; many don't.


**Implication:**


Treat `skills[]` as opportunistic data, not guaranteed data.


## Step 3: How do you retrieve resumes?


Resumes provide the richest unstructured matching signal.


### Endpoint


```plain text
GET /ats/{connection_id}/document
```


### Important Facts

- Most integrations return a short-lived `document_url`.
- Only a small subset of integrations (e.g., Workable, Vincere) return `document_data` (base64) on read.
- Some integrations (e.g., Workday) return neither a document URL nor document data.
- Some ATS integrations have no document object.

### Resume Identification


The normalized document schema includes:


```plain text
type: 'RESUME' | 'COVER_LETTER' | ...
```


However, documentation does not guarantee that all providers consistently label resumes as `RESUME`.


**Implication:**


Resume retrieval depends on integration capability. Always check each provider's 'Readable Fields' list and handle missing documents gracefully.


## Step 4: How do you join candidates to jobs?


Applications link candidates to jobs.


```plain text
GET /ats/{connection_id}/application
```


Key fields:

- `candidate_id`
- `job_id`
- `status`
- `applied_at`
- `rejected_at`
- `hired_at`
- `offers[]`

Your internal model should reflect:


```plain text
Candidate
  ↳ Applications[]
      ↳ Job
```


Matching may occur:

- Before application (proactive sourcing)
- After application (ranking within pipeline)
- Post-interview (scoring finalists)

## Step 5: Which features should you extract?


Because structured skills are inconsistently available, matching should combine:


### Structured signals (when available)

- `minimum_experience_years`
- `minimum_degree`
- `employment_type`
- `remote`
- `tags[]`
- `metadata[]`

### Semi-structured signals

- `experiences[]`
- `education[]`
- `answers[]` (from applications)

### Unstructured signals

- Resume text (if retrievable via document_url or document_data)
- Job description text

Your matching pipeline may include:

- Keyword overlap
- Embedding similarity
- Rule-based scoring
- ML model scoring

Unified.to provides the ingestion layer. The scoring layer lives in your system.


## Step 6: How do you use historical outcomes for training?


If building supervised models, you need labels.


Outcome-related fields in `AtsApplication` include:

- `hired_at`
- `rejected_at`
- `offers[]`
- `offers[].accepted_at`

### Provider Variability

- `hired_at` is returned by fewer than half of integrations.
- `rejected_at` is returned inconsistently.
- Some providers return only `rejected_reason`.
- `offers[]` is supported only in certain integrations.
- When supported, `offers` is often marked as a slow field.
- Documentation does not guarantee that outcome fields are populated across integrations.

**Implication:**


Always design training pipelines to handle missing outcome data.


Example labeling logic:


```typescript
if (application.hired_at) label = 'HIRED';
else if (application.rejected_at) label = 'REJECTED';
else label = 'IN_PROGRESS';
```


But only when those fields are supported by the integration. For preparing that data for model training or [RAG](https://docs.unified.to/guides/retrieval_augmented_generation_rag), see [How to Train AI on ATS Data](https://unified.to/blog/how_to_train_ai_on_ats_data_with_unified_ats_api).


## Step 7: How do you keep matching data current?


Many integrations support `updated_gte` on candidates, jobs and applications, which enables incremental reads:


```typescript
await sdk.ats.listAtsCandidates({
  connectionId,
  updated_gte: lastSyncTimestamp,
  sort: 'updated_at',
  order: 'asc',
});
```


To rescore when records change, subscribe to [webhooks](/blog/replacing_polling_with_event_driven_integrations_using_virtual_webhooks) on supported integrations for:

- `ats_candidate`
- `ats_application`
- `ats_job`
- `ats_document`

Filter availability varies by integration and applies only to virtual webhooks.


## How does the pipeline fit together?


A defensible candidate-job matching pipeline looks like:

1. Ingest open jobs.
2. Ingest candidates.
3. Retrieve documents when supported.
4. Join via applications.
5. Extract structured and unstructured features.
6. Train or score candidates.
7. Update scores incrementally.

[Unified.to](http://unified.to/)'s ATS API provides the normalized data foundation. It does not impose a scoring model, dedupe logic, or cross-provider guarantees.


That separation is what makes it safe to build algorithmic matching across heterogeneous ATS systems.


## How do you build on imperfect data?


Matching candidates to jobs is not about having perfect data. It's about building resilient models on top of imperfect, provider-variable systems.


[Unified.to](http://unified.to/) gives you:

- Consistent job and candidate objects
- Application relationships
- Resume access when supported
- Incremental reads on supported integrations

From there, your algorithm determines the match — not the integration layer.


[→ Start your 30-day free trial](https://app.unified.to/login)


[→ Book a demo](https://calendly.com/d/cph9-g8n-jzg/connect-with-unified)