Getting started with the API
From zero to your first working call: the address, an API key or token, the headers, your company ID, and copy-ready curl, JavaScript and Python.
Mihnaro has a REST API for what you can do on the screens, with the same permissions and checks. This guide takes you from nothing to a working call in about five minutes. Every example below was run against a Mihnaro server, and the answers shown are the real ones (from the demo company, ID 16).
The address
Every endpoint starts with:
https://api.mihnaro.com/api/v1
Requests and answers are JSON. Company endpoints look like /api/v1/app/companies/{company}/…, where {company} is your company's ID (see "Find your company ID" below). The full list of endpoints is in the API reference.
Get a key or a token
There are two ways to sign in. Both act as a real person in your company and can do only what that person can do.
A company API key (for integrations)
Best for a server that talks to Mihnaro on its own: an ERP, a BI tool, a script.
- Sign in to Mihnaro as someone with the Manage API keys and webhooks permission (company owners have it).
- Open Company → API keys and webhooks in the side menu (also under System preferences → Integration setup).
- On the API keys tab choose New API key and fill in:
- Name: what uses it, for example "ERP sync".
- Acts as: the person the key works as. The key can do only what this person may do, in their scope (branches and employees).
- Permissions: Read (GET), Write (POST), Update (PUT and PATCH), Delete (DELETE). Tick only what you need.
- Allowed IP addresses: separate with commas, or leave empty to allow any address.
- Expires on: leave empty for no expiry.
- Choose Create key. The key starts with
mhk_and is shown once: copy it now and keep it somewhere safe. Mihnaro stores only a fingerprint of it, so nobody can show it to you again.
A key works only on its own company's endpoints (/api/v1/app/companies/{its company}/…). It does not work on /api/v1/app/me or on MCP; use a token for those. You can edit or revoke a key on the same screen; a revoked key stops at once.
A user token (for apps and MCP)
Best for an app where people sign in with their own account, and for AI assistants (MCP). The token acts as the person who signed in:
curl -X POST "https://api.mihnaro.com/api/v1/app/token" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"your password","device_name":"My integration"}'
The answer (201), with the token shortened here:
{
"data": {
"token": "9|iZv9PuRH…",
"user": {
"id": 330, "name": "Docs QA", "email": "[email protected]",
"must_change_password": false, "password_expired": false,
"two_factor": {"available": true, "mode": "optional", "enabled": false, "required": false, "recovery_codes_left": 0},
"preferences": {"locale": "en", "theme": "system"}
},
"companies": [
{"id": 16, "name": {"en": "Demo Company", "ar": "الشركة التجريبية"}, "can_access": true, "reason": null, "two_factor_setup_required": false}
]
}
}
emailcan also be a username.device_nameis any label you will recognise later.- With two-step verification on, the first call answers
422with"code": "two_factor_required"; send the same call again with"code": "123456"from the authenticator app (or a recovery code). - Sign-in follows the same rules as the app: wrong passwords lock the account for a while, and a suspended company or an ended license refuses the sign-in.
- Sign out (revoke this token) with
DELETE /api/v1/app/token. - This call allows 10 tries a minute.
The headers
Send these on every call:
Authorization: Bearer <your mhk_ key or token>
Accept: application/json
Content-Type: application/jsonwhen you send a JSON body, ormultipart/form-datawhen you upload a file.Accept-Language: arfor Arabic messages and errors (English is the default).X-Branch: <branch id>orX-Branch: allto work in another branch for this one call (see API concepts).
Find your company ID
- With a token:
GET /api/v1/app/melists your companies and their IDs (thecompanieslist above). - With an API key: the notice that shows the new key also says where to send it: "Send it as "Authorization: Bearer <key>" to /api/v1/app/companies/16/…". The number is your company's ID. The key works only on that company; another ID answers
403.
Then check what the company gives you:
curl "https://api.mihnaro.com/api/v1/app/companies/16" \
-H "Authorization: Bearer $MIHNARO_TOKEN" \
-H "Accept: application/json"
The answer starts like this (each branch has more settings than shown):
{
"data": {
"id": 16,
"name": {"en": "Demo Company", "ar": "الشركة التجريبية"},
"modules": ["hrm", "fsm"],
"license": {"status": "active", "ends_on": "2027-10-31", "access_ends_on": "2027-11-07", "warning": false},
"branches": [
{"id": 76, "name": {"en": "Baghdad HQ", "ar": "المقر - بغداد"}, "code": "BGD", "country": "IQ", "currency": "IQD", "timezone": "Asia/Baghdad", "is_main": true, "is_active": true},
{"id": 77, "name": {"en": "Basra", "ar": "البصرة"}, "code": "BSR", "country": "IQ", "currency": "IQD", "timezone": "Asia/Baghdad", "is_main": false, "is_active": true}
]
}
}
Your first call: list employees
Put your key (or token) and company ID in environment variables so you can paste the examples as they are:
export MIHNARO_TOKEN="mhk_your_key_here"
export MIHNARO_COMPANY=16
The person the key acts as needs the employees.view permission for this call.
curl
curl "https://api.mihnaro.com/api/v1/app/companies/$MIHNARO_COMPANY/employees?per_page=2" \
-H "Authorization: Bearer $MIHNARO_TOKEN" \
-H "Accept: application/json"
JavaScript (fetch)
Node 18 or later, or a browser on your own server. Never put a key in a web page anyone can open.
const BASE = process.env.MIHNARO_BASE ?? 'https://api.mihnaro.com/api/v1';
const TOKEN = process.env.MIHNARO_TOKEN; // mhk_… API key or app token
const COMPANY = process.env.MIHNARO_COMPANY; // your company's ID
const response = await fetch(`${BASE}/app/companies/${COMPANY}/employees?per_page=2`, {
headers: { Authorization: `Bearer ${TOKEN}`, Accept: 'application/json' },
});
const body = await response.json();
if (!response.ok) {
throw new Error(`${response.status} ${body.code ?? ''}: ${body.message}`);
}
console.log(body.meta);
for (const employee of body.data) {
console.log(employee.employee_number, employee.name.en, employee.department_name);
}
Python (requests)
import os
import requests
BASE = os.environ.get("MIHNARO_BASE", "https://api.mihnaro.com/api/v1")
TOKEN = os.environ["MIHNARO_TOKEN"] # mhk_… API key or app token
COMPANY = os.environ["MIHNARO_COMPANY"] # your company's ID
response = requests.get(
f"{BASE}/app/companies/{COMPANY}/employees",
params={"per_page": 2},
headers={"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"},
timeout=30,
)
body = response.json()
if not response.ok:
raise SystemExit(f"{response.status_code} {body.get('code', '')}: {body['message']}")
print(body["meta"])
for employee in body["data"]:
print(employee["employee_number"], employee["name"]["en"], employee["department_name"])
Both scripts print the same thing:
{'current_page': 1, 'last_page': 4, 'total': 8, 'current_employees': 10, 'max_employees': 50}
EMP-0001 Ali Hassan Human Resources
EMP-0002 Sara Qasim Human Resources
The answer
200, with each employee's record (about 70 fields; a few are shown here):
{
"data": [
{
"id": 6211,
"employee_number": "EMP-0001",
"name": {"en": "Ali Hassan", "ar": "علي حسن"},
"email": "[email protected]",
"status": "active",
"branch_id": 76,
"branch_name": "Baghdad HQ",
"department_name": "Human Resources",
"job_title": {"en": "HR Director", "ar": "مدير الموارد البشرية"},
"hire_date": "2026-03-01"
},
{
"id": 6212,
"employee_number": "EMP-0002",
"name": {"en": "Sara Qasim", "ar": "سارة قاسم"},
"email": "[email protected]",
"status": "active",
"branch_id": 76,
"branch_name": "Baghdad HQ",
"department_name": "Human Resources",
"hire_date": "2025-10-01"
}
],
"meta": {"current_page": 1, "last_page": 4, "total": 8, "current_employees": 10, "max_employees": 50}
}
Notice total is 8 while the company has 10 current employees: the list shows the branch you are working in (Baghdad HQ here). Add -H "X-Branch: all" and total becomes 10. API concepts explains why.
This list also takes search (number, name or email), status (current by default, or active, suspended, terminated, archived, all), employee_number, branch_id, salary_status (running or stopped), page and per_page (up to 100).
When something goes wrong
Errors are JSON with a message, and often a code. The common first-day ones, as Mihnaro answers them:
| You see | Why | What to do |
|---|---|---|
401 {"message":"Unauthenticated. |
No Authorization header, or a wrong or revoked token |
Check the header and the token |
401 "code":"invalid_api_key" |
The key is wrong, expired or revoked | Create a new key |
403 "This API key works only on its own company's endpoints. |
The {company} in the address is not the key's company |
Use the key's company ID |
403 "This API key may not make this request. |
The key lacks that method (for example a Read key sending POST) or your IP address is not allowed | Edit the key's permissions or addresses |
403 "code":"branch_forbidden" |
X-Branch names a branch you can't work in |
Use one of your branches, or leave it out |
422 with errors |
The data is not valid | Fix the fields listed in errors |
Everything about errors, pages, dates and limits is in API concepts.
Next steps
- API concepts: permissions and branches, pages and filters, dates and time zones, errors, limits and versions.
- API reference by section: the main endpoints of each section, and the full generated reference.
- Webhooks: get told the moment something changes.
- Use Mihnaro from an AI assistant (MCP).
- Download the OpenAPI file and import it into Postman or Insomnia.
Last updated: