Job descriptions

For HR administrators

Job descriptions are your workspace's own JDs in the Functional Structure: each one is filed under a sub-sector and a job family you selected, lists the skills it requires and the level of each, and can carry a default pay grade. Positions follow a job description. Use these endpoints to keep job descriptions in Jadiroon in sync with your ERP or HR system.

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/job-descriptionsList job descriptions (cursor pagination, updatedSince, external-id lookup)structure.functional.view or recruitment.view
POST /api/v1/job-descriptionsCreate a job descriptionstructure.functional.act
GET /api/v1/job-descriptions/{id}Get one job descriptionstructure.functional.view or recruitment.view
PATCH /api/v1/job-descriptions/{id}Update a job description; omitted fields stay unchangedstructure.functional.act
POST /api/v1/job-descriptions/{id}/archiveArchive a job descriptionstructure.functional.act
POST /api/v1/job-descriptions/{id}/unarchiveRestore an archived job descriptionstructure.functional.act

There is no DELETE: archive a job description instead. Setting or changing the default pay grade (payScale, payGrade) also needs structure.functional.salary; without it the request returns 403 forbidden. Reading pay needs it too: without structure.functional.salary, payScaleId, payGradeId, salaryMin and salaryMax are null in every response (lists, single reads and the responses of writes) and in webhook payloads.

Only your workspace's own job descriptions are listed. To find the sub-sector, job family and skill ids to use, read GET /api/v1/sub-sectors and GET /api/v1/job-families (see Functional Structure).

The job description object

FieldTypeNotes
idstringJadiroon id. Ids differ between Jadiroon environments; link your records with externalRefs.
name{ ar, en }The job description's name.
purpose{ ar, en }The purpose statement of the job.
subSectorIdstring or nullThe sub-sector it is filed under. null only for a title-only job description added from the org chart and not yet filed.
jobFamilyIdstring or nullThe job family (the id in GET /api/v1/job-families).
workspaceJobFamilyIdstring or nullThe family instance (instances[].id of the job family) it is filed under. null when that instance was removed.
mandatestring or nullREGULATORY, EXECUTIVE or INTERNAL: the mandate of that instance.
jobTypestringMANAGERIAL, SUPERVISORY, SPECIALIST or SUPPORT.
workLocation{ ar, en }Default work location.
qualificationsobjecteducation, specialization, experienceField, yearsExperience, certifications: free text, each string or null.
desiredHeadcountintegerTarget number of people for this job description.
saudiPrioritystringSaudization policy: NONE, PREFER or ONLY.
payScaleId, payGradeIdstring or nullThe default pay scale and grade. null without structure.functional.salary.
salaryMin, salaryMaxinteger or nullThe grade's annual salary band in SAR. Read-only: kept in sync with the grade. null without structure.functional.salary.
skillsobject{ technical: [...], general: [...] }, each item { id, slug, name, requiredLevel }. Technical levels run 1–5, general levels 1–3. slug is null for your workspace's own skills.
sscoCodestring or nullThe national SSCO occupation code when the job description was adopted from the national catalog. Such job descriptions are read-only through the API.
archivedAtISO 8601 or nullSet when the job description is archived.
externalRefsarray[{ system, externalId }] links to your systems.
createdAt, updatedAtISO 8601updatedAt also moves when the job description changes through the app, or when its family instance or pay scale is removed.

Creating

POST needs name (both ar and en), subSectorId and jobFamilyId. The rules are the same as in the app's job description editor:

  • The sub-sector must be selected in your Functional Structure (422 validation_failed, details.reason sub_sector_not_selected).
  • The job family must sit under that sub-sector (job_family_not_in_sub_sector) and be selected by your workspace (job_family_not_selected).
  • When your workspace has the family more than once (one instance per mandate), send mandate to pick the instance (mandate_required; an unknown one gives mandate_not_found).
  • Skills must be ones selected for that family instance (skill_not_in_job_family, with details.skillId). New skills can't be created through the API.
  • requiredLevel is 1–5 for technical skills and 1–3 for general skills; anything else is rejected. When omitted it keeps the skill's current level, or takes the default for a newly added skill: 3 (technical) or 2 (general).

Optional fields: purpose, mandate, jobType (default SPECIALIST), workLocation, qualifications, desiredHeadcount (default 1), saudiPriority (default NONE), payScale, payGrade, skills and externalRef. Unknown fields and read-only fields (salaryMin, salaryMax, sscoCode, archivedAt) are rejected with 422 validation_failed.

payScale names a scale by Jadiroon id or by your own id; payGrade takes a Jadiroon id and is required with a scale. The scale must belong to your workspace (422 unresolved_reference) and the grade to the scale (422 validation_failed, details.reason invalid_pay_grade).

externalRef: { externalSystem, externalId } links your own id to the job description in the same request, so positions can name it as jobDescription: { externalSystem, externalId } and you can look it up with GET /api/v1/job-descriptions?externalSystem=…&externalId=…. If that id is already linked to another job description the response is 409 conflict; a link left by a job description that no longer exists is dropped and the id is linked to this one. PATCH accepts it too, to link an existing job description. A job description has one link per system: sending an externalRef for a system it is already linked to replaces the old externalId (the old id no longer finds it).

{
  "externalRef": { "externalSystem": "oracle-hcm", "externalId": "JOB-12" },
  "name": { "ar": "أخصائي موارد بشرية", "en": "HR Specialist" },
  "purpose": { "ar": "إدارة عمليات الموارد البشرية اليومية", "en": "Runs day-to-day HR operations" },
  "subSectorId": "cmvj…",
  "jobFamilyId": "cmvj…",
  "mandate": "EXECUTIVE",
  "skills": {
    "technical": [{ "id": "cmvj…", "requiredLevel": 4 }],
    "general": [{ "id": "cmvj…" }]
  }
}

Updating

PATCH changes only the fields you send. Skills follow the same rule: if you leave out skills.technical (or skills.general, or skills altogether), the job description keeps every skill of that type with its current level. Sending an array replaces that type's skills within the family instance's skill set (the skills you could pick); skills the job description holds outside that set are left as they are, as in the app's editor. Send [] to remove all of that type's skills from the set. A skill sent without requiredLevel keeps its current level.

  • Changing jobFamilyId or mandate moves the job description to another family instance. If you keep a skill array out of the request, every kept skill must also be selected for the new instance; otherwise the response is 422 validation_failed with details.reason skills_not_in_new_job_family, and you should send the arrays.
  • payScale: null removes the default pay grade and clears salaryMin and salaryMax. Changing the scale needs a grade of the new scale. Sending payScale or payGrade re-reads the salary band from the grade.
  • A PATCH without payScale and payGrade leaves salaryMin and salaryMax exactly as stored, including a band set without a pay grade (for example in the data editor).
  • A title-only job description added from the org chart needs subSectorId and jobFamilyId before it can be updated (taxonomy_required).
  • If the job description's family instance was removed and your workspace no longer has the family at all, every PATCH returns 422 validation_failed with details.reason job_family_not_selected (the app's editor still opens it). Send a jobFamilyId (and subSectorId) your workspace uses to re-file it.
  • The sub-sector and family pair is checked on every PATCH: if the sub-sector was removed from your Functional Structure the response is sub_sector_not_selected.

Job descriptions with an sscoCode came from the national catalog during onboarding and are replaced when that step is run again, so they are read-only through the API: PATCH, archive and unarchive return 409 ssco_managed. Edit them in the app.

Archiving

POST /api/v1/job-descriptions/{id}/archive sets archivedAt and closes hiring on the job description's open positions and their open openings. Archiving an archived job description changes nothing and returns it as is. An archived job description can't be chosen for a new position. POST /api/v1/job-descriptions/{id}/unarchive clears archivedAt; hiring stays closed.

Job descriptions are never deleted through the API. They can be deleted in the app (for example when you deselect their sub-sector or job family, or in the data editor). Those deletions appear in GET /api/v1/deleted?resource=job_description for 180 days, and their positions stay with jobDescriptionId: null.

Webhook events

EventWhendata.object
job_description.createdA job description is created or duplicated (app or API), including one copied when a job family is duplicated with its job descriptionsThe job description
job_description.updatedA job description changes, including when one of your own skills it lists is renamed or deleted (changedFields: ["skills"]) and when an edit to its pay grade moves its band (changedFields: ["salaryMin", "salaryMax"]); data.changedFields lists the fields when known. Without structure.functional.salary, pay fields are left out of changedFields, and an update that changed only pay is not sentThe job description
job_description.archivedA job description is archivedThe job description
job_description.unarchivedAn archived job description is restoredThe job description
job_description.deletedA job description is deleted in the app{ id, externalRefs }
job_description.bulk_changedMany job descriptions changed at once (onboarding, the data editor){ resource: "job_description" }

Archiving or deleting a job description, or removing sub-sectors or job families from your Functional Structure, also sends position.updated for each position whose hiring it closed. Archiving or deleting a job description also sends opening.closed for each opening it closed (opening.bulk_changed beyond 50). After job_description.bulk_changed, re-read the list with updatedSince set to the event's occurredAt, and read GET /api/v1/deleted?resource=job_description&since=… for removed ones. Changes to the job description card sections (responsibilities, KPIs, contacts, evaluation) send no event: they are not part of the API object.

Errors

Status and codeMeaning
403 forbiddenThe key lacks the permission in the table above, or pay fields were sent without structure.functional.salary.
404 not_foundNo job description with that id in your workspace.
409 ssco_managedThe job description came from the national catalog and is read-only through the API.
409 conflictThe externalRef is already linked to another job description.
422 validation_failedThe body is invalid or breaks a rule above; details.reason says which.
422 unresolved_referencepayScale could not be found in your workspace.

Example

curl -X PATCH https://jadiroon.com/api/v1/job-descriptions/cmvk2a9d10007kn5s3x8e4ghi \
  -H "Authorization: Bearer $JADIROON_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": { "en": "Senior HR Specialist" } }'

Only the English name changes; the skills and their levels stay as they were:

{
  "id": "cmvk2a9d10007kn5s3x8e4ghi",
  "name": { "ar": "أخصائي موارد بشرية", "en": "Senior HR Specialist" },
  "purpose": { "ar": "إدارة عمليات الموارد البشرية اليومية", "en": "Runs day-to-day HR operations" },
  "subSectorId": "cmvjx0h1t0001kn3o9c2a7def",
  "jobFamilyId": "cmvjx0h1t0002kn3o1b5c8xyz",
  "workspaceJobFamilyId": "cmvjx0h1t0003kn3o6d4e2uvw",
  "mandate": "EXECUTIVE",
  "jobType": "SPECIALIST",
  "workLocation": { "ar": null, "en": null },
  "qualifications": { "education": "Bachelor's degree", "specialization": null, "experienceField": null, "yearsExperience": "3 years", "certifications": null },
  "desiredHeadcount": 2,
  "saudiPriority": "NONE",
  "payScaleId": null,
  "payGradeId": null,
  "salaryMin": null,
  "salaryMax": null,
  "skills": {
    "technical": [{ "id": "cmvjx0h1t0004kn3o7f3g1rst", "slug": "hr-operations", "name": { "ar": "عمليات الموارد البشرية", "en": "HR operations" }, "requiredLevel": 4 }],
    "general": [{ "id": "cmvjx0h1t0005kn3o8h2i9opq", "slug": "communication", "name": { "ar": "التواصل", "en": "Communication" }, "requiredLevel": 2 }]
  },
  "sscoCode": null,
  "archivedAt": null,
  "externalRefs": [],
  "createdAt": "2026-10-22T15:02:11.204Z",
  "updatedAt": "2026-10-22T16:40:03.518Z"
}