Applications

For HR administrators

An application is one candidate on one opening. It moves through the hiring stages (applied, screened, interviewing, offered) until the candidate is hired or rejected. Use these endpoints to add candidates from a job board or another ATS, to follow and move them through the stages, and to hire them, which makes them 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/applicationsList applications (cursor pagination, updatedSince, ?openingId=&candidateId=&stage=)recruitment.view
POST /api/v1/applicationsAdd a candidate to an openingrecruitment.act
GET /api/v1/applications/{id}Get one applicationrecruitment.view
POST /api/v1/applications/{id}/stageMove it to a stagerecruitment.act (OFFERED also needs recruitment.offer.issue)
POST /api/v1/applications/{id}/rejectReject itrecruitment.act
POST /api/v1/applications/{id}/hireHire the candidaterecruitment.hire and recruitment.candidate.contact

recruitment.act includes recruitment.view. An id from another workspace returns 404 not_found.

The application object

FieldTypeNotes
idstringThe application's id.
openingId, positionIdstringThe opening and the position it hires for.
personIdstringThe candidate; after the hire, the same id is the employee.
candidateobject{ id, firstName, lastName, email, phone }. email and phone are masked without recruitment.candidate.contact, also after the hire.
stagestringAPPLIED, SCREENED, ASSESSMENT, INTERVIEWING, OFFERED, HIRED or REJECTED.
screeningobjectAI CV screening: { status, score }. status is PENDING while it runs (only when a CV is on file), then DONE, FAILED or QUOTA_EXCEEDED; score is 0–100 or null.
rejectionReasonstring or nullWhen REJECTED with a documented reason (see below).
hasOfferbooleanAn offer letter was issued; read it from Offers with the same id.
appliedAtISO 8601
externalRefsarray[{ system, externalId }] links to your systems.
createdAt, updatedAtISO 8601

Adding a candidate

POST /api/v1/applications takes { "openingId": "…", "candidate": { "id": "…" } }; the candidate can also be given as { "externalSystem", "externalId" }. The candidate must already exist (create them with Candidates), and the opening must be OPEN (409 conflict, opening_not_open).

  • The opening's hiring pathway must be open to external candidates, as on your careers page: an INTERNAL_ONLY opening returns 409 conflict (opening_internal_only), and an INTERNAL_FIRST opening returns 409 conflict (not_yet_external, with details.externalAt) until its internal window ends.
  • A position whose job description is archived takes no new applications (422 validation_failed, archived).
  • A candidate already on that opening returns 409 conflict with details.reason already_applied and details.applicationId.
  • A Saudi-only job description needs a Saudi candidate (422 validation_failed, saudi_only), and the candidate's nationality must be known to check it (nationality_required).
  • When the candidate has a CV on file, the AI screening runs after the response; watch for application.updated with screening in changedFields.

Moving, rejecting, hiring

  • Stage: POST …/stage with { "stage": "INTERVIEWING" }. Allowed targets are APPLIED, SCREENED, INTERVIEWING and OFFERED; use /reject and /hire for the others. Moving to the current stage changes nothing. Moving to SCREENED assigns the position's assessments, as in the app, and the application can then move on to ASSESSMENT by itself. The candidate receives the stage email your workspace has set up.
  • Reject: POST …/reject with an optional { "reason": "withdrew", "note": "…" }. Reasons: not-shortlisted, not-saudi-saudization-restricted, withdrew, over-qualified, under-qualified, failed-interview, other.
  • Hire: POST …/hire with an optional { "startDate": "2026-11-01", "manager": { "id": "…" } } (manager is an employee, by id or external id). The candidate becomes an employee on the opening's position, with the same id, and appears in GET /api/v1/employees. The candidate's externalRefs move with them to the employee. A full position returns 409 conflict (position_full); a person who is no longer a candidate returns 409 conflict (not_a_candidate); an application whose opening is closed returns 409 conflict (opening_not_open); when two hires of the same candidate run at the same time (for example through two of their applications), one goes through and the other returns 409 conflict (concurrent_hire). Hiring needs recruitment.candidate.contact as well, because it reveals the person's contact details (403 forbidden, candidate_contact_required).
  • Hired and rejected applications: a HIRED application is final. Moving it, rejecting it or hiring it again returns 409 conflict with details.reason already_hired, and nothing changes. A REJECTED application can't be hired: /hire returns 409 conflict (already_rejected). Move it back to an active stage with /stage first if you want to reconsider the candidate. Rejecting an already rejected application still changes nothing. The same rules apply in the app.

Webhook events

EventWhendata.object
application.createdA candidate applies on the careers page, or is added through the APIThe application
application.stage_changedThe stage changes (app, API or automatic screening)The application
application.rejectedThe application is rejected (sent with application.stage_changed)The application
application.updatedThe AI CV screening finishes or is re-run (changedFields: ["screening"])The application
application.hiredThe candidate is hired (app or API)The application

A hire also sends employee.created for the person (same id). Payloads follow the API's masking rule: an endpoint whose integration lacks recruitment.candidate.contact receives a masked candidate.email and candidate.phone.

Errors

Status and codeMeaning
403 forbiddenThe key lacks a permission in the table above, or candidate_contact_required on hire.
404 not_foundNo application with that id in your workspace.
409 conflictalready_applied, opening_not_open, opening_internal_only, not_yet_external, position_full, not_a_candidate, concurrent_hire, already_hired or already_rejected.
422 unresolved_referenceThe opening, candidate or manager reference didn't match.
422 validation_failedThe body is invalid, or saudi_only / nationality_required / archived.

Example

curl -X POST https://jadiroon.com/api/v1/applications \
  -H "Authorization: Bearer $JADIROON_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: jobboard-APP-5531" \
  -d '{ "openingId": "cmvm6inad0002kncr1pytpf2x", "candidate": { "externalSystem": "jobboard", "externalId": "C-881" } }'

The response is 201 Created. For a key without recruitment.candidate.contact it reads:

{
  "id": "cmvm6j7au000jkncrm8jc4e3a",
  "openingId": "cmvm6inad0002kncr1pytpf2x",
  "positionId": "cmvikf8270086knoq85hjhu3u",
  "personId": "cmvm6it2n0008kncr3n9kpkkx",
  "candidate": { "id": "cmvm6it2n0008kncr3n9kpkkx", "firstName": "Faisal", "lastName": "Alqahtani", "email": "f•••@example.com", "phone": "05•• ••• •12" },
  "stage": "APPLIED",
  "screening": { "status": "DONE", "score": null },
  "rejectionReason": null,
  "hasOffer": false,
  "appliedAt": "2026-10-24T09:11:32.023Z",
  "externalRefs": [],
  "createdAt": "2026-10-24T09:11:32.023Z",
  "updatedAt": "2026-10-24T09:11:32.023Z"
}