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 path | What it does | Permission |
|---|---|---|
GET /api/v1/job-descriptions | List job descriptions (cursor pagination, updatedSince, external-id lookup) | structure.functional.view or recruitment.view |
POST /api/v1/job-descriptions | Create a job description | structure.functional.act |
GET /api/v1/job-descriptions/{id} | Get one job description | structure.functional.view or recruitment.view |
PATCH /api/v1/job-descriptions/{id} | Update a job description; omitted fields stay unchanged | structure.functional.act |
POST /api/v1/job-descriptions/{id}/archive | Archive a job description | structure.functional.act |
POST /api/v1/job-descriptions/{id}/unarchive | Restore an archived job description | structure.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
| Field | Type | Notes |
|---|---|---|
id | string | Jadiroon 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. |
subSectorId | string or null | The sub-sector it is filed under. null only for a title-only job description added from the org chart and not yet filed. |
jobFamilyId | string or null | The job family (the id in GET /api/v1/job-families). |
workspaceJobFamilyId | string or null | The family instance (instances[].id of the job family) it is filed under. null when that instance was removed. |
mandate | string or null | REGULATORY, EXECUTIVE or INTERNAL: the mandate of that instance. |
jobType | string | MANAGERIAL, SUPERVISORY, SPECIALIST or SUPPORT. |
workLocation | { ar, en } | Default work location. |
qualifications | object | education, specialization, experienceField, yearsExperience, certifications: free text, each string or null. |
desiredHeadcount | integer | Target number of people for this job description. |
saudiPriority | string | Saudization policy: NONE, PREFER or ONLY. |
payScaleId, payGradeId | string or null | The default pay scale and grade. null without structure.functional.salary. |
salaryMin, salaryMax | integer or null | The grade's annual salary band in SAR. Read-only: kept in sync with the grade. null without structure.functional.salary. |
skills | object | { 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. |
sscoCode | string or null | The national SSCO occupation code when the job description was adopted from the national catalog. Such job descriptions are read-only through the API. |
archivedAt | ISO 8601 or null | Set when the job description is archived. |
externalRefs | array | [{ system, externalId }] links to your systems. |
createdAt, updatedAt | ISO 8601 | updatedAt 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.reasonsub_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
mandateto pick the instance (mandate_required; an unknown one givesmandate_not_found). - Skills must be ones selected for that family instance (
skill_not_in_job_family, withdetails.skillId). New skills can't be created through the API. requiredLevelis 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
jobFamilyIdormandatemoves 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 is422 validation_failedwithdetails.reasonskills_not_in_new_job_family, and you should send the arrays. payScale: nullremoves the default pay grade and clearssalaryMinandsalaryMax. Changing the scale needs a grade of the new scale. SendingpayScaleorpayGradere-reads the salary band from the grade.- A
PATCHwithoutpayScaleandpayGradeleavessalaryMinandsalaryMaxexactly 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
subSectorIdandjobFamilyIdbefore 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
PATCHreturns422 validation_failedwithdetails.reasonjob_family_not_selected(the app's editor still opens it). Send ajobFamilyId(andsubSectorId) 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 issub_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
| Event | When | data.object |
|---|---|---|
job_description.created | A job description is created or duplicated (app or API), including one copied when a job family is duplicated with its job descriptions | The job description |
job_description.updated | A 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 sent | The job description |
job_description.archived | A job description is archived | The job description |
job_description.unarchived | An archived job description is restored | The job description |
job_description.deleted | A job description is deleted in the app | { id, externalRefs } |
job_description.bulk_changed | Many 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 code | Meaning |
|---|---|
403 forbidden | The key lacks the permission in the table above, or pay fields were sent without structure.functional.salary. |
404 not_found | No job description with that id in your workspace. |
409 ssco_managed | The job description came from the national catalog and is read-only through the API. |
409 conflict | The externalRef is already linked to another job description. |
422 validation_failed | The body is invalid or breaks a rule above; details.reason says which. |
422 unresolved_reference | payScale 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"
}