---
title: "How to Source Candidates Externally and Add Them to an ATS with"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_source_candidates_externally_and_add_them_to_an_ats_with-icon.png
date: 2026-02-25T19:16:00.000Z
updated: 2026-10-21T11:22:32.498Z
readingTime: 5
tag: Product
description: "To add externally sourced candidates to your customers' ATS with Unified.to's ATS API, read the job's requirements, match candidates in your own system, search..."
url: "https://unified.to/blog/how_to_source_candidates_externally_and_add_them_to_an_ats_with"
---

# How to Source Candidates Externally and Add Them to an ATS with

To add externally sourced candidates to your customers' ATS with [Unified.to](http://unified.to/)'s [ATS API](/ats), read the job's requirements, match candidates in your own system, search the ATS by email to avoid duplicates, then create the candidate and application and attach the resume. This guide is for sourcing and talent-matching products. Recruiters source from internal talent databases, enrichment vendors and sourcing tools; the integration work starts after that: adding candidates to the ATS without duplicates and with enough context for recruiters to act.


The steps:

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.to provides normalized objects and consistent endpoints. It does not enforce upserts or de-dupe for you.


For field-level write rules (required fields, writable fields, document attachment by ATS), see [How to Create Candidates and Applications in ATS Systems](https://unified.to/blog/how_to_push_candidates_and_applications_into_ats_systems_with_unified_ats_api). As of 19 October 2026, Greenhouse, Lever, Workable, Ashby, Bullhorn, iCIMS, SmartRecruiters and Teamtailor create both candidates and applications; Workday (REST) creates candidates but has no application object; Oracle HCM is in beta. Custom fields go in `metadata` on supported integrations.


## Step 1: How do you read job requirements from the ATS?


### List jobs and filter to open roles


`listAtsJobs` takes a `status` filter on integrations such as Greenhouse, Lever, Ashby and Teamtailor; elsewhere, list jobs and filter locally.


```typescript
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)

Few integrations return the minimum-experience and degree fields (Bullhorn does); treat them, like `skills[]`, as extras when present.


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: How do you 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

For matching on ATS data, see [How to Match Candidates to Jobs Using ATS Data](https://unified.to/blog/how_to_match_candidates_to_jobs_using_ats_data_with_unified_ats_api).


## Step 3: How do you handle duplicate candidates?


Unified.to 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. Not every integration accepts `query` (Teamtailor doesn't); where it's missing, list candidates and match emails locally.


```typescript
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: How do you create a candidate?


```typescript
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: How do you create an application?


```typescript
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 writable on only a few integrations, such as Workday (Legacy).

## Step 6: How do you attach the resume?


You can attach resumes using the Unified.to 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
- Greenhouse, Workable and SmartRecruiters take `document_url` only.

### Variant A: Base64 resume upload


```typescript
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


```typescript
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: '',
});
```


For write flows, if you are providing `document_url`, ensure it is accessible to the ATS integration.


## Step 7: How do you set the stage?


You can update the application status via:


```typescript
await sdk.ats.updateAtsApplication({
  connectionId,
  id: application.id,
  atsApplication: {
    status: 'REVIEWING',
  },
  fields: '',
  raw: '',
});
```


Notes:

- Stage transition rules are not documented as a Unified.to-wide model.
- On some integrations, such as Greenhouse, `status` is a slow field: request it with `fields` when you read it back.
- Handle 429/5XX with backoff and retry.

## Step 8: How do you add tags and source metadata?


If you didn't apply tags at create time or want to add more context:


```typescript
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

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.to 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

## What does [Unified.to](http://unified.to/) handle, and what do you build?


External sourcing only matters if recruiters can act on it inside their ATS.


[Unified.to](http://unified.to/)'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, reliable sourcing comes down to sequencing, deduplication, retries and fallbacks for each integration's limits.


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


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