How to Pull Screening Questions from an ATS and Embed Them in Your Product Using Unified's ATS API
February 25, 2026
Screening questions are often defined inside the ATS.
If you're building:
- A custom apply flow
- A sourcing portal
- A talent CRM
- A recruiting automation layer
- A candidate engagement product
…you need to:
- Pull screening questions from the ATS
- Render them dynamically in your product
- Collect structured responses
- Push those responses back into the ATS
- Attach them to the correct application
This guide walks through that full read + structured write-back workflow using Unified's ATS API.
The Mental Model
The workflow looks like this:
- Retrieve the job from the ATS
- Extract
job.questions[] - Normalize question types
- Render a dynamic form in your UI
- Create (or retrieve) the candidate
- Create the application
- Attach
answers[]to the application
The ATS remains the system of record.
Your product becomes a structured front-end for screening.
Step 1: Retrieve the Job Object
Screening questions live on the job object.
Endpoint
GET /ats/{connection_id}/job/{id}
Node SDK Example
const job = await sdk.ats.getAtsJob({
connectionId,
id: jobId,
fields: '',
raw: '',
});
Screening Question Structure
From the AtsJob model:
questions: [
{
id: string,
question: string,
required: boolean,
description?: string,
prompt?: string,
type:
'TEXT' |
'NUMBER' |
'DATE' |
'BOOLEAN' |
'MULTIPLE_CHOICE' |
'FILE' |
'TEXTAREA' |
'MULTIPLE_SELECT' |
'UNIVERSITY' |
'YES_NO' |
'CURRENCY' |
'URL',
options?: string[]
}
]
Each question includes a unique id.
That id is what you will later use as question_id when writing answers back to the application.
Step 2: Normalize Question Types
Your frontend should normalize ATS question types into your own UI schema.
Example mapping:
| ATS Type | UI Component |
|---|---|
| TEXT | Single-line input |
| TEXTAREA | Multi-line input |
| NUMBER | Numeric input |
| DATE | Date picker |
| BOOLEAN | Checkbox |
| YES_NO | Radio buttons |
| MULTIPLE_CHOICE | Single select dropdown |
| MULTIPLE_SELECT | Multi-select control |
| FILE | File upload |
| URL | URL input |
| CURRENCY | Currency field |
Render dynamically based on:
typerequiredoptions[](when applicable)
Do not hardcode question definitions. Always trust the job object.
Step 3: Collect Candidate Responses
Your form should produce a structured payload like:
[
{
"question_id": "q_123",
"answers": ["Yes"]
},
{
"question_id": "q_456",
"answers": ["5"]
}
]
Important:
answersis always an array of strings- Even for single-value questions
- Maintain order but rely on
question_idfor mapping
Step 4: Handle FILE-Type Questions
For FILE questions, responses must be represented as a string.
Unified's ATS Application model expects:
answers[].answers[]: string[]
For file-based questions, supply:
- A publicly accessible file URL (recommended), or
- A document ID if the downstream ATS requires pre-upload
Typical workflow:
- Upload the file via the Document endpoint
- Capture the returned
document_urlorid - Use that value as the answer
Upload File
POST /ats/{connection_id}/document
Example:
const doc = await sdk.ats.createAtsDocument({
connectionId,
atsDocument: {
filename: 'resume.pdf',
type: 'RESUME',
candidate_id: candidateId,
document_data: base64EncodedFile,
},
});
Then in your answers:
{
"question_id": "file_q_001",
"answers": ["https://your-storage.com/file.pdf"]
}
or:
{
"question_id": "file_q_001",
"answers": ["document_id_789"]
}
File handling requirements vary by integration. Always confirm provider expectations.
Step 5: Create the Candidate (If Needed)
Before attaching answers, ensure the candidate exists.
const candidate = await sdk.ats.createAtsCandidate({
connectionId,
atsCandidate: {
first_name: 'Jane',
last_name: 'Doe',
emails: [{ email: 'jane@example.com', type: 'WORK' }]
},
});
Or dedupe first using:
query: 'jane@example.com'
Step 6: Create the Application With Answers
You can attach screening responses during application creation.
Endpoint
POST /ats/{connection_id}/application
Example
const application = await sdk.ats.createAtsApplication({
connectionId,
atsApplication: {
candidate_id: candidate.id,
job_id: job.id,
answers: [
{
question_id: "q_123",
answers: ["Yes"]
},
{
question_id: "q_456",
answers: ["5"]
}
],
status: "NEW"
},
});
Alternatively, you can update answers after application creation:
await sdk.ats.updateAtsApplication({
connectionId,
id: application.id,
atsApplication: {
answers: [
{
question_id: "q_123",
answers: ["Yes"]
}
]
}
});
Mapping Questions to Answers
There is no explicit documentation stating that Unified automatically resolves mappings between job questions and application answers.
The intended implementation pattern is:
- Retrieve
job.questions[] - Use
job.questions[].id - Submit that same ID as
answers[].question_id
If provider-native IDs are needed, use the raw field to inspect vendor-specific structures.
Step 7: Handling Custom Fields
If your product captures additional metadata:
- Use
metadata[]onAtsApplicationorAtsCandidate - Confirm writable support for your integration
- Avoid assuming universal custom-field availability
Full Workflow Summary
GET job- Extract
job.questions[] - Render dynamic form
- Upload files if needed
- Create candidate (or dedupe)
POST applicationwithanswers[]- Optionally update stage
All structured responses remain inside the ATS.
What Unified Does Not Guarantee
Unified documentation does not explicitly guarantee:
- That all ATS integrations expose
questions[] - That all integrations support writing
answers[] - That question ID formats are consistent across providers
- That file-answer semantics are identical across providers
Always consult integration-specific field support before deploying.
Closing Thoughts
Pulling screening questions from the ATS and embedding them into your product allows you to:
- Build fully custom apply experiences
- Control candidate UX
- Preserve structured screening logic
- Keep the ATS as the source of truth
Unified's ATS API provides:
- Normalized job question definitions
- Structured answer submission
- File upload support
- Application-level write-back
The rest is orchestration: render dynamically, validate locally, and write back cleanly.