How to Match Candidates to Jobs Using ATS Data with
·
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 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:
- Jobs (
AtsJob) – requirements and context - Candidates (
AtsCandidate) – qualifications and profile data - Applications (
AtsApplication) – pipeline relationships - 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
GET /ats/{connection_id}/job
SDK example:
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:
idnamedescriptionstatusminimum_experience_yearsminimum_degreeemployment_typeremotequestions[]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:
GET /ats/{connection_id}/candidate
SDK example:
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_nametitlecompany_nameexperiences[]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
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:
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.
GET /ats/{connection_id}/application
Key fields:
candidate_idjob_idstatusapplied_atrejected_athired_atoffers[]
Your internal model should reflect:
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_yearsminimum_degreeemployment_typeremotetags[]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_atrejected_atoffers[]offers[].accepted_at
Provider Variability
hired_atis returned by fewer than half of integrations.rejected_atis returned inconsistently.- Some providers return only
rejected_reason. offers[]is supported only in certain integrations.- When supported,
offersis 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:
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, see How to Train AI on ATS Data.
Step 7: How do you keep matching data current?
Many integrations support updated_gte on candidates, jobs and applications, which enables incremental reads:
await sdk.ats.listAtsCandidates({
connectionId,
updated_gte: lastSyncTimestamp,
sort: 'updated_at',
order: 'asc',
});
To rescore when records change, subscribe to webhooks on supported integrations for:
ats_candidateats_applicationats_jobats_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:
- Ingest open jobs.
- Ingest candidates.
- Retrieve documents when supported.
- Join via applications.
- Extract structured and unstructured features.
- Train or score candidates.
- Update scores incrementally.
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 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.