API concepts
How permissions and branches apply to API calls, pages and filters, dates and time zones, languages, errors, rate limits, retries and versions.
The rules every Mihnaro API call follows. Read Getting started with the API first if you haven't made a call yet.
Authentication and permissions
- A key or token is a person. An app token is the person who signed in; a company API key acts as the person chosen in Acts as when it was made. Every call is checked as if that person did it on the screens: first the company's license (is the module included, is the license active), then the person's role permission (for example
employees.view), then their scope (which branches, departments or employees the role was given for). - A key can only narrow that person down, never widen it: its Permissions (Read = GET, Write = POST, Update = PUT and PATCH, Delete = DELETE), its Allowed IP addresses and its Expires on date. Keys can't create or manage other keys or webhooks; that needs a person's token.
- Each endpoint's permission is listed in the API reference. Endpoints with no permission listed are open to any active user of the company, and then show only what is theirs (my leaves, my payslips) or what they may see.
- A temporary or expired password blocks everything except
GET /api/v1/app/me,PUT /api/v1/app/me/passwordandDELETE /api/v1/app/token; other calls answer403with"code": "password_change_required". - Platform endpoints (
/api/v1/platform/…,/mcp/platform) are for Mihnaro's own staff and need a platform admin's key. Company keys and tokens can't use them, and platform keys can't use company endpoints.
Branches and isolation
A company can have several branches, and each branch is a separate workspace. Every /api/v1/app/companies/{company}/… call reads and writes one branch:
- the branch named for this call, with the query parameter
branchor the headerX-Branch(a branch ID, orallfor every branch you may work in); - otherwise the branch the person last switched to (
PUT /api/v1/app/companies/{company}/branchwith{"branch": 77}or{"branch": "all"}, andGETon the same path to read it); - otherwise the person's own branch.
A branch the person can't work in answers 403 with "code": "branch_forbidden". Records of another branch aren't returned: an ID from another branch answers 404, or 422 with a message saying which branch it is in when you name it in a form field. Data never crosses from one company to another.
In the first call of Getting started, the employees list said total: 8 in the Baghdad HQ branch and total: 10 with X-Branch: all.
Pages, filters and sorting
- Pages: lists are paged. Ask for a page with
?page=2. Many lists also takeper_page(usually up to 100); the others have a fixed page size. - The page info is in
meta:current_page(on some listspage),last_pageandtotal. Keep asking untilcurrent_pageequalslast_page. - Filters are query parameters, listed per endpoint in the reference. Common names:
searchorq(free text),status,employeeoremployee_id,branch_id,type, andfrom/todates. - Sorting is fixed per list and described in the endpoint (for example employees by employee number, leaves by start date, payroll runs newest first). There is no general
sortparameter.
"meta": {"current_page": 1, "last_page": 4, "total": 8}
Dates, times and time zones
- Dates are
YYYY-MM-DD("hire_date": "2026-03-01"), monthsYYYY-MM(GET …/payroll/2026-10). - Times are on the company's wall clock, the time zone set for the company (Asia/Baghdad unless your company chose another; it is in each branch's
timezone). A clock-in at 08:00 in Baghdad is08:00in the answer, whatever time zone your server is in. - Date-times in answers are ISO 8601 with the offset, for example
2026-11-02T22:40:00+03:00. - When you send a date-time, include the offset (
2026-11-02T08:00:00+03:00) and Mihnaro converts it to the company's clock. Without an offset it is read as the company's wall clock.
Languages
- Messages and errors are in English. Send
Accept-Language: arfor Arabic. - Names you enter in two languages come back as both:
"name": {"en": "Ali Hassan", "ar": "علي حسن"}. When you create or edit such fields, send the same{"en": "…", "ar": "…"}shape.
The same invalid call in Arabic:
{"message": "اختر قيمة من القائمة.", "errors": {"status": ["اختر قيمة من القائمة."]}}
Errors
Errors are always JSON with a message. Many add a code your program can act on, and validation errors add errors, one list of messages per field:
{
"message": "The selected status is invalid. (and 1 more error)",
"errors": {
"status": ["The selected status is invalid."],
"from": ["The from field must be a valid date."]
}
}
| Status | Meaning |
|---|---|
200, 201 |
Done (201 when something was created) |
401 |
Not signed in: no token, or a wrong, expired or revoked one (invalid_api_key for keys) |
403 |
Signed in but not allowed: the permission, the license, the branch (branch_forbidden), the key's limits (forbidden), or password_change_required |
404 |
Not found, or not one you may see |
409 |
The record isn't ready for this yet (for example a signed document's PDF before everyone has signed) |
422 |
The data is not valid; see errors. Some refusals add a code |
426 |
update_required: only for the Mihnaro mobile app, when its version is too old |
429 |
Too many requests; wait for the seconds in Retry-After |
500 |
Our error. Try again later, and tell support if it continues |
Codes you may meet:
| Code | When |
|---|---|
invalid_api_key |
The API key is wrong, expired or revoked (401) |
forbidden |
The key's company, method or IP address doesn't allow the call (403) |
branch_forbidden |
The branch asked for isn't one you may work in (403) |
password_change_required |
The person must change their password first (403) |
two_factor_required |
Signing in needs the two-step code: send code (422) |
update_required |
The mobile app is too old; the answer's update says where to update (426) |
live_location_required |
A clock-in or out refused because the company requires this employee to share their live location from the Mihnaro app (422, message in errors.) |
Clock-in and clock-out refusals all answer 422 with the message in errors.clock and a code such as device_pending, device_blocked, mock_location, wifi_required, held_for_review or live_location_required.
Rate limits
| What | Limit |
|---|---|
Company endpoints (/) |
60 requests a minute |
| Signing in, password and two-step calls | 10 a minute |
| Platform endpoints | 120 a minute |
| MCP: company server / platform server | 60 / 120 a minute |
Some endpoints have their own stricter limit; it is shown in the reference. Limits count per signed-in person. Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining; past the limit you get 429 with Retry-After (seconds).
Retries and idempotency
- The API has no
Idempotency-Keyheader.GET,PUT,PATCHandDELETEare safe to repeat: doing them twice leaves the same result. APOSTthat timed out may have worked: check (for example list the leaves) before you send it again, or you may create it twice. - On
429wait forRetry-After; on500,502or503retry with a growing wait (1, 2, 4 … seconds). - Webhook deliveries keep the same
X-Mihnaro-DeliveryID on every retry and resend: store it and skip the ones you have already handled (Webhooks).
Versions
- The version is in the address:
/api/v1. Withinv1we only add things: new endpoints, new fields in answers, new optional parameters. Write your code to ignore fields it doesn't know. - A change that would break existing code (removing or renaming a field, changing a meaning) goes to a new version,
/api/v2, andv1keeps working. - The API reference and the OpenAPI file are generated from the live code, so they always match the server.
Last updated: