How to Source Candidates Externally and Push Them into an ATS Using Unified's ATS API
February 25, 2026
Sourcing candidates outside the ATS is common.
Recruiters pull from internal talent databases, enrichment providers, sourcing tools, and external pipelines. The integration work starts after sourcing: pushing candidates into the ATS cleanly, without duplicates, and with enough metadata for recruiters to act.
This guide walks through a full write-back workflow using Unified's ATS API:
- Read job requirements from the ATS
- Match against an external talent source
- De-duplicate candidates
- Create candidate
- Create application
- Attach resume
- Set stage
- Add tags and source metadata
The mental model: your sourcing engine is external, the ATS is the system of record
Your matching pipeline lives outside the ATS. But the ATS remains the operational system recruiters use. Your integration must:
- Avoid creating duplicate candidates
- Sequence writes predictably
- Handle provider variability (documents, stages, custom fields)
- Be idempotent in your own application logic
Unified provides normalized objects and consistent endpoints. It does not enforce upserts or de-dupe for you.
Step 1: Pull job requirements from the ATS
List jobs and filter to open roles
listAtsJobs supports a status filter. Use it when the integration returns status reliably; otherwise, pull jobs and filter locally.
const jobs = await sdk.ats.listAtsJobs({
connectionId,
status: 'OPEN', // if supported by the integration's job model
limit: 100,
offset: 0,
sort: 'updated_at',
order: 'asc',
fields: '',
raw: '',
});
What to extract for matching
From AtsJob, the most useful matching signals are:
name,descriptionminimum_experience_years,minimum_degreeemployment_type,remotequestions[](screening criteria)metadata[](customer-specific requirements)
Do not rely on skills[] as a primary requirement signal. It exists in the normalized schema but is not consistently populated across ATS integrations.
Step 2: Match candidates externally
This is your product's logic. A practical baseline is a hybrid approach:
- Hard filters: degree, minimum years, location/remote, work authorization (if you track it)
- Retrieval: embeddings over job description vs resume / experience text
- Reranking: rules + model score
The key output from this step should be:
- The external candidate record
- A match score + explanation (for auditability)
- A normalized candidate payload to write into the ATS
Step 3: Handle duplicate candidates (email-based matching)
Unified does not provide idempotency keys or upsert semantics for ATS writes. You must avoid duplicates explicitly.
listAtsCandidates supports a query parameter documented as:
'Query string to search. eg. email address or name'
Use it for email-based matching.
const existing = await sdk.ats.listAtsCandidates({
connectionId,
query: 'candidate@example.com',
limit: 10,
offset: 0,
sort: 'updated_at',
order: 'asc',
fields: '',
raw: '',
});
If you find a match, reuse that candidate id. If not, create a new candidate.
Step 4: Create a candidate
const candidate = await sdk.ats.createAtsCandidate({
connectionId,
atsCandidate: {
first_name: 'Jane',
last_name: 'Doe',
emails: [{ email: 'jane@example.com', type: 'WORK' }],
title: 'Senior Backend Engineer',
company_name: 'Previous Co',
origin: 'SOURCED',
tags: ['SOURCED', 'EXTERNAL_MATCH'],
metadata: [
{ slug: 'source_system', value: 'external_db' },
{ slug: 'match_score', value: 0.87 },
],
},
fields: '',
raw: '',
});
Notes:
- Required fields vary by ATS integration.
- Some providers may require email/name.
- Treat tags and metadata as provider-dependent fields (they are part of the model, but field-level write support varies).
Step 5: Create an application (link candidate → job)
const application = await sdk.ats.createAtsApplication({
connectionId,
atsApplication: {
candidate_id: candidate.id,
job_id: job.id,
status: 'NEW',
source: 'External sourcing pipeline',
},
fields: '',
raw: '',
});
Notes:
candidate_idandjob_idare typically required (integration-specific).statusis writable on create for many integrations.offers[]is not writable via the ATS unified application object.
Step 6: Attach the resume
You can attach resumes using the unified Document object. The key variability is:
- Some integrations accept
document_data(base64) - Some accept
document_url - Attachments may be candidate-bound or application-bound depending on the ATS integration
Variant A: Base64 resume upload
await sdk.ats.createAtsDocument({
connectionId,
atsDocument: {
filename: 'resume.pdf',
type: 'RESUME',
candidate_id: candidate.id,
document_data: base64EncodedResume,
},
fields: '',
raw: '',
});
Variant B: URL-based resume attachment
await sdk.ats.createAtsDocument({
connectionId,
atsDocument: {
filename: 'resume.pdf',
type: 'RESUME',
application_id: application.id,
document_url: 'https://your-storage.com/resumes/jane-doe.pdf',
},
fields: '',
raw: '',
});
Important detail: document_url values returned by Unified for reading often expire after one hour. For write flows, if you are providing document_url, ensure it is accessible to the ATS integration.
Step 7: Set stage (update the application status)
You can update the application status via:
await sdk.ats.updateAtsApplication({
connectionId,
id: application.id,
atsApplication: {
status: 'REVIEWING',
},
fields: '',
raw: '',
});
Notes:
- Stage transition rules are not documented as a Unified-wide model.
- Some providers classify status/original_status as 'slow fields.'
- Handle 429/5XX with backoff and retry.
Step 8: Add tags and source metadata
If you didn't apply tags at create time or want to add more context:
await sdk.ats.updateAtsCandidate({
connectionId,
id: candidate.id,
atsCandidate: {
tags: ['SOURCED', 'AI_MATCHED', 'HIGH_CONFIDENCE'],
metadata: [
{ slug: 'source_system', value: 'external_db' },
{ slug: 'source_run_id', value: 'run_2026_02_25_001' },
],
},
fields: '',
raw: '',
});
Notes:
- Tags and metadata are part of the normalized model.
- Actual write support may vary by provider; consult integration field support before relying on specific custom fields.
Recommended write sequencing
- Read job
- Run external matching
- Search ATS candidates by email (
query) - Create candidate if needed
- Create application
- Attach resume
- Update stage
- Update tags/metadata
Persist intermediate IDs after each step:
candidate.idapplication.id
Handling retries and safety
Unified does not provide atomic multi-step transactions across these calls. Your application should:
- Retry idempotently on 429/5XX
- Avoid duplicate candidate creation by always searching first
- Log every provider response
- Store 'write state' checkpoints so you can resume after a failure
Closing thoughts
External sourcing only matters if recruiters can act on it inside their ATS.
Unified's ATS API gives you:
- A normalized job model to pull requirements
- Candidate search via
queryto dedupe by email/name - Candidate and application write endpoints
- Document attachment endpoints
- Application status updates
- Tags and metadata support
From there, a reliable sourcing write-back workflow is mostly orchestration: sequencing, dedupe, retries, and provider-aware fallbacks.