Unified.to
All articles

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:

  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

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:

  • 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:

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

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_id
  • job_id
  • status
  • applied_at
  • rejected_at
  • hired_at
  • offers[]

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_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:

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_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'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.

→ Start your 30-day free trial

→ Book a demo

All articles