---
title: "How to Track the Candidate Journey Through an ATS Pipeline with"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_track_the_candidate_journey_through_an_ats_pipeline_with-icon.png
date: 2026-02-02T17:20:00.000Z
updated: 2026-10-21T12:05:49.170Z
readingTime: 5
tag: Product
description: "To track a candidate's journey across ATS platforms with Unified.to, read each application's current status and lifecycle timestamps, then attach its..."
url: "https://unified.to/blog/how_to_track_the_candidate_journey_through_an_ats_pipeline_with"
---

# How to Track the Candidate Journey Through an ATS Pipeline with

To track a candidate's journey across ATS platforms with [Unified.to](http://unified.to/), read each application's current status and lifecycle timestamps, then attach its interviews, scorecards, activities and documents through [Unified.to](http://unified.to/)'s [ATS API](/ats). This guide is for product teams showing a pipeline timeline in a recruiting, assessment or analytics product. [Unified.to](http://unified.to/) reads each ATS at request time and keeps no customer records at rest, so the timeline you build lives in your database.


Every ATS models hiring pipelines differently. Some record a rich set of interview stages; others collapse progress into a single status field. Some provide detailed event logs; others overwrite records in place. Even basic concepts like interviews, feedback, and documents vary in structure and availability.


For a product or platform team, this creates immediate challenges:

- What is the candidate's **current state** in the pipeline?
- What actually constitutes the "journey" — stages, interviews, communications, artifacts?
- How do you show a timeline without inferring steps that were never recorded?
- How do you keep the view accurate without full resyncs or vendor-specific logic?

[Unified.to](http://unified.to/)'s ATS API takes a strict approach. It does **not** reconstruct stage history or infer transitions. Instead, it returns **observable application state**, **explicit lifecycle timestamps**, and **linked objects** (interviews, activities, scorecards, documents) through normalized objects fetched live from the source.


## What counts as the candidate journey?


This guide focuses on **representing** the journey, not predicting outcomes.


We will:

- Determine the current pipeline state per application
- Build a timeline from explicit timestamps and linked objects
- Attach interviews, feedback, communications, and documents
- Keep the journey current using incremental updates and [webhooks](/blog/replacing_polling_with_event_driven_integrations_using_virtual_webhooks)

We will not:

- Reconstruct stage history from overwritten fields
- Assume all ATS systems emit the same events
- Infer transitions that were never recorded

Those decisions belong in your product logic, not the integration layer.


## Why does the journey live on the application?


Candidates don't move through pipelines. **Applications do.**


A single candidate can apply to multiple jobs, hold applications in different states, and accumulate different interviews, activities, and feedback per application.


A "candidate journey" is therefore a composition of candidate identity, one or more applications, job context per application, and a timeline built from observable events and timestamps.


## Which ATS objects make up the journey?


Coverage varies by object (as of 19 October 2026): applications come back on 125 ATS integrations, activities on 70, documents on 69, interviews on 44 and scorecards on 34. Interviews, for example, come back from Greenhouse, Lever, Ashby, SmartRecruiters, iCIMS, Workable and Jobvite, but BambooHR has no interview object. Check each object on the integration's Feature Support tab before you add it to your timeline.


### Application (`AtsApplication`)


The anchor for the journey. Key fields: `id`, `candidate_id`, `job_id`, `status` (normalized enum), `original_status` (ATS-native value), `applied_at`, `rejected_at`, `hired_at`, `rejected_reason`, and `offers[]` (with sent / accepted / rejected / start timestamps). This object defines the current state and the major lifecycle milestones.


### Application status (`AtsApplicationStatus`)


The normalized stage vocabulary. Key fields: `status`, `original_status`, `description`. Use it to display consistent stages while preserving vendor-specific naming.


### Interview (`AtsInterview`)


Scheduled moments in the journey. Key fields: `application_id`, `candidate_id`, `job_id`, `status`, `start_at`, `end_at`, `location`, `user_ids[]`, `external_event_xref`. Interviews turn "in interview" into something concrete and time-bound.


### Activity (`AtsActivity`)


Narrative context and communication. Key fields: `application_id`, `candidate_id`, `job_id`, `interview_id`, `type`, `title`, `description`, `document_ids[]`, `created_at`. Activities explain _what happened_ without implying state transitions.


### Scorecard (`AtsScorecard`)


Structured feedback. Key fields: `application_id`, `interview_id`, `interviewer_id`, `recommendation`, `comment`, `questions[]`. Scorecards are critical for internal "awaiting feedback" logic and post-interview state.


### Document (`AtsDocument`)


Artifacts exchanged during the journey. Key fields: `type`, `filename`, `document_url` (short-lived), `candidate_id`, `application_id`, `job_id`. Documents (resumes, cover letters, offer letters) are what a candidate-facing portal or offer flow shows.


### Job (`AtsJob`)


Context for the application. Key fields: `name`, `status`, `groups[]`, `openings[]`, `public_job_urls[]`. Jobs explain _where_ the candidate sits in the organization's hiring process.


_Lifecycle timestamps are sparse: hired_at comes back on 40 application integrations, rejected_at on 32 and offers[] on 13 (as of 19 October 2026), so treat a missing milestone as "not recorded", not "didn't happen". Confirm enum strings (interview status, document type) on each object's model page before hardcoding them._


## Step 1: Fetch applications for a candidate


The journey is application-first.


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

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

async function listCandidateApplications(connectionId: string, candidateId: string) {
  const out: any[] = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const page = await sdk.ats.listAtsApplications({
      connectionId,
      limit,
      offset,
      sort: 'updated_at',
      order: 'asc',
      candidateId,
    });

    if (!page || page.length === 0) break;
    out.push(...page);
    offset += limit;
  }

  return out;
}
```


If the candidate is already known in your system, you don't need to list candidates at all.


## Step 2: Determine the current stage


Use the normalized status for logic, and the original status for display.


```typescript
function currentStage(app: any) {
  return app.status;
}

function displayStage(app: any) {
  return app.originalStatus ?? app.status;
}
```


This avoids hardcoding stage names or vendor logic.


## Step 3: Build a defensible milestone timeline


Use only timestamps the ATS explicitly recorded.


```typescript
function applicationMilestones(app: any) {
  const milestones: any[] = [];

  if (app.appliedAt) milestones.push({ type: 'APPLIED', at: app.appliedAt });

  if (Array.isArray(app.offers)) {
    for (const offer of app.offers) {
      if (offer.sentAt) milestones.push({ type: 'OFFER_SENT', at: offer.sentAt });
      if (offer.acceptedAt) milestones.push({ type: 'OFFER_ACCEPTED', at: offer.acceptedAt });
      if (offer.rejectedAt) milestones.push({ type: 'OFFER_REJECTED', at: offer.rejectedAt });
      if (offer.startAt) milestones.push({ type: 'START_DATE', at: offer.startAt });
    }
  }

  if (app.rejectedAt) {
    milestones.push({ type: 'REJECTED', at: app.rejectedAt, reason: app.rejectedReason });
  }

  if (app.hiredAt) milestones.push({ type: 'HIRED', at: app.hiredAt });

  return milestones.sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime());
}
```


This produces a timeline you can defend under audit.


## Step 4: Attach interviews


```typescript
async function listApplicationInterviews(connectionId: string, applicationId: string) {
  const out: any[] = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const page = await sdk.ats.listAtsInterviews({
      connectionId,
      limit,
      offset,
      sort: 'updated_at',
      order: 'asc',
      applicationId,
    });

    if (!page || page.length === 0) break;
    out.push(...page);
    offset += limit;
  }

  return out;
}
```


Interviews add scheduled, real-world events to the journey.


## Step 5: Attach scorecards


```typescript
async function listApplicationScorecards(connectionId: string, applicationId: string) {
  const out: any[] = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const page = await sdk.ats.listAtsScorecards({
      connectionId,
      limit,
      offset,
      sort: 'updated_at',
      order: 'asc',
      applicationId,
    });

    if (!page || page.length === 0) break;
    out.push(...page);
    offset += limit;
  }

  return out;
}
```


Scorecards explain post-interview states without guessing.


## Step 6: Attach activities


```typescript
async function listApplicationActivities(connectionId: string, applicationId: string) {
  const out: any[] = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const page = await sdk.ats.listAtsActivities({
      connectionId,
      limit,
      offset,
      sort: 'updated_at',
      order: 'asc',
      applicationId,
    });

    if (!page || page.length === 0) break;
    out.push(...page);
    offset += limit;
  }

  return out;
}
```


Activities provide context, not state.


## Step 7: Attach documents


```typescript
async function listApplicationDocuments(connectionId: string, applicationId: string) {
  const out: any[] = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const page = await sdk.ats.listAtsDocuments({
      connectionId,
      limit,
      offset,
      sort: 'updated_at',
      order: 'asc',
      applicationId,
    });

    if (!page || page.length === 0) break;
    out.push(...page);
    offset += limit;
  }

  return out;
}
```


Document URLs expire after 1 hour, so request them when the user opens the document.


## Step 8: Keep journeys current


On supported integrations, ATS list endpoints accept `updated_gte` for incremental refresh: applications on 61 of 125 integrations, interviews on 16 of 44 and scorecards on 14 of 34 (as of 19 October 2026), including Greenhouse and Lever. Where it isn't offered, re-read the application's linked objects or use change events:

1. Store the last processed `updated_at` per object.
2. Re-fetch with `updated_gte`.
3. Recompute only the impacted applications.

Because Unified.to is pass-through, each `updated_gte` query reads the source directly rather than a synced copy that may already be behind.


To be notified of changes, subscribe to [Unified.to](http://unified.to/) webhooks on ATS objects. Change events exist on supported integrations and objects: application updates on 74 ATS integrations and interview updates on 21 (as of 20 October 2026), most of them [virtual webhooks](/blog/what_are_virtual_webhooks_how_they_differ_from_sync_based_webhooks), which check at an interval you set, as often as every minute on paid plans. Filters (exact match on list parameters such as `application_id` or `job_id`) work on virtual webhooks and, for many integrations, native webhooks; the integration's Feature Support tab lists which apply.


## What doesn't [Unified.to](http://unified.to/) infer?


[Unified.to](http://unified.to/) doesn't reconstruct stage history, guess transition timestamps, treat activities as state changes, or enforce a single pipeline model. It returns what the ATS records and nothing more.


## What should your timeline show?


Candidate journey tracking breaks when "stage" is treated as the journey.


Stage is a label. The journey is what actually happened: applications submitted, interviews scheduled, feedback recorded, documents exchanged, decisions made — each at a timestamp you can explain.


[Unified.to](http://unified.to/)'s ATS API gives you one schema for application state, lifecycle timestamps and the linked interviews, activities, scorecards and documents, on the integrations that return them. Which of those your product shows is a product decision.


Related: [How to Build a Candidate Portal Showing Interview Stages](https://unified.to/blog/how_to_build_a_candidate_portal_showing_real_time_interview_stages_using_unified_ats_api) · [ATS API Integration](https://unified.to/blog/ats_api_integration_real_time_recruiting_data_candidate_pipelines_and_hiring_automation) · [Application model](https://docs.unified.to/ats/application/model)


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