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
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:
POST /api/internal/companies— creates the company, seeds its defaults and creates its first hr-manager. Send your own tenant identifier asexternal_refand the call becomes idempotent: retries, re-installs and double-clicks return the same company instead of a second one.POST /api/internal/companies/{company}/employees— pushes the staff across. Upserts onemployee_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.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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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 مُستخدم من قبل."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/internal/link-codes/redeem
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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 متر — دقة موقع الجوال لا تسمح بأصغر من ذلك."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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 شخصاً عليه. انقلهم إلى فرع آخر أولاً — حذف مقرّ عملهم لا يقرّر أين يعملون."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"..."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": "تم بلوغ الحد الأقصى لعدد الموظفين في باقتك."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"لدى الموظف حساب دخول بالفعل."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
deleted— the login is gone and so is the record.account_revoked— the login is gone and the record stayed, because something still references it. Set their status instead.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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>"
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": "الخطاب لم يُعتمد بعد."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": "لا يمكن أن تتجاوز الغرامات أجر خمسة أيام في الشهر."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": "لا يمكن حذف الأصل — مرتبط بسجل عهدة سابق، وسجلات العهد تُحفظ ولا تُحذف."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": "الأصل غير متاح للتسليم."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": "أُعيد هذا الأصل مسبقاً."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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)، والتصحيح متاح للبصمة غير المسجّلة فقط."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"الإعادة للتصحيح لصاحب الخطوة نفسه."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Only a request still awaiting a decision (pending, or returned for correction) can be deleted. An approved or rejected request is a record the system keeps, and nobody — HR included — deletes it.
- While it is undecided, the requester may withdraw their own (their employee file is enough to name them — a login is not required) and HR (an hr-manager of this company) may delete it.
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": [
"لا تملك حذف هذا الطلب: يحذفه صاحبه أو الموارد البشرية ما دام قيد الاعتماد."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"عيّن مديراً مباشراً للموظف، أو اضبط سلسلة اعتماد لهذا النوع."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"لا تملك حذف هذا الطلب: يحذفه صاحبه أو الموارد البشرية ما دام قيد الاعتماد."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"لا يمكن حذف نوع إجازة عليه طلبات. عطّله بدلاً من ذلك."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"أحد الفروع المختارة لا يتبع هذه الجهة."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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 يوماً مستخدمة."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- With it (
scope: "employee") — that one person, anddatacarries their balances for the year. - Without it (
scope: "company") — every ACTIVE employee in the company, anddatais deliberately empty: a school of three hundred holds some four thousand rows and nobody reads them off the end of a bulk write.meta.employeessays how many people were walked. This exists because the per-employee form is the only one there was, and a consumer opening a year had to loop the roster — one HTTP round trip per person, inside somebody's page load, half-written the moment it times out.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"هذا النوع من الإجازات يتطلب مرفقاً."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"حقل السبب مطلوب."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"لا يمكن حذف طلب معتمد أو مرفوض — يبقى في سجلّات النظام."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"أنت خارج النطاق."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"ليس دورك في سلسلة الاعتماد."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Mobile self-service
My company's logo
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.
Example request:
curl --request GET \
--get "https://hr.getokta.io/api/v1/company/logo" \
--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/logo"
);
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 image):
{}
Example response (404, no logo uploaded):
{
"message": "Not Found"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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):
{}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": []
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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 أصلاً — اكتب الوقت الصحيح."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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):
{}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": []
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"ليس دورك في سلسلة الاعتماد."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": []
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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):
{}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
]
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"اكتب سبب الاعتراض."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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):
{}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": "الخطاب لم يُعتمد بعد."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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"
}
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"كلمة المرور غير صحيحة."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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):
{}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"لا تملك الاطلاع على هذا الطلب."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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": [
"هذا الشخص ليس من موظفي هذه الجهة، أو لا يمكن تعيينه معتمِداً."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.