Candidates

For HR administrators

Candidates are the people in your talent pool: everyone who applied, signed up on your careers page or was added by your recruiters, before they are hired. Use these endpoints to bring candidates in from a job board or another ATS, and to read their profiles and CVs. Hired people move to Employees.

All endpoints sit under /api/v1 and need an API key (Authorization: Bearer jdr_live_…) created by your Workspace Admin in Settings → Integrations.

Endpoints

Method and pathWhat it doesPermission
GET /api/v1/candidatesList candidates (cursor pagination, updatedSince, external-id lookup)recruitment.view
POST /api/v1/candidatesCreate a candidaterecruitment.act
GET /api/v1/candidates/{id}Get one candidaterecruitment.view
PATCH /api/v1/candidates/{id}Update a candidate's profile; omitted fields stay unchangedrecruitment.act
GET /api/v1/candidates/{id}/cvDownload the candidate's CVrecruitment.view and recruitment.candidate.contact
POST /api/v1/candidates/bulkUpsert up to 1,000 candidates by external id, async, with optional CV links (see Bulk upsert)recruitment.act

recruitment.act includes recruitment.view. An id that belongs to an employee, or to another workspace, returns 404 not_found.

Contact details are masked without the contact permission

A key without recruitment.candidate.contact receives the candidate's email, phone and nationalId masked, in every response and in webhook payloads:

{ "email": "s•••@example.com", "phone": "05•• ••• •12", "nationalId": "••••••12" }

Give the integration recruitment.candidate.contact to receive the real values. Without it, POST with a non-empty email, phone or nationalId returns 403 forbidden (details.reason candidate_contact_required), PATCH ignores email, phone and nationalId (the stored values never change), and a masked value is never stored, whoever sends it. The CV download needs the contact permission too, because the file itself shows the candidate's contact details (see below).

The candidate object

FieldTypeNotes
idstringJadiroon id. Ids differ between Jadiroon environments; link your records with externalRefs.
firstName, lastNamestring
email, phone, nationalIdstring or nullMasked without recruitment.candidate.contact. nationalId is the National ID or Iqama number.
nationalitystring or nullISO 3166-1 alpha-2 code, for example SA.
genderstring or nullMALE or FEMALE.
birthDateISO 8601 or null
yearsExperienceinteger or nullTotal years of experience.
specialization, educationstring or null
linkedinUrl, portfolioUrlstring or nullProfile links.
hasCvbooleanA CV is on file; download it from GET /api/v1/candidates/{id}/cv.
archivedAtISO 8601 or nullSet when the candidate is archived in the app.
externalRefsarray[{ system, externalId }] links to your systems.
createdAt, updatedAtISO 8601

Creating and updating

POST needs firstName and lastName; the other profile fields (email, phone, nationality, nationalId, gender, birthDate, yearsExperience, specialization, education, linkedinUrl, portfolioUrl) are optional. PATCH takes the same fields and leaves the omitted ones unchanged. Unknown fields, including status, are rejected with 422 validation_failed: a candidate becomes an employee only by being hired.

On create, a non-empty email, phone or nationalId needs recruitment.candidate.contact: without it the request is refused with 403 forbidden, details.reason candidate_contact_required and details.required ["recruitment.candidate.contact"], before any duplicate check (send null or leave the fields out to create the candidate without contact). With it, 409 conflict with details.reason duplicate_person means another active person in the workspace has the same email or national ID / Iqama. Profile links must be http or https URLs.

Downloading the CV

GET /api/v1/candidates/{id}/cv streams the file itself, with Content-Disposition: attachment; filename="cv-<id>.pdf" (the extension follows the stored file). The storage location is never returned. The response is 404 not_found when the candidate has no CV.

A CV shows the candidate's email and phone, so the download needs recruitment.candidate.contact as well as recruitment.view. A key without it gets 403 forbidden with details.reason candidate_contact_required and details.required ["recruitment.candidate.contact"], whether or not the candidate has a CV. hasCv on the candidate object is shown to every key that can read candidates. Deployment-level keys can download CVs.

curl -o cv.pdf https://jadiroon.com/api/v1/candidates/cmvk3c9e20001kn7d4f8g2hij/cv \
  -H "Authorization: Bearer $JADIROON_KEY"

Webhook events

EventWhendata.object
candidate.createdA candidate is added (app, careers-page sign-up or API), or an employee is made a candidate in the People editorThe candidate
candidate.updatedA candidate's profile or CV changes (by your recruiters, the API or the candidate); data.changedFields lists the fields when known (hasCv for a new CV)The candidate
candidate.archivedA candidate is archivedThe candidate
candidate.deletedA candidate is deleted in the app, deletes their own account, or is made an employee in the People editor{ id, externalRefs }

A hire through Applications sends application.hired and employee.created with the same id, not candidate.deleted: drop the person from your candidate list when you receive them. The candidate's externalRefs move to the employee. If another employee already holds the same { system, externalId }, that link is not moved and the new employee has no link for that system; link it again with the employees by-external endpoint.

Payloads follow the same masking rule as the API: an endpoint whose integration lacks recruitment.candidate.contact receives masked contact fields. Deletions also appear in GET /api/v1/deleted?resource=candidate for 180 days.

Errors

Status and codeMeaning
403 forbiddenThe key lacks the permission in the table above, or candidate_contact_required on the CV download or on a create that sends email, phone or nationalId.
404 not_foundNo candidate with that id in your workspace, or no CV on file.
409 conflictA duplicate person exists.
422 validation_failedThe body is invalid, or it sent a field candidates don't have (for example status); details explains why.

Example

curl -X POST https://jadiroon.com/api/v1/candidates \
  -H "Authorization: Bearer $JADIROON_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: jobboard-C-881" \
  -d '{
    "firstName": "Faisal",
    "lastName": "Alqahtani",
    "email": "faisal@example.com",
    "phone": "0551234512",
    "nationality": "SA",
    "specialization": "Nursing",
    "yearsExperience": 4
  }'

The key needs recruitment.candidate.contact because the body sends email and phone (without it the response is 403 forbidden). The response is 201 Created:

{
  "id": "cmvk3c9e20001kn7d4f8g2hij",
  "firstName": "Faisal",
  "lastName": "Alqahtani",
  "email": "faisal@example.com",
  "phone": "0551234512",
  "nationality": "SA",
  "nationalId": null,
  "gender": null,
  "birthDate": null,
  "yearsExperience": 4,
  "specialization": "Nursing",
  "education": null,
  "archivedAt": null,
  "externalRefs": [],
  "createdAt": "2026-10-22T15:02:11.204Z",
  "updatedAt": "2026-10-22T15:02:11.204Z",
  "linkedinUrl": null,
  "portfolioUrl": null,
  "hasCv": false
}