Skip to content
Mihnaro
Help center

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/password and DELETE /api/v1/app/token; other calls answer 403 with "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:

  1. the branch named for this call, with the query parameter branch or the header X-Branch (a branch ID, or all for every branch you may work in);
  2. otherwise the branch the person last switched to (PUT /api/v1/app/companies/{company}/branch with {"branch": 77} or {"branch": "all"}, and GET on the same path to read it);
  3. 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 take per_page (usually up to 100); the others have a fixed page size.
  • The page info is in meta: current_page (on some lists page), last_page and total. Keep asking until current_page equals last_page.
  • Filters are query parameters, listed per endpoint in the reference. Common names: search or q (free text), status, employee or employee_id, branch_id, type, and from / to dates.
  • 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 sort parameter.
"meta": {"current_page": 1, "last_page": 4, "total": 8}

Dates, times and time zones

  • Dates are YYYY-MM-DD ("hire_date": "2026-03-01"), months YYYY-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 is 08:00 in 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: ar for 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)

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 (/api/v1/app/…) 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-Key header. GET, PUT, PATCH and DELETE are safe to repeat: doing them twice leaves the same result. A POST that timed out may have worked: check (for example list the leaves) before you send it again, or you may create it twice.
  • On 429 wait for Retry-After; on 500, 502 or 503 retry with a growing wait (1, 2, 4 … seconds).
  • Webhook deliveries keep the same X-Mihnaro-Delivery ID 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. Within v1 we 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, and v1 keeps working.
  • The API reference and the OpenAPI file are generated from the live code, so they always match the server.

Last updated:

Still need help?

Our support team answers in English and Arabic.

Email support

We use cookies to understand how the site is used. You can accept or decline.