Unified.to
All articles

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:

  1. Read job requirements from the ATS
  2. Match against an external talent source
  3. De-duplicate candidates
  4. Create candidate
  5. Create application
  6. Attach resume
  7. Set stage
  8. 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, description
  • minimum_experience_years, minimum_degree
  • employment_type, remote
  • questions[] (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).
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_id and job_id are typically required (integration-specific).
  • status is 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.
  1. Read job
  2. Run external matching
  3. Search ATS candidates by email (query)
  4. Create candidate if needed
  5. Create application
  6. Attach resume
  7. Update stage
  8. Update tags/metadata

Persist intermediate IDs after each step:

  • candidate.id
  • application.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 query to 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.

→ Start your 30-day free trial

→ Book a demo

All articles