Candidate updates and status changes
First keeps your ATS up to date as candidates are screened, shortlisted, moved and rejected.
Part one puts candidates in your ATS. Now we need to keep them up to date. We add the assessment summary and scores, move candidates between your stages, and disqualify them when they are rejected in First.
You can switch each of these on or off, per organisation and per role. So you can take notes without stage sync, or stage sync without disqualification.
| Event in First | Calls we make |
|---|---|
| Candidate applies or is pushed to your ATS | Create application, then the CV |
| Candidate submits their application | Note with their answers and a link into First |
| Assessment completes | Note with the summary and scores |
| Candidate shortlisted | Move + note |
| Candidate moves pipeline stage | Move |
| Candidate rejected | Disqualify + note |
| Candidate withdraws | Disqualify |
| Candidate stalls (unresponsive) | Move to a configured stalled stage, or disqualify |
As in part one, these are a reference shape rather than a mandate. We write a per-provider adapter and can map names and wrapper objects; what we cannot work around is a capability you do not have.
/applications/{applicationId}Get an application
We read before we write: to check whether a CV is already attached, to confirm a candidate still exists before posting a note, and to see the current stage and tags.
Return 404 for a candidate that has been deleted or merged away. We handle that explicitly, including the case where a create succeeded but the record was later removed as a duplicate. If you merge duplicate candidates, ideally point at the surviving record.
The response is a single candidate:
{ "candidate": {} }These are the properties we read from it:
| Field | Required | Notes |
|---|---|---|
id | Yes | The same identifier the create returned, as a string. |
email | Yes | An email address, like name@example.com: how we confirm we are looking at the right person. |
cv.url | No | A URL to the file already attached, if any. Expose it and we will not upload a CV twice; without it we cannot tell whether one is already attached. |
stage.id / stage.name | No | Where they currently sit in your pipeline, so we do not move someone who is already there. |
status | No | A string from your own vocabulary: whether they are still active or already disqualified. Send us the full list of possible values. |
tags | No | An array of strings. Lets us tell applications First created from ones that reached you another way, so we do not re-import our own. |
job_id | No | The job the candidate belongs to, matching the job list's id. |
full_name | No | The candidate's name as one string. Useful in logs when something needs chasing. |
Example Response▼
{ "candidate": { "id": "cand_8871", "job_id": "job_9f3a", "full_name": "Jo Bloggs", "email": "jo.bloggs@example.com", "stage": { "id": "stage_2", "name": "Screening" }, "status": "active", "cv": { "url": "https://ats.example.com/files/…" }, "tags": ["first_shortlisted"], "created_at": "2026-08-04T09:31:22Z", "updated_at": "2026-08-04T10:02:11Z" } }
/applications/{applicationId}Update an application
Send only what changed. Anything we leave out must stay as it is on your record, including fields a recruiter has edited themselves.
The body is the same application object as the create payload. We send id and url every time so you can confirm which application this is; everything else arrives only when it changes. assessment is the one addition.
| Field | Required | Notes |
|---|---|---|
id | Yes | Our identifier for the application, as a string. Store it against your record: it is how we recognise the application if we ever have to look it up again. |
url | Yes | A link to the application in First, where a recruiter sees the full assessment. If you have a custom field that renders as a clickable URL, tell us and we will populate it. |
email | No | An email address, like name@example.com. Our primary identity key. |
full_name | No | One string, as the candidate gave it, like Jo Bloggs. We do not hold first and last names separately, so split it yourself if your ATS needs them apart. |
preferred_name | No | What they asked to be called, like Jo. Falls back to their full name where they did not give one. |
phone | No | A mobile number with its country code, like +44 7700 900000. |
linkedin_url | No | A LinkedIn profile URL, normalised to https://www.linkedin.com/in/…. |
location | No | Where the candidate is: city, country and coordinates where we hold them. See below. |
cv | No | The CV they uploaded, as a url and filename. See below. |
submitted_at | No | When they submitted, ISO 8601 with timezone, like 2026-08-04T09:31:22Z. |
answers | No | What they answered to the role's questions, as question and answer pairs. See below. |
assessment | No | First's screening result, as a score_percentage and criteria. It does not exist when the application is created. See below. |
source | No | How the application reached First, as a type, method and name. See below. |
Example Request▼
{ "application": { "assessment": { "score_percentage": 0.82, "criteria": [ { "name": "3+ years Go", "outcome": "met" }, { "name": "Right to work in the UK", "outcome": "met" } ] } } }
Assessment▼
How the candidate scored against the role's criteria. This is the part your recruiters care about, and the reason the integration is worth having.
| Field | Required | Notes |
|---|---|---|
score_percentage | Yes | A decimal from 0 to 1, like 0.82: how well they met the role's criteria. |
criteria | Yes | Per-criterion outcomes, each with a name and an outcome. This is the detail behind the score. |
We also post the same result as a note, so it is readable by a recruiter whose ATS has nowhere structured to put it. Tell us if you would rather we did not send both.
Source▼
How the application reached First. It matters for your reporting: an application we forwarded from a job board is not the same as one a recruiter went out and found.
| Field | Required | Notes |
|---|---|---|
type | Yes | direct when they applied through First, or ats when the application started in another system and First picked it up. |
method | Yes | applied when they came to us, or sourced when a recruiter found them. Several ATSs report on the two separately. |
name | Yes | Where it came from, as free text, for us to attribute the hire. First for a direct application, otherwise the system it arrived from. |
Location▼
Where the candidate is, as far as we know it. Geocoding is best-effort, so an application may carry a city with no coordinates, or nothing at all.
| Field | Required | Notes |
|---|---|---|
city | No | The town or city the candidate gave, as free text, like London. |
country | No | An ISO 3166-1 alpha-2 code, for example GB. |
latitude | No | Decimal degrees, like 51.5072, where we managed to geocode them. Sent with longitude or not at all. |
longitude | No | Decimal degrees, like -0.1276, alongside latitude. |
Answers▼
An array, one entry per question the candidate answered. The questions are the role's own, so they differ between roles and change when a recruiter edits them.
| Field | Required | Notes |
|---|---|---|
question | Yes | The question as the candidate saw it, as text. |
answer | Yes | The answer, as text. |
CV▼
Candidates upload a CV to First and we need to get it into your ATS. For the minimal integration there is one mechanism to support, and it is the least work for both sides: we put a link on the payload and you fetch the file.
| Field | Required | Notes |
|---|---|---|
url | Yes | A time-limited signed URL to the file itself. No credentials needed, so a plain GET is enough. |
filename | Yes | The name as the candidate uploaded it, for example Jo_Bloggs_CV.pdf. Use it rather than deriving a name from the URL, which carries a signature. |
Fetch it promptly and store your own copy. The URL expires, so a link saved against your record will stop working.
We send the file the candidate uploaded, unconverted, so it can be PDF, DOCX, RTF, TXT, Markdown or HTML. Accept PDF and DOCX at minimum, and tell us if any of the others would be rejected.
If fetching a URL is awkward for you, we can push the bytes instead, by multipart upload or base64 in JSON. See part two for details, as this requires an additional endpoint.
/applications/{applicationId}/notesPost a note on an application
This is how your recruiters see First's work without leaving your ATS: assessment summaries, question answers, stage changes, and a link back into First.
These are the properties we send:
| Field | Required | Notes |
|---|---|---|
body | Yes | The note itself, including a link back into First. Tell us whether you want plain text, Markdown or HTML, and whether links render as links. We format notes per provider, so we will send whichever you ask for. |
author_id | No | The account notes are attributed to. Tell us how to identify one of your users and you pick one during setup. If notes are always attributed to the API token's own identity, say so and drop the field. |
Example Request▼
{ "author_id": "user_44", "body": "Candidate submitted their application for Senior Backend Engineer on First.\n\n**Right to work:** Yes\n**Notice period:** 1 month\n\n[View in First](https://admin.example.com/share-application/org_1/app_88)" }
/applications/{applicationId}/moveMove an application to a stage
Called when a candidate is shortlisted, stalls, or moves pipeline stage in First, so your pipeline mirrors ours.
These are the properties we send:
| Field | Required | Notes |
|---|---|---|
target_stage | Yes | The stage to move them to. Tell us what this accepts, whether an ID, a slug or a name, and send us the full list of stages on a job. See below for the stages First uses. |
author_id | No | Who the move is attributed to, if you support it. |
Behaviour we depend on:
- Return
422rather than500when the move is legitimately refused (candidate already disqualified, stage not on this job's pipeline, backwards move not permitted). We log those and move on rather than retrying. - Moving a candidate to the stage they are already in should be a no-op, not an error.
- Tell us whether a move triggers automation on your side, such as emails to candidates or scorecard requests. Customers must know before they turn stage sync on.
Example Request▼
{ "target_stage": "stage_3", "author_id": "user_44" }First's stage vocabulary▼
You map these onto your own stages during setup, and you do not need a stage for every one. You can also define your own pipeline stages in First beyond these, and map each to a stage in your ATS.
| First stage | Typical meaning |
|---|---|
applied | Candidate applied through First |
sourced | Recruiter sourced them |
in progress | Working through the First assessment |
stalled | Started but went quiet |
shortlisted | Passed assessment, recommended |
rejected | Rejected |
withdrawn | Withdrew |
hired | Hired |
/applications/{applicationId}/disqualifyDisqualify an application
Called when a candidate is rejected in First, withdraws, or goes unresponsive.
These are the properties we send:
| Field | Required | Notes |
|---|---|---|
reason_id | No | One of your rejection reasons, mapped from First's four cases below. Skip it if your endpoint does not take a reason. |
note | No | Why they were rejected, for example which must-have criterion they missed. |
author_id | No | Who the disqualification is attributed to. |
First distinguishes four cases, and you map each onto one of your reasons:
| First's reason | Meaning |
|---|---|
assessment | Did not meet the role's must-have criteria. Can be mapped per criterion, so “failed right to work” and “failed experience bar” land as different reasons in your ATS. |
withdrawn | Candidate withdrew |
stalled | Went unresponsive and timed out |
default | Anything else |
Same 422 rule as stage moves: business-rule refusals must not look like server errors. Disqualifying an already-disqualified candidate should be a no-op. If you have a way to undo a disqualification, tell us. We do not call one today, but we would like to know it exists.
Example Request▼
{ "reason_id": "reason_1", "author_id": "user_44", "note": "Did not meet must-have criteria: 3+ years Go" }
/applications/{applicationId}/cvUpload a CV file
Only build this if fetching a URL is awkward for you. Part one puts a signed cv.url on the create payload for you to fetch, which is less work on both sides. This endpoint is the alternative: we send you the bytes.
When we call it: once the application exists and the candidate has uploaded a CV.
The body is multipart/form-data with a single part:
| Part | Notes |
|---|---|
file | The CV itself. The name the candidate uploaded it under travels in the part's Content-Disposition header, and its type in Content-Type. |
Accept PDF and DOCX at minimum. We send the file unconverted, so it can also arrive as RTF, TXT, Markdown or HTML. Tell us if any of those would be rejected, and whether a second upload replaces the first or adds another.
Example Request▼
POST /applications/app_88/cv Content-Type: multipart/form-data; boundary=----first ------first Content-Disposition: form-data; name="file"; filename="Jo_Bloggs_CV.pdf" Content-Type: application/pdf <binary> ------first--
On top of the part one checklist:
- Note formatting: plain text, Markdown or HTML, and whether links render
- Whether notes can be attributed to a chosen user, or are always the token's own identity
- Whether stage moves and disqualifications trigger automation on your side
- The complete stage and disqualification-reason lists, and whether stages are per-job or global
- Whether tags are supported, and whether ours survive your updates
- Whether you can fetch our signed CV URL, or need us to upload the file
Something here impossible or expensive in your ATS?
Tell us early. There is usually an alternative, and we would rather adjust the specification than have you build the wrong thing. Get in touch with your First contact, or at support@first.cx