MENU navbar-image
Okta HR Okta HR API Reference

Introduction

REST API for Okta HR — mobile self-service, HR operations and Okta Suite internal integration.

Welcome to the Okta HR API — use it to power the employee mobile app, integrate accounting/ERP systems, or connect other Okta Suite products.

Responses are JSON:API documents: {"data": [{"id", "type", "attributes": {…}}]}.

Two APIs, two kinds of caller

*`/api/v1/— on behalf of a person.** Sanctum bearer tokens. The caller is a user, and everything they can see is what that user is allowed to see. Employee self-service lives under/api/v1/me/*`.

*`/api/internal/— on behalf of a product.** A service key in theX-Service-Keyheader, issued withphp artisan okta:issue-service-key `. This is for other Okta Suite products, which have their own users and cannot obtain a token for any of them.

A service key names a product, not a person, and it is suite-level: it carries no company. So every internal route addresses its company explicitly in the path — /api/internal/companies/{company}/… — and any endpoint that records who did something takes an acting_employee_id alongside the decision. Without it the approval chain would file an approval attributed to nobody.

The person acting is named one of three ways: acting_employee_id (their employee file), acting_user_email (their account), or — when your product's own role system says they administer the tenant — acting_as_tenant_admin: true. The third is a complete answer on its own: the administrator is often the one person you have no usable name for (a roster file with no login, an account with no address), and they are the person approval chains already wait on. The write then goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account — its first HR manager, the one provisioning made — otherwise. It is the same claim as an email, under the same service key, with the email taken out of it. For «اعتماداتي», approver_as_tenant_admin does the same.

Money and dates

Money is integer halalas (1 SAR = 100) everywhere, in both directions. Rates are basis points. Dates are stored UTC and displayed Asia/Riyadh; Y-m-d for dates, ISO-8601 for timestamps.

Punching zones (geofences)

A zone belongs to a branch, not to a company: GET /geofences lists every branch's circle, and meta.unassigned counts the people attached to no branch at all. Those people are measured against every zone the company enforces — a punch from inside any of them passes, and the nearest edge is the one reported — so assigning somebody to their branch narrows them to that branch's circle alone. A person on a branch with no zone (or with its zone switched off) is not measured at all: that is how field staff are exempted, on purpose.

Send the fix's accuracy (metres, as the device reports it) with every punch and every geofence read. A fix coarser than both the zone and 500 m — a phone with precise location switched off — cannot say whether anybody is inside a small circle, so it is refused with a sentence asking for precise location, and the read answers imprecise: true. An omitted accuracy raises no objection.

Radii are metres with a 50 m floor: a phone's own position is rarely better than a few dozen metres, so a tighter circle would refuse people standing inside the office. geofence_radius is what was stored; effective_geofence_radius is what the server enforces (an older value below the floor is widened, never narrowed) and is the one to draw. geofence_enforced answers "does this branch measure where people punch from" in a single boolean.

To DRAW the map, call GET /api/internal/map-config (or GET /api/v1/map-config from the phone) once at startup: it returns the tile URL template, its attribution and max zoom, the palette the platform draws zones and punches in, the radius bounds, and shape: "circle" — stated so a client never offers a polygon the server cannot check. Do not hardcode a tile URL: the day this platform moves to a keyed provider, a consumer that asked follows and a consumer that guessed shows grey squares.

Each zone in GET /geofences carries zone_bounds — a viewport that frames the circle. Use it rather than computing your own: framing a circle is arithmetic anybody can get subtly wrong, and the failure mode is a map that opens on an empty field, which reads as missing data rather than a bad viewport.

To show WHERE somebody punched, GET /employees/{employee}/punch-locations?date= (or GET /api/v1/me/attendance/punch-locations for the caller's own day) returns one entry per punch plus the branch circle and its viewport. inside is null where there is no verdict — no position was recorded, or the branch enforces no zone — and a point with a null latitude must be listed and never plotted: an absent position is not evidence of anything.

Before offering a punch button, ask GET /employees/{employee}/geofence (or GET /api/v1/me/attendance/geofence from the phone) with the device's coordinates: the verdict comes from the same computation that allows or refuses the punch itself, so a screen can never promise what the write will reject.

Onboarding a tenant from another suite product

The three calls, in order:

  1. POST /api/internal/companies — creates the company, seeds its defaults and creates its first hr-manager. Send your own tenant identifier as external_ref and the call becomes idempotent: retries, re-installs and double-clicks return the same company instead of a second one.
  2. POST /api/internal/companies/{company}/employees — pushes the staff across. Upserts on employee_number, so a nightly re-sync updates rows rather than growing a duplicate roster. Departments and branches are matched by name and created if new.
  3. POST /api/internal/companies/{company}/employees/{employee}/account — gives an employee a login. Deliberately separate from step 2: an HR record and a person who can sign in are different decisions, and syncing a roster should not silently mint dozens of accounts.

Send each employee's email in step 2. It is the only field both systems hold for the same human, so it is how your product later maps its signed-in user back onto an employee here — and GET /employees?search= matches on it.

Errors

401 no or unknown credentials · 404 the resource does not exist or belongs to another company · 422 your input, with a errors object naming the fields — the message is written to be shown to a user · 429 rate limited — metered per service key and per company, never per address: 1200 requests a minute for each company, 12000 for one key across every company it serves, and 30 a minute on link-codes/redeem. Honour Retry-After; a retry sooner is refused again.

Authenticating requests

To authenticate requests, include an Authorization header with the value "Bearer {YOUR_TOKEN}".

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

Obtain a Sanctum bearer token via POST /api/v1/auth/login (email, password, device_name), then send it as Authorization: Bearer {token}. The login endpoint itself and /api/internal/* (which uses the X-Service-Key header) do not use bearer tokens.

Employees

An employee's whole file

requires authentication

Money is in halalas. Every human label comes as an _ar/_en pair.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/employees/16/file" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/employees/16/file"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "employee": {
            "id": "12",
            "employee_number": "1042",
            "name_ar": "سارة العتيبي",
            "name_en": "Sara Alotaibi",
            "nationality": "سعودي",
            "gender": "female",
            "birth_date": "1994-03-11",
            "marital_status": "married",
            "national_id": "1098765432",
            "mobile": "0551234567",
            "email": "sara@example.test",
            "city": "الرياض",
            "national_address": "RRRD2929",
            "address": "حي الياسمين",
            "job_title": "محاسبة",
            "contract_type": "full_time",
            "hire_date": "2022-05-01",
            "contract_end_date": "2027-04-30",
            "probation_end_date": "2022-07-30",
            "status": "active",
            "has_account": true,
            "shift_template_id": 3,
            "manager_id": 4,
            "department": {
                "id": 2,
                "name_ar": "المالية",
                "name_en": "Finance"
            },
            "branch": {
                "id": 1,
                "name_ar": "المقر الرئيسي",
                "name_en": "Head office"
            },
            "manager": {
                "id": 4,
                "name_ar": "خالد الحربي",
                "name_en": "Khalid Alharbi"
            },
            "gender_label_ar": "أنثى",
            "gender_label_en": "Female",
            "marital_status_label_ar": "متزوج",
            "marital_status_label_en": "Married",
            "contract_type_label_ar": "دوام كامل",
            "contract_type_label_en": "Full time",
            "status_label_ar": "على رأس العمل",
            "status_label_en": "Active",
            "shift": {
                "id": 3,
                "name_ar": "الدوام الصباحي",
                "name_en": "Morning",
                "start_time": "08:00",
                "end_time": "16:00"
            },
            "service_years": 4,
            "service_months": 5,
            "contract_days_left": 200
        },
        "sections": {
            "finance": true,
            "violations": true,
            "account": true
        },
        "finance": {
            "basic_salary": 800000,
            "housing_allowance": 200000,
            "transport_allowance": 50000,
            "other_allowances": 0,
            "total_wage": 1050000,
            "iban": "SA0380000000608010167519",
            "bank_name": "الراجحي",
            "payslips": [
                {
                    "id": "81",
                    "type": "payslips",
                    "attributes": {
                        "period": "2026-09",
                        "basic_salary": 800000,
                        "housing_allowance": 200000,
                        "transport_allowance": 50000,
                        "other_allowances": 0,
                        "overtime_amount": 0,
                        "extra_earnings": 0,
                        "absence_deduction": 0,
                        "late_deduction": 0,
                        "unpaid_leave_deduction": 0,
                        "extra_deductions": 0,
                        "gosi_employee": 97500,
                        "gross": 1050000,
                        "total_deductions": 97500,
                        "net": 952500
                    }
                }
            ]
        },
        "attendance": {
            "month": "2026-10",
            "counts": {
                "present": 8,
                "late": 1,
                "absent": 0,
                "on_leave": 0,
                "mission": 0,
                "remote": 0
            },
            "records": []
        },
        "leaves": {
            "year": 2026,
            "balances": [],
            "current_leave": null,
            "requests": []
        },
        "documents": [],
        "custody": [],
        "warnings": [],
        "violations": [],
        "account": {
            "has_account": true,
            "email": "sara@example.test",
            "roles": [
                {
                    "name": "employee",
                    "label_ar": "موظف",
                    "label_en": "Employee"
                }
            ]
        }
    }
}
 

Example response (403, not HR, not their manager, not themselves):


{
    "message": "This action is unauthorized."
}
 

Request      

GET api/v1/employees/{employee_id}/file

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

employee_id   integer     

The ID of the employee. Example: 16

employee   integer     

Example: 12

Endpoints

Sign in

Issues a Sanctum token for the mobile app / API consumers.

The phone app names its device (device_id, stable per installation). The first device an account signs in from is bound on sight; any other raises a request to the company and device.status comes back pending — the token then only reads where the request stands (GET /me/device-access) until somebody approves it, and every other call answers 403 device_pending. A refused device answers rejected, and may ask again (POST /me/device-access/request). A client that names no device gets device: null and signs in as before.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/auth/login" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"email\": \"sara@example.test\",
    \"password\": \"secret-password\",
    \"device_name\": \"android\",
    \"device_id\": \"3f2a9c7e5b8d4e1fa6c0b2d4e6f8a1c3\",
    \"device_model\": \"Pixel 8\",
    \"platform\": \"android\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/auth/login"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "email": "sara@example.test",
    "password": "secret-password",
    "device_name": "android",
    "device_id": "3f2a9c7e5b8d4e1fa6c0b2d4e6f8a1c3",
    "device_model": "Pixel 8",
    "platform": "android"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "token": "1|abc123",
        "token_type": "Bearer",
        "device": {
            "id": 5,
            "status": "pending",
            "status_label_ar": "بانتظار الموافقة",
            "status_label_en": "Awaiting approval",
            "name": "Pixel 8",
            "platform": "android",
            "requested_at": "2026-10-22T06:10:00+00:00",
            "decided_at": null,
            "awaiting": {
                "name_ar": "ماجد المدير",
                "name_en": "Majed Manager"
            },
            "rejection_reason": null
        }
    }
}
 

Request      

POST api/v1/auth/login

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

email   string     

Example: sara@example.test

password   string     

Example: secret-password

device_name   string     

A label for the token. Example: android

device_id   string  optional    

The installation's own id. Example: 3f2a9c7e5b8d4e1fa6c0b2d4e6f8a1c3

device_model   string  optional    

What the approver reads. Example: Pixel 8

platform   string  optional    

android or ios. Example: android

Revoke the token used for the current request.

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/auth/logout" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/auth/logout"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Request      

POST api/v1/auth/logout

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

The authenticated user's profile (mobile self-service).

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Everything the app needs to draw a map: tiles, palette, radius bounds.

requires authentication

Served rather than hardcoded in the app, so the day this platform moves to a keyed tile provider the app follows instead of showing grey squares nobody can explain from the phone.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/map-config" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/map-config"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "tiles": {
        "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
        "attribution": "© OpenStreetMap",
        "max_zoom": 19
    },
    "fallback_center": {
        "latitude": 24.7136,
        "longitude": 46.6753,
        "zoom": 11
    },
    "colors": {
        "zone": "#7d55c7",
        "punch_inside": "#0e9f6e",
        "punch_outside": "#dc2626"
    },
    "radius": {
        "min": 50,
        "max": 20000
    },
    "shape": "circle"
}
 

Request      

GET api/v1/map-config

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

List employees (paginated, filterable by status and search).

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/employees" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/employees"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/employees

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create an employee. Money fields are integers in halalas.

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/employees" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_number\": \"bngzmiyvdljnikhw\",
    \"name_ar\": \"a\",
    \"name_en\": \"y\",
    \"nationality\": \"k\",
    \"gender\": \"male\",
    \"birth_date\": \"2026-10-25T09:50:16\",
    \"marital_status\": \"divorced\",
    \"national_id\": \"cmyuwpwlvqwrsitc\",
    \"mobile\": \"pscqldzsnrwtujwv\",
    \"email\": \"jhaag@example.net\",
    \"national_address\": \"l\",
    \"city\": \"q\",
    \"address\": \"p\",
    \"job_title\": \"p\",
    \"contract_type\": \"temporary\",
    \"hire_date\": \"2026-10-25T09:50:16\",
    \"contract_end_date\": \"2026-10-25T09:50:16\",
    \"probation_end_date\": \"2026-10-25T09:50:16\",
    \"status\": \"terminated\",
    \"basic_salary\": 9,
    \"housing_allowance\": 16,
    \"transport_allowance\": 27,
    \"other_allowances\": 4,
    \"iban\": \"w\",
    \"bank_name\": \"t\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/employees"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_number": "bngzmiyvdljnikhw",
    "name_ar": "a",
    "name_en": "y",
    "nationality": "k",
    "gender": "male",
    "birth_date": "2026-10-25T09:50:16",
    "marital_status": "divorced",
    "national_id": "cmyuwpwlvqwrsitc",
    "mobile": "pscqldzsnrwtujwv",
    "email": "jhaag@example.net",
    "national_address": "l",
    "city": "q",
    "address": "p",
    "job_title": "p",
    "contract_type": "temporary",
    "hire_date": "2026-10-25T09:50:16",
    "contract_end_date": "2026-10-25T09:50:16",
    "probation_end_date": "2026-10-25T09:50:16",
    "status": "terminated",
    "basic_salary": 9,
    "housing_allowance": 16,
    "transport_allowance": 27,
    "other_allowances": 4,
    "iban": "w",
    "bank_name": "t"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/employees

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

employee_number   string     

يجب ألا يزيد طول value عن 20 حرفًا. Example: bngzmiyvdljnikhw

name_ar   string     

يجب ألا يزيد طول value عن 255 حرفًا. Example: a

name_en   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: y

nationality   string  optional    

يجب ألا يزيد طول value عن 100 حرفًا. Example: k

gender   string  optional    

Example: male

Must be one of:
  • male
  • female
birth_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:16

marital_status   string  optional    

Example: divorced

Must be one of:
  • single
  • married
  • divorced
  • widowed
national_id   string  optional    

يجب ألا يزيد طول value عن 20 حرفًا. Example: cmyuwpwlvqwrsitc

mobile   string  optional    

يجب ألا يزيد طول value عن 20 حرفًا. Example: pscqldzsnrwtujwv

email   string  optional    

يجب أن يكون value بريدًا إلكترونيًا صحيحًا. يجب ألا يزيد طول value عن 255 حرفًا. Example: jhaag@example.net

national_address   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: l

city   string  optional    

يجب ألا يزيد طول value عن 100 حرفًا. Example: q

address   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: p

job_title   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: p

department_id   string  optional    

Must match an existing stored value.

branch_id   string  optional    

Must match an existing stored value.

manager_id   string  optional    

Must match an existing stored value.

contract_type   string  optional    

Example: temporary

Must be one of:
  • full_time
  • part_time
  • temporary
  • remote
hire_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:16

contract_end_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:16

probation_end_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:16

status   string  optional    

Example: terminated

Must be one of:
  • active
  • suspended
  • resigned
  • terminated
basic_salary   integer     

يجب ألا يقل value عن 0. Example: 9

housing_allowance   integer  optional    

يجب ألا يقل value عن 0. Example: 16

transport_allowance   integer  optional    

يجب ألا يقل value عن 0. Example: 27

other_allowances   integer  optional    

يجب ألا يقل value عن 0. Example: 4

iban   string  optional    

يجب ألا يزيد طول value عن 34 حرفًا. Example: w

bank_name   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: t

Show a single employee.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/employees/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/employees/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/employees/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the employee. Example: 16

Update an employee.

requires authentication

Example request:
curl --request PUT \
    "https://hr.getokta.io/api/v1/employees/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_number\": \"bngzmiyvdljnikhw\",
    \"name_ar\": \"a\",
    \"name_en\": \"y\",
    \"nationality\": \"k\",
    \"gender\": \"male\",
    \"birth_date\": \"2026-10-25T09:50:17\",
    \"marital_status\": \"divorced\",
    \"national_id\": \"cmyuwpwlvqwrsitc\",
    \"mobile\": \"pscqldzsnrwtujwv\",
    \"email\": \"jhaag@example.net\",
    \"national_address\": \"l\",
    \"city\": \"q\",
    \"address\": \"p\",
    \"job_title\": \"p\",
    \"contract_type\": \"part_time\",
    \"hire_date\": \"2026-10-25T09:50:17\",
    \"contract_end_date\": \"2026-10-25T09:50:17\",
    \"probation_end_date\": \"2026-10-25T09:50:17\",
    \"status\": \"suspended\",
    \"basic_salary\": 9,
    \"housing_allowance\": 16,
    \"transport_allowance\": 27,
    \"other_allowances\": 4,
    \"iban\": \"w\",
    \"bank_name\": \"t\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/employees/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_number": "bngzmiyvdljnikhw",
    "name_ar": "a",
    "name_en": "y",
    "nationality": "k",
    "gender": "male",
    "birth_date": "2026-10-25T09:50:17",
    "marital_status": "divorced",
    "national_id": "cmyuwpwlvqwrsitc",
    "mobile": "pscqldzsnrwtujwv",
    "email": "jhaag@example.net",
    "national_address": "l",
    "city": "q",
    "address": "p",
    "job_title": "p",
    "contract_type": "part_time",
    "hire_date": "2026-10-25T09:50:17",
    "contract_end_date": "2026-10-25T09:50:17",
    "probation_end_date": "2026-10-25T09:50:17",
    "status": "suspended",
    "basic_salary": 9,
    "housing_allowance": 16,
    "transport_allowance": 27,
    "other_allowances": 4,
    "iban": "w",
    "bank_name": "t"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

PUT api/v1/employees/{id}

PATCH api/v1/employees/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the employee. Example: 16

Body Parameters

employee_number   string  optional    

يجب ألا يزيد طول value عن 20 حرفًا. Example: bngzmiyvdljnikhw

name_ar   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: a

name_en   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: y

nationality   string  optional    

يجب ألا يزيد طول value عن 100 حرفًا. Example: k

gender   string  optional    

Example: male

Must be one of:
  • male
  • female
birth_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

marital_status   string  optional    

Example: divorced

Must be one of:
  • single
  • married
  • divorced
  • widowed
national_id   string  optional    

يجب ألا يزيد طول value عن 20 حرفًا. Example: cmyuwpwlvqwrsitc

mobile   string  optional    

يجب ألا يزيد طول value عن 20 حرفًا. Example: pscqldzsnrwtujwv

email   string  optional    

يجب أن يكون value بريدًا إلكترونيًا صحيحًا. يجب ألا يزيد طول value عن 255 حرفًا. Example: jhaag@example.net

national_address   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: l

city   string  optional    

يجب ألا يزيد طول value عن 100 حرفًا. Example: q

address   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: p

job_title   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: p

department_id   string  optional    

Must match an existing stored value.

branch_id   string  optional    

Must match an existing stored value.

manager_id   string  optional    

Must match an existing stored value.

contract_type   string  optional    

Example: part_time

Must be one of:
  • full_time
  • part_time
  • temporary
  • remote
hire_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

contract_end_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

probation_end_date   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

status   string  optional    

Example: suspended

Must be one of:
  • active
  • suspended
  • resigned
  • terminated
basic_salary   integer  optional    

يجب ألا يقل value عن 0. Example: 9

housing_allowance   integer  optional    

يجب ألا يقل value عن 0. Example: 16

transport_allowance   integer  optional    

يجب ألا يقل value عن 0. Example: 27

other_allowances   integer  optional    

يجب ألا يقل value عن 0. Example: 4

iban   string  optional    

يجب ألا يزيد طول value عن 34 حرفًا. Example: w

bank_name   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: t

List all departments of the current company.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/departments" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/departments"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/departments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

List all branches of the current company.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/branches" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/branches"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/branches

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

List company attendance records, filterable by date range.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/attendance-records" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"from\": \"2026-10-25T09:50:17\",
    \"to\": \"2026-10-25T09:50:17\",
    \"employee_id\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/attendance-records"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "from": "2026-10-25T09:50:17",
    "to": "2026-10-25T09:50:17",
    "employee_id": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/attendance-records

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

from   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

to   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

employee_id   integer  optional    

Example: 16

List company leave requests (HR/manager).

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/leave-requests" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/leave-requests"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/leave-requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Act on the current approval step (approve|reject).

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/leave-requests/16/act" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"decision\": \"approve\",
    \"notes\": \"b\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/leave-requests/16/act"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "decision": "approve",
    "notes": "b"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/leave-requests/{leaveRequest_id}/act

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

leaveRequest_id   integer     

The ID of the leaveRequest. Example: 16

Body Parameters

decision   string     

Example: approve

Must be one of:
  • approve
  • reject
notes   string  optional    

A rejection must say why; the note is optional when approving. يجب ألا يزيد طول value عن 255 حرفًا. Example: b

List payroll runs (HR only).

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/payroll-runs" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/payroll-runs"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/payroll-runs

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Build the payroll run for a month.

requires authentication

The same PayrollService::generateRun() the platform's «إنشاء مسيّر» calls, under the same policy — an HR manager, or anyone the company's roles screen granted payroll: manage. A month that already has a run is refused with the platform's own sentence.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/payroll-runs" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"year\": 2026,
    \"month\": 10
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/payroll-runs"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "year": 2026,
    "month": 10
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/payroll-runs

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

year   integer     

Example: 2026

month   integer     

1-12. Example: 10

A run with its employee lines.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/payroll-runs/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/payroll-runs/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/payroll-runs/{payrollRun_id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

payrollRun_id   integer     

The ID of the payrollRun. Example: 16

List company assets with their current holder.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/assets" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/assets"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/assets

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Published announcements addressed to the caller.

requires authentication

The company-wide ones, and those targeted at the caller's department, branch or employee file — the same people the announcement's notification is sent to. HR sees every announcement.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/announcements" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/announcements"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/announcements

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Publish an announcement (HR).

requires authentication

The platform's form, field for field: who it is for, how important it is, which channel carries it and an optional file. Same writer as the platform and the suite API, so the announcement is written and its audience — and only its audience — is notified. Refused for anyone the announcement policy does not let create one.

Send as multipart/form-data when there is an attachment.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/announcements" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"title\": \"تعليق الدوام غداً\",
    \"body\": \"يُعلَّق الدوام يوم الأحد بمناسبة اليوم الوطني.\",
    \"audience_type\": \"all\",
    \"audience_id\": 3,
    \"importance\": \"important\",
    \"channel\": \"both\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/announcements"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "title": "تعليق الدوام غداً",
    "body": "يُعلَّق الدوام يوم الأحد بمناسبة اليوم الوطني.",
    "audience_type": "all",
    "audience_id": 3,
    "importance": "important",
    "channel": "both"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": "12",
        "type": "announcements",
        "attributes": {
            "title": "تعليق الدوام غداً",
            "body": "يُعلَّق الدوام يوم الأحد.",
            "importance": "important",
            "published_at": "2026-10-11T09:00:00+00:00",
            "attachment": null
        }
    }
}
 

Request      

POST api/v1/announcements

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

title   string     

Example: تعليق الدوام غداً

body   string     

Example: يُعلَّق الدوام يوم الأحد بمناسبة اليوم الوطني.

audience_type   string  optional    

all, department, branch, employee or trainee. Defaults to all. Example: all

audience_id   integer  optional    

Required unless audience_type is all; an id from GET /announcements/audiences. Example: 3

importance   string  optional    

normal or important. Defaults to normal. Example: important

channel   string  optional    

both (in-app and email), database (in-app only) or mail (email only). Defaults to both. Example: both

attachment   file  optional    

Any file, up to 20 MB.

Who an announcement can be addressed to.

requires authentication

The lists the platform's form offers under «التحديد»: the company's departments, branches, employees and active trainees, each with both names (the request locale is the device's, not the company's). Only for those who may publish — the employee list is the whole company.

trainees lists those still training; has_account false means the trainee has no login, so an announcement addressed to them reaches nobody.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/announcements/audiences" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/announcements/audiences"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "departments": [
            {
                "id": 3,
                "name_ar": "الموارد البشرية",
                "name_en": "Human Resources"
            }
        ],
        "branches": [
            {
                "id": 1,
                "name_ar": "الفرع الرئيسي",
                "name_en": "Main Branch"
            }
        ],
        "employees": [
            {
                "id": 7,
                "name_ar": "سارة العتيبي",
                "name_en": "Sara Alotaibi",
                "employee_number": "1007"
            }
        ],
        "trainees": [
            {
                "id": 4,
                "name_ar": "ريم القحطاني",
                "name_en": "Reem Alqahtani",
                "trainee_number": "TR-0004",
                "has_account": true
            }
        ]
    }
}
 

Request      

GET api/v1/announcements/audiences

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

One published announcement.

requires authentication

What a push notification opens: the announcement push carries data.announcement_id, and the app fetches the announcement itself rather than searching the first page of the list for it. Another company's announcement, one not yet published, or one addressed to somebody else is a 404.

attachment describes the announcement's file, or is null when there is none; the file itself is GET /announcements/{id}/attachment.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/announcements/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/announcements/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": "12",
        "type": "announcements",
        "attributes": {
            "title": "تعليق الدوام غداً",
            "body": "يُعلَّق الدوام يوم الأحد.",
            "importance": "important",
            "published_at": "2026-10-11T09:00:00+00:00",
            "attachment": {
                "id": 41,
                "name": "تعميم.jpg",
                "mime_type": "image/jpeg",
                "size": 182044,
                "is_image": true
            }
        }
    }
}
 

Request      

GET api/v1/announcements/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the announcement. Example: 16

announcement   integer     

Example: 12

An announcement's attachment.

requires authentication

The file the announcement was published with — an image the app shows under the text, or any other file it offers to open. Served to a token, to exactly the people who may open the announcement: another company's, one not yet published, one addressed to somebody else, or one with no file is a 404. Key any cache on attachment.id from the announcement, which changes when the file is replaced.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/announcements/16/attachment" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/announcements/16/attachment"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the file, with its own content type):


{}
 

Example response (404, no file, or not yours to read):


{
    "message": "Not Found"
}
 

Request      

GET api/v1/announcements/{announcement_id}/attachment

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

announcement_id   integer     

The ID of the announcement. Example: 16

announcement   integer     

Example: 12

Add a public holiday (phone app).

requires authentication

Same writer as the platform's «العطلات الرسمية» tab and the suite API, so the leaves overlapping it are recounted and its days are not charged as absences. Under PublicHolidayPolicy::create: an HR manager, or a role the company granted leaves: manage or payroll: manage. Covers every branch unless branch_ids narrows it.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/public-holidays" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"اليوم الوطني\",
    \"name_en\": \"National Day\",
    \"start_date\": \"2026-09-23\",
    \"end_date\": \"2026-09-24\",
    \"branch_ids\": [
        4
    ]
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/public-holidays"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "اليوم الوطني",
    "name_en": "National Day",
    "start_date": "2026-09-23",
    "end_date": "2026-09-24",
    "branch_ids": [
        4
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": "7",
        "type": "public-holidays",
        "attributes": {
            "name_ar": "اليوم الوطني",
            "name_en": "National Day",
            "start_date": "2026-09-23",
            "end_date": "2026-09-24",
            "days": 2
        }
    }
}
 

Request      

POST api/v1/public-holidays

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name_ar   string     

Example: اليوم الوطني

name_en   string  optional    

Example: National Day

start_date   string     

Y-m-d. Example: 2026-09-23

end_date   string     

Y-m-d, on or after start_date. Example: 2026-09-24

branch_ids   integer[]  optional    

Limit the holiday to these branches of the company.

The authenticated user's attendance records for a month (?month=YYYY-MM).

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/attendance" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/attendance"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me/attendance

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Where the caller stands against their branch's zone, before punching.

requires authentication

The phone app asks this to render the same in/out state the punch will act on: the answer comes from AttendanceService::geofenceStatus — the one computation that also allows or refuses the punch — so the screen can never promise what the write rejects.

enforced: false means no zone applies (no branch, no circle, or the company switched enforcement off) and the punch needs no location at all. located: false with enforced: true means a position is required and none was supplied.

imprecise: true means a position arrived but its accuracy was too coarse to answer (a phone with precise location switched off); the punch would be refused for that reason, and inside is false.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/attendance/geofence?latitude=24.71375&longitude=46.6753&accuracy=12.5" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"latitude\": -89,
    \"longitude\": -179,
    \"accuracy\": 84
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/attendance/geofence"
);

const params = {
    "latitude": "24.71375",
    "longitude": "46.6753",
    "accuracy": "12.5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "latitude": -89,
    "longitude": -179,
    "accuracy": 84
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "enforced": true,
    "located": true,
    "imprecise": false,
    "inside": true,
    "radius": 150,
    "distance": 27,
    "branch": {
        "id": 1,
        "name_ar": "المقر الرئيسي",
        "name_en": "HQ",
        "latitude": "24.7136000",
        "longitude": "46.6753000"
    }
}
 

Request      

GET api/v1/me/attendance/geofence

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

latitude   number  optional    

Example: 24.71375

longitude   number  optional    

Example: 46.6753

accuracy   number  optional    

The fix's uncertainty in metres. Example: 12.5

Body Parameters

latitude   number  optional    

يجب أن يكون value بين -90 و 90. Example: -89

longitude   number  optional    

يجب أن يكون value بين -180 و 180. Example: -179

accuracy   number  optional    

يجب ألا يقل value عن 0. Example: 84

The caller's own punches on one day, ready to draw.

requires authentication

Same report the HR board reads, so the employee's map and their manager's cannot disagree about which punch was inside the zone. inside is null where there is no verdict — no position recorded, or no zone on the branch — and a point with a null latitude is listed but never plotted: an absent position is not evidence of anything.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/attendance/punch-locations?date=2026-09-14" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"date\": \"2026-10-25\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/attendance/punch-locations"
);

const params = {
    "date": "2026-09-14",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "date": "2026-10-25"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me/attendance/punch-locations

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

date   string  optional    

Defaults to today (Asia/Riyadh). Example: 2026-09-14

Body Parameters

date   string  optional    

Must be a valid date in the format Y-m-d. Example: 2026-10-25

Mobile check-in with geofence validation against the branch.

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/attendance/check-in" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"latitude\": -89,
    \"longitude\": -179,
    \"accuracy\": 84
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/attendance/check-in"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "latitude": -89,
    "longitude": -179,
    "accuracy": 84
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/me/attendance/check-in

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

latitude   number  optional    

يجب أن يكون value بين -90 و 90. Example: -89

longitude   number  optional    

يجب أن يكون value بين -180 و 180. Example: -179

accuracy   number  optional    

يجب ألا يقل value عن 0. Example: 84

Mobile check-out.

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/attendance/check-out" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"latitude\": -89,
    \"longitude\": -179,
    \"accuracy\": 84
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/attendance/check-out"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "latitude": -89,
    "longitude": -179,
    "accuracy": 84
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/me/attendance/check-out

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

latitude   number  optional    

يجب أن يكون value بين -90 و 90. Example: -89

longitude   number  optional    

يجب أن يكون value بين -180 و 180. Example: -179

accuracy   number  optional    

يجب ألا يقل value عن 0. Example: 84

The authenticated employee's leave requests.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/leaves" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/leaves"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me/leaves

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Submit a leave request (enters the approval chain).

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/leaves" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "leave_type_id=architecto"\
    --form "start_date=2052-11-17"\
    --form "end_date=2052-11-17"\
    --form "reason=n"\
    --form "attachments[]=@/tmp/phpq53h714u3h198vArBbA" 
const url = new URL(
    "https://hr.getokta.io/api/v1/me/leaves"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('leave_type_id', 'architecto');
body.append('start_date', '2052-11-17');
body.append('end_date', '2052-11-17');
body.append('reason', 'n');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Request      

POST api/v1/me/leaves

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

Body Parameters

leave_type_id   string     

Example: architecto

start_date   string     

value ليس تاريخًا صحيحًا. يجب أن يكون value تاريخًا بعد أو يساوي today. Example: 2052-11-17

end_date   string     

value ليس تاريخًا صحيحًا. يجب أن يكون value تاريخًا بعد أو يساوي start_date. Example: 2052-11-17

reason   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: n

attachments   file[]  optional    

Up to 5 supporting documents, 10MB each, as multipart/form-data. Required when the type says requires_attachment.

Current-year leave balances.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/leave-balances" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/leave-balances"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me/leave-balances

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

The authenticated employee's payslips from closed runs.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/payslips" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/payslips"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me/payslips

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

The user's in-app notifications.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/notifications" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/notifications"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me/notifications

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Mark one (or all) notifications as read.

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/notifications/read" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"id\": \"architecto\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/notifications/read"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "id": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/me/notifications/read

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

id   string  optional    

Example: architecto

Register (or refresh) an FCM device token for the authenticated user.

requires authentication

A token is globally unique: if the same device was previously bound to another user it is reassigned here, never duplicated. Registration is idempotent — the app calls it on every launch and token rotation.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/devices" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"token\": \"b\",
    \"platform\": \"android\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/devices"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "token": "b",
    "platform": "android"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/me/devices

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

token   string     

يجب ألا يزيد طول value عن 512 حرفًا. Example: b

platform   string  optional    

Example: android

Must be one of:
  • android
  • ios
  • web
  • unknown

Revoke a device token (logout / uninstall). Only the owner may revoke.

requires authentication

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/v1/me/devices" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"token\": \"b\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/devices"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "token": "b"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

DELETE api/v1/me/devices

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

token   string     

يجب ألا يزيد طول value عن 512 حرفًا. Example: b

Every subscription this company has.

requires authentication

The secret is never returned — not here, not anywhere. It is stored encrypted and WebhookSubscription marks it hidden; a listing that handed it back would put a live signing key in whatever log the caller keeps of its own HTTP traffic.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/webhook-subscriptions" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/webhook-subscriptions"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 4,
            "name": "Okta module",
            "url": "https://okta.example/api/okta-hr/webhooks/hr",
            "events": [
                "employee.created",
                "employee.updated"
            ],
            "is_active": true,
            "created_at": "2026-09-22T09:00:00+00:00"
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/webhook-subscriptions

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Register a receiver — idempotent on the URL.

requires authentication

The caller presses setup twice, re-runs it after an install, or lands here from a scheduled reconcile. Creating a row each time would not fail: it would DOUBLE every delivery, and a mirroring consumer would apply each change twice with nothing anywhere reporting a fault. So the URL is the key, and a second call updates the events and re-arms the subscription instead of adding one.

The SECRET is supplied by the caller, and this is the deliberate part. Generating it here means handing it back exactly once — the column is encrypted and cannot be read again — so a consumer that loses it (a re-install, a restore, a second app server) can never verify another signature and has no way to ask. A caller that holds the service key already holds more than this secret protects, so letting it choose costs nothing and removes the one-shot failure mode. It is still generated here when the caller does not supply one.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/webhook-subscriptions" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"url\": \"https:\\/\\/okta.example\\/api\\/okta-hr\\/webhooks\\/hr\",
    \"events\": [
        \"employee.created\",
        \"employee.updated\"
    ],
    \"name\": \"Okta module\",
    \"secret\": \"architecto\",
    \"is_active\": false
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/webhook-subscriptions"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "url": "https:\/\/okta.example\/api\/okta-hr\/webhooks\/hr",
    "events": [
        "employee.created",
        "employee.updated"
    ],
    "name": "Okta module",
    "secret": "architecto",
    "is_active": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4,
        "name": "Okta module",
        "url": "https://okta.example/api/okta-hr/webhooks/hr",
        "events": [
            "employee.created",
            "employee.updated"
        ],
        "is_active": true,
        "created_at": "2026-09-22T09:00:00+00:00",
        "created": true,
        "secret": "generated-only-when-you-did-not-supply-one"
    }
}
 

Request      

POST api/internal/companies/{company_id}/webhook-subscriptions

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

url   string     

HTTPS endpoint that will receive deliveries. Example: https://okta.example/api/okta-hr/webhooks/hr

events   string[]     

Event names, or ["*"] for all.

name   string  optional    

A label for the tenant's own webhooks screen. Example: Okta module

secret   string  optional    

The HMAC signing secret. 32-128 chars. Generated when omitted, and then returned once. Example: architecto

is_active   boolean  optional    

Defaults to true. Example: false

Remove a receiver.

requires authentication

Its delivery log goes with it — the rows are that subscription's history and mean nothing without it.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/webhook-subscriptions/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/webhook-subscriptions/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "deleted": true
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/webhook-subscriptions/{subscription}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

subscription   string     

Example: architecto

Internal (suite)

List companies

requires authentication

Every company the service key can see — ids, names and status, nothing more.

A service key is suite-level, so unfiltered this returns every company in the suite. That is fine for an operator's own tooling and wrong for anything a tenant looks at: one school's screen must never be built out of a list of every other school. Pass external_ref to ask the only question a consuming product actually has — "do you already hold a company for MY tenant?" — and get back nought or one row instead of the whole suite. Answering that question by downloading everything and filtering client-side works, and quietly puts every other tenant's name inside the asking tenant's process.

has_users is what tells a caller its onboarding is unfinished. A company with staff and nobody who can log in looks provisioned from every other angle, and the caller has no way to notice — provisioning answers "already done" forever once the company exists.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies?external_ref=okta%3Atenant-42" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"external_ref\": \"b\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies"
);

const params = {
    "external_ref": "okta:tenant-42",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "external_ref": "b"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "type": "companies",
            "attributes": {
                "name_ar": "مدرسة النور",
                "name_en": "Al Noor School",
                "status": "active",
                "external_ref": "okta:tenant-42",
                "has_users": true
            }
        }
    ]
}
 

Request      

GET api/internal/companies

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

external_ref   string  optional    

Return only the company carrying this identifier — the one you sent when provisioning. Example: okta:tenant-42

Body Parameters

external_ref   string  optional    

يجب ألا يزيد طول value عن 255 حرفًا. Example: b

Provision a company

requires authentication

Creates a company, seeds its defaults (leave types, asset categories, onboarding checklist) and creates its first hr-manager — the whole set, in one transaction. This is step one of onboarding a tenant from another Okta Suite product.

This call is idempotent on external_ref. Send your own tenant identifier and you can call it on every install, every retry and every double-clicked button: the first call creates, the rest return the same company untouched with created: false. Without it, a retry silently creates a second company for the same school and half the staff end up in each.

The admin's password is returned once, and only when we generated it. There is no way to read it back.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"مدرسة النور\",
    \"name_en\": \"Al Noor School\",
    \"commercial_register\": \"1010101010\",
    \"external_ref\": \"okta:tenant-42\",
    \"admin_name\": \"خالد العتيبي\",
    \"admin_email\": \"khalid@alnoor.test\",
    \"admin_password\": \"s3cret-passphrase\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "مدرسة النور",
    "name_en": "Al Noor School",
    "commercial_register": "1010101010",
    "external_ref": "okta:tenant-42",
    "admin_name": "خالد العتيبي",
    "admin_email": "khalid@alnoor.test",
    "admin_password": "s3cret-passphrase"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, already provisioned):


{
    "data": {
        "id": 7,
        "type": "companies",
        "attributes": {
            "name_ar": "مدرسة النور",
            "name_en": "Al Noor School",
            "status": "active",
            "external_ref": "okta:tenant-42",
            "created": false,
            "admin": null
        }
    }
}
 

Example response (201):


{
    "data": {
        "id": 7,
        "type": "companies",
        "attributes": {
            "name_ar": "مدرسة النور",
            "name_en": "Al Noor School",
            "status": "active",
            "external_ref": "okta:tenant-42",
            "created": true,
            "admin": {
                "id": 31,
                "name": "خالد العتيبي",
                "email": "khalid@alnoor.test",
                "password": "Xk29fBq1zLmA"
            }
        }
    }
}
 

Example response (422, admin email already belongs to someone):


{
    "message": "The given data was invalid.",
    "errors": {
        "admin_email": [
            "حقل admin email مُستخدم من قبل."
        ]
    }
}
 

Request      

POST api/internal/companies

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name_ar   string     

The company's Arabic name. Example: مدرسة النور

name_en   string  optional    

The company's English name. Example: Al Noor School

commercial_register   string  optional    

Commercial register number. Example: 1010101010

external_ref   string  optional    

Your own identifier for this tenant. Makes the call idempotent — send it. Example: okta:tenant-42

admin_name   string  optional    

The first hr-manager's name. Example: خالد العتيبي

admin_email   string  optional    

The first hr-manager's email. Omit to create a company with no users yet. Example: khalid@alnoor.test

admin_password   string  optional    

Their password. Omit and one is generated and returned once. Example: s3cret-passphrase

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/link-codes/redeem" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"code\": \"b\",
    \"used_by\": \"n\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/link-codes/redeem"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "code": "b",
    "used_by": "n"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Everything needed to DRAW a map, in one call.

requires authentication

A consuming product should not hardcode a tile URL somebody copied from this repository on a different afternoon: the day this platform moves to a keyed provider, every consumer that asked would follow and every consumer that guessed would show grey squares. The palette comes with it so a zone drawn in another product reads as the same product.

Suite-level: a map is not a tenant's property.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/map-config" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/map-config"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "tiles": {
        "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
        "attribution": "© OpenStreetMap",
        "max_zoom": 19
    },
    "fallback_center": {
        "latitude": 24.7136,
        "longitude": 46.6753,
        "zoom": 11
    },
    "colors": {
        "zone": "#7d55c7",
        "punch_inside": "#0e9f6e",
        "punch_outside": "#dc2626"
    },
    "radius": {
        "min": 50,
        "max": 20000
    },
    "shape": "circle"
}
 

Request      

GET api/internal/map-config

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

The nationality catalogue

requires authentication

The list the platform's own employee form offers — the value and both labels. The VALUE is the Arabic demonym («سعودي») because that is what the employee record stores and what payroll's Saudi detection reads: a consumer that stored an English label would file a Saudi as a foreigner for GOSI. Suite-level and the same for every company, so a consumer may cache it for a day.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/nationalities" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/nationalities"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "value": "سعودي",
            "label_ar": "سعودي",
            "label_en": "Saudi"
        },
        {
            "value": "مصري",
            "label_ar": "مصري",
            "label_en": "Egyptian"
        }
    ]
}
 

Request      

GET api/internal/nationalities

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

List the company's employees.

requires authentication

Filterable so a consuming product does not have to page the whole roster to find the active staff of one department.

Every row carries its manager block (id + bilingual names), so a consumer can resolve a reporting line — "who is on this manager's team" — without one extra call per person. manager_id narrows to one manager's direct reports; a consumer should still verify each returned row's manager.id, so that running against a platform predating this filter degrades to an empty team rather than to the whole roster.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees?manager_id=12" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"architecto\",
    \"department_id\": 16,
    \"branch_id\": 16,
    \"manager_id\": 16,
    \"search\": \"n\",
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees"
);

const params = {
    "manager_id": "12",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "status": "architecto",
    "department_id": 16,
    "branch_id": 16,
    "manager_id": 16,
    "search": "n",
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Invalid service key."
}
 

Request      

GET api/internal/companies/{company_id}/employees

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

manager_id   integer  optional    

Only this manager's direct reports. Example: 12

Body Parameters

status   string  optional    

Example: architecto

department_id   integer  optional    

Example: 16

branch_id   integer  optional    

Example: 16

manager_id   integer  optional    

Example: 16

search   string  optional    

يجب ألا يزيد طول value عن 120 حرفًا. Example: n

per_page   integer  optional    

Example: 16

One employee.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Invalid service key."
}
 

Request      

GET api/internal/companies/{company_id}/employees/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   integer     

The ID of the employee. Example: 16

The profile photo

requires authentication

The image itself, for a consuming product drawing the employee's file. has_photo on the employee says whether there is one; this 404s when there is not, rather than answering 200 with an empty body — which a browser renders as a broken image and a person reads as a bug.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/photo" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/photo"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Invalid service key."
}
 

Example response (404, no photo):


{
    "message": "No photo on file."
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/photo

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

List the company's departments.

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/departments" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/departments"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Invalid service key."
}
 

Request      

GET api/internal/companies/{company_id}/departments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

List the company's branches.

requires authentication

employees_count is the number of EMPLOYEES filed on the branch — the figure the platform's own «الفروع» table shows, so a consumer mirroring that table says the same number beside the same name.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/branches" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "type": "branches",
            "id": "1",
            "attributes": {
                "name_ar": "المقر الرئيسي",
                "name_en": "HQ",
                "city": "الرياض",
                "address": "طريق الملك فهد",
                "phone": "0112345678",
                "latitude": "24.7136000",
                "longitude": "46.6753000",
                "geofence_radius": 150,
                "effective_geofence_radius": 150,
                "geofence_active": true,
                "tracking_method": "geofence",
                "geofence_enforced": true,
                "zone_bounds": {
                    "south": 24.7119,
                    "west": 46.6734,
                    "north": 24.7153,
                    "east": 46.6772
                },
                "employees_count": 4
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/branches

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

List every branch with its zone.

requires authentication

geofence_radius is what was typed; effective_geofence_radius is what the server enforces — an older value below the floor is widened, never narrowed, so a consumer drawing the stored figure would draw a circle the platform does not use. geofence_enforced is the single answer to "does this branch measure where people punch from".

meta.unassigned counts the people (employees + trainees) attached to no branch at all: no zone can apply to them, and nothing fails when they punch from the other side of the country.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/geofences" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/geofences"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "type": "branches",
            "id": "1",
            "attributes": {
                "name_ar": "المقر الرئيسي",
                "name_en": "HQ",
                "city": "الرياض",
                "address": "طريق الملك فهد",
                "phone": "0112345678",
                "latitude": "24.7136000",
                "longitude": "46.6753000",
                "geofence_radius": 150,
                "effective_geofence_radius": 150,
                "geofence_active": true,
                "tracking_method": "geofence",
                "geofence_enforced": true,
                "zone_bounds": {
                    "south": 24.7119,
                    "west": 46.6734,
                    "north": 24.7153,
                    "east": 46.6772
                }
            }
        }
    ],
    "meta": {
        "unassigned": 0,
        "min_radius": 50,
        "max_radius": 20000
    }
}
 

Request      

GET api/internal/companies/{company_id}/geofences

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Create a branch, optionally with its zone in the same call.

requires authentication

A zone cannot exist without a branch, so a product setting up a site that has none would otherwise be stuck: the platform's own settings screen shows nothing at all until a branch exists.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/branches" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"فرع جدة\",
    \"name_en\": \"Jeddah\",
    \"city\": \"جدة\",
    \"address\": \"طريق الأمير سلطان\",
    \"phone\": \"0126543210\",
    \"latitude\": 21.4858,
    \"longitude\": 39.1925,
    \"radius\": 120,
    \"geofence_active\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "فرع جدة",
    "name_en": "Jeddah",
    "city": "جدة",
    "address": "طريق الأمير سلطان",
    "phone": "0126543210",
    "latitude": 21.4858,
    "longitude": 39.1925,
    "radius": 120,
    "geofence_active": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/internal/companies/{company_id}/branches

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

name_ar   string     

Example: فرع جدة

name_en   string  optional    

Example: Jeddah

city   string  optional    

Example: جدة

address   string  optional    

Example: طريق الأمير سلطان

phone   string  optional    

Example: 0126543210

latitude   number  optional    

Sent together with longitude and radius to draw the zone now. Example: 21.4858

longitude   number  optional    

Example: 39.1925

radius   integer  optional    

Metres; at least 50. Example: 120

geofence_active   boolean  optional    

Defaults to true. Example: true

Place or move a branch's zone.

requires authentication

The radius floor (50 m) is enforced here as well as on the screen: a phone's own position is rarely better than a few dozen metres, so a tighter circle refuses people standing inside the office — it measures GPS error, not attendance.

Example request:
curl --request PUT \
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/geofence" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"latitude\": 24.7136,
    \"longitude\": 46.6753,
    \"radius\": 150,
    \"geofence_active\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/geofence"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "latitude": 24.7136,
    "longitude": 46.6753,
    "radius": 150,
    "geofence_active": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (422, radius below the floor):


{
    "message": "...",
    "errors": {
        "radius": [
            "أقل نطاق مسموح به 50 متر — دقة موقع الجوال لا تسمح بأصغر من ذلك."
        ]
    }
}
 

Request      

PUT api/internal/companies/{company_id}/branches/{branch_id}/geofence

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

branch_id   string     

The ID of the branch. Example: architecto

Body Parameters

latitude   number     

Example: 24.7136

longitude   number     

Example: 46.6753

radius   integer     

Metres; at least 50. Example: 150

geofence_active   boolean  optional    

Leave it out to keep the branch's current setting. Example: true

Remove a branch's zone.

requires authentication

Not "radius 0": zero is a circle nobody can ever stand inside, while an absent zone is the different and honest claim that this branch does not measure where people punch from.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/geofence" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/geofence"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Request      

DELETE api/internal/companies/{company_id}/branches/{branch_id}/geofence

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

branch_id   string     

The ID of the branch. Example: architecto

Delete a branch.

requires authentication

Refused while anybody is still attached to it, and the refusal names how many. Every branch_id in the schema is nullOnDelete, so the delete would otherwise succeed and quietly leave those people with no branch, measured against whatever zones remain rather than their own.

Attendance already recorded is kept. Those rows lose the branch they name, not the punches or the coordinates they carry.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204):

Empty response
 

Example response (422):


{
    "message": "لا يمكن حذف هذا الفرع: ما زال 12 شخصاً عليه. انقلهم إلى فرع آخر أولاً — حذف مقرّ عملهم لا يقرّر أين يعملون.",
    "errors": {
        "branch": [
            "لا يمكن حذف هذا الفرع: ما زال 12 شخصاً عليه. انقلهم إلى فرع آخر أولاً — حذف مقرّ عملهم لا يقرّر أين يعملون."
        ]
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/branches/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   string     

The ID of the branch. Example: architecto

Edit a branch's name, city, address or phone.

requires authentication

Writes what arrives and only that: city: null is a real answer ("no city on file"), and reading it as "not sent" would make the field impossible to clear. The zone is not edited here — it has its own call, because the 50 m floor lives in the service that writes it.

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"فرع جدة\",
    \"name_en\": \"Jeddah\",
    \"city\": \"جدة\",
    \"address\": \"طريق الأمير سلطان\",
    \"phone\": \"0126543210\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "فرع جدة",
    "name_en": "Jeddah",
    "city": "جدة",
    "address": "طريق الأمير سلطان",
    "phone": "0126543210"
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "type": "branches",
        "id": "2",
        "attributes": {
            "name_ar": "فرع جدة",
            "name_en": "Jeddah",
            "city": "جدة",
            "address": "طريق الأمير سلطان",
            "phone": "0126543210",
            "latitude": null,
            "longitude": null,
            "geofence_radius": null,
            "effective_geofence_radius": null,
            "geofence_active": true,
            "tracking_method": "geofence",
            "geofence_enforced": false,
            "zone_bounds": null,
            "employees_count": 0
        }
    }
}
 

Request      

PATCH api/internal/companies/{company_id}/branches/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   string     

The ID of the branch. Example: architecto

Body Parameters

name_ar   string  optional    

Required when sent. Example: فرع جدة

name_en   string  optional    

Example: Jeddah

city   string  optional    

Example: جدة

address   string  optional    

Example: طريق الأمير سلطان

phone   string  optional    

Example: 0126543210

Read the punch-reminder settings.

requires authentication

A company that never saved any reminds nobody: both switches read false. audience says who a reminder can actually reach — someone with no login has no phone and no inbox, so a switch that is on can still reach nobody, and the screen should say so.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/punch-reminders" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/punch-reminders"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "check_in_enabled": true,
        "check_in_minutes_before": 10,
        "check_out_enabled": false,
        "check_out_minutes_after": 0,
        "max_minutes": 180,
        "window_minutes": 5,
        "audience": {
            "reachable": 42,
            "without_account": 3
        }
    }
}
 

Request      

GET api/internal/companies/{company_id}/punch-reminders

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Change the punch-reminder settings.

requires authentication

Writes only the fields sent; the rest keep their value. Minutes are 0–180: check_in_minutes_before before the shift starts, check_out_minutes_after after it ends (0 = at the end). A check-out reminder never goes out before the shift allows checking out.

Example request:
curl --request PUT \
    "https://hr.getokta.io/api/internal/companies/16/punch-reminders" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"check_in_enabled\": true,
    \"check_in_minutes_before\": 10,
    \"check_out_enabled\": true,
    \"check_out_minutes_after\": 0
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/punch-reminders"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "check_in_enabled": true,
    "check_in_minutes_before": 10,
    "check_out_enabled": true,
    "check_out_minutes_after": 0
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "check_in_enabled": true,
        "check_in_minutes_before": 10,
        "check_out_enabled": true,
        "check_out_minutes_after": 0,
        "max_minutes": 180,
        "window_minutes": 5,
        "audience": {
            "reachable": 42,
            "without_account": 3
        }
    }
}
 

Example response (422, minutes out of range):


{
    "message": "...",
    "errors": {
        "check_in_minutes_before": [
            "..."
        ]
    }
}
 

Request      

PUT api/internal/companies/{company_id}/punch-reminders

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

check_in_enabled   boolean  optional    

Example: true

check_in_minutes_before   integer  optional    

Minutes before the shift starts. Example: 10

check_out_enabled   boolean  optional    

Example: true

check_out_minutes_after   integer  optional    

Minutes after the shift ends. Example: 0

Put employees on this branch.

requires authentication

Somebody already on another branch is MOVED, not copied — a person has one branch. Ids of another company's employees are ignored rather than refused: under this company's scope they do not exist. moved counts the people whose branch actually changed, so a person already here is not counted twice.

Written through the same service as the platform's own screen, which also moves the leave days a branch-bound public holiday no longer (or now) covers.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/members" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_ids\": [
        12,
        13
    ]
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/members"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_ids": [
        12,
        13
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "moved": 2,
    "employees_count": 6
}
 

Request      

POST api/internal/companies/{company_id}/branches/{branch_id}/members

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

branch_id   string     

The ID of the branch. Example: architecto

Body Parameters

employee_ids   integer[]     

Our employee ids.

Take one employee off this branch.

requires authentication

One person at a time and never in bulk: somebody with no branch is measured against every zone the company enforces instead of their own site's, and a company whose only zone was this branch's leaves them with none. An employee who is not on this branch is left where they are.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/members/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/members/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204):

Empty response
 

Request      

DELETE api/internal/companies/{company_id}/branches/{branch_id}/members/{employee}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

branch_id   string     

The ID of the branch. Example: architecto

employee   string     

Example: architecto

The company's lateness policy and its configured levels.

requires authentication

A consuming product needs this before it can explain a warning at all: the period a tally is counted over, and what each rung takes. A level carrying both thresholds is crossed by EITHER, which is why both are sent rather than one combined figure.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/lateness-policy" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/lateness-policy"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "is_active": true,
        "period": "month",
        "min_late_minutes": 5,
        "levels": [
            {
                "id": 1,
                "level": 1,
                "name_ar": "تنبيه",
                "name_en": "Notice",
                "min_occurrences": 3,
                "min_minutes": null,
                "is_active": true
            },
            {
                "id": 2,
                "level": 2,
                "name_ar": "إنذار أول",
                "name_en": "First warning",
                "min_occurrences": 5,
                "min_minutes": 180,
                "is_active": true
            }
        ]
    }
}
 

Request      

GET api/internal/companies/{company_id}/lateness-policy

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

The warning reasons a manual warning may be issued for.

requires authentication

Sent rather than hardcoded on the consumer: a catalogue copied into another product drifts the day one is added here, and the drift shows up as a picker missing an option nobody can explain.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/warning-reasons" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/warning-reasons"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "value": "lateness",
            "label": "تأخير عن الدوام",
            "group": "attendance"
        },
        {
            "value": "negligence",
            "label": "إهمال العمل",
            "group": "conduct"
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/warning-reasons

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Warnings issued to one employee, newest first.

requires authentication

details is the day-by-day evidence AS IT STOOD when the warning was issued, not a live read of attendance: a punch corrected afterwards must not quietly restate what somebody was warned about.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/warnings?status=issued" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/warnings"
);

const params = {
    "status": "issued",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 9,
            "source": "auto",
            "reason_code": "lateness",
            "title": "إنذار أول",
            "reason": null,
            "notes": null,
            "level": 2,
            "level_name": "إنذار أول",
            "period_start": "2026-09-01",
            "period_end": "2026-09-30",
            "occurrences": 5,
            "total_minutes": 140,
            "status": "issued",
            "issued_at": "2026-09-17T03:30:00.000000Z",
            "issued_by": null,
            "responded_at": null,
            "response_note": null,
            "details": [
                {
                    "date": "2026-09-02",
                    "minutes": 25
                },
                {
                    "date": "2026-09-06",
                    "minutes": 30
                }
            ]
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/warnings

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Query Parameters

status   string  optional    

Filter by issued | acknowledged | disputed. Example: issued

Issue a warning by hand.

requires authentication

The code classifies it and reason says what happened — the reason is what the employee reads, the code is what a report counts. On other the reason is REQUIRED: a warning whose only statement is «other» tells the person nothing they can acknowledge or object to.

The issuer is checked, not assumed: a service key names a product, and the file has to record which person issued this.

Attachments are not accepted here — this endpoint is JSON, and the platform's own screen is where documents are added.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/warnings" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reason_code\": \"absence_without_excuse\",
    \"reason\": \"تغيّب يوم الأحد دون إشعار\",
    \"notes\": \"سبق تنبيهه شفهياً\",
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/warnings"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reason_code": "absence_without_excuse",
    "reason": "تغيّب يوم الأحد دون إشعار",
    "notes": "سبق تنبيهه شفهياً",
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 12,
        "source": "manual",
        "reason_code": "absence_without_excuse",
        "title": "غياب بدون عذر",
        "reason": "تغيّب يوم الأحد دون إشعار",
        "notes": null,
        "level": null,
        "level_name": null,
        "period_start": null,
        "period_end": null,
        "occurrences": null,
        "total_minutes": null,
        "status": "issued",
        "issued_at": "2026-09-17T09:00:00.000000Z",
        "issued_by": 3,
        "responded_at": null,
        "response_note": null,
        "details": []
    }
}
 

Request      

POST api/internal/companies/{company_id}/employees/{employee}/warnings

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Body Parameters

reason_code   string     

One of the values from warning-reasons. Example: absence_without_excuse

reason   string  optional    

What happened. Required when reason_code is other. Example: تغيّب يوم الأحد دون إشعار

notes   string  optional    

Optional notes, read by the employee too. Example: سبق تنبيهه شفهياً

acting_employee_id   integer  optional    

The person issuing it — required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Correct a manual warning.

requires authentication

An AUTOMATIC one is refused with a 422: its occurrences, minutes and day-by-day evidence are what the register said the day it was served, and retyping them would make the document claim something the register never did. Withdraw it instead.

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/internal/companies/16/warnings/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reason_code\": \"negligence\",
    \"reason\": \"تكرار عدم إنجاز المهام في وقتها\",
    \"notes\": \"بعد اجتماع المتابعة\",
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/warnings/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reason_code": "negligence",
    "reason": "تكرار عدم إنجاز المهام في وقتها",
    "notes": "بعد اجتماع المتابعة",
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 12,
        "source": "manual",
        "reason_code": "negligence",
        "title": "إهمال العمل",
        "reason": "تكرار عدم إنجاز المهام",
        "notes": null,
        "level": null,
        "level_name": null,
        "period_start": null,
        "period_end": null,
        "occurrences": null,
        "total_minutes": null,
        "status": "issued",
        "issued_at": "2026-09-17T09:00:00.000000Z",
        "issued_by": 3,
        "responded_at": null,
        "response_note": null,
        "details": []
    }
}
 

Request      

PATCH api/internal/companies/{company_id}/warnings/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   string     

The ID of the warning. Example: architecto

Body Parameters

reason_code   string     

Example: negligence

reason   string  optional    

Example: تكرار عدم إنجاز المهام في وقتها

notes   string  optional    

Example: بعد اجتماع المتابعة

acting_employee_id   integer  optional    

Required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Record the employee's own answer: an acknowledgement or an objection.

requires authentication

The acting person is checked, not assumed: an acknowledgement made by somebody else is not an acknowledgement, and an objection filed on their behalf puts words in their mouth. An objection needs its reason for the same reason a rejected request does.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/warnings/architecto/respond" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"answer\": \"disputed\",
    \"note\": \"كنت في مهمة رسمية\",
    \"acting_employee_id\": 12,
    \"acting_user_email\": \"sara@example.com\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/warnings/architecto/respond"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "answer": "disputed",
    "note": "كنت في مهمة رسمية",
    "acting_employee_id": 12,
    "acting_user_email": "sara@example.com",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 9,
        "source": "auto",
        "reason_code": "lateness",
        "title": "إنذار أول",
        "reason": null,
        "notes": null,
        "level": 2,
        "level_name": "إنذار أول",
        "period_start": "2026-09-01",
        "period_end": "2026-09-30",
        "occurrences": 5,
        "total_minutes": 140,
        "status": "disputed",
        "issued_at": "2026-09-17T03:30:00.000000Z",
        "issued_by": null,
        "responded_at": "2026-09-17T09:12:00.000000Z",
        "response_note": "كنت في مهمة رسمية",
        "details": [
            {
                "date": "2026-09-02",
                "minutes": 25
            }
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/warnings/{warning}/respond

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

warning   string     

The warning. Example: architecto

Body Parameters

answer   string     

acknowledged | disputed. Example: disputed

note   string  optional    

The employee's words. Required when objecting. Example: كنت في مهمة رسمية

acting_employee_id   integer  optional    

The employee answering — required unless acting_user_email is sent. Example: 12

acting_user_email   string  optional    

The account answering — required unless acting_employee_id is sent. Example: sara@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Withdraw a warning.

requires authentication

A real delete, not a cancelled status: the ladder re-reads what has been issued to decide what to issue next, so a withdrawn warning left on file would keep blocking its own rung for the rest of the period.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/warnings/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/warnings/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (204):

Empty response
 

Request      

DELETE api/internal/companies/{company_id}/warnings/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   string     

The ID of the warning. Example: architecto

Body Parameters

acting_employee_id   integer  optional    

The person withdrawing it — required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Attach everybody who has no branch to this one.

requires authentication

A person with no branch is measured against every zone the company enforces rather than their own site's; this narrows them to this one. Anybody who already has a branch is left alone — this fills a gap, it does not reorganise a company.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/assign-unassigned" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/branches/architecto/assign-unassigned"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "assigned": {
        "employees": 12,
        "trainees": 2
    },
    "unassigned": 0
}
 

Request      

POST api/internal/companies/{company_id}/branches/{branch}/assign-unassigned

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

branch   string     

The branch. Example: architecto

Where one employee stands against their branch's zone.

requires authentication

The consuming product asks this before offering a punch button, and the answer comes from AttendanceService::geofenceStatus — the same computation that allows or refuses the punch itself, so a screen can never promise what the write will reject.

Omit the coordinates to ask only whether a zone applies at all.

Somebody attached to NO branch is measured against the nearest zone their company enforces, so enforced can be true while branch is null — the zone answers even though the person has no branch of their own. imprecise: true means the fix's accuracy was too coarse to answer; the punch would be refused for that reason.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/geofence?latitude=24.71375&longitude=46.6753&accuracy=12.5" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"latitude\": -89,
    \"longitude\": -179,
    \"accuracy\": 84
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/geofence"
);

const params = {
    "latitude": "24.71375",
    "longitude": "46.6753",
    "accuracy": "12.5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "latitude": -89,
    "longitude": -179,
    "accuracy": 84
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "enforced": true,
    "located": true,
    "imprecise": false,
    "inside": true,
    "radius": 150,
    "distance": 27,
    "branch": {
        "id": 1,
        "name_ar": "المقر الرئيسي",
        "name_en": "HQ",
        "latitude": "24.7136000",
        "longitude": "46.6753000"
    }
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/geofence

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Query Parameters

latitude   number  optional    

Example: 24.71375

longitude   number  optional    

Example: 46.6753

accuracy   number  optional    

The fix's uncertainty in metres. Example: 12.5

Body Parameters

latitude   number  optional    

يجب أن يكون value بين -90 و 90. Example: -89

longitude   number  optional    

يجب أن يكون value بين -180 و 180. Example: -179

accuracy   number  optional    

يجب ألا يقل value عن 0. Example: 84

The punches of one person on one day, ready to draw.

requires authentication

The twin of the platform's own «مواقع البصمات» sheet, and it reads the same report — so the two cannot disagree about which punch was inside the zone. zone carries the circle plus a viewport that frames it; points carries one entry per punch, with inside null when there is no verdict to give (no position recorded, or the branch enforces no zone). A point with a null latitude must be listed and never plotted: an absent position is not evidence of anything.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/punch-locations?date=2026-09-14" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"date\": \"2026-10-25\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/punch-locations"
);

const params = {
    "date": "2026-09-14",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "date": "2026-10-25"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "date": "2026-09-14",
    "points": [
        {
            "kind": "in",
            "time": "08:02",
            "at": "2026-09-14T05:02:00+00:00",
            "source": "mobile",
            "source_label": "تطبيق الجوال",
            "latitude": 24.71375,
            "longitude": 46.6753,
            "distance": 17,
            "inside": true
        }
    ],
    "zone": {
        "latitude": 24.7136,
        "longitude": 46.6753,
        "radius": 150,
        "bounds": {
            "south": 24.7119,
            "west": 46.6734,
            "north": 24.7153,
            "east": 46.6772
        }
    },
    "branch": {
        "id": 1,
        "name_ar": "المقر الرئيسي",
        "name_en": "HQ"
    }
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/punch-locations

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Query Parameters

date   string  optional    

Defaults to today (Asia/Riyadh). Example: 2026-09-14

Body Parameters

date   string  optional    

Must be a valid date in the format Y-m-d. Example: 2026-10-25

Every employee number on file

requires authentication

One flat list of employee_number, unpaginated, and nothing else. It answers a single question — which of my people does HR already hold? — that a consuming product otherwise has to answer by downloading the whole directory and comparing it row by row.

That download is not a fair way to ask it. [employees] is paginated and capped at 200 rows a page, so a product diffing its roster against one page reports everyone past row 200 as missing — and a sync built on that answer files them all a second time. Here the whole set comes back in one call: a number is a few dozen bytes, a directory row is a few hundred with department, branch, manager and wage hanging off it.

Every status is included. Someone who resigned still has a file, and leaving them out would report them as never synced at all.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employee-numbers" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employee-numbers"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        "01K9RS4X0J7Q2ZC3VH8T5NB1AE",
        "E-1042"
    ],
    "meta": {
        "count": 2
    }
}
 

Request      

GET api/internal/companies/{company_id}/employee-numbers

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Create or update an employee

requires authentication

The third step of onboarding a tenant: pushing its staff across from the consuming product into HR.

Upserts on employee_number, which is the only stable key the two systems can agree on. That makes a re-sync safe — run it nightly, run it twice, and you update the same rows rather than growing a duplicate roster. Department and branch are matched by NAME and created if they are new, because the calling product has its own ids for them and cannot know ours.

Money is in halalas (1 SAR = 100), the same as everywhere else here.

The whole employee form is writable here — the same fields the platform's own «تعديل موظف» screen carries, in the same shape — so a consuming product can offer that form rather than a subset of it. shift_template_id and manager_id are OUR ids (read them from shift-templates and employees) and must belong to this company: anything else is a 422, never a silent write. The photo travels as multipart on the same call and is read back from GET employees/{employee}/photo; has_photo on the employee says whether there is one.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/employees" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "employee_number=E-1042"\
    --form "name_ar=سعيد الغامدي"\
    --form "name_en=Saeed Alghamdi"\
    --form "email=saeed@alnoor.test"\
    --form "mobile=0551234567"\
    --form "national_id=1012345678"\
    --form "nationality=سعودي"\
    --form "gender=male"\
    --form "birth_date=1990-05-14"\
    --form "marital_status=married"\
    --form "national_address=RRRD1234"\
    --form "city=الرياض"\
    --form "address=حي النرجس"\
    --form "job_title=معلم رياضيات"\
    --form "department=الشؤون التعليمية"\
    --form "branch=الفرع الرئيسي"\
    --form "shift_template_id=3"\
    --form "manager_id=12"\
    --form "hire_date=2024-09-01"\
    --form "contract_end_date=2027-08-31"\
    --form "probation_end_date=2024-11-30"\
    --form "contract_type=unlimited"\
    --form "status=active"\
    --form "basic_salary=800000"\
    --form "housing_allowance=200000"\
    --form "transport_allowance=50000"\
    --form "other_allowances=30000"\
    --form "iban=SA0380000000608010167519"\
    --form "bank_name=الأهلي"\
    --form "acting_employee_id=88"\
    --form "acting_user_email=hr@school.sa"\
    --form "acting_as_tenant_admin=1"\
    --form "photo=@/tmp/phpko26hmb0d3nt0IWtcx0" 
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('employee_number', 'E-1042');
body.append('name_ar', 'سعيد الغامدي');
body.append('name_en', 'Saeed Alghamdi');
body.append('email', 'saeed@alnoor.test');
body.append('mobile', '0551234567');
body.append('national_id', '1012345678');
body.append('nationality', 'سعودي');
body.append('gender', 'male');
body.append('birth_date', '1990-05-14');
body.append('marital_status', 'married');
body.append('national_address', 'RRRD1234');
body.append('city', 'الرياض');
body.append('address', 'حي النرجس');
body.append('job_title', 'معلم رياضيات');
body.append('department', 'الشؤون التعليمية');
body.append('branch', 'الفرع الرئيسي');
body.append('shift_template_id', '3');
body.append('manager_id', '12');
body.append('hire_date', '2024-09-01');
body.append('contract_end_date', '2027-08-31');
body.append('probation_end_date', '2024-11-30');
body.append('contract_type', 'unlimited');
body.append('status', 'active');
body.append('basic_salary', '800000');
body.append('housing_allowance', '200000');
body.append('transport_allowance', '50000');
body.append('other_allowances', '30000');
body.append('iban', 'SA0380000000608010167519');
body.append('bank_name', 'الأهلي');
body.append('acting_employee_id', '88');
body.append('acting_user_email', 'hr@school.sa');
body.append('acting_as_tenant_admin', '1');
body.append('photo', document.querySelector('input[name="photo"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (200, updated):


{
    "data": {
        "id": 88,
        "type": "employees",
        "attributes": {
            "employee_number": "E-1042",
            "name_ar": "سعيد الغامدي",
            "email": "saeed@alnoor.test",
            "status": "active"
        }
    }
}
 

Example response (201, created):


{
    "data": {
        "id": 88,
        "type": "employees",
        "attributes": {
            "employee_number": "E-1042",
            "name_ar": "سعيد الغامدي",
            "email": "saeed@alnoor.test",
            "status": "active"
        }
    }
}
 

Example response (422, plan limit reached):


{
    "message": "تم بلوغ الحد الأقصى لعدد الموظفين في باقتك."
}
 

Request      

POST api/internal/companies/{company_id}/employees

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_number   string     

Your stable identifier for this person. Example: E-1042

name_ar   string     

Arabic full name. Example: سعيد الغامدي

name_en   string  optional    

English full name. Example: Saeed Alghamdi

email   string  optional    

Work email. This is how a suite product later maps its signed-in user back to this employee — send it. Example: saeed@alnoor.test

mobile   string  optional    

Mobile number. Example: 0551234567

national_id   string  optional    

National / Iqama id. Example: 1012345678

nationality   string  optional    

One of the values GET /api/internal/nationalities lists — the Arabic demonym, which is what payroll's Saudi detection reads. Example: سعودي

gender   string  optional    

male or female. Example: male

birth_date   string  optional    

Y-m-d. Example: 1990-05-14

marital_status   string  optional    

One of single, married, divorced, widowed. Example: married

national_address   string  optional    

Example: RRRD1234

city   string  optional    

Example: الرياض

address   string  optional    

Example: حي النرجس

job_title   string  optional    

Example: معلم رياضيات

department   string  optional    

Department name — matched, then created if new. Example: الشؤون التعليمية

branch   string  optional    

Branch name — matched, then created if new. Example: الفرع الرئيسي

shift_template_id   integer  optional    

One of this company's shift templates. Example: 3

manager_id   integer  optional    

The direct manager: an employee of this company, and not this person. Example: 12

hire_date   string  optional    

Y-m-d. Example: 2024-09-01

contract_end_date   string  optional    

Y-m-d. Example: 2027-08-31

probation_end_date   string  optional    

Y-m-d. Example: 2024-11-30

contract_type   string  optional    

One of the contract type enum values. Example: unlimited

status   string  optional    

Employment status. Defaults to active on create. Example: active

basic_salary   integer  optional    

Basic salary in halalas. Example: 800000

housing_allowance   integer  optional    

Housing allowance in halalas. Example: 200000

transport_allowance   integer  optional    

Transport allowance in halalas. Example: 50000

other_allowances   integer  optional    

Other allowances in halalas. Example: 30000

iban   string  optional    

Example: SA0380000000608010167519

bank_name   string  optional    

Example: الأهلي

photo   file  optional    

The profile photo — JPG or PNG up to 2 MB, multipart only. Replaces the current one. Example: /tmp/phpko26hmb0d3nt0IWtcx0

acting_employee_id   integer  optional    

Who is making the change, so the activity log names a person instead of a product. Optional and best effort — a roster sync names nobody, and a name this company does not answer to is written unattributed rather than refused. Example: 88

acting_user_email   string  optional    

The same, for somebody with no employee file. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Provision a login account for an employee

requires authentication

Creates the user, assigns the employee role and links it to the employee record. The password is returned once — it is stored hashed and there is no way to read it back.

Deliberately separate from creating the employee: an HR record and a person who can log in are different decisions, and syncing a roster should not silently mint dozens of accounts.

role is what the account may DO here, and it matters most to a school that never opens this platform directly. Left out, the account is an employee — self-service and nothing more — which is right for the roster and wrong for the one or two people who operate HR from your product: an employee account is refused by the payroll policy, by the settings screens and by every approval it is not personally on, so issuing one for an administrator produces a login that can do nothing and a 403 where a 422 used to be. Send hr-manager for them. It is a deliberate grant by somebody who already administers the tenant, the same decision /settings/roles carries here, and super-admin — which operates the platform across every tenant — is not grantable at all.

An account that already exists under this email is LINKED rather than refused, and one that already carries roles keeps them: joining a file to a login is not a reason to re-rank anybody.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/account" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"role\": \"hr-manager\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/account"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "role": "hr-manager"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 31,
        "type": "users",
        "attributes": {
            "name": "سعيد الغامدي",
            "email": "saeed@alnoor.test",
            "password": "Xk29fBq1zLmA",
            "employee_id": 88
        }
    }
}
 

Example response (422, already has one):


{
    "message": "...",
    "errors": {
        "account": [
            "لدى الموظف حساب دخول بالفعل."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/employees/{employee}/account

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Body Parameters

role   string  optional    

One of employee, manager, hr-manager. Defaults to employee. Example: hr-manager

Delete an employee

requires authentication

This erases the person's history, not just their row. Everything that references an employee cascades — payroll items, attendance records, leave requests, approvals, documents, custody, violations — so a deleted employee takes all of it with them and none of it comes back. It is for a record entered by mistake. Somebody who actually left is marked resigned or terminated; that is what the status is for, and it keeps the file.

The login behind the file is revoked FIRST and unconditionally, before the record is attempted, so that a refusal on the second half can never leave a dismissed employee able to sign in. The outcome says which happened:

message is the sentence to show, already in the caller's language.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/employees/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 42,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 42,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "outcome": "account_revoked",
        "message": "تم إلغاء حساب دخول الموظف (لن يستطيع الدخول)، لكن تعذّر حذف سجله لارتباطه ببيانات (رواتب أو حضور...). يمكنك تغيير حالته."
    }
}
 

Example response (403, the named person may not delete employees):


{
    "message": "This action is unauthorized."
}
 

Request      

DELETE api/internal/companies/{company_id}/employees/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   integer     

The ID of the employee. Example: 16

Body Parameters

acting_employee_id   integer  optional    

The employee acting. Required unless acting_user_email or acting_as_tenant_admin is sent. Example: 42

acting_user_email   string  optional    

The email of the account acting, for somebody with no employee file or no login. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List an employee's documents

requires authentication

Contracts, iqamas, licences and the rest, with their expiry dates so a consuming product can show the same early warning HR sees.

The rows are metadata; has_file says whether a scan is attached, and the file itself is served by its own endpoint (.../documents/{id}/file) rather than inline. A list is read to see what exists and when it expires — attaching the images of everybody's identity papers to that answer would move megabytes nobody asked for.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/documents?expiring_within_days=60" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"expiring_within_days\": 1
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents"
);

const params = {
    "expiring_within_days": "60",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "expiring_within_days": 1
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 12,
            "type": "employee-documents",
            "attributes": {
                "employee_id": 88,
                "type": "iqama",
                "number": "2412345678",
                "issue_date": "2024-02-01",
                "expiry_date": "2027-01-31",
                "notify_before_days": 60,
                "days_until_expiry": 172,
                "is_expired": false,
                "has_file": true
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/documents

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Query Parameters

expiring_within_days   integer  optional    

Only documents expiring within this many days. Example: 60

Body Parameters

expiring_within_days   integer  optional    

يجب ألا يقل value عن 0. يجب ألا يكون value أكبر من 3650. Example: 1

Record a document

requires authentication

An iqama, a passport, a contract or a licence — its number, its dates and optionally the scan itself — so the platform's expiry warnings cover it.

Send the file as multipart under file to attach it; the ceiling is 10 MB, the same one the platform's own form uses. One rule for one thing: a scan the web form accepts and the API refuses is a difference nobody could explain to the person holding the passport.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "type=iqama"\
    --form "number=2412345678"\
    --form "issue_date=2024-02-01"\
    --form "expiry_date=2027-01-31"\
    --form "notify_before_days=60"\
    --form "file=@/tmp/phppa52v1r11083d2FEmLz" 
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('type', 'iqama');
body.append('number', '2412345678');
body.append('issue_date', '2024-02-01');
body.append('expiry_date', '2027-01-31');
body.append('notify_before_days', '60');
body.append('file', document.querySelector('input[name="file"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 12,
        "type": "employee-documents",
        "attributes": {
            "employee_id": 88,
            "type": "iqama",
            "number": "2412345678",
            "issue_date": "2024-02-01",
            "expiry_date": "2027-01-31",
            "notify_before_days": 60,
            "days_until_expiry": 172,
            "is_expired": false,
            "has_file": false
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/employees/{employee}/documents

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Body Parameters

type   string     

One of: iqama, passport, contract, certificate, driving_license, other. Example: iqama

number   string  optional    

The document's number. Example: 2412345678

issue_date   date  optional    

Example: 2024-02-01

expiry_date   date  optional    

Must not precede the issue date. Example: 2027-01-31

notify_before_days   integer  optional    

How many days ahead HR is warned. Default 30. Example: 60

file   file  optional    

The scan, up to 10 MB. Multipart only. Example: /tmp/phppa52v1r11083d2FEmLz

Update a document

requires authentication

A partial update: only the fields you send are written, so correcting an expiry date does not blank the number beside it. A file REPLACES whatever scan is attached — the collection is the document's single file, and two copies of an iqama with no way to tell which is current is worse than the one that was wrong.

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "type=iqama"\
    --form "number=2412345678"\
    --form "issue_date=2024-02-01"\
    --form "expiry_date=2027-01-31"\
    --form "notify_before_days=60"\
    --form "file=@/tmp/php8dujdfcr1it9eo2QbpT" 
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('type', 'iqama');
body.append('number', '2412345678');
body.append('issue_date', '2024-02-01');
body.append('expiry_date', '2027-01-31');
body.append('notify_before_days', '60');
body.append('file', document.querySelector('input[name="file"]').files[0]);

fetch(url, {
    method: "PATCH",
    headers,
    body,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 12,
        "type": "employee-documents",
        "attributes": {
            "employee_id": 88,
            "type": "iqama",
            "number": "2412345678",
            "issue_date": "2024-02-01",
            "expiry_date": "2027-01-31",
            "notify_before_days": 60,
            "days_until_expiry": 172,
            "is_expired": false,
            "has_file": false
        }
    }
}
 

Request      

PATCH api/internal/companies/{company_id}/employees/{employee}/documents/{document}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

document   string     

The document. Example: architecto

Body Parameters

type   string  optional    

One of: iqama, passport, contract, certificate, driving_license, other. Example: iqama

number   string  optional    

Example: 2412345678

issue_date   date  optional    

Example: 2024-02-01

expiry_date   date  optional    

Example: 2027-01-31

notify_before_days   integer  optional    

Example: 60

file   file  optional    

A new scan, replacing the current one. Multipart only. Example: /tmp/php8dujdfcr1it9eo2QbpT

Delete a document

requires authentication

Deletes the RECORD and whatever file is attached to it. A document added by mistake has to be removable by whoever added it, or the only way out of a typo is another product.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (200):


{
    "deleted": true
}
 

Request      

DELETE api/internal/companies/{company_id}/employees/{employee}/documents/{document}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

document   string     

The document. Example: architecto

Download a document's file

requires authentication

The scan itself, streamed — for a consuming product that already shows the employee's file to the same HR people who see it on the platform.

This is the one endpoint in the suite API that serves a FILE, and it is addressed through both the company and the employee, so an id from another employee 404s rather than resolving.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/documents/architecto/file" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/documents/architecto/file"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Invalid service key."
}
 

Example response (404, no file attached):


{
    "message": "No file attached to this document."
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/documents/{document}/file

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

document   string     

The document. Example: architecto

The employee's detailed report (PDF)

requires authentication

The same التقرير المفصل the platform's own file offers — identity, pay, a year of attendance, requests, leaves and letters — rendered by the same builder, so the two cannot describe one employee differently.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/report" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/report"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, a PDF):


"<binary>"
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/report

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

List an employee's letters

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/letters" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/letters"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 5,
            "type": "official-letters",
            "attributes": {
                "employee_id": 88,
                "reference": "A1B2-C3D4E5F6",
                "letter_type": "salary_certificate",
                "language": "ar",
                "addressed_to": null,
                "status": "approved",
                "is_releasable": true
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/letters

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Issue an official letter

requires authentication

A salary certificate, an employment certificate or a salary-transfer letter. The letter enters the company's configured approval chain exactly as one raised on the platform does — a letter issued from another product must not be a way around an approval the company set up — so a company with a chain gets a pending letter, and one without gets a releasable one.

acting_employee_id names the PERSON issuing it: a service key identifies a product, and a letter has to record who signed it off.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/letters" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"type\": \"salary_certificate\",
    \"language\": \"ar\",
    \"addressed_to\": \"البنك الأهلي\",
    \"acting_employee_id\": 88,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/letters"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "type": "salary_certificate",
    "language": "ar",
    "addressed_to": "البنك الأهلي",
    "acting_employee_id": 88,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 5,
        "type": "official-letters",
        "attributes": {
            "employee_id": 88,
            "reference": "A1B2-C3D4E5F6",
            "letter_type": "salary_certificate",
            "language": "ar",
            "addressed_to": "البنك الأهلي",
            "status": "pending",
            "is_releasable": false
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/employees/{employee}/letters

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Body Parameters

type   string     

salary_certificate, employment_certificate or salary_transfer. Example: salary_certificate

language   string     

ar or en. Example: ar

addressed_to   string  optional    

Who the letter is addressed to. Example: البنك الأهلي

acting_employee_id   integer  optional    

The employee issuing it — required unless acting_user_email is sent. Example: 88

acting_user_email   string  optional    

The signed-in account issuing it, for an issuer with no employee file or no login. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Download an issued letter (PDF)

requires authentication

403 until the approval chain has cleared it: an unapproved letter is a draft, and a draft that prints on letterhead is the thing the chain exists to prevent.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/letters/architecto/file" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/letters/architecto/file"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
 

{
    "message": "Invalid service key."
}
 

Example response (403, not approved yet):


{
    "message": "الخطاب لم يُعتمد بعد."
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/letters/{letter}/file

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

letter   string     

The letter. Example: architecto

List an employee's custody

requires authentication

The assets currently in their hands, and optionally everything ever handed to them. An open assignment is one that has not been returned — which is what a clearance blocks on when someone leaves.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/custody?include_returned=1" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"include_returned\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/custody"
);

const params = {
    "include_returned": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "include_returned": true
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 9,
            "type": "asset-assignments",
            "attributes": {
                "employee_id": 88,
                "asset_id": 4,
                "asset_name": "لابتوب Dell 5540",
                "asset_serial": "SN-99182",
                "assigned_at": "2025-03-02",
                "returned_at": null,
                "is_open": true
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/custody

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Query Parameters

include_returned   boolean  optional    

Include closed (returned) assignments. Example: true

Body Parameters

include_returned   boolean  optional    

Example: true

List an employee's violations

requires authentication

The disciplinary file (Saudi labour law art. 66–71): what the employee is said to have done and the ONE penalty imposed for it. A fine carries the payroll line it created, so what the payslip withholds and what the file claims cannot drift apart.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/violations" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/violations"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 3,
            "type": "employee-violations",
            "attributes": {
                "employee_id": 88,
                "date": "2026-08-02",
                "violation_type": "lateness",
                "penalty": "deduction",
                "description": "تأخر ساعتين",
                "deduction_amount": 15000,
                "salary_adjustment_id": 41,
                "recorded_on": "2026-08-02"
            }
        }
    ],
    "meta": {
        "monthly_cap": 50000,
        "remaining_this_month": 35000
    }
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/violations

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Record a violation

requires authentication

ONE penalty per violation (art. 71), and a deduction penalty must carry its amount in halalas — it writes a real payroll deduction for that month, with its own approval chain, so the file and the payslip say the same thing.

The art. 68 ceiling (five days' wage a month in fines) is enforced by the platform: an amount that would breach it is refused with a 422 naming what is left, not silently clipped.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/violations" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"date\": \"2026-08-02\",
    \"type\": \"lateness\",
    \"penalty\": \"deduction\",
    \"description\": \"تأخر ساعتين عن الدوام\",
    \"deduction_amount\": 15000,
    \"acting_employee_id\": 88,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/violations"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "date": "2026-08-02",
    "type": "lateness",
    "penalty": "deduction",
    "description": "تأخر ساعتين عن الدوام",
    "deduction_amount": 15000,
    "acting_employee_id": 88,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 3,
        "type": "employee-violations",
        "attributes": {
            "employee_id": 88,
            "date": "2026-08-02",
            "violation_type": "lateness",
            "penalty": "deduction",
            "description": "تأخر ساعتين",
            "deduction_amount": 15000,
            "salary_adjustment_id": 41,
            "recorded_on": "2026-08-02"
        }
    }
}
 

Example response (422, over the monthly cap):


{
    "message": "لا يمكن أن تتجاوز الغرامات أجر خمسة أيام في الشهر."
}
 

Request      

POST api/internal/companies/{company_id}/employees/{employee}/violations

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Body Parameters

date   date     

When it happened. Example: 2026-08-02

type   string     

lateness, absence, conduct, negligence, safety or other. Example: lateness

penalty   string     

verbal_warning, written_warning, deduction, suspension or dismissal. Example: deduction

description   string  optional    

What happened. Example: تأخر ساعتين عن الدوام

deduction_amount   integer  optional    

Halalas. Required for a deduction. Example: 15000

acting_employee_id   integer  optional    

The employee recording it. Example: 88

acting_user_email   string  optional    

The signed-in account recording it, when they have no employee file or no login. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Withdraw a violation

requires authentication

Deletes the record AND the payroll line it created: a penalty that was withdrawn must not keep taking money off the payslip.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/employees/16/violations/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/violations/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (200):


{
    "deleted": true
}
 

Request      

DELETE api/internal/companies/{company_id}/employees/{employee}/violations/{violation}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

violation   string     

The violation. Example: architecto

List the company's custody

requires authentication

Every handover in the company, newest first — the same rows the per-employee call serves, without having to know whose file to open. A consuming product building a custody screen needs "who holds what across the school", and asking that one employee at a time is one request per employee for a page that fits on one screen.

Each row carries its holder inline (id, number and BOTH names) so the list can be read without a second call, and the employee name follows the suite rule: never resolved server-side, because the caller is a server and the locale on the request is the browser's, not the tenant's.

Open assignments only unless include_returned is set — an open one is what a clearance blocks on when somebody leaves, and a list mixing the two cannot be read for that at a glance.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/custody?include_returned=1&employee_id=88&search=%D9%84%D8%A7%D8%A8%D8%AA%D9%88%D8%A8&per_page=50" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"include_returned\": false,
    \"employee_id\": 16,
    \"search\": \"n\",
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/custody"
);

const params = {
    "include_returned": "1",
    "employee_id": "88",
    "search": "لابتوب",
    "per_page": "50",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "include_returned": false,
    "employee_id": 16,
    "search": "n",
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 9,
            "type": "asset-assignments",
            "attributes": {
                "employee_id": 88,
                "employee_name_ar": "سعيد الغامدي",
                "employee_name_en": "Saeed Alghamdi",
                "employee_number": "E-1042",
                "asset_id": 4,
                "asset_name": "لابتوب Dell 5540",
                "asset_serial": "SN-99182",
                "assigned_at": "2025-03-02",
                "returned_at": null,
                "is_open": true
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/custody

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

include_returned   boolean  optional    

Include closed (returned) assignments. Example: true

employee_id   integer  optional    

Only this employee's custody. Example: 88

search   string  optional    

Matches the asset name or its serial number. Example: لابتوب

per_page   integer  optional    

Default 50, max 200. Example: 50

Body Parameters

include_returned   boolean  optional    

Example: false

employee_id   integer  optional    

Example: 16

search   string  optional    

يجب ألا يزيد طول value عن 120 حرفًا. Example: n

per_page   integer  optional    

Example: 16

List assets

requires authentication

The company's asset register — what exists, its state, and who is holding it right now. Filter by status=available to get exactly the assets a handover can name.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/assets?status=available&category_id=3&search=%D9%84%D8%A7%D8%A8%D8%AA%D9%88%D8%A8&per_page=50" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"available\",
    \"category_id\": 16,
    \"search\": \"n\",
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/assets"
);

const params = {
    "status": "available",
    "category_id": "3",
    "search": "لابتوب",
    "per_page": "50",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "status": "available",
    "category_id": 16,
    "search": "n",
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 17,
            "type": "assets",
            "attributes": {
                "name": "لابتوب Dell 5540",
                "serial_number": "SN-90412",
                "status": "assigned",
                "category": {
                    "id": 3,
                    "name_ar": "أجهزة حاسب",
                    "name_en": "Computers"
                },
                "deletable": false,
                "holder": {
                    "employee_id": 88,
                    "name_ar": "سعيد الغامدي",
                    "name_en": "Saeed Alghamdi",
                    "assignment_id": 54,
                    "assigned_at": "2026-03-01T07:00:00+00:00"
                }
            }
        }
    ],
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "total": 1
    }
}
 

Request      

GET api/internal/companies/{company_id}/assets

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

status   string  optional    

available, assigned or retired. Example: available

category_id   integer  optional    

Example: 3

search   string  optional    

Matches the name or the serial on the sticker. Example: لابتوب

per_page   integer  optional    

Default 50, max 200. Example: 50

Body Parameters

status   string  optional    

Example: available

Must be one of:
  • available
  • assigned
  • retired
category_id   integer  optional    

Example: 16

search   string  optional    

يجب ألا يزيد طول value عن 120 حرفًا. Example: n

per_page   integer  optional    

Example: 16

Add an asset

requires authentication

Registers a new item in the company's custody register. It starts available, which is what makes it nameable in a handover.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/assets" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"لابتوب Dell 5540\",
    \"category_id\": 3,
    \"serial_number\": \"SN-90412\",
    \"notes\": \"اشتُري ضمن دفعة ١٤٤٧\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/assets"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "لابتوب Dell 5540",
    "category_id": 3,
    "serial_number": "SN-90412",
    "notes": "اشتُري ضمن دفعة ١٤٤٧"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 17,
        "type": "assets",
        "attributes": {
            "name": "لابتوب Dell 5540",
            "serial_number": "SN-90412",
            "status": "available",
            "category": {
                "id": 3,
                "name_ar": "أجهزة حاسب",
                "name_en": "Computers"
            },
            "deletable": true,
            "holder": null
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/assets

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

name   string     

Example: لابتوب Dell 5540

category_id   integer     

A category of this company. Example: 3

serial_number   string  optional    

The serial on the sticker. Example: SN-90412

notes   string  optional    

Example: اشتُري ضمن دفعة ١٤٤٧

Delete an asset

requires authentication

Removes an asset that was never in anybody's custody. Refused (422, with the reason in the tenant's language) while the asset is handed to an employee, and for any asset with a custody record — even a returned one: those records are the signed handover and return, and they go with the asset. Such an asset stays on the register. deletable on each row of the list says in advance which of the two a DELETE will meet.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/assets/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 7,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/assets/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 7,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "deleted": true
}
 

Example response (422, linked to a custody record):


{
    "message": "لا يمكن حذف الأصل — مرتبط بسجل عهدة سابق، وسجلات العهد تُحفظ ولا تُحذف."
}
 

Request      

DELETE api/internal/companies/{company_id}/assets/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   string     

The ID of the asset. Example: architecto

asset   integer     

An asset of this company. Example: 17

Body Parameters

acting_employee_id   integer  optional    

Who is deleting it. Required unless acting_user_email is given. Example: 7

acting_user_email   string  optional    

The email of the account deleting it. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. Example: true

List asset categories

requires authentication

Every asset belongs to one. A consuming product adding an asset needs this list to name the category, and the ids are this company's own.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/asset-categories" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/asset-categories"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 3,
            "type": "asset-categories",
            "attributes": {
                "name_ar": "أجهزة حاسب",
                "name_en": "Computers"
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/asset-categories

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Hand an asset over

requires authentication

Records a محضر تسليم: the asset passes to the employee, its state at handover is written down, and the asset stops being available.

signature is the recipient's mark as a data-URL (data:image/png;base64,…), the same value the platform's signature pad writes and the same one the printed محضر renders. It is optional here for the case where the product genuinely has no way to capture one — but a record without it claims a handover nobody signed for, which is exactly what the signature exists to prevent, so capture it when you can.

With an asset_assignment approval workflow configured, this starts that chain and the asset stays available until the chain approves — status in the response says which of the two happened.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/custody" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"asset_id\": 17,
    \"employee_id\": 88,
    \"condition\": \"جديد بالكرتون\",
    \"notes\": \"مع الشاحن والحقيبة\",
    \"signature\": \"architecto\",
    \"acting_employee_id\": 7,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/custody"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "asset_id": 17,
    "employee_id": 88,
    "condition": "جديد بالكرتون",
    "notes": "مع الشاحن والحقيبة",
    "signature": "architecto",
    "acting_employee_id": 7,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 54,
        "type": "asset-assignments",
        "attributes": {
            "asset_id": 17,
            "asset_name": "لابتوب Dell 5540",
            "asset_serial": "SN-90412",
            "asset_status": "assigned",
            "employee_id": 88,
            "employee_name_ar": "سعيد الغامدي",
            "employee_name_en": "Saeed Alghamdi",
            "assigned_at": "2026-09-13T07:20:00+00:00",
            "returned_at": null,
            "condition_out": "جديد بالكرتون",
            "condition_in": null,
            "notes_out": "مع الشاحن والحقيبة",
            "notes_in": null,
            "awaiting_approval": false
        }
    }
}
 

Example response (422, asset already out):


{
    "message": "الأصل غير متاح للتسليم."
}
 

Request      

POST api/internal/companies/{company_id}/custody

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

asset_id   integer     

An available asset of this company. Example: 17

employee_id   integer     

Who is receiving it. Example: 88

condition   string     

Its state at handover. Example: جديد بالكرتون

notes   string  optional    

Example: مع الشاحن والحقيبة

signature   string  optional    

The recipient's signature as a data-URL. Example: architecto

acting_employee_id   integer  optional    

Who is handing it over. Required unless acting_user_email is given. Example: 7

acting_user_email   string  optional    

The email of the account handing it over. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Take an asset back

requires authentication

Closes the assignment (محضر استلام) and returns the asset to the available pool, recording its state on the way back in.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/custody/architecto/return" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"condition\": \"سليم مع خدش بسيط\",
    \"notes\": \"أُعيد الشاحن أيضاً\",
    \"signature\": \"architecto\",
    \"acting_employee_id\": 7,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/custody/architecto/return"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "condition": "سليم مع خدش بسيط",
    "notes": "أُعيد الشاحن أيضاً",
    "signature": "architecto",
    "acting_employee_id": 7,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 54,
        "type": "asset-assignments",
        "attributes": {
            "asset_id": 17,
            "asset_name": "لابتوب Dell 5540",
            "asset_serial": "SN-90412",
            "asset_status": "available",
            "employee_id": 88,
            "employee_name_ar": "سعيد الغامدي",
            "employee_name_en": "Saeed Alghamdi",
            "assigned_at": "2026-03-01T07:00:00+00:00",
            "returned_at": "2026-09-13T07:40:00+00:00",
            "condition_out": "جديد بالكرتون",
            "condition_in": "سليم مع خدش بسيط",
            "notes_out": null,
            "notes_in": null,
            "awaiting_approval": false
        }
    }
}
 

Example response (422, already returned):


{
    "message": "أُعيد هذا الأصل مسبقاً."
}
 

Request      

POST api/internal/companies/{company_id}/custody/{assignment}/return

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

assignment   string     

Example: architecto

Body Parameters

condition   string     

Its state on return. Example: سليم مع خدش بسيط

notes   string  optional    

Example: أُعيد الشاحن أيضاً

signature   string  optional    

The returning employee's signature as a data-URL. Example: architecto

acting_employee_id   integer  optional    

Who is receiving it back. Required unless acting_user_email is given. Example: 7

acting_user_email   string  optional    

The email of the account receiving it back. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List self-service requests

requires authentication

Every request type the platform models — resignation, attendance correction, overtime, expense claim, exit/re-entry, data change — as one list, because they share one approval chain.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/requests?employee_id=88&approver_employee_id=42&approver_email=hr%40school.sa&approver_as_tenant_admin=1&approver_may_decide=1&awaiting=1&acting_employee_id=42&acting_user_email=hr%40school.sa&acting_as_tenant_admin=1&acting_may_decide=1&status=pending&type=overtime&per_page=50" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 16,
    \"status\": \"n\",
    \"type\": \"g\",
    \"approver_employee_id\": 16,
    \"approver_email\": \"zbailey@example.net\",
    \"approver_as_tenant_admin\": true,
    \"approver_may_decide\": true,
    \"awaiting\": true,
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/requests"
);

const params = {
    "employee_id": "88",
    "approver_employee_id": "42",
    "approver_email": "hr@school.sa",
    "approver_as_tenant_admin": "1",
    "approver_may_decide": "1",
    "awaiting": "1",
    "acting_employee_id": "42",
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": "1",
    "acting_may_decide": "1",
    "status": "pending",
    "type": "overtime",
    "per_page": "50",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 16,
    "status": "n",
    "type": "g",
    "approver_employee_id": 16,
    "approver_email": "zbailey@example.net",
    "approver_as_tenant_admin": true,
    "approver_may_decide": true,
    "awaiting": true,
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 41,
            "type": "employee-requests",
            "attributes": {
                "employee_id": 88,
                "trainee_id": null,
                "employee_name_ar": "سعيد الغامدي",
                "employee_name_en": "Saeed Alghamdi",
                "request_type": "overtime",
                "request_type_label_ar": "عمل إضافي",
                "request_type_label_en": "Overtime",
                "summary_ar": "3 ساعات إضافية يوم 2026-08-02",
                "summary_en": "3 overtime hours on 2026-08-02",
                "status": "pending",
                "payload": {
                    "date": "2026-08-02",
                    "hours": 3
                },
                "submitted_at": "2026-08-02T09:12:00+00:00",
                "decided_at": null,
                "rejection_reason": null,
                "current_approver": {
                    "id": 7,
                    "name": "سارة القحطاني",
                    "employee_id": 42,
                    "email": "sara@school.sa"
                },
                "current_step": 1,
                "total_steps": 2
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

employee_id   integer  optional    

Only this employee's requests. Example: 88

approver_employee_id   integer  optional    

Requests this employee may decide — «اعتماداتي»: their own steps, decided or not, and (for an HR manager, who may decide in the assigned approver's place) everything still awaiting a decision. meta.approver_scope says which of the two you got. Example: 42

approver_email   string  optional    

Same, for an approver named by account email. Sent WITH approver_employee_id it widens rather than narrows: the two are one claim in two spellings, and an approver with no employee file is matched by the email alone. Example: hr@school.sa

approver_as_tenant_admin   boolean  optional    

The same for «اعتماداتي»: the administrator's inbox, resolved the way the act endpoints resolve them. Example: true

approver_may_decide   boolean  optional    

With an approver filter: the person holds the permission to decide employee requests in your product, so — like an HR manager — they get every request still awaiting a decision, not only their own steps. Their own requests are never in that part. Example: true

awaiting   boolean  optional    

With an approver filter, keep only what that person can decide NOW: their step must be the chain's current (lowest pending) one, or — for somebody who may decide on behalf — any pending step. For an inbox of approve/reject buttons; without it a decided step and a step whose turn has not come are listed too. Example: true

acting_employee_id   integer  optional    

The person LOOKING, so each row carries can_decide (and can_return — a return is the assigned approver's alone), computed by the same rule the act endpoint enforces. Draw the buttons from them and they are never refused. Example: 42

acting_user_email   string  optional    

The person looking, by account email. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

The person looking administers this tenant in your product. Example: true

acting_may_decide   boolean  optional    

The person looking holds the permission to decide employee requests in your product. Example: true

status   string  optional    

pending, approved, rejected, returned or cancelled. Example: pending

type   string  optional    

A request type key. Example: overtime

per_page   integer  optional    

Default 50, max 200. Example: 50

Body Parameters

employee_id   integer  optional    

Example: 16

status   string  optional    

يجب ألا يزيد طول value عن 30 حرفًا. Example: n

type   string  optional    

يجب ألا يزيد طول value عن 50 حرفًا. Example: g

approver_employee_id   integer  optional    

«اعتماداتي» — every request whose chain has a step assigned to this person, decided or not. current_approver answers only «who holds it now», so a consumer building the tab from it loses every request the moment it is decided — which is exactly when somebody goes looking for what they approved. Example: 16

approver_email   string  optional    

يجب ألا يزيد طول value عن 191 حرفًا. Example: zbailey@example.net

approver_as_tenant_admin   boolean  optional    

Example: true

approver_may_decide   boolean  optional    

Example: true

awaiting   boolean  optional    

Example: true

per_page   integer  optional    

Example: 16

Submit a self-service request

requires authentication

Goes through the same service the platform's own صفحة الطلبات uses, so it starts the company's configured approval chain and, on final approval, applies the same effects — a resignation really does change employment status, overtime really does become a salary adjustment.

The payload is type-specific and validated by that service; see the request types documented on GET /requests.

A punch correction (attendance_correction) may name which punch it is about in payload.correction_scope — check_in, check_out or both. Named, the times it covers are required and a time outside it is dropped; left out, the request carries whichever time(s) it has, as it always did, and the scope is recorded from them.

A correction is only for a punch that is MISSING: a time for a side the day already has on record is refused on that side's key, and a day with both punches on record is refused on payload.date.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/requests" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 88,
    \"type\": \"overtime\",
    \"payload\": {
        \"date\": \"2026-08-02\",
        \"hours\": 3,
        \"reason\": \"تغطية اختبارات\"
    }
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/requests"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 88,
    "type": "overtime",
    "payload": {
        "date": "2026-08-02",
        "hours": 3,
        "reason": "تغطية اختبارات"
    }
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 41,
        "type": "employee-requests",
        "attributes": {
            "employee_id": 88,
            "request_type": "overtime",
            "status": "pending"
        }
    }
}
 

Example response (422, no approval chain configured):


{
    "message": "Approver chain is empty"
}
 

Example response (422, a correction scope without its time):


{
    "message": "حقل وقت الانصراف مطلوب.",
    "errors": {
        "payload.check_out": [
            "حقل وقت الانصراف مطلوب."
        ]
    }
}
 

Example response (422, correcting a punch already on record):


{
    "message": "بصمة الحضور مسجّلة لهذا اليوم (07:58)، والتصحيح متاح للبصمة غير المسجّلة فقط.",
    "errors": {
        "payload.check_in": [
            "بصمة الحضور مسجّلة لهذا اليوم (07:58)، والتصحيح متاح للبصمة غير المسجّلة فقط."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Whose request this is. Example: 88

type   string     

The request type key. Example: overtime

payload   object  optional    

The type-specific fields.

Decide a request

requires authentication

Approve, reject or return one self-service request for correction, from whichever product the approver happens to be sitting in. A consuming product could already SHOW who a request is waiting on; without this it could only send that person to the HR platform to press the button, so every approver needed a second account and a second login there.

The decision is still a named person's act — that is what the chain exists to record, and it is enforced here rather than assumed. Name the actor with acting_employee_id, or with acting_user_email when they have no employee file (an HR manager, the platform operator).

Who may decide is exactly who may decide on the platform's own screen: the approver the step is assigned to, or — on their behalf — an HR manager or platform operator of that company, which is what keeps an approver on leave from stopping the company. A delegated decision is recorded in acted_by; approver_id is never rewritten, so the chain still says who it was routed to. Somebody your product lets decide employee requests may too — send acting_may_decide: true; the listings mark the same rows can_decide, so a button drawn from that flag is never refused. Nobody decides their own request in somebody else's place, and a return stays the assigned approver's alone (can_return). Anybody else is refused.

An employee file with no login is a complete name: an account is made for that file when they decide, so no email is needed.

A rejection and a return both require a reason, exactly as the platform's own screens do — a second surface is not a way around that.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/requests/architecto/act" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"decision\": \"approve\",
    \"notes\": \"التاريخ لا يطابق السجل\",
    \"acting_employee_id\": 7,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_decide\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/requests/architecto/act"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "decision": "approve",
    "notes": "التاريخ لا يطابق السجل",
    "acting_employee_id": 7,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true,
    "acting_may_decide": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 41,
        "type": "employee-requests",
        "attributes": {
            "employee_id": 88,
            "trainee_id": null,
            "employee_name_ar": "سعيد الغامدي",
            "employee_name_en": "Saeed Alghamdi",
            "request_type": "overtime",
            "request_type_label_ar": "عمل إضافي",
            "request_type_label_en": "Overtime",
            "summary_ar": "3 ساعات إضافية يوم 2026-08-02",
            "summary_en": "3 overtime hours on 2026-08-02",
            "status": "approved",
            "payload": {
                "date": "2026-08-02",
                "hours": 3
            },
            "submitted_at": "2026-08-02T09:12:00+00:00",
            "decided_at": "2026-08-03T06:40:00+00:00",
            "rejection_reason": null,
            "current_approver": null,
            "current_step": null,
            "total_steps": 2
        }
    }
}
 

Example response (422, not the current approver):


{
    "message": "ليس دورك في سلسلة الاعتماد.",
    "errors": {
        "decision": [
            "ليس دورك في سلسلة الاعتماد."
        ]
    }
}
 

Example response (422, their own request):


{
    "message": "لا يمكنك اتخاذ قرار في طلبك أنت.",
    "errors": {
        "decision": [
            "لا يمكنك اتخاذ قرار في طلبك أنت."
        ]
    }
}
 

Example response (422, a return by somebody deciding on the approver's behalf):


{
    "message": "الإعادة للتصحيح لصاحب الخطوة نفسه.",
    "errors": {
        "decision": [
            "الإعادة للتصحيح لصاحب الخطوة نفسه."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/requests/{employeeRequest}/act

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employeeRequest   string     

Example: architecto

Body Parameters

decision   string     

approve, reject or return. Example: approve

notes   string  optional    

The reason. Required for reject and return. Example: التاريخ لا يطابق السجل

acting_employee_id   integer  optional    

The employee deciding. Required unless acting_user_email is given. Example: 7

acting_user_email   string  optional    

The email of the account deciding. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_decide   boolean  optional    

The person holds the permission to decide employee requests in your product, so they may decide one assigned to somebody else. Example: true

Delete a request

requires authentication

Removes the request with its approval steps and its conversation — the same rule, and the same code, as the delete in the platform's own request drawer:

Every delete leaves a record of who deleted the request and when, and is written to the activity log under the named person.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/requests/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 88,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/requests/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 88,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (204, deleted):

Empty response
 

Example response (422, already decided):


{
    "message": "لا يمكن حذف طلب معتمد أو مرفوض — يبقى في سجلّات النظام.",
    "errors": {
        "request": [
            "لا يمكن حذف طلب معتمد أو مرفوض — يبقى في سجلّات النظام."
        ]
    }
}
 

Example response (422, not theirs):


{
    "message": "لا تملك حذف هذا الطلب: يحذفه صاحبه أو الموارد البشرية ما دام قيد الاعتماد.",
    "errors": {
        "request": [
            "لا تملك حذف هذا الطلب: يحذفه صاحبه أو الموارد البشرية ما دام قيد الاعتماد."
        ]
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/requests/{employeeRequest}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employeeRequest   string     

Example: architecto

Body Parameters

acting_employee_id   integer  optional    

The employee deleting. Required unless acting_user_email is given. Example: 88

acting_user_email   string  optional    

The email of the account deleting. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List early-leave requests (طلبات الاستئذان)

requires authentication

An early leave is permission to go before the shift ends. It is not a leave day and draws down no balance — which is why it is its own model rather than a leave_request with a time on it.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/early-leave-requests?employee_id=88&from=2026-09-01&to=2026-09-30&per_page=50" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 16,
    \"from\": \"2026-10-25T09:50:17\",
    \"to\": \"2026-10-25T09:50:17\",
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/early-leave-requests"
);

const params = {
    "employee_id": "88",
    "from": "2026-09-01",
    "to": "2026-09-30",
    "per_page": "50",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 16,
    "from": "2026-10-25T09:50:17",
    "to": "2026-10-25T09:50:17",
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 12,
            "type": "early-leave-requests",
            "attributes": {
                "employee_id": 88,
                "employee_name_ar": "سعيد الغامدي",
                "employee_name_en": "Saeed Alghamdi",
                "date": "2026-09-10",
                "leave_time": "13:30",
                "reason": "ارتباط خارجي",
                "status": "pending",
                "submitted_at": "2026-09-09T07:12:00+00:00",
                "rejection_reason": null,
                "current_approver": {
                    "id": 7,
                    "name": "سارة القحطاني",
                    "employee_id": 42,
                    "email": "sara@school.sa"
                },
                "current_step": 1,
                "total_steps": 2
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/early-leave-requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

employee_id   integer  optional    

Only this employee's requests. Example: 88

from   string  optional    

Y-m-d, earliest date. Example: 2026-09-01

to   string  optional    

Y-m-d, latest date. Example: 2026-09-30

per_page   integer  optional    

Default 50, max 200. Example: 50

Body Parameters

employee_id   integer  optional    

Example: 16

from   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

to   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

per_page   integer  optional    

Example: 16

Submit an early-leave request (استئذان)

requires authentication

Enters the company's configured chain for early_leave_request, exactly as the platform's own صفحة حضوري does — so a request filed here is indistinguishable downstream from one the employee filed themselves.

Approval is what GRANTS the departure: AttendanceService refuses a check-out punched before the shift's end, and an approved request for that date lifts the refusal from leave_time onward. Filing one is therefore the supported way for an employee to leave early and still record their own punch.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/early-leave-requests" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 88,
    \"date\": \"2026-09-10\",
    \"leave_time\": \"13:30\",
    \"reason\": \"ارتباط خارجي\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/early-leave-requests"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 88,
    "date": "2026-09-10",
    "leave_time": "13:30",
    "reason": "ارتباط خارجي"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 12,
        "type": "early-leave-requests",
        "attributes": {
            "employee_id": 88,
            "date": "2026-09-10",
            "leave_time": "13:30",
            "reason": "ارتباط خارجي",
            "status": "pending",
            "current_approver": {
                "id": 7,
                "name": "سارة القحطاني",
                "employee_id": 42,
                "email": "sara@school.sa"
            },
            "current_step": 1,
            "total_steps": 2
        }
    }
}
 

Example response (422, nobody can approve it):


{
    "message": "...",
    "errors": {
        "employee_id": [
            "عيّن مديراً مباشراً للموظف، أو اضبط سلسلة اعتماد لهذا النوع."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/early-leave-requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Example: 88

date   string     

Y-m-d. Example: 2026-09-10

leave_time   string     

HH:MM, the hour they mean to go. Example: 13:30

reason   string     

Why. Example: ارتباط خارجي

Delete an early-leave request

requires authentication

The same rule as deleting any other request (see DELETE /requests/{id}): only while it is undecided, by its requester or by HR. An approved early leave is a record the system keeps and cannot be deleted.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/early-leave-requests/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 88,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/early-leave-requests/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 88,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (204, deleted):

Empty response
 

Example response (422, not theirs):


{
    "message": "لا تملك حذف هذا الطلب: يحذفه صاحبه أو الموارد البشرية ما دام قيد الاعتماد.",
    "errors": {
        "request": [
            "لا تملك حذف هذا الطلب: يحذفه صاحبه أو الموارد البشرية ما دام قيد الاعتماد."
        ]
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/early-leave-requests/{earlyLeaveRequest}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

earlyLeaveRequest   string     

Example: architecto

Body Parameters

acting_employee_id   integer  optional    

The employee deleting. Required unless acting_user_email is given. Example: 88

acting_user_email   string  optional    

The email of the account deleting. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List shift templates

requires authentication

Returns both shapes of every shift: the STORED columns, and form — the same fields the platform's own شاشة أوقات العمل fills in. A consuming product renders an edit screen from form and posts it back unchanged, so it never has to know that «no penalty until 08:15» is stored as fifteen grace minutes.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/shift-templates" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/shift-templates"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 3,
            "type": "shift-templates",
            "attributes": {
                "name_ar": "الدوام الصباحي",
                "name_en": "Morning",
                "shift_type": "morning",
                "start_time": "07:00",
                "end_time": "14:00",
                "daily_hours": 7,
                "working_days": [
                    0,
                    1,
                    2,
                    3,
                    4
                ],
                "late_grace_minutes": 10,
                "early_leave_grace_minutes": 0,
                "checkout_not_required": false,
                "enforce_checkout_time": true,
                "auto_overtime": false,
                "color": "#7D55C7",
                "assigned_count": 24,
                "form": {
                    "name_ar": "الدوام الصباحي",
                    "name_en": "Morning",
                    "type": "morning",
                    "start_time": "07:00",
                    "end_time": "14:00",
                    "daily_hours": 7,
                    "working_days": [
                        0,
                        1,
                        2,
                        3,
                        4
                    ],
                    "checkin_window_start": null,
                    "checkin_window_end": null,
                    "checkout_window_start": null,
                    "checkout_window_end": null,
                    "no_penalty_checkin": "07:10",
                    "no_penalty_checkout": null,
                    "checkout_not_required": false,
                    "enforce_checkout_time": true,
                    "auto_overtime": false,
                    "color": "#7D55C7"
                }
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/shift-templates

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Create a shift template

requires authentication

Takes the FORM's fields, not the stored columns: the two clock times the day runs between, and optional absolute times after which lateness or an early departure starts to count.

Send end_time. daily_hours is still accepted for a caller written against the older contract — one of the two is required for a timed type — but the end time wins and the hours are then computed from it, not the reverse. An end_time at or before the start means the day runs past midnight.

flexible and remote have no clock, so none of this is required for them — there is no start to be late for.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/shift-templates" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"الدوام الصباحي\",
    \"name_en\": \"Morning shift\",
    \"type\": \"morning\",
    \"start_time\": \"07:00\",
    \"end_time\": \"14:20\",
    \"daily_hours\": 7,
    \"working_days\": [
        0,
        1,
        2,
        3,
        4
    ],
    \"no_penalty_checkin\": \"07:10\",
    \"no_penalty_checkout\": \"13:50\",
    \"checkin_window_start\": \"06:00\",
    \"checkin_window_end\": \"10:00\",
    \"checkout_window_start\": \"13:30\",
    \"checkout_window_end\": \"20:00\",
    \"enforce_checkout_time\": true,
    \"checkout_not_required\": false,
    \"auto_overtime\": false,
    \"color\": \"#7D55C7\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/shift-templates"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "الدوام الصباحي",
    "name_en": "Morning shift",
    "type": "morning",
    "start_time": "07:00",
    "end_time": "14:20",
    "daily_hours": 7,
    "working_days": [
        0,
        1,
        2,
        3,
        4
    ],
    "no_penalty_checkin": "07:10",
    "no_penalty_checkout": "13:50",
    "checkin_window_start": "06:00",
    "checkin_window_end": "10:00",
    "checkout_window_start": "13:30",
    "checkout_window_end": "20:00",
    "enforce_checkout_time": true,
    "checkout_not_required": false,
    "auto_overtime": false,
    "color": "#7D55C7"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 3,
        "type": "shift-templates",
        "attributes": {
            "name_ar": "الدوام الصباحي",
            "name_en": null,
            "shift_type": "morning",
            "start_time": "07:00",
            "end_time": "14:00",
            "daily_hours": 7,
            "working_days": [
                0,
                1,
                2,
                3,
                4
            ],
            "late_grace_minutes": 10,
            "early_leave_grace_minutes": 0,
            "checkout_not_required": false,
            "enforce_checkout_time": true,
            "auto_overtime": false,
            "color": "#7D55C7",
            "assigned_count": 0,
            "form": {
                "name_ar": "الدوام الصباحي",
                "name_en": null,
                "type": "morning",
                "start_time": "07:00",
                "end_time": "14:00",
                "daily_hours": 7,
                "working_days": [
                    0,
                    1,
                    2,
                    3,
                    4
                ],
                "checkin_window_start": null,
                "checkin_window_end": null,
                "checkout_window_start": null,
                "checkout_window_end": null,
                "no_penalty_checkin": "07:10",
                "no_penalty_checkout": null,
                "checkout_not_required": false,
                "enforce_checkout_time": true,
                "auto_overtime": false,
                "color": "#7D55C7"
            }
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/shift-templates

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

name_ar   string     

Example: الدوام الصباحي

name_en   string  optional    

Example: Morning shift

type   string     

One of: morning, evening, part_time, flexible, remote. Example: morning

start_time   string  optional    

Required for a timed type, HH:MM. Example: 07:00

end_time   string  optional    

The day's end, HH:MM. Required for a timed type unless daily_hours is sent. Example: 14:20

daily_hours   number  optional    

Legacy alternative to end_time, 0.5–24. Example: 7

working_days   integer[]     

Weekdays, 0 = Sunday.

no_penalty_checkin   string  optional    

Lateness counts only after this clock time. Example: 07:10

no_penalty_checkout   string  optional    

An early departure counts only before this clock time. Example: 13:50

checkin_window_start   string  optional    

Earliest punch accepted, HH:MM. Example: 06:00

checkin_window_end   string  optional    

Latest punch accepted, HH:MM. Example: 10:00

checkout_window_start   string  optional    

Earliest check-out accepted, HH:MM. Example: 13:30

checkout_window_end   string  optional    

Latest check-out accepted, HH:MM. Example: 20:00

enforce_checkout_time   boolean  optional    

Refuse a check-out before the day ends. Default true. Example: true

checkout_not_required   boolean  optional    

Example: false

auto_overtime   boolean  optional    

Example: false

color   string  optional    

A #RRGGBB swatch for the board. Example: #7D55C7

Update a shift template

requires authentication

Same body as creating one. Note that the times and grace windows apply to every future day judged against this shift — it is the clock, not a per-person setting.

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/internal/companies/16/shift-templates/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/shift-templates/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "PATCH",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 3,
        "type": "shift-templates",
        "attributes": {
            "name_ar": "الدوام الصباحي",
            "name_en": null,
            "shift_type": "morning",
            "start_time": "07:30",
            "end_time": "14:30",
            "daily_hours": 7,
            "working_days": [
                0,
                1,
                2,
                3,
                4
            ],
            "late_grace_minutes": 0,
            "early_leave_grace_minutes": 0,
            "checkout_not_required": false,
            "enforce_checkout_time": true,
            "auto_overtime": false,
            "color": "#7D55C7",
            "assigned_count": 24,
            "form": {
                "name_ar": "الدوام الصباحي",
                "name_en": null,
                "type": "morning",
                "start_time": "07:30",
                "end_time": "14:30",
                "daily_hours": 7,
                "working_days": [
                    0,
                    1,
                    2,
                    3,
                    4
                ],
                "checkin_window_start": null,
                "checkin_window_end": null,
                "checkout_window_start": null,
                "checkout_window_end": null,
                "no_penalty_checkin": null,
                "no_penalty_checkout": null,
                "checkout_not_required": false,
                "enforce_checkout_time": true,
                "auto_overtime": false,
                "color": "#7D55C7"
            }
        }
    }
}
 

Request      

PATCH api/internal/companies/{company_id}/shift-templates/{shiftTemplate}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

shiftTemplate   string     

Example: architecto

Delete a shift template

requires authentication

Refused while anybody is still on it. Deleting it would leave those people with no clock at all — every day unscheduled, nothing late, and nothing saying why — which is a worse outcome than a refusal naming the number of people to move first.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/shift-templates/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/shift-templates/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204):

Empty response
 

Example response (422, still assigned):


{
    "message": "...",
    "errors": {
        "shift": [
            "24 employees are still on this shift."
        ]
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/shift-templates/{shiftTemplate}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

shiftTemplate   string     

Example: architecto

Put people on a shift

requires authentication

Assigns the template to the employees and trainees named, and — for the employees — takes OFF it anyone currently on it who is not in the list, exactly as the platform's own roster sheet does. So the call states the whole roster of that shift rather than adding to it, and sending an empty employee_ids clears it.

A custom rest-day pattern is cleared with the move: it was the complement of the OLD shift's working days, and carrying it onto a new clock silently gives somebody days off nobody granted.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/shift-templates/architecto/assign" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_ids\": [
        12,
        13
    ],
    \"trainee_ids\": [
        4
    ]
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/shift-templates/architecto/assign"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_ids": [
        12,
        13
    ],
    "trainee_ids": [
        4
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "shift_template_id": 3,
        "employees_assigned": 2,
        "trainees_assigned": 1,
        "employees_removed": 4
    }
}
 

Request      

POST api/internal/companies/{company_id}/shift-templates/{shiftTemplate}/assign

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

shiftTemplate   string     

Example: architecto

Body Parameters

employee_ids   integer[]  optional    

The whole employee roster of this shift.

trainee_ids   integer[]  optional    

Trainees to put on it. Additive — trainees are not taken off.

List the weekly duty roster

requires authentication

Who supervises on which weekday (0 = Sunday). This is the recurring pattern; a substitution covering one specific date is a separate record and is not served here.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/duties?employee_id=12" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/duties"
);

const params = {
    "employee_id": "12",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 8,
            "type": "duty-assignments",
            "attributes": {
                "employee_id": 12,
                "employee_name_ar": "سعيد الغامدي",
                "employee_name_en": "Saeed Alghamdi",
                "day_of_week": 1,
                "title_ar": "إشراف الفسحة",
                "title_en": null,
                "start_time": "09:30",
                "end_time": "10:00",
                "color": "#0e9f6e",
                "notes": null,
                "is_active": true
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/duties

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

employee_id   integer  optional    

Only this person's duties. Example: 12

Body Parameters

employee_id   integer  optional    

Example: 16

Add a duty

requires authentication

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/duties" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 12,
    \"day_of_week\": 1,
    \"title_ar\": \"إشراف الفسحة\",
    \"title_en\": \"Break supervision\",
    \"start_time\": \"09:30\",
    \"end_time\": \"10:00\",
    \"color\": \"#0e9f6e\",
    \"notes\": \"البوابة الشمالية\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/duties"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 12,
    "day_of_week": 1,
    "title_ar": "إشراف الفسحة",
    "title_en": "Break supervision",
    "start_time": "09:30",
    "end_time": "10:00",
    "color": "#0e9f6e",
    "notes": "البوابة الشمالية"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 8,
        "type": "duty-assignments",
        "attributes": {
            "employee_id": 12,
            "employee_name_ar": "سعيد الغامدي",
            "employee_name_en": "Saeed Alghamdi",
            "day_of_week": 1,
            "title_ar": "إشراف الفسحة",
            "title_en": null,
            "start_time": "09:30",
            "end_time": "10:00",
            "color": "#0e9f6e",
            "notes": null,
            "is_active": true
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/duties

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Who is on duty. Example: 12

day_of_week   integer     

0 = Sunday … 6 = Saturday. Example: 1

title_ar   string  optional    

What the duty is called. Example: إشراف الفسحة

title_en   string  optional    

Example: Break supervision

start_time   string  optional    

HH:MM. Leave both times blank for an all-day duty. Example: 09:30

end_time   string  optional    

HH:MM, after start_time. Example: 10:00

color   string  optional    

A #RRGGBB swatch. Example: #0e9f6e

notes   string  optional    

Example: البوابة الشمالية

Update a duty

requires authentication

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/internal/companies/16/duties/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/duties/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "PATCH",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 8,
        "type": "duty-assignments",
        "attributes": {
            "employee_id": 12,
            "employee_name_ar": "سعيد الغامدي",
            "employee_name_en": "Saeed Alghamdi",
            "day_of_week": 2,
            "title_ar": "إشراف الفسحة",
            "title_en": null,
            "start_time": "09:30",
            "end_time": "10:00",
            "color": "#0e9f6e",
            "notes": null,
            "is_active": true
        }
    }
}
 

Request      

PATCH api/internal/companies/{company_id}/duties/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   string     

The ID of the duty. Example: architecto

Remove a duty

requires authentication

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/duties/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/duties/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204):

Empty response
 

Request      

DELETE api/internal/companies/{company_id}/duties/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

id   string     

The ID of the duty. Example: architecto

List announcements

requires authentication

The company's announcements, newest first, with their audience and importance so a consuming product can surface the important ones the way the platform does.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/announcements?per_page=25" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/announcements"
);

const params = {
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 5,
            "type": "announcements",
            "attributes": {
                "title": "إجازة اليوم الوطني",
                "body": "...",
                "importance": "important",
                "audience_type": "all",
                "audience_id": null,
                "published_at": "2026-09-20T07:00:00+00:00",
                "has_attachment": false
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/announcements

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

per_page   integer  optional    

Default 25, max 200. Example: 25

Body Parameters

per_page   integer  optional    

Example: 16

Publish an announcement

requires authentication

Writes the announcement AND notifies the audience it names, through the same recipientsQuery()/AnnouncementPublishedNotification pair the platform's own صفحة الإعلانات uses — so an announcement published from a consuming product reaches people's inboxes and their in-app bell exactly like one published from the HR platform. Skipping the notification would make this a row nobody is told about, which is not an announcement.

audience_id names a department, a branch, an employee or a trainee of THIS company — audience_type: all takes none.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/announcements" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"title\": \"تعليق الدوام غداً\",
    \"body\": \"يُعلَّق الدوام يوم الأحد بمناسبة اليوم الوطني.\",
    \"audience_type\": \"all\",
    \"audience_id\": 3,
    \"importance\": \"important\",
    \"channels\": [
        \"database\",
        \"mail\"
    ],
    \"acting_employee_id\": 7,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/announcements"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "title": "تعليق الدوام غداً",
    "body": "يُعلَّق الدوام يوم الأحد بمناسبة اليوم الوطني.",
    "audience_type": "all",
    "audience_id": 3,
    "importance": "important",
    "channels": [
        "database",
        "mail"
    ],
    "acting_employee_id": 7,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 5,
        "type": "announcements",
        "attributes": {
            "title": "إجازة اليوم الوطني",
            "body": "...",
            "importance": "important",
            "audience_type": "all",
            "audience_id": null,
            "published_at": "2026-09-20T07:00:00+00:00",
            "has_attachment": false
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/announcements

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

title   string     

Example: تعليق الدوام غداً

body   string     

Example: يُعلَّق الدوام يوم الأحد بمناسبة اليوم الوطني.

audience_type   string  optional    

all, department, branch, employee or trainee. Defaults to all. Example: all

audience_id   integer  optional    

Required unless audience_type is all. Example: 3

importance   string  optional    

normal or important. Defaults to normal. Example: important

channels   string[]  optional    

Any of database, mail. Defaults to both.

acting_employee_id   integer  optional    

Who is publishing. Required unless acting_user_email is given. Example: 7

acting_user_email   string  optional    

The email of the account publishing. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List leave types

requires authentication

tracks_balance says whether the type draws down an annual balance — sick and unpaid leave do not, so showing a "remaining days" figure against them tells the employee something untrue.

requires_attachment says the type cannot be filed without a document (a medical certificate, a marriage contract). store REFUSES such a request when nothing is attached, so read this before you submit: a rule the consumer cannot see until the server rejects them is a trap, and the employee filling the form is the one who pays for it.

is_statutory is what splits a consuming product's catalogue into «إجازات مكتب العمل» and «الإجازات المخصصة». It is a COLUMN, not a property of the code: a company granting a labour-office leave nobody anticipated has to be able to put it on that tab, and deriving the split from the enum capped it at the codes shipped in the enum.

meta.missing lists the statutory leaves in the catalogue this company never got, so the consumer can offer them back rather than making somebody retype the statute. It empties as they are added, and a deleted one reappears — which is what makes a mistaken delete recoverable.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/leave-types" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-types"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "type": "leave-types",
            "attributes": {
                "code": "annual",
                "name_ar": "إجازة سنوية",
                "name_en": "Annual leave",
                "description_ar": "المادة 109: 21 يوماً في السنة",
                "description_en": "Art. 109: 21 days a year",
                "is_active": true,
                "is_statutory": false,
                "is_annual": true,
                "tracks_balance": true,
                "requires_attachment": false,
                "is_paid": true,
                "days_per_year": 21,
                "allows_carry_over": true,
                "max_carry_over_days": 10,
                "in_use": true
            }
        }
    ],
    "meta": {
        "missing": [
            {
                "code": "exam",
                "name_ar": "اختبارات",
                "name_en": "Exam",
                "days_per_year": 10,
                "description_ar": "المادة 115"
            }
        ]
    }
}
 

Request      

GET api/internal/companies/{company_id}/leave-types

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Create a leave type

requires authentication

The company's own — a leave the catalogue never anticipated, or a labour-office one it grants under its own name. is_statutory decides which tab it sits on and nothing else; the code is always custom, because the codes are the platform's vocabulary and a consumer minting one would be naming a rule the platform then cannot read.

To pick a leave up from the LABOUR-LAW CATALOGUE instead, send its code from meta.missing — the statute's own days, wording and article come with it, and nobody retypes them.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/leave-types" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"code\": \"exam\",
    \"name_ar\": \"إجازة اختبارات\",
    \"name_en\": \"Exam leave\",
    \"description_ar\": \"المادة 115\",
    \"description_en\": \"Art. 115\",
    \"days_per_year\": 10,
    \"is_paid\": true,
    \"requires_attachment\": false,
    \"is_statutory\": true,
    \"acting_employee_id\": 42,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-types"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "code": "exam",
    "name_ar": "إجازة اختبارات",
    "name_en": "Exam leave",
    "description_ar": "المادة 115",
    "description_en": "Art. 115",
    "days_per_year": 10,
    "is_paid": true,
    "requires_attachment": false,
    "is_statutory": true,
    "acting_employee_id": 42,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 9,
        "type": "leave-types",
        "attributes": {
            "code": "custom",
            "name_ar": "إجازة اختبارات",
            "name_en": "Exam leave",
            "description_ar": "المادة 115",
            "description_en": null,
            "is_active": true,
            "is_statutory": true,
            "is_annual": false,
            "tracks_balance": true,
            "requires_attachment": false,
            "is_paid": true,
            "days_per_year": 10,
            "allows_carry_over": false,
            "max_carry_over_days": null,
            "in_use": false
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/leave-types

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

code   string  optional    

A code from meta.missing — adds that statutory leave with the catalogue's own figures. Everything else is then ignored. Example: exam

name_ar   string     

unless a catalogue code is sent. Example: إجازة اختبارات

name_en   string  optional    

Example: Exam leave

description_ar   string  optional    

The rule in words — the article it comes from. Example: المادة 115

description_en   string  optional    

Example: Art. 115

days_per_year   number  optional    

Null for a type that tracks no balance. Example: 10

is_paid   boolean  optional    

Example: true

requires_attachment   boolean  optional    

Refuses the request without a document. Example: false

is_statutory   boolean  optional    

Which catalogue tab it belongs on. Example: true

acting_employee_id   integer  optional    

The employee acting. Required unless acting_user_email or acting_as_tenant_admin is sent. Example: 42

acting_user_email   string  optional    

The email of the account acting, for somebody with no employee file or no login. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Update a leave type

requires authentication

Everything the platform's own screen edits: the wording, the days, the pay, whether a document is required, the carry-over cap, and the status switch. Send only what changes — a key that is not sent is left alone, so a consumer flipping is_active does not have to resend the name.

The ANNUAL type is edited through here too. It is not a special endpoint because it is not a special write: it is the same row, and the entitlement on it is the company-wide policy every balance is opened at.

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/internal/companies/16/leave-types/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"إجازة سنوية\",
    \"name_en\": \"Annual leave\",
    \"description_ar\": \"المادة 109\",
    \"description_en\": \"Art. 109\",
    \"days_per_year\": 30,
    \"is_paid\": true,
    \"requires_attachment\": false,
    \"is_active\": true,
    \"allows_carry_over\": true,
    \"max_carry_over_days\": 10,
    \"acting_employee_id\": 42,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-types/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "إجازة سنوية",
    "name_en": "Annual leave",
    "description_ar": "المادة 109",
    "description_en": "Art. 109",
    "days_per_year": 30,
    "is_paid": true,
    "requires_attachment": false,
    "is_active": true,
    "allows_carry_over": true,
    "max_carry_over_days": 10,
    "acting_employee_id": 42,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1,
        "type": "leave-types",
        "attributes": {
            "code": "annual",
            "name_ar": "سنوية",
            "name_en": "Annual",
            "description_ar": "المادة 109",
            "description_en": null,
            "is_active": true,
            "is_statutory": false,
            "is_annual": true,
            "tracks_balance": true,
            "requires_attachment": false,
            "is_paid": true,
            "days_per_year": 30,
            "allows_carry_over": true,
            "max_carry_over_days": 10,
            "in_use": true
        }
    }
}
 

Request      

PATCH api/internal/companies/{company_id}/leave-types/{leaveType}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

leaveType   string     

Example: architecto

Body Parameters

name_ar   string  optional    

Example: إجازة سنوية

name_en   string  optional    

Example: Annual leave

description_ar   string  optional    

Example: المادة 109

description_en   string  optional    

Example: Art. 109

days_per_year   number  optional    

Send null for a type that tracks no balance. Example: 30

is_paid   boolean  optional    

Example: true

requires_attachment   boolean  optional    

Example: false

is_active   boolean  optional    

Turning it off stops people asking for it. Example: true

allows_carry_over   boolean  optional    

Example: true

max_carry_over_days   integer  optional    

Ignored unless carry-over is on. Example: 10

acting_employee_id   integer  optional    

The employee acting. Required unless acting_user_email or acting_as_tenant_admin is sent. Example: 42

acting_user_email   string  optional    

The email of the account acting, for somebody with no employee file or no login. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Delete a leave type

requires authentication

Refused while any leave points at it: the days those employees took have to stay readable in their history, and a type that vanishes takes the name of the leave with it. Turn it off instead — an inactive type cannot be asked for and keeps every record intact.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/leave-types/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 42,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-types/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 42,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "deleted": true
    }
}
 

Example response (422, In use):


{
    "message": "لا يمكن حذف نوع إجازة عليه طلبات. عطّله بدلاً من ذلك.",
    "errors": {
        "leave_type": [
            "لا يمكن حذف نوع إجازة عليه طلبات. عطّله بدلاً من ذلك."
        ]
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/leave-types/{leaveType}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

leaveType   string     

Example: architecto

Body Parameters

acting_employee_id   integer  optional    

The employee acting. Required unless acting_user_email or acting_as_tenant_admin is sent. Example: 42

acting_user_email   string  optional    

The email of the account acting, for somebody with no employee file or no login. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List public holidays

requires authentication

Every holiday of the company, active or not — a consuming product mirroring the platform's screen shows the status switch on each row, so an inactive one is listed and says so in is_active.

year keeps the holidays that TOUCH that calendar year: a range crossing New Year is listed under both years. Ordered by start date.

covers_all_branches: true means everybody, people with no branch included, and then branch_ids / branches are empty. Otherwise the holiday covers exactly the branches listed — and nobody on no branch.

days is the calendar length, both ends included.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/public-holidays?year=2026" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"year\": 1
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/public-holidays"
);

const params = {
    "year": "2026",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "year": 1
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 3,
            "type": "public-holidays",
            "attributes": {
                "name_ar": "عيد الفطر",
                "name_en": "Eid al-Fitr",
                "start_date": "2026-03-19",
                "end_date": "2026-03-23",
                "days": 5,
                "covers_all_branches": false,
                "branch_ids": [
                    4
                ],
                "branches": [
                    {
                        "id": 4,
                        "name_ar": "فرع الرياض",
                        "name_en": "Riyadh branch"
                    }
                ],
                "is_active": true
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/public-holidays

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

year   integer  optional    

Only holidays touching this calendar year. Omit for all. Example: 2026

Body Parameters

year   integer  optional    

يجب ألا يقل value عن 2000. يجب ألا يكون value أكبر من 2100. Example: 1

Create a public holiday

requires authentication

Leaves already filed over these dates are recounted on the spot: an approved one gives the holiday days back to the balance, a pending one will be charged without them when it is approved.

branch_ids must all be this company's branches — the service key is suite-level, so a branch of another tenant is refused with a 422 rather than silently dropped. It is required when covers_all_branches is false, and ignored when it is true.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/public-holidays" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"عيد الفطر\",
    \"name_en\": \"Eid al-Fitr\",
    \"start_date\": \"2026-03-19\",
    \"end_date\": \"2026-03-23\",
    \"covers_all_branches\": false,
    \"branch_ids\": [
        4
    ],
    \"is_active\": true,
    \"acting_employee_id\": 12,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/public-holidays"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "عيد الفطر",
    "name_en": "Eid al-Fitr",
    "start_date": "2026-03-19",
    "end_date": "2026-03-23",
    "covers_all_branches": false,
    "branch_ids": [
        4
    ],
    "is_active": true,
    "acting_employee_id": 12,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 3,
        "type": "public-holidays",
        "attributes": {
            "name_ar": "عيد الفطر",
            "name_en": "Eid al-Fitr",
            "start_date": "2026-03-19",
            "end_date": "2026-03-23",
            "days": 5,
            "covers_all_branches": false,
            "branch_ids": [
                4
            ],
            "branches": [
                {
                    "id": 4,
                    "name_ar": "فرع الرياض",
                    "name_en": "Riyadh branch"
                }
            ],
            "is_active": true
        }
    }
}
 

Example response (403, the acting person does not run HR):


{
    "message": "This action is unauthorized."
}
 

Example response (422, a branch of another company):


{
    "message": "...",
    "errors": {
        "branch_ids": [
            "أحد الفروع المختارة لا يتبع هذه الجهة."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/public-holidays

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

name_ar   string     

Example: عيد الفطر

name_en   string  optional    

Example: Eid al-Fitr

start_date   string     

Y-m-d. Example: 2026-03-19

end_date   string     

Y-m-d, on or after start_date. At most 366 days after it. Example: 2026-03-23

covers_all_branches   boolean  optional    

Defaults to true: everybody, people with no branch included. Example: false

branch_ids   integer[]  optional    

Required when covers_all_branches is false. This company's branches only.

is_active   boolean  optional    

Defaults to true. An inactive holiday covers nobody. Example: true

acting_employee_id   integer  optional    

The employee making the change. Required unless acting_user_email is given. Example: 12

acting_user_email   string  optional    

The email of the account making the change. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Update a public holiday

requires authentication

Send only what changes — a key that is not sent is left alone, so a consumer flipping is_active does not resend the dates. name_en may be sent as null to clear it. branch_ids, when sent, REPLACES the list; narrowing a company-wide holiday (covers_all_branches: false) without sending one keeps the list it already had, and is refused when that is empty.

The leaves overlapping the holiday — its old dates AND its new ones — are recounted, so days it stopped covering are charged again and days it now covers are given back.

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/internal/companies/16/public-holidays/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name_ar\": \"عيد الأضحى\",
    \"name_en\": \"Eid al-Adha\",
    \"start_date\": \"2026-05-26\",
    \"end_date\": \"2026-05-30\",
    \"covers_all_branches\": true,
    \"branch_ids\": [
        4,
        7
    ],
    \"is_active\": false,
    \"acting_employee_id\": 12,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/public-holidays/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name_ar": "عيد الأضحى",
    "name_en": "Eid al-Adha",
    "start_date": "2026-05-26",
    "end_date": "2026-05-30",
    "covers_all_branches": true,
    "branch_ids": [
        4,
        7
    ],
    "is_active": false,
    "acting_employee_id": 12,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 3,
        "type": "public-holidays",
        "attributes": {
            "name_ar": "عيد الأضحى",
            "name_en": "Eid al-Adha",
            "start_date": "2026-05-26",
            "end_date": "2026-05-30",
            "days": 5,
            "covers_all_branches": true,
            "branch_ids": [],
            "branches": [],
            "is_active": false
        }
    }
}
 

Example response (404, a holiday of another company):


{
    "message": "Not Found"
}
 

Request      

PATCH api/internal/companies/{company_id}/public-holidays/{publicHoliday}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

publicHoliday   string     

Example: architecto

Body Parameters

name_ar   string  optional    

Example: عيد الأضحى

name_en   string  optional    

Send null to clear. Example: Eid al-Adha

start_date   string  optional    

Y-m-d. Example: 2026-05-26

end_date   string  optional    

Y-m-d, on or after the start date (sent or stored). Example: 2026-05-30

covers_all_branches   boolean  optional    

Example: true

branch_ids   integer[]  optional    

Replaces the branch list. This company's branches only.

is_active   boolean  optional    

Turning it off makes its days ordinary days again. Example: false

acting_employee_id   integer  optional    

The employee making the change. Required unless acting_user_email is given. Example: 12

acting_user_email   string  optional    

The email of the account making the change. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

Delete a public holiday

requires authentication

Nothing points at a holiday, so it can always be deleted. Its days are charged again to the approved leaves that overlapped it — deleting Eid by mistake costs those employees days until it is put back — so turning it off (is_active: false) is the reversible alternative.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/public-holidays/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 12,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/public-holidays/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 12,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "deleted": true
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/public-holidays/{publicHoliday}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

publicHoliday   string     

Example: architecto

Body Parameters

acting_employee_id   integer  optional    

The employee making the change. Required unless acting_user_email is given. Example: 12

acting_user_email   string  optional    

The email of the account making the change. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List leave balances

requires authentication

Defaults to the current year. pending_days is already deducted: balances are debited on SUBMISSION, not approval, or five overlapping requests each pass the balance check individually and overdraw.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/leave-balances?employee_id=88&year=2026&per_page=50" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 16,
    \"year\": 22,
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-balances"
);

const params = {
    "employee_id": "88",
    "year": "2026",
    "per_page": "50",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 16,
    "year": 22,
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 3,
            "type": "leave-balances",
            "attributes": {
                "employee_id": 88,
                "year": 2026,
                "entitled_days": 21,
                "used_days": 5,
                "pending_days": 2,
                "remaining_days": 14
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/leave-balances

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

employee_id   integer  optional    

Only this employee's balances. Example: 88

year   integer  optional    

Defaults to the current year. Example: 2026

per_page   integer  optional    

Default 50, max 200. Example: 50

Body Parameters

employee_id   integer  optional    

Example: 16

year   integer  optional    

يجب ألا يقل value عن 2000. يجب ألا يكون value أكبر من 2100. Example: 22

per_page   integer  optional    

Example: 16

Set one employee's leave balance

requires authentication

The entitlement for ONE employee, ONE leave type and ONE year, written by hand — a contract that grants more than the policy, days carried in from a system being migrated from, a settlement.

It may never be cut below what has already been approved. Approved leave has been taken: the person was away, the register says so and payroll has counted it, so a smaller entitlement would not reduce a figure, it would claim days back that nobody can return. Such a request is refused with entitled_days naming the days already used, rather than clamped — a clamp reads as a save.

The written figure STICKS: the platform reconciles entitlements against the company policy on every read (a raised default, or an employee crossing the art. 109 five-year tier), and an overridden row is left alone. Send reset: true to put it back under the policy.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/leave-balances" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 88,
    \"leave_type_id\": 4,
    \"year\": 2026,
    \"reset\": false,
    \"entitled_days\": 25
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-balances"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 88,
    "leave_type_id": 4,
    "year": 2026,
    "reset": false,
    "entitled_days": 25
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 3,
        "type": "leave-balances",
        "attributes": {
            "employee_id": 88,
            "year": 2026,
            "entitled_days": 25,
            "used_days": 5,
            "pending_days": 0,
            "remaining_days": 20
        }
    }
}
 

Example response (422):


{
    "message": "لا يمكن أن يقلّ الرصيد عمّا اعتُمد فعلاً: 5 يوماً مستخدمة.",
    "errors": {
        "entitled_days": [
            "لا يمكن أن يقلّ الرصيد عمّا اعتُمد فعلاً: 5 يوماً مستخدمة."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/leave-balances

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Example: 88

leave_type_id   integer     

Example: 4

year   integer  optional    

Defaults to the current year. Example: 2026

reset   boolean  optional    

Put the balance back under the company policy. Example: false

entitled_days   number  optional    

Required unless reset is true. 0–365. Example: 25

Open default leave balances for a year

requires authentication

Creates the balance row for every active leave type that tracks one, at the entitlement the policy gives each employee — the same LeaveBalance::openYearFor() the platform's «فتح أرصدة السنة» button and the monthly okta:open-leave-balances job call, so the three can never disagree about what a fresh year holds.

Idempotent: rows that already exist are left exactly as they are, so a second call changes nothing. meta.opened says how many rows were actually created.

employee_id is optional, and that decides the scope, which meta.scope names back rather than leaving a consumer to infer it:

Why it exists at all: a consuming product shows an employee's leaves tab as «لا توجد بيانات» when no row was ever opened — somebody filed after the year began, or a tenant provisioned before balances opened at creation — and until now that screen could only wait for the monthly job.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/leave-balances/open" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 88,
    \"year\": 2026
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-balances/open"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 88,
    "year": 2026
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 3,
            "type": "leave-balances",
            "attributes": {
                "employee_id": 88,
                "year": 2026,
                "entitled_days": 21,
                "used_days": 0,
                "pending_days": 0,
                "remaining_days": 21
            }
        }
    ],
    "meta": {
        "opened": 4,
        "year": 2026,
        "scope": "employee",
        "employees": 1
    }
}
 

Request      

POST api/internal/companies/{company_id}/leave-balances/open

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer  optional    

Omit to open the whole company. Example: 88

year   integer  optional    

Defaults to the current year. Example: 2026

List leave requests

requires authentication

from and to together return requests that OVERLAP that window, not only ones fully inside it — a leave spanning the month boundary is the one you most need to see.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/leave-requests?status=pending&employee_id=88&approver_employee_id=42&approver_email=hr%40school.sa&approver_as_tenant_admin=1&approver_may_decide=1&awaiting=1&acting_employee_id=42&acting_user_email=hr%40school.sa&acting_as_tenant_admin=1&acting_may_decide=1&from=2026-08-01&to=2026-08-31&per_page=20" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"architecto\",
    \"employee_id\": 16,
    \"approver_employee_id\": 16,
    \"approver_email\": \"zbailey@example.net\",
    \"approver_as_tenant_admin\": false,
    \"approver_may_decide\": true,
    \"awaiting\": true,
    \"from\": \"2026-10-25T09:50:17\",
    \"to\": \"2026-10-25T09:50:17\",
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-requests"
);

const params = {
    "status": "pending",
    "employee_id": "88",
    "approver_employee_id": "42",
    "approver_email": "hr@school.sa",
    "approver_as_tenant_admin": "1",
    "approver_may_decide": "1",
    "awaiting": "1",
    "acting_employee_id": "42",
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": "1",
    "acting_may_decide": "1",
    "from": "2026-08-01",
    "to": "2026-08-31",
    "per_page": "20",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "status": "architecto",
    "employee_id": 16,
    "approver_employee_id": 16,
    "approver_email": "zbailey@example.net",
    "approver_as_tenant_admin": false,
    "approver_may_decide": true,
    "awaiting": true,
    "from": "2026-10-25T09:50:17",
    "to": "2026-10-25T09:50:17",
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 42,
            "type": "leave-requests",
            "attributes": {
                "employee_id": 88,
                "employee": {
                    "id": 88,
                    "name_ar": "سعيد الغامدي",
                    "name_en": "Saeed Alghamdi",
                    "employee_number": "E-100"
                },
                "start_date": "2026-08-10",
                "end_date": "2026-08-14",
                "days": 5,
                "holiday_days": 0,
                "reason": "سفر عائلي",
                "status": "pending",
                "leave_type": {
                    "id": 1,
                    "name_ar": "إجازة سنوية",
                    "name_en": "Annual leave",
                    "code": "annual",
                    "is_paid": true
                },
                "attachments": [
                    {
                        "id": 7,
                        "name": "تقرير-طبي.pdf",
                        "size": 48213,
                        "mime_type": "application/pdf"
                    }
                ],
                "submitted_at": "2026-08-02T09:12:00+00:00",
                "rejection_reason": null,
                "current_approver": {
                    "id": 7,
                    "name": "سارة القحطاني",
                    "employee_id": 42,
                    "email": "sara@school.sa"
                },
                "current_step": 1,
                "total_steps": 2
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/leave-requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

status   string  optional    

pending, approved, rejected or cancelled. Example: pending

employee_id   integer  optional    

Example: 88

approver_employee_id   integer  optional    

Leaves this employee may decide — «اعتماداتي»: their own steps, decided or not, and (for an HR manager, who may decide in the assigned approver's place) everything still awaiting a decision. meta.approver_scope says which of the two you got. Example: 42

approver_email   string  optional    

Same, for an approver named by account email. Sent WITH approver_employee_id it widens rather than narrows: the two are one claim in two spellings, and an approver with no employee file is matched by the email alone. Example: hr@school.sa

approver_as_tenant_admin   boolean  optional    

The same for «اعتماداتي»: the administrator's inbox, resolved the way the act endpoints resolve them. Example: true

approver_may_decide   boolean  optional    

With an approver filter: the person holds the permission to decide leaves in your product, so — like an HR manager — they get every leave still awaiting a decision, not only their own steps. Their own requests are never in that part. Example: true

awaiting   boolean  optional    

With an approver filter, keep only what that person can decide NOW: their step must be the chain's current (lowest pending) one, or — for somebody who may decide on behalf — any pending step. For an inbox of approve/reject buttons; without it a decided step and a step whose turn has not come are listed too. Example: true

acting_employee_id   integer  optional    

The person LOOKING, so each row carries can_decide — whether the act endpoint would take their decision — computed by the same rule the act endpoint enforces. Draw approve/reject from it and they are never refused. Example: 42

acting_user_email   string  optional    

The person looking, by account email. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

The person looking administers this tenant in your product. Example: true

acting_may_decide   boolean  optional    

The person looking holds the permission to decide leaves in your product. Example: true

from   string  optional    

Y-m-d. Requires to. Example: 2026-08-01

to   string  optional    

Y-m-d. Requires from. Example: 2026-08-31

per_page   integer  optional    

Default 20, max 200. Example: 20

Body Parameters

status   string  optional    

Example: architecto

employee_id   integer  optional    

Example: 16

approver_employee_id   integer  optional    

«اعتماداتي» — the leaves this person may decide. Word for word the filter on employee-requests, because a consumer's approvals tab lists both and must not answer «who is this» twice, nor hold one kind and miss the other. Example: 16

approver_email   string  optional    

يجب ألا يزيد طول value عن 191 حرفًا. Example: zbailey@example.net

approver_as_tenant_admin   boolean  optional    

Example: false

approver_may_decide   boolean  optional    

Example: true

awaiting   boolean  optional    

Example: true

from   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

to   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

per_page   integer  optional    

Example: 16

Submit a leave request

requires authentication

Enters the ordinary approval chain — a request created here is indistinguishable downstream from one the employee filed themselves, which is the point: one queue, one set of rules, one audit trail.

The same balance and overlap checks apply. A request that would overdraw the balance or double-book dates is refused with a 422 whose message names the real numbers; show that message rather than writing your own, or the employee sees two different explanations for one refusal.

Public holidays inside the range (see public-holidays) are not charged: the balance is checked against — and later debited by — the calendar days less the holiday days covering the employee's branch. holiday_days on the response says how many of days those were.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/leave-requests" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "employee_id=88"\
    --form "leave_type_id=1"\
    --form "start_date=2026-08-10"\
    --form "end_date=2026-08-14"\
    --form "reason=سفر عائلي"\
    --form "attachments[]=@/tmp/php2crk9q87loff3KpjqmT" 
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-requests"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('employee_id', '88');
body.append('leave_type_id', '1');
body.append('start_date', '2026-08-10');
body.append('end_date', '2026-08-14');
body.append('reason', 'سفر عائلي');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 42,
        "type": "leave-requests",
        "attributes": {
            "employee_id": 88,
            "employee": {
                "id": 88,
                "name_ar": "سعيد الغامدي",
                "name_en": "Saeed Alghamdi",
                "employee_number": "E-100"
            },
            "start_date": "2026-08-10",
            "end_date": "2026-08-14",
            "days": 5,
            "holiday_days": 0,
            "reason": "سفر عائلي",
            "status": "pending",
            "leave_type": {
                "id": 1,
                "name_ar": "إجازة سنوية",
                "name_en": "Annual leave",
                "code": "annual",
                "is_paid": true
            },
            "attachments": [
                {
                    "id": 7,
                    "name": "تقرير-طبي.pdf",
                    "size": 48213,
                    "mime_type": "application/pdf"
                }
            ],
            "submitted_at": "2026-08-02T09:12:00+00:00",
            "rejection_reason": null,
            "current_approver": {
                "id": 7,
                "name": "سارة القحطاني",
                "employee_id": 42,
                "email": "sara@school.sa"
            },
            "current_step": 1,
            "total_steps": 2
        }
    }
}
 

Example response (422, not enough balance):


{
    "message": "...",
    "errors": {
        "start_date": [
            "رصيدك 3.5 يوم وطلبت 5 أيام."
        ]
    }
}
 

Example response (422, overlaps an existing request):


{
    "message": "...",
    "errors": {
        "start_date": [
            "يوجد طلب إجازة آخر في نفس الفترة."
        ]
    }
}
 

Example response (422, the type requires a document):


{
    "message": "...",
    "errors": {
        "attachments": [
            "هذا النوع من الإجازات يتطلب مرفقاً."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/leave-requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Example: 88

leave_type_id   integer     

Must be an active type of this company. Example: 1

start_date   string     

Y-m-d. Example: 2026-08-10

end_date   string     

Y-m-d, on or after start_date. Example: 2026-08-14

reason   string  optional    

Example: سفر عائلي

attachments   file[]  optional    

Up to 5 supporting documents, 10MB each. Send as multipart/form-data. REQUIRED when the leave type says requires_attachment — read that off the types endpoint first.

Approve or reject a leave request

requires authentication

Acts on the current step of the chain — earlier steps are already decided and later ones are not actionable yet.

The actor is required and is checked against the step's approver: a service key says which PRODUCT is calling, never which person decided, and an approval attributed to a product is not one anyone can stand behind later. Somebody who is neither the pending approver nor an HR manager of this company is refused, not silently ignored.

An HR manager of the company may decide on the assigned approver's behalf, exactly as they can on POST /requests/{id}/act and on the platform's own drawer — an approver who is away must not be able to stop the company. The step keeps its approver_id and acted_by records who actually decided, so the chain still names both.

Name them with acting_employee_id, or with acting_user_email when they have no employee file — an HR manager who signed in long before anybody filed their record, or the platform operator. Chains route to those people, so leaving them unnameable meant a consuming product could show «waiting on X» and have no way to let X decide.

Somebody your product lets decide leaves may decide here too — send acting_may_decide: true when the person holds that permission in your product. The same rule marks each row can_decide on the listings, so a button drawn from that flag is never refused. Nobody decides their own leave in somebody else's place.

An employee file with no login is a complete name: an account is made for that file when they decide — the one naming them in a chain would make — so no email is needed.

A rejection requires notes. The requester is shown that sentence as the reason, and "rejected, no reason given" is the single most complained-about thing an approval system can do.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/leave-requests/16/act" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"decision\": \"reject\",
    \"notes\": \"الفترة مزدحمة بالاختبارات\",
    \"acting_employee_id\": 12,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_decide\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-requests/16/act"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "decision": "reject",
    "notes": "الفترة مزدحمة بالاختبارات",
    "acting_employee_id": 12,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true,
    "acting_may_decide": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 42,
        "type": "leave-requests",
        "attributes": {
            "status": "rejected"
        }
    }
}
 

Example response (422, neither the pending approver nor somebody who may decide on their behalf):


{
    "message": "...",
    "errors": {
        "acting_employee_id": [
            "ليس دورك في سلسلة الاعتماد."
        ]
    }
}
 

Example response (422, their own request):


{
    "message": "...",
    "errors": {
        "acting_employee_id": [
            "لا يمكنك اتخاذ قرار في طلبك أنت."
        ]
    }
}
 

Example response (422, rejected without a reason):


{
    "message": "...",
    "errors": {
        "notes": [
            "حقل السبب مطلوب."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/leave-requests/{leaveRequest}/act

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

leaveRequest   integer     

Example: 16

Body Parameters

decision   string     

approve or reject. Example: reject

notes   string  optional    

Required when rejecting. Example: الفترة مزدحمة بالاختبارات

acting_employee_id   integer  optional    

The employee making the decision. Required unless acting_user_email is given. Example: 12

acting_user_email   string  optional    

The email of the account deciding. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_decide   boolean  optional    

The person holds the permission to decide leave requests in your product, so they may decide one assigned to somebody else. Example: true

Delete a leave request

requires authentication

The same rule as deleting any other request (see DELETE /requests/{id}): only while the leave is undecided, by its requester (named by their employee file alone if that is all they have) or by HR. An approved or rejected leave is a record the system keeps and cannot be deleted.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/leave-requests/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 88,
    \"acting_user_email\": \"hr@school.sa\",
    \"acting_as_tenant_admin\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/leave-requests/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 88,
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (204, deleted):

Empty response
 

Example response (422, already decided):


{
    "message": "لا يمكن حذف طلب معتمد أو مرفوض — يبقى في سجلّات النظام.",
    "errors": {
        "request": [
            "لا يمكن حذف طلب معتمد أو مرفوض — يبقى في سجلّات النظام."
        ]
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/leave-requests/{leaveRequest}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

leaveRequest   integer     

Example: 16

Body Parameters

acting_employee_id   integer  optional    

The employee deleting. Required unless acting_user_email is given. Example: 88

acting_user_email   string  optional    

The email of the account deleting. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

List attendance records

requires authentication

Minutes, not times, are the numbers to trust here: late_minutes, early_leave_minutes and worked_minutes are computed against the employee's shift and its grace window, which a consumer cannot rederive from the raw punches without reimplementing the same rules.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/attendance-records?employee_id=88&from=2026-08-01&to=2026-08-31&per_page=50" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"from\": \"2026-10-25T09:50:17\",
    \"to\": \"2026-10-25T09:50:17\",
    \"employee_id\": 16,
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/attendance-records"
);

const params = {
    "employee_id": "88",
    "from": "2026-08-01",
    "to": "2026-08-31",
    "per_page": "50",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "from": "2026-10-25T09:50:17",
    "to": "2026-10-25T09:50:17",
    "employee_id": 16,
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 501,
            "type": "attendance-records",
            "attributes": {
                "employee_id": 88,
                "employee": {
                    "id": 88,
                    "name_ar": "سعيد الغامدي",
                    "name_en": "Saeed Alghamdi",
                    "employee_number": "E-100"
                },
                "date": "2026-08-03",
                "check_in_at": "2026-08-03T04:58:00+00:00",
                "check_out_at": "2026-08-03T12:04:00+00:00",
                "check_in_2_at": null,
                "check_out_2_at": null,
                "status": "present",
                "late_minutes": 0,
                "early_leave_minutes": 0,
                "worked_minutes": 426
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/attendance-records

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

employee_id   integer  optional    

Example: 88

from   string  optional    

Y-m-d. Example: 2026-08-01

to   string  optional    

Y-m-d. Example: 2026-08-31

per_page   integer  optional    

Default 50, max 200. Example: 50

Body Parameters

from   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

to   string  optional    

value ليس تاريخًا صحيحًا. Example: 2026-10-25T09:50:17

employee_id   integer  optional    

Example: 16

per_page   integer  optional    

Example: 16

The day's follow-up board

requires authentication

One row per person expected today — employees and, unless you opt out, trainees — whether or not they punched. This is the register AS THE BOARD READS IT: kind is the day's verdict (present · late · early · incomplete · absent · on_leave · holiday · rest · off · unscheduled), and the counters are the same tallies the screen shows above its table.

A PUBLIC HOLIDAY covering the person's branch makes the day holiday — never absent — with nothing scheduled. holiday (bool) and holiday_name_ar / holiday_name_en say so on every row, including one where somebody punched anyway: the punch outranks the holiday for kind (it reads unscheduled), but the day is still the holiday.

Read this rather than deriving a board from attendance-records. The rules that decide a day — the shift and its grace window, a per-date schedule override, a rest day, an approved leave outranking an absence while a PENDING one does not, a trainee only being expected inside their training period — all live behind this endpoint. A consumer that rebuilds them disagrees with the HR screen about the same person on the same day, and nothing reports it.

Times are Asia/Riyadh "HH:MM". diff is worked minus scheduled minutes and is null while a day is unfinished — an open day is not a shortfall. open_minutes is the running total for somebody still at work.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/attendance/follow-up?date=2026-09-21&department_id=4&shift_id=2&include_trainees=1" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"date\": \"2026-10-25\",
    \"department_id\": 16,
    \"shift_id\": 16,
    \"include_trainees\": false
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/attendance/follow-up"
);

const params = {
    "date": "2026-09-21",
    "department_id": "4",
    "shift_id": "2",
    "include_trainees": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "date": "2026-10-25",
    "department_id": 16,
    "shift_id": 16,
    "include_trainees": false
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "subject_id": 88,
            "is_trainee": false,
            "name_ar": "سعيد الغامدي",
            "name_en": "Saeed Alghamdi",
            "number": "E-100",
            "role_label": "محاسب",
            "department_ar": "المالية",
            "department_en": "Finance",
            "kind": "late",
            "leave_label": null,
            "pending_leave_label": null,
            "early_in": false,
            "late": true,
            "late_minutes": 14,
            "rest_day": false,
            "holiday": false,
            "holiday_name_ar": null,
            "holiday_name_en": null,
            "punches": 2,
            "check_in": "08:14",
            "check_out": "16:02",
            "check_in_2": null,
            "check_out_2": null,
            "in_corrected": false,
            "out_corrected": false,
            "scheduled_start": "08:00",
            "scheduled_end": "16:00",
            "second_start": null,
            "second_end": null,
            "overridden": false,
            "scheduled_minutes": 480,
            "shift_label": "الدوام الصباحي",
            "shift_color": "#7d55c7",
            "timed": true,
            "working_day": true,
            "worked": 468,
            "open_minutes": null,
            "diff": -12,
            "deduction": 14
        }
    ],
    "meta": {
        "date": "2026-09-21",
        "total": 1,
        "counts": {
            "late": 1,
            "early_in": 0,
            "early": 0,
            "absent": 0,
            "on_leave": 0,
            "incomplete": 0,
            "unscheduled": 0,
            "holiday": 0
        },
        "footer": {
            "no_shift": 0,
            "rest": 0,
            "absent": 0,
            "on_leave": 0,
            "holiday": 0
        }
    }
}
 

Request      

GET api/internal/companies/{company_id}/attendance/follow-up

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

date   string  optional    

Y-m-d, Asia/Riyadh. Defaults to today. Example: 2026-09-21

department_id   integer  optional    

Example: 4

shift_id   integer  optional    

Example: 2

include_trainees   boolean  optional    

Default true. Example: true

Body Parameters

date   string  optional    

Must be a valid date in the format Y-m-d. Example: 2026-10-25

department_id   integer  optional    

Example: 16

shift_id   integer  optional    

Example: 16

include_trainees   boolean  optional    

Example: false

Export the attendance report (PDF or Excel)

requires authentication

The same register the platform's own «تصدير» button prints — A4 landscape, the four-figure summary (employees · present · absent · late) and one row per record — rendered from the stored records, never recomputed. Staff only: trainees keep a register of their own.

The figures are the ones the attendance service already wrote, so a report downloaded here and one downloaded from the HR screen are the same paper for the same filters.

Deciding WHO may export is the caller's job: a service key names a product, not a person. Pass manager_user_id to limit the report to the people a manager may read (their reports and themselves).

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/attendance/export?format=pdf&from=2026-10-01&to=2026-10-31&department_id=3&branch_id=2&shift_id=1&employee_id=88&status=late&manager_user_id=12&group=employee" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"format\": \"architecto\",
    \"from\": \"2026-10-25\",
    \"to\": \"2052-11-17\",
    \"department_id\": 16,
    \"branch_id\": 16,
    \"shift_id\": 16,
    \"employee_id\": 16,
    \"manager_user_id\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/attendance/export"
);

const params = {
    "format": "pdf",
    "from": "2026-10-01",
    "to": "2026-10-31",
    "department_id": "3",
    "branch_id": "2",
    "shift_id": "1",
    "employee_id": "88",
    "status": "late",
    "manager_user_id": "12",
    "group": "employee",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "format": "architecto",
    "from": "2026-10-25",
    "to": "2052-11-17",
    "department_id": 16,
    "branch_id": 16,
    "shift_id": 16,
    "employee_id": 16,
    "manager_user_id": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


The report file (application/pdf or an .xlsx workbook).
 

Example response (422):


{
    "message": "The to field must be a date after or equal to from.",
    "errors": {
        "to": [
            "The to field must be a date after or equal to from."
        ]
    }
}
 

Request      

GET api/internal/companies/{company_id}/attendance/export

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

format   string     

pdf or xlsx. Example: pdf

from   string     

First day, Y-m-d. Example: 2026-10-01

to   string     

Last day, Y-m-d (at most 366 days after from). Example: 2026-10-31

department_id   integer  optional    

Only this department. Example: 3

branch_id   integer  optional    

Only this branch. Example: 2

shift_id   integer  optional    

Only employees on this shift template. Example: 1

employee_id   integer  optional    

Only this employee. Example: 88

status   string  optional    

One of present, late, absent, on_leave, mission, remote. Example: late

manager_user_id   integer  optional    

Limit to the people this manager's account may read. Example: 12

group   string  optional    

Layout of a multi-day report: day (each day on its own, default) or employee (each employee's period on its own). Example: employee

Body Parameters

format   string     

Example: architecto

from   string     

Must be a valid date in the format Y-m-d. Example: 2026-10-25

to   string     

Must be a valid date in the format Y-m-d. يجب أن يكون value تاريخًا بعد أو يساوي from. Example: 2052-11-17

department_id   integer  optional    

Example: 16

branch_id   integer  optional    

Example: 16

shift_id   integer  optional    

Example: 16

employee_id   integer  optional    

Example: 16

status   string  optional    
group   string  optional    
manager_user_id   integer  optional    

Example: 16

Check an employee in

requires authentication

Times are computed in Asia/Riyadh and stored UTC. Lateness is measured against the shift's start plus its grace window, so an on-time arrival inside grace records zero late minutes rather than a small positive number.

latitude/longitude are validated against the zone that applies to the employee — their branch's, or, for somebody attached to no branch, the nearest zone their company enforces; a check-in outside it is refused with a 422. Send them whenever the device can supply them — a branch may have enforcement enabled without your product knowing.

Send accuracy too — the fix's own uncertainty in metres, as the device reports it. A fix coarser than both the zone and 500 m (a phone with «precise location» switched off) cannot say whether anybody is inside a small circle, so it is refused with a sentence that says to turn precise location on, rather than a confident «outside».

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/attendance/check-in" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 88,
    \"latitude\": 24.7136,
    \"longitude\": 46.6753,
    \"accuracy\": 12.5
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/attendance/check-in"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 88,
    "latitude": 24.7136,
    "longitude": 46.6753,
    "accuracy": 12.5
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 501,
        "type": "attendance-records",
        "attributes": {
            "employee_id": 88,
            "date": "2026-08-03",
            "check_in_at": "2026-08-03T04:58:00+00:00",
            "check_out_at": null,
            "check_in_2_at": null,
            "check_out_2_at": null,
            "status": "present",
            "late_minutes": 0,
            "early_leave_minutes": 0,
            "worked_minutes": 0
        }
    }
}
 

Example response (422, outside the branch geofence):


{
    "message": "...",
    "errors": {
        "attendance": [
            "أنت خارج النطاق."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/attendance/check-in

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Example: 88

latitude   number  optional    

Example: 24.7136

longitude   number  optional    

Example: 46.6753

accuracy   number  optional    

The fix's uncertainty in metres. Example: 12.5

Check an employee out

requires authentication

Closes the day's record and computes worked_minutes and any early leave. Checking out with no open check-in is a 422 rather than a silently created half-record.

Measured against the same zone as check-in, with the same accuracy rule: a self-service punch claims «I am at work now» at either end of the day.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/attendance/check-out" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 88,
    \"latitude\": 24.7136,
    \"longitude\": 46.6753,
    \"accuracy\": 12.5
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/attendance/check-out"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 88,
    "latitude": 24.7136,
    "longitude": 46.6753,
    "accuracy": 12.5
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 501,
        "type": "attendance-records",
        "attributes": {
            "employee_id": 88,
            "date": "2026-08-03",
            "check_in_at": "2026-08-03T04:58:00+00:00",
            "check_out_at": "2026-08-03T12:04:00+00:00",
            "check_in_2_at": null,
            "check_out_2_at": null,
            "status": "present",
            "late_minutes": 0,
            "early_leave_minutes": 0,
            "worked_minutes": 426
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/attendance/check-out

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Example: 88

latitude   number  optional    

Example: 24.7136

longitude   number  optional    

Example: 46.6753

accuracy   number  optional    

The fix's uncertainty in metres. Example: 12.5

List payroll runs

requires authentication

Read-only, deliberately: creating or closing a run from a service key would be a financial write attributed to no person. Closing stays in the platform, where the permissions and the activity log are.

status moves draft → review → approved → closed, and a closed run is immutable. Totals are in halalas.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/payroll-runs?status=closed&year=2026&per_page=12" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"architecto\",
    \"year\": 22,
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs"
);

const params = {
    "status": "closed",
    "year": "2026",
    "per_page": "12",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "status": "architecto",
    "year": 22,
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 9,
            "type": "payroll-runs",
            "attributes": {
                "period": "2026-07",
                "year": 2026,
                "month": 7,
                "status": "closed",
                "editable": false,
                "allowed_transitions": [],
                "can_reopen": true,
                "total_gross": 168000000,
                "total_deductions": 14600000,
                "total_net": 153400000,
                "employees_count": 42
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/payroll-runs

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

status   string  optional    

draft, review, approved or closed. Example: closed

year   integer  optional    

Example: 2026

per_page   integer  optional    

Default 12, max 200. Example: 12

Body Parameters

status   string  optional    

Example: architecto

year   integer  optional    

يجب ألا يقل value عن 2000. يجب ألا يكون value أكبر من 2100. Example: 22

per_page   integer  optional    

Example: 16

Create a payroll run for a month and build its lines.

requires authentication

The run is built from attendance, approved unpaid leave and the month's salary adjustments by PayrollService — the same code the platform's own screen calls, so two products can never compute one payslip differently. A run already existing for that month is refused rather than replaced: rebuilding is recalculate, and a second run for one month is two answers to what somebody was paid.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"year\": 2026,
    \"month\": 7,
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_run_payroll\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "year": 2026,
    "month": 7,
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true,
    "acting_may_run_payroll": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 9,
        "type": "payroll-runs",
        "attributes": {
            "period": "2026-07",
            "year": 2026,
            "month": 7,
            "status": "draft",
            "editable": true,
            "allowed_transitions": [
                "review"
            ],
            "can_reopen": false,
            "total_gross": 168000000,
            "total_deductions": 14600000,
            "total_net": 153400000,
            "employees_count": 42,
            "items": [
                {
                    "employee_id": 88,
                    "employee_name_ar": "سعيد",
                    "employee_name_en": "Saeed",
                    "employee_number": "EMP-0088",
                    "basic_salary": 800000,
                    "housing_allowance": 200000,
                    "transport_allowance": 50000,
                    "other_allowances": 0,
                    "overtime_amount": 0,
                    "extra_earnings": 0,
                    "gosi_employee": 97500,
                    "absence_deduction": 0,
                    "late_deduction": 0,
                    "unpaid_leave_deduction": 0,
                    "extra_deductions": 0,
                    "other_allowances_total": 50000,
                    "absence_late_deduction": 0,
                    "gross": 1050000,
                    "total_deductions": 97500,
                    "net": 952500
                }
            ]
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/payroll-runs

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

year   integer     

Example: 2026

month   integer     

1-12. Example: 7

acting_employee_id   integer  optional    

The person creating it — required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_run_payroll   boolean  optional    

Send true when the person acting holds YOUR product's permission to operate payroll. It counts here as it counts there: the write is accepted even when their account here holds no payroll role, and somebody with no login is named by acting_employee_id alone (an account is made for them so the write has somebody to stand behind it). Example: true

Show a payroll run

requires authentication

The run with a line per employee: gross, the GOSI deduction, other earnings and deductions, and net — all in halalas.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 9,
        "type": "payroll-runs",
        "attributes": {
            "period": "2026-07",
            "year": 2026,
            "month": 7,
            "status": "closed",
            "editable": false,
            "allowed_transitions": [],
            "can_reopen": true,
            "total_gross": 168000000,
            "total_deductions": 14600000,
            "total_net": 153400000,
            "employees_count": 42,
            "items": [
                {
                    "employee_id": 88,
                    "employee_name_ar": "سعيد",
                    "employee_name_en": "Saeed",
                    "employee_number": "EMP-0088",
                    "basic_salary": 800000,
                    "housing_allowance": 200000,
                    "transport_allowance": 50000,
                    "other_allowances": 0,
                    "overtime_amount": 0,
                    "extra_earnings": 0,
                    "gosi_employee": 97500,
                    "absence_deduction": 0,
                    "late_deduction": 0,
                    "unpaid_leave_deduction": 0,
                    "extra_deductions": 0,
                    "other_allowances_total": 50000,
                    "absence_late_deduction": 0,
                    "gross": 1050000,
                    "total_deductions": 97500,
                    "net": 952500
                }
            ]
        }
    }
}
 

Request      

GET api/internal/companies/{company_id}/payroll-runs/{payrollRun}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

payrollRun   integer     

Example: 16

Rebuild an editable run from what attendance and adjustments say today.

requires authentication

Refused on a run past review — the platform refuses it too, and the refusal comes from PayrollService, not from a copy of the rule here.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/recalculate" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_run_payroll\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/recalculate"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true,
    "acting_may_run_payroll": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 9,
        "type": "payroll-runs",
        "attributes": {
            "period": "2026-07",
            "year": 2026,
            "month": 7,
            "status": "draft",
            "editable": true,
            "allowed_transitions": [
                "review"
            ],
            "can_reopen": false,
            "total_gross": 168000000,
            "total_deductions": 14600000,
            "total_net": 153400000,
            "employees_count": 42,
            "items": [
                {
                    "employee_id": 88,
                    "employee_name_ar": "سعيد",
                    "employee_name_en": "Saeed",
                    "employee_number": "EMP-0088",
                    "basic_salary": 800000,
                    "housing_allowance": 200000,
                    "transport_allowance": 50000,
                    "other_allowances": 0,
                    "overtime_amount": 0,
                    "extra_earnings": 0,
                    "gosi_employee": 97500,
                    "absence_deduction": 0,
                    "late_deduction": 0,
                    "unpaid_leave_deduction": 0,
                    "extra_deductions": 0,
                    "other_allowances_total": 50000,
                    "absence_late_deduction": 0,
                    "gross": 1050000,
                    "total_deductions": 97500,
                    "net": 952500
                }
            ]
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/payroll-runs/{payrollRun}/recalculate

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

payrollRun   integer     

Example: 16

Body Parameters

acting_employee_id   integer  optional    

Required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_run_payroll   boolean  optional    

Send true when the person acting holds YOUR product's permission to operate payroll. It counts here as it counts there: the write is accepted even when their account here holds no payroll role, and somebody with no login is named by acting_employee_id alone (an account is made for them so the write has somebody to stand behind it). Example: true

Move a run along its state machine: draft → review → approved → closed.

requires authentication

reopen is the one way back, from closed to approved, and it is a separate right (PayrollRunPolicy::reopen) because it un-finalises pay that has already been published — the employee's payslips disappear until the run is closed again.

The order itself is enforced by PayrollStatus::canTransitionTo, so a skipped stage is refused here for exactly the reason it is refused on screen.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/transition" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"approved\",
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_run_payroll\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/transition"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "status": "approved",
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true,
    "acting_may_run_payroll": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 9,
        "type": "payroll-runs",
        "attributes": {
            "period": "2026-07",
            "year": 2026,
            "month": 7,
            "status": "approved",
            "editable": false,
            "allowed_transitions": [
                "review",
                "closed"
            ],
            "can_reopen": false,
            "total_gross": 168000000,
            "total_deductions": 14600000,
            "total_net": 153400000,
            "employees_count": 42,
            "items": [
                {
                    "employee_id": 88,
                    "employee_name_ar": "سعيد",
                    "employee_name_en": "Saeed",
                    "employee_number": "EMP-0088",
                    "basic_salary": 800000,
                    "housing_allowance": 200000,
                    "transport_allowance": 50000,
                    "other_allowances": 0,
                    "overtime_amount": 0,
                    "extra_earnings": 0,
                    "gosi_employee": 97500,
                    "absence_deduction": 0,
                    "late_deduction": 0,
                    "unpaid_leave_deduction": 0,
                    "extra_deductions": 0,
                    "other_allowances_total": 50000,
                    "absence_late_deduction": 0,
                    "gross": 1050000,
                    "total_deductions": 97500,
                    "net": 952500
                }
            ]
        }
    }
}
 

Request      

POST api/internal/companies/{company_id}/payroll-runs/{payrollRun}/transition

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

payrollRun   integer     

Example: 16

Body Parameters

status   string     

review, approved, closed or reopen. Example: approved

acting_employee_id   integer  optional    

Required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_run_payroll   boolean  optional    

Send true when the person acting holds YOUR product's permission to operate payroll. It counts here as it counts there: the write is accepted even when their account here holds no payroll role, and somebody with no login is named by acting_employee_id alone (an account is made for them so the write has somebody to stand behind it). Example: true

Download the payroll register (كشف الرواتب) as a PDF

requires authentication

The same document the platform's own screen prints, rendered by the same view and carrying the company stamp when one is uploaded — so a register produced from a consuming product and one produced here are the same paper, not two documents that happen to share a heading.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/register" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/register"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


The register PDF (application/pdf).
 

Example response (404):


{
    "message": "Payroll run not found in this company."
}
 

Request      

GET api/internal/companies/{company_id}/payroll-runs/{payrollRun}/register

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

payrollRun   integer     

Example: 16

Download the WPS (Mudad) bank file for the run

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/wps" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/wps"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


The wage-protection file (text/csv).
 

Example response (404):


{
    "message": "Payroll run not found in this company."
}
 

Request      

GET api/internal/companies/{company_id}/payroll-runs/{payrollRun}/wps

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

payrollRun   integer     

Example: 16

Download one line's payslip as a PDF

requires authentication

Addressed by the run and the EMPLOYEE, not the payroll-item id: that id is an internal row number a consumer has no other reason to hold, and the employee is what its screen already knows.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/payslip/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-runs/16/payslip/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


The payslip PDF (application/pdf).
 

Example response (404):


{
    "message": "No payslip for that employee in this run."
}
 

Request      

GET api/internal/companies/{company_id}/payroll-runs/{payrollRun}/payslip/{employee}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

payrollRun   integer     

Example: 16

employee   string     

Example: architecto

The company stamp printed on the register

requires authentication

Metadata, not the image: whether one is on file. The picture itself is served by payroll-stamp/image, because a consumer asking «is there a stamp» to decide which button to draw should not be handed a file it did not ask for.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/payroll-stamp" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-stamp"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "has_stamp": true
    }
}
 

Request      

GET api/internal/companies/{company_id}/payroll-stamp

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Download the company stamp image

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/payroll-stamp/image" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-stamp/image"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


The stamp image.
 

Example response (404):


{
    "message": "No stamp on file."
}
 

Request      

GET api/internal/companies/{company_id}/payroll-stamp/image

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Upload or replace the company stamp

requires authentication

A single file: a second stamp with nothing saying which is current is worse than one that is wrong, so this replaces rather than adds.

Authorised as the acting person against the same right that operates a run — the stamp goes onto a document the company signs, so it is not a lesser decision than building the register it sits on.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/payroll-stamp" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "acting_employee_id=3"\
    --form "acting_user_email=hr@example.com"\
    --form "acting_as_tenant_admin=1"\
    --form "acting_may_run_payroll=1"\
    --form "stamp=@/tmp/php4u1i73mbmpc44H3MQXB" 
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-stamp"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('acting_employee_id', '3');
body.append('acting_user_email', 'hr@example.com');
body.append('acting_as_tenant_admin', '1');
body.append('acting_may_run_payroll', '1');
body.append('stamp', document.querySelector('input[name="stamp"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "has_stamp": true
    }
}
 

Request      

POST api/internal/companies/{company_id}/payroll-stamp

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

stamp   file     

The stamp image — PNG or JPG up to 2 MB, multipart only. Replaces the current one. Example: /tmp/php4u1i73mbmpc44H3MQXB

acting_employee_id   integer  optional    

Required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_run_payroll   boolean  optional    

Send true when the person acting holds YOUR product's permission to operate payroll. It counts here as it counts there: the write is accepted even when their account here holds no payroll role, and somebody with no login is named by acting_employee_id alone (an account is made for them so the write has somebody to stand behind it). Example: true

Remove the company stamp

requires authentication

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/payroll-stamp" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_run_payroll\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/payroll-stamp"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true,
    "acting_may_run_payroll": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "has_stamp": false
    }
}
 

Request      

DELETE api/internal/companies/{company_id}/payroll-stamp

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

acting_employee_id   integer  optional    

Required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_run_payroll   boolean  optional    

Send true when the person acting holds YOUR product's permission to operate payroll. It counts here as it counts there: the write is accepted even when their account here holds no payroll role, and somebody with no login is named by acting_employee_id alone (an account is made for them so the write has somebody to stand behind it). Example: true

The month's one-off earnings and deductions.

requires authentication

Sent with counted, which is the question a screen actually asks: an adjustment only reaches the payslip once its own approval chain is complete, so «recorded» and «in the run» are different states and a consumer that shows only the amount cannot tell them apart.

And with where that chain stands — status (pending · approved · rejected), current_approver / current_step / total_steps and rejection_reason — because «بانتظار الاعتماد» on its own names nobody, and the person reading it is left asking the one question it exists to answer: waiting on whom, and where do they decide it.

«اعتماداتي»: approver_employee_id / approver_email / approver_as_tenant_admin (with approver_may_decide and awaiting) narrow the list exactly as they narrow requests and leave-requests — one rule, so an approvals tab over all three holds what the decision endpoints accept. meta then says approver_resolved and approver_scope.

Name who is LOOKING (acting_employee_id / acting_user_email / acting_as_tenant_admin, with acting_may_decide) and every row also says can_decide — the rule POST salary-adjustments/{id}/act enforces.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/salary-adjustments?year=2026&month=7&employee_id=88&approver_employee_id=42&approver_email=sara%40school.sa&approver_as_tenant_admin=1&approver_may_decide=1&awaiting=1&acting_employee_id=42&acting_user_email=sara%40school.sa&acting_as_tenant_admin=1&acting_may_decide=1" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"year\": 1,
    \"month\": 4,
    \"employee_id\": 16,
    \"approver_employee_id\": 16,
    \"approver_email\": \"zbailey@example.net\",
    \"approver_as_tenant_admin\": true,
    \"approver_may_decide\": false,
    \"awaiting\": false
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/salary-adjustments"
);

const params = {
    "year": "2026",
    "month": "7",
    "employee_id": "88",
    "approver_employee_id": "42",
    "approver_email": "sara@school.sa",
    "approver_as_tenant_admin": "1",
    "approver_may_decide": "1",
    "awaiting": "1",
    "acting_employee_id": "42",
    "acting_user_email": "sara@school.sa",
    "acting_as_tenant_admin": "1",
    "acting_may_decide": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "year": 1,
    "month": 4,
    "employee_id": 16,
    "approver_employee_id": 16,
    "approver_email": "zbailey@example.net",
    "approver_as_tenant_admin": true,
    "approver_may_decide": false,
    "awaiting": false
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 41,
            "employee_id": 88,
            "employee_name_ar": "سعيد",
            "employee_name_en": "Saeed",
            "employee_number": "1042",
            "type": "earning",
            "reason": "مكافأة أداء",
            "amount": 50000,
            "year": 2026,
            "month": 7,
            "counted": false,
            "status": "pending",
            "submitted_at": "2026-07-12T08:30:00+00:00",
            "rejection_reason": null,
            "current_approver": {
                "id": 7,
                "name": "سارة القحطاني",
                "employee_id": 42,
                "email": "sara@school.sa"
            },
            "current_step": 1,
            "total_steps": 2
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/salary-adjustments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

year   integer  optional    

Example: 2026

month   integer  optional    

Example: 7

employee_id   integer  optional    

Example: 88

approver_employee_id   integer  optional    

Only lines whose chain has a step for this person (and, with approver_may_decide, every line still awaiting a decision). Example: 42

approver_email   string  optional    

The same person by the email of their account. Example: sara@school.sa

approver_as_tenant_admin   boolean  optional    

The approver is the tenant's administrator in your product. Example: true

approver_may_decide   boolean  optional    

The approver holds your product's permission to decide these. Example: true

awaiting   boolean  optional    

Only what the approver can decide right now. Example: true

acting_employee_id   integer  optional    

Who is looking — each row then says can_decide. Example: 42

acting_user_email   string  optional    

Who is looking, by account email. Example: sara@school.sa

acting_as_tenant_admin   boolean  optional    

The person looking administers the tenant in your product. Example: true

acting_may_decide   boolean  optional    

The person looking holds your product's permission to decide these. Example: true

Body Parameters

year   integer  optional    

يجب ألا يقل value عن 2000. يجب ألا يكون value أكبر من 2100. Example: 1

month   integer  optional    

يجب ألا يقل value عن 1. يجب ألا يكون value أكبر من 12. Example: 4

employee_id   integer  optional    

Example: 16

approver_employee_id   integer  optional    

Example: 16

approver_email   string  optional    

يجب ألا يزيد طول value عن 191 حرفًا. Example: zbailey@example.net

approver_as_tenant_admin   boolean  optional    

Example: true

approver_may_decide   boolean  optional    

Example: false

awaiting   boolean  optional    

Example: false

Add a one-off earning or deduction to a month.

requires authentication

It starts its own approval chain, exactly as the payroll screen does, so an adjustment counts toward a payslip only once somebody has approved it. With no chain configured it counts at once — the company decided that by not configuring one.

Amounts are halalas.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/salary-adjustments" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"employee_id\": 88,
    \"type\": \"earning\",
    \"amount\": 50000,
    \"reason\": \"مكافأة أداء\",
    \"year\": 2026,
    \"month\": 7,
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_run_payroll\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/salary-adjustments"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "employee_id": 88,
    "type": "earning",
    "amount": 50000,
    "reason": "مكافأة أداء",
    "year": 2026,
    "month": 7,
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true,
    "acting_may_run_payroll": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 41,
        "employee_id": 88,
        "employee_name_ar": "سعيد",
        "employee_name_en": "Saeed",
        "employee_number": "1042",
        "type": "earning",
        "reason": "مكافأة أداء",
        "amount": 50000,
        "year": 2026,
        "month": 7,
        "counted": false,
        "status": "pending",
        "submitted_at": "2026-07-12T08:30:00+00:00",
        "rejection_reason": null,
        "current_approver": {
            "id": 7,
            "name": "سارة القحطاني",
            "employee_id": 42,
            "email": "sara@school.sa"
        },
        "current_step": 1,
        "total_steps": 2
    }
}
 

Request      

POST api/internal/companies/{company_id}/salary-adjustments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Body Parameters

employee_id   integer     

Example: 88

type   string     

earning or deduction. Example: earning

amount   integer     

Halalas, above zero. Example: 50000

reason   string     

Example: مكافأة أداء

year   integer     

Example: 2026

month   integer     

Example: 7

acting_employee_id   integer  optional    

Required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_run_payroll   boolean  optional    

Send true when the person acting holds YOUR product's permission to operate payroll. It counts here as it counts there: the write is accepted even when their account here holds no payroll role, and somebody with no login is named by acting_employee_id alone (an account is made for them so the write has somebody to stand behind it). Example: true

Remove an adjustment.

requires authentication

Refused once the run for its month is closed: a closed run has been paid out, and deleting a line behind it would make the file disagree with the bank.

Example request:
curl --request DELETE \
    "https://hr.getokta.io/api/internal/companies/16/salary-adjustments/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"acting_employee_id\": 3,
    \"acting_user_email\": \"hr@example.com\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_run_payroll\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/salary-adjustments/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "acting_employee_id": 3,
    "acting_user_email": "hr@example.com",
    "acting_as_tenant_admin": true,
    "acting_may_run_payroll": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (204):

Empty response
 

Request      

DELETE api/internal/companies/{company_id}/salary-adjustments/{adjustment}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

adjustment   string     

Example: architecto

Body Parameters

acting_employee_id   integer  optional    

Required unless acting_user_email is sent. Example: 3

acting_user_email   string  optional    

Required unless acting_employee_id is sent. Example: hr@example.com

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. The write goes through their own account when a name you also sent reaches an HR manager here, and through the company's administrator account (its first HR manager) otherwise — so the administrator is never refused for lacking an email or a login. Example: true

acting_may_run_payroll   boolean  optional    

Send true when the person acting holds YOUR product's permission to operate payroll. It counts here as it counts there: the write is accepted even when their account here holds no payroll role, and somebody with no login is named by acting_employee_id alone (an account is made for them so the write has somebody to stand behind it). Example: true

Decide a salary adjustment

requires authentication

Approve or reject the step the line's approval chain is waiting on. Until now the only place to do that was the HR platform's own approvals screen, so a consuming product could file a line, show it «بانتظار الاعتماد», and offer nowhere to decide it.

The same rule as every other decision here (see DecisionRule): the assigned approver decides their own step; an HR manager of this company, or somebody the product says holds the decision permission (acting_may_decide), may decide it in their place — recorded as such, the chain still naming who it was routed to. A line that pays the person deciding is never theirs to decide. A rejection needs a reason.

The run follows the decision: once the last step approves, the person's line in every editable run of that month is rebuilt, so the amount counts without anybody pressing «إعادة الاحتساب».

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/salary-adjustments/564/act" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"decision\": \"approve\",
    \"notes\": \"خارج سياسة المكافآت\",
    \"acting_employee_id\": 42,
    \"acting_user_email\": \"sara@school.sa\",
    \"acting_as_tenant_admin\": true,
    \"acting_may_decide\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/salary-adjustments/564/act"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "decision": "approve",
    "notes": "خارج سياسة المكافآت",
    "acting_employee_id": 42,
    "acting_user_email": "sara@school.sa",
    "acting_as_tenant_admin": true,
    "acting_may_decide": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 41,
        "employee_id": 88,
        "employee_name_ar": "سعيد",
        "employee_name_en": "Saeed",
        "employee_number": "1042",
        "type": "earning",
        "reason": "مكافأة أداء",
        "amount": 50000,
        "year": 2026,
        "month": 7,
        "counted": true,
        "status": "approved",
        "submitted_at": "2026-07-12T08:30:00+00:00",
        "rejection_reason": null,
        "current_approver": null,
        "current_step": null,
        "total_steps": 1,
        "can_decide": false
    }
}
 

Example response (422, not their step):


{
    "message": "ليس دورك في سلسلة الاعتماد.",
    "errors": {
        "acting_employee_id": [
            "ليس دورك في سلسلة الاعتماد."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/salary-adjustments/{adjustment}/act

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

adjustment   string     

Example: 564

Body Parameters

decision   string     

approve or reject. Example: approve

notes   string  optional    

Required when rejecting. Example: خارج سياسة المكافآت

acting_employee_id   integer  optional    

The employee deciding. Required unless acting_user_email or acting_as_tenant_admin is sent. Example: 42

acting_user_email   string  optional    

The email of the account deciding. Example: sara@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. Example: true

acting_may_decide   boolean  optional    

The person deciding holds your product's permission to decide these, so they may decide a step routed to somebody else. Example: true

List an employee's payslips

requires authentication

Only lines from closed runs. A draft or in-review line is still moving, and a consuming product that shows it invites "why is my salary different from what the app said last week" — to which the honest answer is that it was never final.

Amounts are halalas.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/payslips" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 16
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/payslips"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 310,
            "type": "payslips",
            "attributes": {
                "period": "2026-07",
                "basic_salary": 800000,
                "housing_allowance": 200000,
                "transport_allowance": 50000,
                "other_allowances": 0,
                "overtime_amount": 0,
                "extra_earnings": 0,
                "absence_deduction": 0,
                "late_deduction": 0,
                "unpaid_leave_deduction": 0,
                "extra_deductions": 0,
                "gosi_employee": 97500,
                "gross": 1050000,
                "total_deductions": 97500,
                "net": 952500
            }
        }
    ]
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/payslips

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Body Parameters

per_page   integer  optional    

Example: 16

The employee's full personal cost

requires authentication

One answer to "what does this person cost?": the contractual wage, the current month's movement as payroll computed it (or a contract-only estimate when no run has touched the month — month.source says which), the employer's additions (GOSI share, the month's end-of-service accrual per art. 84), the accrued end-of-service liability to date, and the last twelve months of employer cost. A month with no run is cost: null, not an invented figure.

month carries the wage components of the line it reports — basic_salary, housing_allowance, transport_allowance, other_allowances — the run's own copy when a run holds the month, the contract's when it is an estimate. Read them rather than contract: a closed month keeps what it paid after the salary moves.

Money in halalas everywhere, like the rest of this API.

month re-anchors the whole report to a past month — the consuming app's month arrows send the period.key shape straight back, so the device never does date arithmetic of its own. A future month is refused: there is nothing honest to report about it.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/employees/16/cost?month=2026-07" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"month\": \"6425-(03\"
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/employees/16/cost"
);

const params = {
    "month": "2026-07",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "month": "6425-(03"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "as_of": "2026-08-27",
        "period": {
            "year": 2026,
            "month": 8,
            "key": "2026-08"
        },
        "contract": {
            "basic": 900000,
            "housing": 225000,
            "transport": 80000,
            "other": 40000,
            "total": 1245000,
            "contributory": 1125000,
            "is_saudi": true
        },
        "month": {
            "source": "run",
            "run_id": 12,
            "run_status": "closed",
            "basic_salary": 900000,
            "housing_allowance": 225000,
            "transport_allowance": 80000,
            "other_allowances": 40000,
            "overtime_amount": 33750,
            "extra_earnings": 18000,
            "absence_deduction": 41500,
            "late_deduction": 8213,
            "unpaid_leave_deduction": 0,
            "extra_deductions": 30000,
            "gosi_employee": 109688,
            "gosi_employer": 132188,
            "gross": 1296750,
            "total_deductions": 189401,
            "net": 1107349,
            "absent_days": 1,
            "late_minutes": 95,
            "overtime_minutes": 360
        },
        "employer": {
            "gosi_employer": 132188,
            "eos_monthly_accrual": 51875,
            "monthly_cost": 1480813,
            "annual_cost": 17769756,
            "day_cost": 49360,
            "burden_ratio": 0.1419,
            "wedge_ratio": 0.3372
        },
        "eos": {
            "service_years": 3.99,
            "accrued": 2483775,
            "first_five": 2483775,
            "beyond_five": 0
        },
        "months": [
            {
                "key": "2025-09",
                "year": 2025,
                "month": 9,
                "cost": 1394000,
                "gross": 1230000,
                "net": 1080000
            }
        ]
    }
}
 

Request      

GET api/internal/companies/{company_id}/employees/{employee}/cost

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

employee   integer     

The employee. Example: 16

Query Parameters

month   string  optional    

A past or current month, YYYY-MM. Example: 2026-07

Body Parameters

month   string  optional    

Must match the regex /^\d{4}-(0[1-9]. Example: 6425-(03

Company statistics

requires authentication

Everything a consuming product's landing screen needs, in one call — so a dashboard is one request rather than eight, and every product shows the same numbers because they are counted in one place.

Counts only: anything a user can click through to has its own endpoint.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/stats?expiry_horizon_days=60" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"expiry_horizon_days\": 1
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/stats"
);

const params = {
    "expiry_horizon_days": "60",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "expiry_horizon_days": 1
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": "7",
        "type": "company-stats",
        "attributes": {
            "headcount": 42,
            "employees_by_status": {
                "active": 40,
                "resigned": 2
            },
            "attendance_today": {
                "present": 38,
                "absent": 1,
                "on_leave": 1
            },
            "pending_leave_requests": 3,
            "on_leave_today": 1,
            "documents_expiring": 4,
            "documents_expired": 1,
            "contracts_ending": 2,
            "last_closed_payroll": {
                "year": 2026,
                "month": 7,
                "total_net": 153400000
            },
            "expiry_horizon_days": 60
        }
    }
}
 

Request      

GET api/internal/companies/{company_id}/stats

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Query Parameters

expiry_horizon_days   integer  optional    

How far ahead to look for expiring documents and contracts. Default 60, max 365. Example: 60

Body Parameters

expiry_horizon_days   integer  optional    

يجب ألا يقل value عن 1. يجب ألا يكون value أكبر من 365. Example: 1

Mobile self-service

requires authentication

The logo HR uploaded on the company settings page, for the app to show beside the signed-in person's name. It is always the caller's own company: there is no id to ask for another one.

Ask only when /me says company.has_logo, and key any cache on company.logo_version, which changes whenever the logo is replaced.

Company requests

requires authentication

Every request filed in the caller's company — requests, leaves and early leaves in one list, newest first — for an HR manager or a manager (/me.abilities.view_company_requests); anybody else gets 403.

type is a kind from meta.types (leave and early_leave cover both tables they are filed into); q matches the requester's name in either language, their employee or trainee number, or the request number. meta.counts (per status, with type and q applied) and meta.types (the kinds this company has filed) come on page 1 only. Each row's can_decide / can_return say what the caller may do with it; decide through /me/approvals/{source}/{id}/act.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/company-requests?status=pending&type=leave&q=%D8%B3%D8%B9%D9%8A%D8%AF&page=1&per_page=20" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"q\": \"b\",
    \"page\": 22,
    \"per_page\": 7
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/company-requests"
);

const params = {
    "status": "pending",
    "type": "leave",
    "q": "سعيد",
    "page": "1",
    "per_page": "20",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "q": "b",
    "page": 22,
    "per_page": 7
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "leave-12",
            "type": "company-requests",
            "attributes": {
                "source": "leave",
                "source_id": 12,
                "request_type": "leave",
                "type_label_ar": "طلب إجازة",
                "type_label_en": "Leave Request",
                "summary_ar": "سنوية — من 2026-10-05 إلى 2026-10-07 (3 يوم)",
                "summary_en": "Annual — 2026-10-05 to 2026-10-07 (3 days)",
                "status": "pending",
                "status_label_ar": "قيد الاعتماد",
                "status_label_en": "Pending",
                "submitted_at": "2026-10-01T06:10:00+00:00",
                "current_approver": {
                    "name_ar": "سارة القحطاني",
                    "name_en": "Sara Alqahtani"
                },
                "current_step": 1,
                "total_steps": 2,
                "rejection_reason": null,
                "employee": {
                    "id": 88,
                    "is_trainee": false,
                    "name_ar": "سعيد الغامدي",
                    "name_en": "Saeed Alghamdi",
                    "employee_number": "1042"
                },
                "can_decide": true,
                "can_return": false
            }
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 20,
        "total": 1,
        "has_more": false,
        "counts": {
            "all": 1,
            "pending": 1,
            "approved": 0,
            "rejected": 0,
            "returned": 0,
            "cancelled": 0
        },
        "types": [
            {
                "key": "leave",
                "label_ar": "طلب إجازة",
                "label_en": "Leave request"
            }
        ]
    }
}
 

Example response (403, not HR or a manager):


{
    "message": "This action is unauthorized."
}
 

Request      

GET api/v1/company-requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

status   string  optional    

all, pending, approved, rejected, returned or cancelled. Example: pending

type   string  optional    

A kind from meta.types. Example: leave

q   string  optional    

Name, employee number or request number. Example: سعيد

page   integer  optional    

Example: 1

per_page   integer  optional    

Up to 50. Example: 20

Body Parameters

status   string  optional    
type   string  optional    
q   string  optional    

يجب ألا يزيد طول value عن 100 حرفًا. Example: b

page   integer  optional    

يجب ألا يقل value عن 1. Example: 22

per_page   integer  optional    

يجب ألا يقل value عن 1. يجب ألا يكون value أكبر من 50. Example: 7

One company request

requires authentication

The request in full, as the platform's drawer shows it, plus what the caller may do with it now (can_decide / can_return).

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/company-requests/leave/12" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/company-requests/leave/12"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 12,
        "source": "leave",
        "request_type": "leave",
        "title_ar": "طلب إجازة",
        "title_en": "Leave Request",
        "status": "pending",
        "status_label_ar": "قيد الاعتماد",
        "status_label_en": "Pending",
        "submitted_at": "2026-10-01T06:10:00+00:00",
        "requester": {
            "employee_id": 88,
            "trainee_id": null,
            "is_trainee": false,
            "name_ar": "سعيد الغامدي",
            "name_en": "Saeed Alghamdi",
            "job_title": "معلم",
            "employee_number": "1042"
        },
        "rows": [],
        "attachments": [],
        "custody": [],
        "rejection_reason": null,
        "auto_approval": null,
        "current_approver": null,
        "current_step": 1,
        "total_steps": 2,
        "reroutes": [],
        "chain": [],
        "comments": [],
        "can_decide": true,
        "can_return": false
    }
}
 

Example response (404, not in the caller's company, or not theirs to browse):


{
    "message": "Not Found"
}
 

Request      

GET api/v1/company-requests/{source}/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

source   string     

request, leave or early_leave. Example: leave

id   integer     

Example: 12

Download a file on a company request

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/company-requests/leave/12/files/301" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/company-requests/leave/12/files/301"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the file):


{}
 

Request      

GET api/v1/company-requests/{source}/{id}/files/{media}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

source   string     

Example: leave

id   integer     

Example: 12

media   integer     

Example: 301

Comment on a company request

requires authentication

The same conversation the requester and the chain read; everybody on it is notified.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/company-requests/leave/12/comments" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "body=أرسل التقرير الطبي من فضلك"\
    --form "attachments[]=@/tmp/phpbk7t4f5orhq54pKPPg0" 
const url = new URL(
    "https://hr.getokta.io/api/v1/company-requests/leave/12/comments"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('body', 'أرسل التقرير الطبي من فضلك');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 12,
        "source": "leave",
        "comments": [
            {
                "id": 5,
                "author": {
                    "id": 3,
                    "name": "سارة القحطاني"
                },
                "body": "أرسل التقرير الطبي من فضلك",
                "created_at": "2026-10-01T08:00:00+00:00",
                "attachments": []
            }
        ]
    }
}
 

Request      

POST api/v1/company-requests/{source}/{id}/comments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

source   string     

request, leave or early_leave. Example: leave

id   integer     

Example: 12

Body Parameters

body   string     

Example: أرسل التقرير الطبي من فضلك

attachments   file[]  optional    

Up to 5 files, 10 MB each.

My leave types

requires authentication

The company's active leave types, with what the caller has left of each this year. remaining_days is null for a type that keeps no balance, or whose balance has not been opened for the year — never a zero, which would claim «you have none left». For annual leave it is what may be requested NOW: earned to date plus carried over, less used and pending — the same figure the submit check refuses on.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/leave-types" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/leave-types"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "3",
            "type": "leave-types",
            "attributes": {
                "name_ar": "سنوية",
                "name_en": "Annual",
                "days_per_year": 21,
                "requires_attachment": false,
                "is_paid": true,
                "remaining_days": 14.5
            }
        }
    ]
}
 

Request      

GET api/v1/me/leave-types

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Request form catalogues

requires authentication

The request types self-service may file (the same list the web «طلب جديد» offers), the correction scopes, and the two reason catalogues. Each item is {key, label_ar, label_en}; send the key back.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/request-options" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/request-options"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "types": [
            {
                "key": "attendance_correction",
                "label_ar": "تصحيح الحضور والانصراف",
                "label_en": "Attendance correction"
            }
        ],
        "correction_scopes": [
            {
                "key": "check_in",
                "label_ar": "تصحيح الحضور",
                "label_en": "Check-in correction"
            }
        ],
        "correction_reasons": [
            {
                "key": "forgot_punch",
                "label_ar": "نسيان البصمة",
                "label_en": "Forgot to punch"
            }
        ],
        "early_leave_reasons": [
            {
                "key": "medical_appointment",
                "label_ar": "موعد طبي",
                "label_en": "Medical appointment"
            }
        ]
    }
}
 

Request      

GET api/v1/me/request-options

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Punches already recorded on a day

requires authentication

What the day's record holds, so the correction form offers only the missing side — the same answer the platform refuses a correction by.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/attendance/recorded-punches?date=2026-10-08" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"date\": \"2026-10-25\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/attendance/recorded-punches"
);

const params = {
    "date": "2026-10-08",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "date": "2026-10-25"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "check_in": "07:58",
        "check_out": null
    }
}
 

Request      

GET api/v1/me/attendance/recorded-punches

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

date   string     

The day, YYYY-MM-DD. Example: 2026-10-08

Body Parameters

date   string     

Must be a valid date in the format Y-m-d. Example: 2026-10-25

Public holidays that apply to me

requires authentication

Active holidays touching the year that cover the caller's branch. Their days are never charged to a leave balance nor counted as absences.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/holidays?year=2026" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/holidays"
);

const params = {
    "year": "2026",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "4",
            "type": "public-holidays",
            "attributes": {
                "name_ar": "اليوم الوطني",
                "name_en": "National Day",
                "start_date": "2026-09-23",
                "end_date": "2026-09-23",
                "days": 1
            }
        }
    ]
}
 

Request      

GET api/v1/me/holidays

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

year   integer  optional    

The calendar year (defaults to this year). Example: 2026

This device's access

requires authentication

approved, pending (with who it waits on) or rejected (with why), for the device this token was issued to. data is null for a token that names no device — an integration, or an app from before devices were checked.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/device-access" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/device-access"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 5,
        "status": "pending",
        "status_label_ar": "بانتظار الموافقة",
        "status_label_en": "Awaiting approval",
        "name": "Pixel 8",
        "platform": "android",
        "requested_at": "2026-10-22T06:10:00+00:00",
        "decided_at": null,
        "awaiting": {
            "name_ar": "ماجد المدير",
            "name_en": "Majed Manager"
        },
        "rejection_reason": null
    }
}
 

Request      

GET api/v1/me/device-access

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Ask again for this device

requires authentication

After the company refused the device, a new request — asked for by the person, by name, rather than raised again on every sign-in. A device that is approved or still waiting answers as it stands.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/device-access/request" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/device-access/request"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 6,
        "status": "pending",
        "status_label_ar": "بانتظار الموافقة",
        "status_label_en": "Awaiting approval",
        "name": "Pixel 8",
        "platform": "android",
        "requested_at": "2026-10-22T07:00:00+00:00",
        "decided_at": null,
        "awaiting": {
            "name_ar": "ماجد المدير",
            "name_en": "Majed Manager"
        },
        "rejection_reason": null
    }
}
 

Example response (404, the token names no device):


{
    "message": "Not Found"
}
 

Request      

POST api/v1/me/device-access/request

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

My requests

requires authentication

Self-service requests, leaves and early leaves in one list, newest submitted first. id is <source>-<id>; address the request's details by source and source_id. status is the request's own — all (the default) also lists cancelled ones.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/requests?status=pending" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/requests"
);

const params = {
    "status": "pending",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "leave-12",
            "type": "my-requests",
            "attributes": {
                "source": "leave",
                "source_id": 12,
                "request_type": "leave",
                "type_label_ar": "طلب إجازة",
                "type_label_en": "Leave Request",
                "summary_ar": "سنوية · 2026-10-05 ← 2026-10-07 · 3 أيام",
                "summary_en": "Annual · 2026-10-05 → 2026-10-07 · 3 days",
                "status": "pending",
                "status_label_ar": "قيد الاعتماد",
                "status_label_en": "Pending",
                "submitted_at": "2026-10-01T06:10:00+00:00",
                "current_approver": {
                    "name_ar": "سارة القحطاني",
                    "name_en": "Sara Alqahtani"
                },
                "current_step": 1,
                "total_steps": 2,
                "rejection_reason": null
            }
        }
    ]
}
 

Request      

GET api/v1/me/requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

status   string  optional    

pending, approved, rejected, returned or all. Example: pending

File a request

requires authentication

Any self-service request type (see request-options), with its fields under payload — the same rules, chain and effects as the platform's «طلب جديد» form, because both go through one form definition and EmployeeRequestService::submit. An attendance correction may only fix a punch the day is missing. Send as multipart when there are files.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/requests" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "type=attendance_correction"\
    --form "attachments[]=@/tmp/php1fmoulocro8eeK8i6s2" 
const url = new URL(
    "https://hr.getokta.io/api/v1/me/requests"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('type', 'attendance_correction');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": "request-41",
        "type": "my-requests",
        "attributes": {
            "source": "request",
            "source_id": 41,
            "request_type": "attendance_correction",
            "type_label_ar": "تصحيح الحضور والانصراف",
            "type_label_en": "Attendance correction",
            "summary_ar": "2026-10-08",
            "summary_en": "2026-10-08",
            "status": "pending",
            "status_label_ar": "قيد الاعتماد",
            "status_label_en": "Pending",
            "submitted_at": "2026-10-09T06:10:00+00:00",
            "current_approver": {
                "name_ar": "سارة القحطاني",
                "name_en": "Sara Alqahtani"
            },
            "current_step": 1,
            "total_steps": 1,
            "rejection_reason": null
        }
    }
}
 

Example response (422, the time asked for is already the one on record):


{
    "message": "وقت الحضور المسجّل هو 07:58 أصلاً — اكتب الوقت الصحيح.",
    "errors": {
        "payload.check_in": [
            "وقت الحضور المسجّل هو 07:58 أصلاً — اكتب الوقت الصحيح."
        ]
    }
}
 

Request      

POST api/v1/me/requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

Body Parameters

type   string     

The request type key. Example: attendance_correction

payload   object     

The type's fields.

attachments   file[]  optional    

Up to 5 files, 10 MB each.

Ask to leave early

requires authentication

Approved, it lifts the check-out lock from leave_time onward. The reason is a key from request-options.early_leave_reasons; other needs reason_other. The request and its chain are one unit: when nobody can approve it, nothing is filed.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/early-leave-requests" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"date\": \"2026-10-09\",
    \"leave_time\": \"13:30\",
    \"reason\": \"medical_appointment\",
    \"reason_other\": \"مراجعة\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/early-leave-requests"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "date": "2026-10-09",
    "leave_time": "13:30",
    "reason": "medical_appointment",
    "reason_other": "مراجعة"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": "early_leave-7",
        "type": "my-requests",
        "attributes": {
            "source": "early_leave",
            "source_id": 7,
            "request_type": "early_leave",
            "type_label_ar": "طلب انصراف مبكر",
            "type_label_en": "Early leave request",
            "summary_ar": "2026-10-09 · 13:30",
            "summary_en": "2026-10-09 · 13:30",
            "status": "pending",
            "status_label_ar": "قيد الاعتماد",
            "status_label_en": "Pending",
            "submitted_at": "2026-10-09T06:10:00+00:00",
            "current_approver": {
                "name_ar": "سارة القحطاني",
                "name_en": "Sara Alqahtani"
            },
            "current_step": 1,
            "total_steps": 1,
            "rejection_reason": null
        }
    }
}
 

Request      

POST api/v1/me/early-leave-requests

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

date   string     

YYYY-MM-DD. Example: 2026-10-09

leave_time   string     

HH:MM. Example: 13:30

reason   string     

A reason key. Example: medical_appointment

reason_other   string  optional    

Required when reason is other. Example: مراجعة

One of my requests

requires authentication

The request in full, as the platform's drawer shows it — fields, files, chain and comments, every string in both languages.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/requests/leave/12" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/requests/leave/12"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 12,
        "source": "leave",
        "request_type": "leave",
        "title_ar": "طلب إجازة",
        "title_en": "Leave Request",
        "status": "pending",
        "status_label_ar": "قيد الاعتماد",
        "status_label_en": "Pending",
        "submitted_at": "2026-10-01T06:10:00+00:00",
        "requester": {
            "employee_id": 88,
            "trainee_id": null,
            "is_trainee": false,
            "name_ar": "سعيد الغامدي",
            "name_en": "Saeed Alghamdi",
            "job_title": "معلم",
            "employee_number": "1042"
        },
        "rows": [],
        "attachments": [],
        "custody": [],
        "rejection_reason": null,
        "auto_approval": null,
        "current_approver": null,
        "current_step": 1,
        "total_steps": 1,
        "reroutes": [],
        "chain": [],
        "comments": []
    }
}
 

Example response (404, not mine):


{
    "message": "Not Found"
}
 

Request      

GET api/v1/me/requests/{source}/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

source   string     

request, leave or early_leave. Example: leave

id   integer     

The request's id in that table. Example: 12

Download a file on my request

requires authentication

A file filed with the request or attached to one of its comments.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/requests/leave/12/files/301" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/requests/leave/12/files/301"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the file):


{}
 

Request      

GET api/v1/me/requests/{source}/{id}/files/{media}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

source   string     

request, leave or early_leave. Example: leave

id   integer     

Example: 12

media   integer     

The file's id. Example: 301

Comment on my request

requires authentication

Adds to the conversation under the request and notifies everybody on its chain. Send as multipart when there are files.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/requests/leave/12/comments" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "body=أرفقت التقرير الطبي"\
    --form "attachments[]=@/tmp/phpiafqhqpltdoc4OUuow2" 
const url = new URL(
    "https://hr.getokta.io/api/v1/me/requests/leave/12/comments"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('body', 'أرفقت التقرير الطبي');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 12,
        "source": "leave",
        "comments": [
            {
                "id": 4,
                "author": {
                    "id": 88,
                    "name": "سعيد الغامدي"
                },
                "body": "أرفقت التقرير الطبي",
                "created_at": "2026-10-01T07:05:00+00:00",
                "attachments": []
            }
        ]
    }
}
 

Request      

POST api/v1/me/requests/{source}/{id}/comments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

source   string     

request, leave or early_leave. Example: leave

id   integer     

Example: 12

Body Parameters

body   string     

Example: أرفقت التقرير الطبي

attachments   file[]  optional    

Up to 5 files, 10 MB each.

My approvals

requires authentication

pending (the default) lists what waits on the caller's decision now; all also keeps what they already decided, so it does not vanish from the only screen that says where it went. An HR manager also sees every request still waiting on somebody else, which they may decide on behalf.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/approvals?scope=pending" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/approvals"
);

const params = {
    "scope": "pending",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "leave-12",
            "type": "approval-items",
            "attributes": {
                "source": "leave",
                "source_id": 12,
                "request_type": "leave",
                "type_label_ar": "طلب إجازة",
                "type_label_en": "Leave Request",
                "summary_ar": "سنوية — من 2026-10-05 إلى 2026-10-07 (3 يوم)",
                "summary_en": "Annual — 2026-10-05 to 2026-10-07 (3 days)",
                "status": "pending",
                "status_label_ar": "قيد الاعتماد",
                "status_label_en": "Pending",
                "submitted_at": "2026-10-01T06:10:00+00:00",
                "current_approver": {
                    "name_ar": "سارة القحطاني",
                    "name_en": "Sara Alqahtani"
                },
                "current_step": 1,
                "total_steps": 2,
                "rejection_reason": null,
                "employee": {
                    "id": 88,
                    "is_trainee": false,
                    "name_ar": "سعيد الغامدي",
                    "name_en": "Saeed Alghamdi",
                    "employee_number": "1042"
                },
                "can_decide": true,
                "can_return": false
            }
        }
    ]
}
 

Request      

GET api/v1/me/approvals

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

scope   string  optional    

pending or all. Example: pending

One request in my approvals

requires authentication

The request in full, as the platform's drawer shows it. Only a request in the caller's inbox opens.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/approvals/leave/12" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/approvals/leave/12"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 12,
        "source": "leave",
        "request_type": "leave",
        "title_ar": "طلب إجازة",
        "title_en": "Leave Request",
        "status": "pending",
        "status_label_ar": "قيد الاعتماد",
        "status_label_en": "Pending",
        "submitted_at": "2026-10-01T06:10:00+00:00",
        "requester": {
            "employee_id": 88,
            "trainee_id": null,
            "is_trainee": false,
            "name_ar": "سعيد الغامدي",
            "name_en": "Saeed Alghamdi",
            "job_title": "معلم",
            "employee_number": "1042"
        },
        "rows": [],
        "attachments": [],
        "custody": [],
        "rejection_reason": null,
        "auto_approval": null,
        "current_approver": null,
        "current_step": 1,
        "total_steps": 2,
        "reroutes": [],
        "chain": [],
        "comments": []
    }
}
 

Example response (404, not in my inbox):


{
    "message": "Not Found"
}
 

Request      

GET api/v1/me/approvals/{source}/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

source   string     

request, leave, early_leave, adjustment or device. Example: leave

id   integer     

Example: 12

Decide a request

requires authentication

approve, reject (with a reason) or return for correction (with a note; an employee request, by the step's own approver only). Answers with the updated row.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/approvals/leave/12/act" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"decision\": \"reject\",
    \"notes\": \"يتعارض مع جدول الاختبارات\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/approvals/leave/12/act"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "decision": "reject",
    "notes": "يتعارض مع جدول الاختبارات"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": "leave-12",
        "type": "approval-items",
        "attributes": {
            "source": "leave",
            "source_id": 12,
            "status": "rejected",
            "can_decide": false,
            "can_return": false
        }
    }
}
 

Example response (422, not my turn):


{
    "message": "ليس دورك في سلسلة الاعتماد.",
    "errors": {
        "decision": [
            "ليس دورك في سلسلة الاعتماد."
        ]
    }
}
 

Request      

POST api/v1/me/approvals/{source}/{id}/act

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

source   string     

request, leave, early_leave, adjustment or device. Example: leave

id   integer     

Example: 12

Body Parameters

decision   string     

approve, reject or return. Example: reject

notes   string  optional    

Required for reject and return. Example: يتعارض مع جدول الاختبارات

Comment on a request in my approvals

requires authentication

Same conversation the requester reads, notifying everybody on the chain.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/approvals/leave/12/comments" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "body=أرسل التقرير الطبي من فضلك"\
    --form "attachments[]=@/tmp/php67r44qtgq2e41eNlECX" 
const url = new URL(
    "https://hr.getokta.io/api/v1/me/approvals/leave/12/comments"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('body', 'أرسل التقرير الطبي من فضلك');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 12,
        "source": "leave",
        "comments": [
            {
                "id": 5,
                "author": {
                    "id": 3,
                    "name": "سارة القحطاني"
                },
                "body": "أرسل التقرير الطبي من فضلك",
                "created_at": "2026-10-01T08:00:00+00:00",
                "attachments": []
            }
        ]
    }
}
 

Request      

POST api/v1/me/approvals/{source}/{id}/comments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

source   string     

request, leave, early_leave, adjustment or device. Example: leave

id   integer     

Example: 12

Body Parameters

body   string     

Example: أرسل التقرير الطبي من فضلك

attachments   file[]  optional    

Up to 5 files, 10 MB each.

Download a file on a request in my approvals

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/approvals/leave/12/files/301" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/approvals/leave/12/files/301"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the file):


{}
 

Request      

GET api/v1/me/approvals/{source}/{id}/files/{media}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

source   string     

Example: leave

id   integer     

Example: 12

media   integer     

Example: 301

My custody

requires authentication

Open handovers by default; include_returned=1 adds the ones already handed back. An asset carries one name, so asset_name_ar and asset_name_en are the same string; the category is bilingual.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/custody?include_returned=" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/custody"
);

const params = {
    "include_returned": "0",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "9",
            "type": "custody-items",
            "attributes": {
                "asset_name_ar": "لابتوب Dell 5540",
                "asset_name_en": "لابتوب Dell 5540",
                "serial_number": "SN-99182",
                "category_ar": "أجهزة حاسب",
                "category_en": "Computers",
                "assigned_at": "2025-03-02",
                "returned_at": null,
                "condition": "جيد",
                "status": "open"
            }
        }
    ]
}
 

Request      

GET api/v1/me/custody

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

include_returned   boolean  optional    

Include returned assignments. Example: false

My warnings

requires authentication

Newest first. response is null until answered, then acknowledged or objected. source is automatic (the lateness ladder — details_text states the tally) or manual (details_text is what HR wrote).

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/warnings" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/warnings"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "7",
            "type": "warnings",
            "attributes": {
                "reason_key": "repeated_lateness",
                "reason_label_ar": "تكرار التأخير",
                "reason_label_en": "Repeated lateness",
                "source": "manual",
                "level_name": null,
                "details_text": "تأخر ثلاث مرات هذا الأسبوع.",
                "issued_at": "2026-10-01T06:00:00+00:00",
                "status": "issued",
                "response": null,
                "response_note": null,
                "responded_at": null,
                "attachments": [
                    {
                        "id": 31,
                        "name": "report.pdf",
                        "size": 20480,
                        "mime_type": "application/pdf"
                    }
                ]
            }
        }
    ]
}
 

Request      

GET api/v1/me/warnings

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Answer a warning

requires authentication

Once, and only by the person it was issued to. Objecting requires a note.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/v1/me/warnings/7/respond" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"response\": \"object\",
    \"note\": \"كان التأخير بسبب موعد طبي معتمد.\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/warnings/7/respond"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "response": "object",
    "note": "كان التأخير بسبب موعد طبي معتمد."
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": "7",
        "type": "warnings",
        "attributes": {
            "status": "disputed",
            "response": "objected",
            "response_note": "كان التأخير بسبب موعد طبي معتمد.",
            "responded_at": "2026-10-02T07:00:00+00:00"
        }
    }
}
 

Example response (422, objection without a note):


{
    "message": "اكتب سبب الاعتراض.",
    "errors": {
        "note": [
            "اكتب سبب الاعتراض."
        ]
    }
}
 

Request      

POST api/v1/me/warnings/{id}/respond

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

Example: 7

Body Parameters

response   string     

acknowledge or object. Example: object

note   string  optional    

Required when objecting. Example: كان التأخير بسبب موعد طبي معتمد.

Download a warning attachment

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/warnings/7/files/31" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/warnings/7/files/31"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the file):


{}
 

Request      

GET api/v1/me/warnings/{id}/files/{media}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

Example: 7

media   integer     

Example: 31

My documents

requires authentication

days_until_expiry is negative once a document has expired, and null when it does not expire.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/documents" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/documents"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "12",
            "type": "employee-documents",
            "attributes": {
                "document_type": "iqama",
                "type_label_ar": "إقامة",
                "type_label_en": "Iqama",
                "number": "2412345678",
                "issue_date": "2025-01-10",
                "expiry_date": "2027-01-09",
                "days_until_expiry": 457,
                "has_file": true
            }
        }
    ]
}
 

Request      

GET api/v1/me/documents

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Download one of my documents

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/documents/12/file" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/documents/12/file"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the file):


{}
 

Example response (404, no file, or not my document):


{
    "message": "Not Found"
}
 

Request      

GET api/v1/me/documents/{id}/file

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

Example: 12

My official letters

requires authentication

Draw the download from is_releasable: the PDF exists only once the approval chain has cleared it.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/letters" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/letters"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "5",
            "type": "official-letters",
            "attributes": {
                "reference": "A1B2-C3D4E5F6",
                "letter_type": "salary_certificate",
                "type_label_ar": "شهادة راتب",
                "type_label_en": "Salary certificate",
                "language": "ar",
                "addressed_to": "البنك الأهلي",
                "status": "approved",
                "is_releasable": true,
                "issued_on": "2026-10-01"
            }
        }
    ]
}
 

Request      

GET api/v1/me/letters

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Download my letter (PDF)

requires authentication

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/letters/5/pdf" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/letters/5/pdf"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the PDF):


{}
 

Example response (403, not approved yet):


{
    "message": "الخطاب لم يُعتمد بعد."
}
 

Request      

GET api/v1/me/letters/{id}/pdf

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

Example: 5

Download my payslip

requires authentication

The same PDF the web «قسائم الرواتب» serves, with the company stamp. Only the caller's own payslip of a closed run; anything else is a 404.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/v1/me/payslips/41/pdf" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/payslips/41/pdf"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the PDF):


{}
 

Example response (404, not mine, or the run is not closed):


{
    "message": "Not Found"
}
 

Request      

GET api/v1/me/payslips/{id}/pdf

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The payslip id from GET /me/payslips. Example: 41

Update my profile

requires authentication

Example request:
curl --request PATCH \
    "https://hr.getokta.io/api/v1/me/profile" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"سعيد الغامدي\",
    \"phone\": \"0501234567\",
    \"locale\": \"ar\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/profile"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "سعيد الغامدي",
    "phone": "0501234567",
    "locale": "ar"
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": "7",
        "type": "users",
        "attributes": {
            "name": "سعيد الغامدي",
            "email": "saeed@example.com",
            "phone": "0501234567",
            "locale": "ar",
            "roles": [
                "employee"
            ],
            "company": {
                "id": 1,
                "name_ar": "مدرسة النور",
                "name_en": "Alnoor School"
            }
        }
    }
}
 

Request      

PATCH api/v1/me/profile

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Example: سعيد الغامدي

phone   string  optional    

Example: 0501234567

locale   string     

ar or en. Example: ar

Change my password

requires authentication

Example request:
curl --request PUT \
    "https://hr.getokta.io/api/v1/me/password" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"current_password\": \"old-secret-1\",
    \"password\": \"new-secret-1\",
    \"password_confirmation\": \"new-secret-1\"
}"
const url = new URL(
    "https://hr.getokta.io/api/v1/me/password"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "current_password": "old-secret-1",
    "password": "new-secret-1",
    "password_confirmation": "new-secret-1"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "message": "تم تغيير كلمة المرور بنجاح"
}
 

Example response (422, wrong current password):


{
    "message": "كلمة المرور غير صحيحة.",
    "errors": {
        "current_password": [
            "كلمة المرور غير صحيحة."
        ]
    }
}
 

Request      

PUT api/v1/me/password

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

current_password   string     

Example: old-secret-1

password   string     

At least 8 characters. Example: new-secret-1

password_confirmation   string     

Example: new-secret-1

Suite / Requests

One request in full — what the platform's own request drawer shows: who asked and when, every type-specific field, the files filed with it, the approval chain with each step's decision, and the conversation under it.

A request lives in one of three tables, and a row 7 exists in each of them, so the address carries the source every request row already does: request (self-service requests), leave or early_leave.

Show a request's details

requires authentication

Every string a reader prints comes in both languages — the caller is a server, and its Accept-Language is its own rather than the tenant's. rows are the type-specific fields, already formatted (dates spelled out, coded values named, money in riyals); each carries a stable key. status is the request's own, the value the lists show. chain is ordered by step; acted_by is set only when somebody decided a step in the assigned approver's place. comments are oldest first. Files are listed by name — fetch one from …/files/{media}.

Name the person LOOKING (acting_employee_id, acting_user_email, acting_as_tenant_admin, acting_may_decide — the act endpoints' names) and the answer adds can_decide and can_return: whether their decision would be taken, by the rule the act endpoints enforce. Draw the drawer's buttons from them and they are never refused. An early leave is never decidable here — it has no act endpoint.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/request-details/leave/12?acting_employee_id=42&acting_user_email=hr%40school.sa&acting_as_tenant_admin=1&acting_may_decide=1" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/request-details/leave/12"
);

const params = {
    "acting_employee_id": "42",
    "acting_user_email": "hr@school.sa",
    "acting_as_tenant_admin": "1",
    "acting_may_decide": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 12,
        "source": "leave",
        "request_type": "leave",
        "title_ar": "طلب إجازة",
        "title_en": "Leave Request",
        "status": "pending",
        "status_label_ar": "قيد الاعتماد",
        "status_label_en": "Pending",
        "submitted_at": "2026-10-01T06:10:00+00:00",
        "requester": {
            "employee_id": 88,
            "trainee_id": null,
            "is_trainee": false,
            "name_ar": "سعيد الغامدي",
            "name_en": "Saeed Alghamdi",
            "job_title": "معلم",
            "employee_number": "1042"
        },
        "rows": [
            {
                "key": "leave_type",
                "label_ar": "نوع الإجازة",
                "label_en": "Leave Type",
                "value_ar": "سنوية",
                "value_en": "Annual"
            },
            {
                "key": "from_date",
                "label_ar": "من تاريخ",
                "label_en": "From date",
                "value_ar": "5 أكتوبر 2026",
                "value_en": "5 October 2026"
            }
        ],
        "attachments": [
            {
                "id": 301,
                "name": "report.pdf",
                "size": 48213,
                "mime_type": "application/pdf"
            }
        ],
        "custody": [
            {
                "id": 9,
                "name": "Laptop"
            }
        ],
        "rejection_reason": null,
        "auto_approval": null,
        "current_approver": {
            "id": 7,
            "name": "سارة القحطاني",
            "employee_id": 42,
            "email": "sara@school.sa"
        },
        "current_step": 1,
        "total_steps": 2,
        "reroutes": [],
        "chain": [
            {
                "step": 1,
                "approver": {
                    "id": 7,
                    "name": "سارة القحطاني",
                    "employee_id": 42
                },
                "acted_by": null,
                "status": "pending",
                "status_label_ar": "قيد الاعتماد",
                "status_label_en": "Pending",
                "notes": null,
                "acted_at": null
            }
        ],
        "comments": [
            {
                "id": 3,
                "author": {
                    "id": 7,
                    "name": "سارة القحطاني"
                },
                "body": "أرفق التقرير من فضلك",
                "created_at": "2026-10-01T07:00:00+00:00",
                "attachments": []
            }
        ]
    }
}
 

Example response (404, not in this company, or an unknown source):


{
    "message": "Request not found in this company."
}
 

Request      

GET api/internal/companies/{company_id}/request-details/{source}/{id}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

source   string     

request, leave, early_leave or adjustment. Example: leave

id   integer     

The request's id in that table. Example: 12

Query Parameters

acting_employee_id   integer  optional    

The person looking, for can_decide. Example: 42

acting_user_email   string  optional    

The person looking, by account email. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

The person looking administers this tenant in your product. Example: true

acting_may_decide   boolean  optional    

The person looking holds the permission to decide this kind of request in your product. Example: true

Print a request

requires authentication

The PDF the platform's own print button produces — the fields, the approval chain and the comments — streamed inline.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/request-details/request/41/pdf" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/request-details/request/41/pdf"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the PDF):


{}
 

Request      

GET api/internal/companies/{company_id}/request-details/{source}/{id}/pdf

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

source   string     

request, leave, early_leave or adjustment. Example: request

id   integer     

The request's id in that table. Example: 41

Download a request's file

requires authentication

A file filed with the request, or attached to one of its comments. Nothing else resolves: media ids are global, so a lookup that did not check the owner would serve any company's upload.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/request-details/leave/12/files/301" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/request-details/leave/12/files/301"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, the file):


{}
 

Example response (404, not this request's file):


{
    "message": "File not found on this request."
}
 

Request      

GET api/internal/companies/{company_id}/request-details/{source}/{id}/files/{media}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

source   string     

request, leave, early_leave or adjustment. Example: leave

id   integer     

The request's id in that table. Example: 12

media   integer     

The file's id from attachments. Example: 301

Comment on a request

requires authentication

Adds to the conversation under a request, optionally with files, and notifies everybody watching it — the requester and the people on its chain — exactly as a comment written in the platform's drawer does. Send as multipart when there are files.

The comment is a named person's: whoever may open the request on the platform may comment on it — the requester, anybody on its chain, or HR.

Example request:
curl --request POST \
    "https://hr.getokta.io/api/internal/companies/16/request-details/architecto/564/comments" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "body=أرفقت التقرير الطبي"\
    --form "acting_employee_id=88"\
    --form "acting_user_email=hr@school.sa"\
    --form "acting_as_tenant_admin=1"\
    --form "attachments[]=@/tmp/phptd7s9kqs2ups26KIUDu" 
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/request-details/architecto/564/comments"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('body', 'أرفقت التقرير الطبي');
body.append('acting_employee_id', '88');
body.append('acting_user_email', 'hr@school.sa');
body.append('acting_as_tenant_admin', '1');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

Example response (201, the request, with the comment):


{
    "data": {
        "id": 12,
        "source": "leave",
        "comments": [
            {
                "id": 4,
                "author": {
                    "id": 88,
                    "name": "سعيد الغامدي"
                },
                "body": "أرفقت التقرير الطبي",
                "created_at": "2026-10-01T07:05:00+00:00",
                "attachments": []
            }
        ]
    }
}
 

Example response (422, may not open this request):


{
    "message": "لا تملك الاطلاع على هذا الطلب.",
    "errors": {
        "request": [
            "لا تملك الاطلاع على هذا الطلب."
        ]
    }
}
 

Request      

POST api/internal/companies/{company_id}/request-details/{source}/{id}/comments

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

source   string     

Example: architecto

id   string     

The ID of the {source}. Example: 564

Body Parameters

body   string     

The comment. Example: أرفقت التقرير الطبي

attachments   file[]  optional    

Up to 5 files, 10 MB each.

acting_employee_id   integer  optional    

The employee commenting. Required unless acting_user_email is given. Example: 88

acting_user_email   string  optional    

The email of the account commenting. Required unless acting_employee_id is given. Example: hr@school.sa

acting_as_tenant_admin   boolean  optional    

Sent INSTEAD of a name when the person acting administers this tenant in your product. Example: true

Suite API — approval workflows

Read and set the per-request-type approval chains a company routes its requests through — the same rows /settings/approvals edits, and the same ones ApproverChainResolver prefers over the default manager → HR fallback.

WHY THIS EXISTS ON THE API AT ALL. Every request the suite files through POST /requests and POST /leave-requests enters a chain, and a company with none configured falls back to the default — which fails outright when the employee has no manager, refusing the request with «لا يوجد معتمِد لهذا الموظف». A consuming product could therefore file requests it had no way to make routable, and no way to see why. The vocabulary needed to fix that — which types exist, which roles and people may approve — was only ever on a Livewire page.

List the chains, and the vocabulary for editing them

requires authentication

Every request type is returned, configured or not — a caller building an editor needs the whole list, and «this type has no chain» is an answer, not an absence. uses_default says which it is: with no row the platform falls back to direct manager → HR, and that fallback is not a chain anybody chose.

meta.employees is the roster a «شخص محدّد» step picks from — every employee who has not left, whether or not they have a login. Naming one by employee_id makes the account their decisions are recorded against on save (user_id here is that account when it already exists, so a saved step can be shown as the employee it means). meta.users remains for accounts with no employee file — an HR manager who is not on the payroll — and unavailable_approvers is always empty now: nobody on the roster is unnameable. It stays for consumers that still read it.

Example request:
curl --request GET \
    --get "https://hr.getokta.io/api/internal/companies/16/approval-workflows" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/approval-workflows"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "request_type": "leave_request",
            "label_ar": "طلب إجازة",
            "label_en": "Leave request",
            "is_live": true,
            "uses_default": false,
            "auto_approve": false,
            "steps": [
                {
                    "type": "direct_manager",
                    "label_ar": "المدير المباشر",
                    "label_en": "Direct manager"
                },
                {
                    "type": "user",
                    "user_id": 7,
                    "user_name": "سارة القحطاني",
                    "employee_id": 31,
                    "label_ar": "شخص محدد: سارة القحطاني",
                    "label_en": "Specific person: سارة القحطاني"
                }
            ]
        }
    ],
    "meta": {
        "step_types": [
            "direct_manager",
            "role",
            "user"
        ],
        "roles": [
            "hr-manager",
            "manager",
            "super-admin"
        ],
        "users": [
            {
                "id": 7,
                "name": "سارة القحطاني",
                "employee_number": "1042",
                "job_title": "معلمة"
            }
        ],
        "employees": [
            {
                "employee_id": 31,
                "name_ar": "سارة القحطاني",
                "name_en": "Sara Alqahtani",
                "employee_number": "1042",
                "job_title": "معلمة",
                "user_id": 7
            },
            {
                "employee_id": 88,
                "name_ar": "محمد الدوخي",
                "name_en": "Mohammed Aldoukhi",
                "employee_number": "1043",
                "job_title": "معلم",
                "user_id": null
            }
        ],
        "unavailable_approvers": []
    }
}
 

Request      

GET api/internal/companies/{company_id}/approval-workflows

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

Replace one type's chain, and/or switch its automatic approval

requires authentication

WHOLE-LIST REPLACE, like the settings page's save: the steps you send become the chain, in the order you send them. An EMPTY list deletes the row, which restores the default fallback rather than leaving a chain with nobody in it — a request routed to nobody is pending forever, in no queue, and looks submitted to the person who filed it.

A user step names its person by employee_id (anyone in meta.employees) or by user_id (an account in meta.users). An employee with no login gets one on save — under their address, or under a reserved address that receives no mail when the file has none — so the step has an account to record the decision against. What is stored is always the account; the account's decisions come from the suite, where the person is named by their employee file.

auto_approve switches the type between its chain and approval on submission. It never touches the steps: switching it on keeps the chain, switching it off brings that chain back. Send either key alone — steps omitted leaves the chain as it is, auto_approve omitted leaves the setting as it is.

The change also reaches requests of THIS type still waiting in their chain: switching auto-approval on approves them, and a changed chain moves them to the approvers it names now (steps already approved stay). Approved, rejected and returned requests are not touched. meta.pending_auto_approved / meta.pending_rerouted say how many.

Example request:
curl --request PUT \
    "https://hr.getokta.io/api/internal/companies/16/approval-workflows/architecto" \
    --header "Authorization: Bearer {YOUR_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"steps\": [
        {
            \"type\": \"user\",
            \"role\": \"hr-manager\",
            \"user_id\": 7,
            \"employee_id\": 88
        }
    ],
    \"auto_approve\": true
}"
const url = new URL(
    "https://hr.getokta.io/api/internal/companies/16/approval-workflows/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "steps": [
        {
            "type": "user",
            "role": "hr-manager",
            "user_id": 7,
            "employee_id": 88
        }
    ],
    "auto_approve": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "request_type": "leave_request",
        "uses_default": false,
        "auto_approve": false,
        "steps": [
            {
                "type": "user",
                "user_id": 12,
                "user_name": "محمد الدوخي",
                "employee_id": 88,
                "label_ar": "شخص محدد: محمد الدوخي",
                "label_en": "Specific person: محمد الدوخي"
            }
        ]
    },
    "meta": {
        "pending_auto_approved": 0,
        "pending_rerouted": 3
    }
}
 

Example response (422, unknown approver):


{
    "message": "هذا الشخص ليس من موظفي هذه الجهة، أو لا يمكن تعيينه معتمِداً.",
    "errors": {
        "steps.0.employee_id": [
            "هذا الشخص ليس من موظفي هذه الجهة، أو لا يمكن تعيينه معتمِداً."
        ]
    }
}
 

Request      

PUT api/internal/companies/{company_id}/approval-workflows/{type}

Headers

Authorization        

Example: Bearer {YOUR_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 16

type   string     

Example: architecto

Body Parameters

steps   object[]  optional    

The ordered steps. Send [] to restore the default chain; omit to leave the chain as it is.

type   string     

One of: direct_manager, role, user. Example: user

role   string  optional    

Required when type is role. Example: hr-manager

user_id   integer  optional    

For a user step naming an account with no employee file: an id from meta.users. Example: 7

employee_id   integer  optional    

For a user step: an employee from meta.employees. Example: 88

auto_approve   boolean  optional    

Approve requests of this type on submission, by nobody. Omit to leave as it is. Example: true