How to Build a Candidate Portal Showing Real-Time Interview Stages Using Unified's ATS API
February 25, 2026
Candidates increasingly expect visibility into their application progress.
If you're building:
- A candidate portal
- A recruiting automation layer
- A branded apply experience
- A talent CRM with applicant login
- A hiring marketplace
You may need to display:
- Current application stage
- Upcoming interviews
- Interview outcomes
- Rejection or hire status
This guide walks through how to build a candidate-facing portal that reflects real-time ATS data using Unified's ATS API.
The focus is:
- Current stage visibility
- Interview scheduling visibility
- Real-time updates
- Safe synchronization
Unified exposes the current state of applications and interviews. It does not reconstruct stage history or enforce stage ordering.
The Mental Model
A candidate portal should reflect three layers:
- Application status (
AtsApplication.status) - Interview schedule (
AtsInterview) - Stage vocabulary (
AtsStatus)
All data should be synced into your own database and updated via webhooks or incremental refresh.
The ATS remains the source of truth. Your portal becomes a read-only projection layer.
Step 1: Retrieve the Candidate's Application
You need the application associated with the candidate and job.
Endpoint
GET /ats/{connection_id}/application
Filter by candidate:
const applications = await sdk.ats.listAtsApplications({
connectionId,
candidate_id: candidateId,
limit: 50,
offset: 0,
sort: 'updated_at',
order: 'asc',
});
From each AtsApplication, extract:
idstatusoriginal_statusapplied_atrejected_athired_at
Current Stage Display
The primary stage to show in a portal is:
application.status
Optionally display:
application.original_status
This allows you to show the recruiter-defined stage label exactly as configured inside the ATS.
Important: Unified exposes the current status only. It does not provide a stage transition history timeline.
Step 2: Retrieve Stage Vocabulary
To render consistent labels and descriptions, retrieve available statuses:
GET /ats/{connection_id}/applicationstatus
Node SDK
const statuses = await sdk.ats.listAtsApplicationstatuses({
connectionId,
limit: 50,
offset: 0,
});
AtsStatus includes:
statusoriginal_statusdescription
Use this to:
- Display friendly stage names
- Show stage descriptions in your UI
- Handle unknown raw statuses safely
Note: The API does not define stage ordering or progression logic. If you want ordered stages, you must define ordering in your own application layer.
Step 3: Retrieve Interviews
If the ATS integration supports interviews, you can surface scheduling data.
Interview visibility depends on provider support.
Endpoint
GET /ats/{connection_id}/interview
Filter by application:
const interviews = await sdk.ats.listAtsInterviews({
connectionId,
application_id: application.id,
limit: 50,
offset: 0,
sort: 'updated_at',
order: 'asc',
});
From AtsInterview, extract:
statusstart_atend_atlocation
Interview Display Logic
A practical portal layout:
Upcoming Interviews
status === 'SCHEDULED'start_at > now
Completed Interviews
status === 'COMPLETE'
Awaiting Feedback
status === 'AWAITING_FEEDBACK'
Display:
- Date and time
- Location (or virtual indicator)
- Interview status
Do not assume stage transitions based on interview count. The ATS controls stage progression.
Step 4: Handle Real-Time Updates
A 'real-time' portal requires updates when:
- Application status changes
- Interviews are scheduled
- Interviews are updated or cancelled
Use Unified webhooks.
Subscribe to Application Updates
{
"hook_url": "https://your-app.com/webhooks",
"connection_id": "<connection_id>",
"object_type": "ats_application",
"event": "updated"
}
Subscribe to Interview Updates
{
"hook_url": "https://your-app.com/webhooks",
"connection_id": "<connection_id>",
"object_type": "ats_interview",
"event": "updated"
}
When a webhook fires:
- Validate signature
- Upsert record into your database
- Recompute portal state
- Notify candidate UI (if needed)
Unified retries failed webhook deliveries and uses backoff strategies. Your webhook handler should be idempotent.
Step 5: Initial Sync Strategy
Before enabling webhooks, perform an initial sync.
Use pagination:
limit(default 100)offset
Example:
const page = await sdk.ats.listAtsApplications({
connectionId,
limit: 100,
offset: 0,
});
Then store:
- Applications
- Interviews
- Status vocabulary
After initial sync, rely on webhooks for updates.
Optional: Polling Fallback
If webhooks are unavailable, use incremental polling:
updated_gte: lastSyncTimestamp
This allows you to:
- Retrieve only records updated since your last sync
- Avoid reloading full datasets
Portal UI Design Recommendations
Keep the portal simple and state-based:
Application Overview
- Applied date
- Current stage
- Stage description
Interview Section
- Upcoming interviews
- Past interviews
- Interview status
Outcome
- If
hired_atexists → show 'Offer Accepted' - If
rejected_atexists → show 'Application Closed'
Avoid exposing internal recruiter notes or metadata unless intentionally supported.
Recommended Architecture
- Initial full sync
- Store ATS records in your database
- Subscribe to webhooks
- Update records on webhook events
- Render portal off your internal DB
Do not render directly from live ATS API calls for each page load.
Closing Thoughts
A candidate portal showing real-time interview stages requires:
- Current application status
- Interview scheduling visibility
- Webhook-driven updates
- Defensive handling of provider variability
Unified's ATS API provides:
- Normalized application and interview models
- Stage vocabulary via
applicationstatus - Webhook support for real-time updates
- Incremental sync support
The portal layer is your responsibility. Unified ensures you have consistent, structured ATS data to power it.