Custom ATS Integration · Part 2 of 3

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.

Tip
This part depends on part one. Every endpoint here is uses the identifier you returned when we created the application. Part one's conventions for auth, errors, pagination and identifiers apply here too.
BehaviourWhat First sends you

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 FirstCalls we make
Candidate applies or is pushed to your ATSCreate application, then the CV
Candidate submits their applicationNote with their answers and a link into First
Assessment completesNote with the summary and scores
Candidate shortlistedMove + note
Candidate moves pipeline stageMove
Candidate rejectedDisqualify + note
Candidate withdrawsDisqualify
Candidate stalls (unresponsive)Move to a configured stalled stage, or disqualify
ReferenceEndpoint specifications

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.

GET/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:

json
{ "candidate": {} }

These are the properties we read from it:

FieldRequiredNotes
idYesThe same identifier the create returned, as a string.
emailYesAn email address, like name@example.com: how we confirm we are looking at the right person.
cv.urlNoA 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.nameNoWhere they currently sit in your pipeline, so we do not move someone who is already there.
statusNoA string from your own vocabulary: whether they are still active or already disqualified. Send us the full list of possible values.
tagsNoAn 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_idNoThe job the candidate belongs to, matching the job list's id.
full_nameNoThe candidate's name as one string. Useful in logs when something needs chasing.
Example Response
json
{
  "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"
  }
}
PATCH/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.

FieldRequiredNotes
idYesOur 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.
urlYesA 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.
emailNoAn email address, like name@example.com. Our primary identity key.
full_nameNoOne 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_nameNoWhat they asked to be called, like Jo. Falls back to their full name where they did not give one.
phoneNoA mobile number with its country code, like +44 7700 900000.
linkedin_urlNoA LinkedIn profile URL, normalised to https://www.linkedin.com/in/….
locationNoWhere the candidate is: city, country and coordinates where we hold them. See below.
cvNoThe CV they uploaded, as a url and filename. See below.
submitted_atNoWhen they submitted, ISO 8601 with timezone, like 2026-08-04T09:31:22Z.
answersNoWhat they answered to the role's questions, as question and answer pairs. See below.
assessmentNoFirst's screening result, as a score_percentage and criteria. It does not exist when the application is created. See below.
sourceNoHow the application reached First, as a type, method and name. See below.
Example Request
json
{
  "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.

FieldRequiredNotes
score_percentageYesA decimal from 0 to 1, like 0.82: how well they met the role's criteria.
criteriaYesPer-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.

FieldRequiredNotes
typeYesdirect when they applied through First, or ats when the application started in another system and First picked it up.
methodYesapplied when they came to us, or sourced when a recruiter found them. Several ATSs report on the two separately.
nameYesWhere 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.

FieldRequiredNotes
cityNoThe town or city the candidate gave, as free text, like London.
countryNoAn ISO 3166-1 alpha-2 code, for example GB.
latitudeNoDecimal degrees, like 51.5072, where we managed to geocode them. Sent with longitude or not at all.
longitudeNoDecimal 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.

FieldRequiredNotes
questionYesThe question as the candidate saw it, as text.
answerYesThe 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.

FieldRequiredNotes
urlYesA time-limited signed URL to the file itself. No credentials needed, so a plain GET is enough.
filenameYesThe 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.

POST/applications/{applicationId}/notes

Post 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:

FieldRequiredNotes
bodyYesThe 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_idNoThe 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
json
{
  "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)"
}
POST/applications/{applicationId}/move

Move 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:

FieldRequiredNotes
target_stageYesThe 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_idNoWho the move is attributed to, if you support it.

Behaviour we depend on:

  • Return 422 rather than 500 when 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
json
{ "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 stageTypical meaning
appliedCandidate applied through First
sourcedRecruiter sourced them
in progressWorking through the First assessment
stalledStarted but went quiet
shortlistedPassed assessment, recommended
rejectedRejected
withdrawnWithdrew
hiredHired
POST/applications/{applicationId}/disqualify

Disqualify an application

Called when a candidate is rejected in First, withdraws, or goes unresponsive.

These are the properties we send:

FieldRequiredNotes
reason_idNoOne of your rejection reasons, mapped from First's four cases below. Skip it if your endpoint does not take a reason.
noteNoWhy they were rejected, for example which must-have criterion they missed.
author_idNoWho the disqualification is attributed to.

First distinguishes four cases, and you map each onto one of your reasons:

First's reasonMeaning
assessmentDid 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.
withdrawnCandidate withdrew
stalledWent unresponsive and timed out
defaultAnything 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
json
{
  "reason_id": "reason_1",
  "author_id": "user_44",
  "note": "Did not meet must-have criteria: 3+ years Go"
}
POST/applications/{applicationId}/cv

Upload 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:

PartNotes
fileThe 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
http
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--
HandoverWhat to tell us

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

First.cx logo
Copyright © OpenDigital Limited 2026
First.cx logo
Copyright © OpenDigital Limited 2026

We use cookies to analyse site traffic and improve your experience. By clicking "Accept", you consent to our use of analytics cookies. See our Privacy Policy for details.