---
title: "How to Ingest Jobs and Candidates from ATS Systems with Unified's ATS API"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_ingest_jobs_and_candidates_from_ats_systems_with_unified_ats_api-icon.png
date: 2026-02-02T17:20:00.000Z
tag: Product
description: "Ingesting ATS data sounds simple, pull candidates, pull jobs, store them, and move on."
url: "https://unified.to/blog/how_to_ingest_jobs_and_candidates_from_ats_systems_with_unified_ats_api"
---

# How to Ingest Jobs and Candidates from ATS Systems with Unified's ATS API
------
_February 2, 2026_

<iframe width="560" height="315" src="https://www.youtube.com/embed/JUYir5xRiHc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


_Updated June 2026_


Ingesting ATS data sounds simple: pull candidates, pull jobs, store them, and move on.


In practice, it's rarely that clean.


Every ATS models candidates, applications, and jobs differently. Some systems treat prospects and applicants the same. Others separate them. Some expose structured offer and interview data. Others bury critical information in activity logs. Even basic fields like status, source, or ownership vary across providers.


If your product depends on accurate talent-pool visibility, ingestion has to be:

- Consistent across ATS systems
- Incremental and efficient
- Explicit about identity and relationships
- Grounded in what the ATS actually records

This guide shows how to ingest jobs and candidates using Unified.to's [ATS API](/ats) in a way that's technically sound and portable across providers.


## The ingestion mental model


There are three primary entities to ingest:

1. **Candidates (**`AtsCandidate`**)**
2. **Jobs (**`AtsJob`**)**
3. **Applications (**`AtsApplication`**)**

Applications connect candidates to jobs. They carry the state and lifecycle of a candidate within a specific hiring pipeline.


A correct ingestion strategy pulls these objects independently and models their relationships explicitly.


## Step 1: Ingest candidates


Candidates represent people stored in the ATS — active applicants, past applicants, and general prospects not tied to a job.


```plain text
GET /ats/{connection_id}/candidate
```


Node SDK:


```typescript
import { UnifiedTo } from '@unified-api/typescript-sdk';

const sdk = new UnifiedTo({
  security: { jwt: process.env.UNIFIED_API_KEY! },
});

async function ingestCandidates(connectionId: string) {
  return await sdk.ats.listAtsCandidates({
    connectionId,
    limit: 100,
    offset: 0,
    sort: 'updated_at',
    order: 'asc',
  });
}
```


Key `AtsCandidate` fields:

- `id`
- `first_name`, `last_name`
- `emails[]`
- `origin`
- `external_identifier`
- `company_id`
- `metadata[]` (custom fields)

Treat `id` as the stable primary key **within a connection**. Do not assume identity is consistent across connections.


## Step 2: Ingest jobs


Jobs provide the context for applications and pipeline reporting.


```plain text
GET /ats/{connection_id}/job
```


```typescript
async function ingestJobs(connectionId: string) {
  return await sdk.ats.listAtsJobs({
    connectionId,
    limit: 100,
    offset: 0,
    sort: 'updated_at',
    order: 'asc',
  });
}
```


Key `AtsJob` fields:

- `id`
- `name`
- `status` (`OPEN`, `CLOSED`, etc.)
- `groups[]`
- `public_job_urls[]`
- `openings[]`
- `company_id`

Store jobs separately from candidates. Applications connect them.


## Step 3: Ingest applications (the join layer)


Applications link candidates to jobs and define pipeline state.


```plain text
GET /ats/{connection_id}/application
```


```typescript
async function ingestApplications(connectionId: string) {
  return await sdk.ats.listAtsApplications({
    connectionId,
    limit: 100,
    offset: 0,
    sort: 'updated_at',
    order: 'asc',
  });
}
```


Important `AtsApplication` fields:

- `id`
- `candidate_id`
- `job_id`
- `status` (normalized enum)
- `original_status` (the raw vendor label)
- `applied_at`
- `rejected_at`
- `hired_at`
- `offers[]`

This object tells you who applied to which job, what stage they're in, when key transitions occurred, and whether offers were sent or accepted. The normalized `status` enum gives you a consistent pipeline view across every ATS, while `original_status` preserves the source-of-truth value.


Do not flatten applications into candidate records. Applications are separate lifecycle instances.


## Step 4: Model relationships explicitly


After ingestion, your internal model should reflect:


```plain text
Candidate
  ↳ Applications[]
      ↳ Job
```

- A candidate can have multiple applications.
- A job can have many candidates.
- Applications carry stage and lifecycle timestamps.

Keep these entities distinct to avoid corrupting reporting logic.


## Step 5: Use incremental sync with `updated_gte`


Full resyncs don't scale. All ATS list endpoints support `updated_gte`:

1. Perform an initial full load.
2. Store the latest `updated_at` processed.
3. On subsequent runs, fetch only records updated since that timestamp.

```typescript
async function ingestUpdatedCandidates(connectionId: string, updatedSince: string) {
  return await sdk.ats.listAtsCandidates({
    connectionId,
    updated_gte: updatedSince,
    sort: 'updated_at',
    order: 'asc',
    limit: 100,
    offset: 0,
  });
}
```


Apply the same pattern to jobs and applications. Because Unified.to is pass-through, an `updated_gte` query hits the source system directly — you're reading what the ATS holds now, not a copy that may already be hours behind a sync schedule.


## Optional: webhook-based ingestion


If you need near-source-fresh ingestion without polling, Unified.to supports [webhooks](/blog/replacing_polling_with_unified_webhooks_and_virtual_streams) for ATS objects, including `ats_candidate`, `ats_application`, `ats_job`, `ats_activity`, `ats_document`, `ats_interview`, `ats_scorecard`, `ats_company`, and `ats_applicationstatus`.


Event types are `created` and `updated` across these objects; `deleted` events are emitted for a subset — candidates, applications, jobs, companies, and scorecards — while delivery of deletions also depends on what the provider exposes. Native webhook behavior varies by provider; [virtual webhooks](/blog/unlock_real_time_data_with_virtual_webhooks) deliver created and updated records based on polling at a configured interval.


Filters are supported for **virtual webhooks** and must match the object's list-endpoint parameters (for example, parameters ending in `_id` or `type`). Filter availability varies by integration and object. Native webhooks don't accept filters.


Polling via `updated_gte` remains universally supported and is sufficient for most ingestion workflows.


## Modeling identity and deduplication


Ingestion exposes duplicates. It doesn't eliminate them.

- `id` is unique per connection.
- Multiple connections create separate identity namespaces.
- A candidate may have multiple applications.
- ATS systems may contain duplicate candidates.
- `external_identifier` can assist correlation if consistently populated.

Unified.to does not merge identities across connections. Deduplication belongs in your application layer — typically email-based matching, external-identifier matching, or candidate-plus-job dedupe.


## Handling custom fields


ATS providers often include custom fields beyond the normalized objects. Unified.to exposes them through `metadata[]` on supported objects. If your product depends on custom attributes, persist the `metadata[]` array alongside the normalized fields so custom values can be rendered or processed without depending on provider-specific schemas.


## What Unified.to does not do


Unified.to does not:

- Merge duplicate candidates across systems
- Reconstruct deleted records unless the ATS provides them
- Infer relationships not present in the data
- Enforce identical field support across all integrations

It exposes what the ATS records, consistently.


## Ingesting correctly


Ingesting ATS data correctly means keeping candidates, jobs, and applications separate, and using incremental updates to keep them current. Unified.to's ATS API provides normalized objects and consistent list semantics, fetched live from the source, so you ingest jobs and candidates across providers without per-vendor logic.


From there, talent-pool visibility becomes a modeling problem, not an integration problem.


[Start your 30-day free trial](https://app.unified.to/) or [book a demo](https://calendly.com/d/cph9-g8n-jzg/connect-with-unified).