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 path | What it does | Permission |
|---|---|---|
GET /api/v1/candidates | List candidates (cursor pagination, updatedSince, external-id lookup) | recruitment.view |
POST /api/v1/candidates | Create a candidate | recruitment.act |
GET /api/v1/candidates/{id} | Get one candidate | recruitment.view |
PATCH /api/v1/candidates/{id} | Update a candidate's profile; omitted fields stay unchanged | recruitment.act |
GET /api/v1/candidates/{id}/cv | Download the candidate's CV | recruitment.view and recruitment.candidate.contact |
POST /api/v1/candidates/bulk | Upsert 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
| Field | Type | Notes |
|---|---|---|
id | string | Jadiroon id. Ids differ between Jadiroon environments; link your records with externalRefs. |
firstName, lastName | string | |
email, phone, nationalId | string or null | Masked without recruitment.candidate.contact. nationalId is the National ID or Iqama number. |
nationality | string or null | ISO 3166-1 alpha-2 code, for example SA. |
gender | string or null | MALE or FEMALE. |
birthDate | ISO 8601 or null | |
yearsExperience | integer or null | Total years of experience. |
specialization, education | string or null | |
linkedinUrl, portfolioUrl | string or null | Profile links. |
hasCv | boolean | A CV is on file; download it from GET /api/v1/candidates/{id}/cv. |
archivedAt | ISO 8601 or null | Set when the candidate is archived in the app. |
externalRefs | array | [{ system, externalId }] links to your systems. |
createdAt, updatedAt | ISO 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
| Event | When | data.object |
|---|---|---|
candidate.created | A candidate is added (app, careers-page sign-up or API), or an employee is made a candidate in the People editor | The candidate |
candidate.updated | A 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.archived | A candidate is archived | The candidate |
candidate.deleted | A 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 code | Meaning |
|---|---|
403 forbidden | The 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_found | No candidate with that id in your workspace, or no CV on file. |
409 conflict | A duplicate person exists. |
422 validation_failed | The 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
}