Positions

For HR administrators

Positions are the seats of your Organizational Structure: each one sits in an org unit, follows a job description, and has a capacity (how many people it holds). Use these endpoints to keep positions 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/positionsList positions (cursor pagination, updatedSince, external-id lookup)structure.org.view
POST /api/v1/positionsCreate a positionstructure.org.act
GET /api/v1/positions/{id}Get one positionstructure.org.view
PATCH /api/v1/positions/{id}Update a position; omitted fields stay unchangedstructure.org.act
DELETE /api/v1/positions/{id}Delete a position with no employees and no hiring historystructure.org.act
POST /api/v1/positions/{id}/archiveArchive a positionstructure.org.act (plus recruitment.hiring.open when it has an open opening)
PUT /api/v1/positions/by-external/{system}/{externalId}Create or update by your own system's idstructure.org.act
POST /api/v1/positions/bulkUpsert up to 1,000 positions by external id, async (see Bulk upsert)structure.org.act

structure.org.act includes structure.org.view. Setting or changing pay (payScale, payGrade, payStepNumber) also needs structure.functional.salary; without it the request returns 403 forbidden. Reading pay needs it too: without structure.functional.salary, the pay fields (payScaleId, payGradeId, payStepNumber, baseSalary, payPeriod) are null in every response (lists, single reads and the responses of writes) and in webhook payloads.

The position object

FieldTypeNotes
idstringJadiroon id. Ids differ between Jadiroon environments; link your records with externalRefs.
title{ ar, en }The position's own title. When empty, the app shows the job description's name.
jobDescriptionIdstring or nullThe job description the position follows. null only when that job description was deleted.
orgUnitIdstring or nullThe org unit it sits in; null when unplaced.
capacityintegerNumber of seats (1 or more).
measurementModestringSPECIALIZED or ROUTINE (workload measurement), read-only.
hiringStatusstringOPEN or CLOSED. Read-only: it changes when hiring is opened or closed.
hiringPathwaystring or nullINTERNAL_ONLY, INTERNAL_FIRST or EXTERNAL while open. Read-only.
hiringOpenedAt, externalAtISO 8601 or nullWhen hiring opened and when the posting becomes external. Read-only.
posting{ ar, en }The public posting text, read-only.
payScaleId, payGradeIdstring or nullThe pay scale and grade, when set. null without structure.functional.salary, as are the three pay fields below.
payStepNumberinteger or nullThe step within the grade; null shows the grade's whole range.
baseSalaryinteger or nullBase pay at that step, in payPeriod units (SAR). Read-only, computed.
payPeriodstring or nullMONTHLY or ANNUAL, from the pay scale.
archivedAtISO 8601 or nullSet when the position is archived.
externalRefsarray[{ system, externalId }] links to your systems.
createdAt, updatedAtISO 8601updatedAt also moves when the position changes through the app, or when its org unit or job description is deleted.

Creating and updating

POST takes jobDescription and optionally title, orgUnit, capacity, payScale, payGrade and payStepNumber. Unknown fields are rejected with 422 validation_failed, and so are the read-only hiring fields (hiringStatus, hiringPathway, externalAt): those change when an opening is opened or closed.

jobDescription, orgUnit and payScale name the record by Jadiroon id or by your own id; payGrade takes a Jadiroon id:

{
  "jobDescription": { "externalSystem": "oracle-hcm", "externalId": "JOB-12" },
  "orgUnit": { "id": "cmv…" },
  "payScale": { "id": "cmv…" },
  "payGrade": { "id": "cmv…" },
  "payStepNumber": 3
}

To name a job description by your own id, link it first with externalRef when you create or update it (see Job descriptions). The job description must be one of your workspace's own job descriptions, and the org unit and pay scale must belong to your workspace; otherwise the response is 422 unresolved_reference. The grade must belong to the scale and the step must be within the grade's steps (422 validation_failed, details.reason invalid_pay_grade or invalid_pay_step). A grade or step sent without a pay scale (none in the body and none on the position, or payScale: null) is rejected the same way.

An archived job description can't be chosen for a new position, or as a position's new job description: the response is 422 validation_failed with details.reason archived. A position already linked to an archived job description keeps it.

On PATCH, orgUnit: null unplaces the position and payScale: null removes the whole pay link. Changing the scale needs a grade of the new scale; changing the grade clears the step unless you send one. Lowering capacity below the number of employees in the position returns 409 conflict with details.reason capacity_below_filled and details.filled.

Upsert by external id

PUT /api/v1/positions/by-external/oracle-hcm/POS-310 looks up the position linked to oracle-hcm / POS-310:

  • Linked position found: the body is applied like a PATCH and the response is 200.
  • No linked position: a position is created from the body (send at least jobDescription) and linked to the external id, and the response is 201.

If the linked position was deleted in the app, the old link is dropped and the call creates a new position with a new link. To find a position by your id without changing it, call GET /api/v1/positions?externalSystem=oracle-hcm&externalId=POS-310 (send both parameters).

Archiving and deleting

POST /api/v1/positions/{id}/archive closes hiring on the position and its open openings, and sets archivedAt. Archiving an archived position changes nothing and returns it as is.

Closing an open opening needs recruitment.hiring.open, as in the app. When the position has an open opening and your key lacks it, the response is 403 forbidden with details.reason hiring_permission_required and details.permission recruitment.hiring.open, and nothing changes. A position with no open opening archives with structure.org.act alone.

DELETE is checked before anything is removed:

  • 409 has_people (details.assigned) when employees are assigned to the position.
  • 409 has_openings (details.pipelines, details.rubrics) when the position has ever had an opening or an interview rubric. Archive it instead.

When a position is deleted, its workload tasks go with it, its external links are removed, and the deletion appears in GET /api/v1/deleted?resource=position for 180 days. Former employees whose currentPositionId was that position get currentPositionId: null, their updatedAt moves, and each sends employee.updated (changedFields: ["currentPositionId"]), or one employee.bulk_changed beyond 50.

Positions can also be deleted in the app through the data editor. Those deletions appear in GET /api/v1/deleted?resource=position the same way; a data editor commit sends one position.bulk_changed rather than position.deleted per position, so read the deletion feed after it. When a job description is deleted in the app (for example by deselecting its sub-sector), its positions stay with jobDescriptionId: null and their updatedAt moves.

Webhook events

EventWhendata.object
position.createdA position is created or duplicated (app or API), including a position created and opened in one step from the openings flow, a position copied when a job family is duplicated with its positions, and a draft position created by applying a workforce plan headcount shortageThe position
position.updatedA position changes, including its hiring fields when an opening is opened, closed or made external, and its payStepNumber / baseSalary when an edit to its pay grade reprices it; 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 position
position.archivedA position is archivedThe position
position.deletedA position is deleted{ id, externalRefs }
position.bulk_changedPositions were imported in the app, or many changed at once{ resource: "position" }

Deleting an org unit sends position.updated for each position it unplaces. After position.bulk_changed, re-read the list with updatedSince set to the event's occurredAt, and read GET /api/v1/deleted?resource=position&since=… for removed positions.

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 position with that id in your workspace.
409 has_peopleEmployees are assigned to the position.
409 has_openingsThe position has an opening or an interview rubric.
409 conflictThe external id is already linked to another position, or capacity is below the employees in it.
422 validation_failedThe body is invalid, or a read-only field was sent; details explains why.
422 unresolved_referencejobDescription, orgUnit or payScale could not be found in your workspace.

Example

curl -X PUT https://jadiroon.com/api/v1/positions/by-external/oracle-hcm/POS-310 \
  -H "Authorization: Bearer $JADIROON_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": { "ar": "أخصائي موارد بشرية", "en": "HR Specialist" },
    "jobDescription": { "externalSystem": "oracle-hcm", "externalId": "JOB-12" },
    "orgUnit": { "externalSystem": "oracle-hcm", "externalId": "ORG-200" },
    "capacity": 2
  }'

The first call creates and links the position and answers 201 Created; repeating it updates the same position and answers 200 OK with the same body shape:

{
  "id": "cmvk1q2w30004kn8x7d1f0abc",
  "title": { "ar": "أخصائي موارد بشرية", "en": "HR Specialist" },
  "jobDescriptionId": "cmvjx0h1t0001kn3o9c2a7def",
  "orgUnitId": "cmvjmf15k0008kn4q404rqwgw",
  "capacity": 2,
  "measurementMode": "SPECIALIZED",
  "hiringStatus": "CLOSED",
  "hiringPathway": null,
  "hiringOpenedAt": null,
  "externalAt": null,
  "posting": { "ar": null, "en": null },
  "payScaleId": null,
  "payGradeId": null,
  "payStepNumber": null,
  "baseSalary": null,
  "payPeriod": null,
  "archivedAt": null,
  "externalRefs": [{ "system": "oracle-hcm", "externalId": "POS-310" }],
  "createdAt": "2026-10-22T15:02:11.204Z",
  "updatedAt": "2026-10-22T15:02:11.204Z"
}