Getting started
From an API key to a finished v4 check: hosts, headers, the two-call flow, statuses, errors and a complete client.
A v4 check takes seconds to a minute, because Mantys asks the payer's own system. So the API is asynchronous: you create a check, then poll it until it is finished. This page takes you from your credentials to a working client.
What you need
Mantys gives you three values. Keep them out of your source code.
| Value | Header | Notes |
|---|---|---|
| API key | X-Api-Key | Long-lived; there is no token exchange. One key can cover several clinics. |
| Clinic id | X-Clinic-Id | The clinic the check is for. It has to be one of your key's clinics, or you get 403. |
| Tenant id | X-Client-ID | Your organisation, for example yourorg. |
If you already call v3, these are the same three values. Only the path changes: /v4/... instead
of /v2/api-integration-v3/....
Hosts
| Host | Use it for |
|---|---|
https://hrcm-api.themantys.com | Production. Real checks, and test IDs. |
https://uat.hrcm-api.themantys.com | Building your client. It answers test IDs and nothing else: any other request is 403 UAT_TEST_IDS_ONLY. |
Every path in these docs hangs off one of those, for example
https://hrcm-api.themantys.com/v4/eligibility-checks. A test check belongs to the host that
created it, so create and poll on the same host.
A real ID on production is a real check
Production asks the payer's live system on your clinic's account. Build and test with 990- test
IDs, which never contact a payer, and send real patients only once your client works.
Set up your shell
The examples on these pages read your credentials from the environment:
export MANTYS_API_KEY=... # without the "Bearer " prefix
export MANTYS_CLINIC_ID=...
export MANTYS_CLIENT_ID=...
export HOST=https://uat.hrcm-api.themantys.comThe flow
status is final, the body you already have is the answer. There is no separate result endpoint.That is the whole integration: one POST to start a check, then the same GET until the check's
status is final. Search-all checks, several IDs per patient and benefit plans all use these two
calls; only the body you read at the end has more in it.
Your first check
These four steps use test ID 990-01, which answers like a real Nextcare (TPA002) check of an
eligible member. Nothing is sent to a payer.
Create the check
curl -sS -X POST "$HOST/v4/eligibility-checks" \
-H "X-Api-Key: Bearer $MANTYS_API_KEY" \
-H "X-Clinic-Id: $MANTYS_CLINIC_ID" \
-H "X-Client-ID: $MANTYS_CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{
"tpa_name": "TPA002",
"identifiers": [{ "id_type": "EMIRATESID", "id_value": "990-01" }],
"visit_type": "OUTPATIENT",
"name": "Test Patient"
}'201 Created. Keep the id: it is the only handle on the check.
{
"id": "00000000-0000-4000-a000-000000099001",
"status": "IN_QUEUE",
"is_search_all": false,
"search_all": null
}Poll it
CHECK_ID=00000000-0000-4000-a000-000000099001
curl -sS "$HOST/v4/eligibility-checks/$CHECK_ID" \
-H "X-Api-Key: Bearer $MANTYS_API_KEY" \
-H "X-Clinic-Id: $MANTYS_CLINIC_ID" \
-H "X-Client-ID: $MANTYS_CLIENT_ID"200 OK while it runs. Both answer lists are empty, and policy_authority is null.
{
"id": "00000000-0000-4000-a000-000000099001",
"status": "NAVIGATING_WEBSITE",
"created_at": "2026-11-06T17:01:34",
"updated_at": "2026-11-06T17:01:37.243315",
"request": { "tpa_name": "TPA002", "visit_type": "OUTPATIENT", "...": "..." },
"eligible_responses": [],
"other_responses": [],
"policy_authority": null
}Stop at a final status
Repeat the same GET every 5 to 10 seconds. A test check finishes 10 seconds after the create:
Read the answer
{
"id": "00000000-0000-4000-a000-000000099001",
"status": "COMPLETE",
"created_at": "2026-11-06T17:01:34",
"updated_at": "2026-11-06T17:01:44",
"eligible_responses": [
{
"id": "00000000-0000-4000-a000-000000099001:TPA002",
"tpa_id": "TPA002",
"tpa_name": "NEXTCARE ARAB GULF HEALTH SERVICES - TPA002",
"status": "found",
"is_eligible": true,
"message": null,
"completed_at": "2026-11-06T17:01:44",
"data": { "patient_info": { "...": "..." }, "policy_network": { "...": "..." } }
}
],
"other_responses": [],
"policy_authority": "MOH"
}Every payer answer is in exactly one of the two lists. Here the payer said eligible, so the row is
in eligible_responses. A member the payer could not find, or a payer that could not be checked,
lands in other_responses with its own status. Get a check's result explains
every field in the row.
To try other cases without a terminal, use Try it: it plays the same calls in the browser with the real answers for every test ID.
Headers
Send these on every call except GET /v4/health:
| Header | Value | Example |
|---|---|---|
X-Api-Key | Bearer + your API key | Bearer $MANTYS_API_KEY |
X-Clinic-Id | The clinic the request is for. It must be one your key is allowed to use. | $MANTYS_CLINIC_ID |
X-Client-ID | Your tenant id, as given by Mantys | $MANTYS_CLIENT_ID |
Content-Type | application/json (on POST) |
The Bearer prefix is case-sensitive
Exactly Bearer, a capital B, then one space, then the key. bearer <key> or a bare key is 401
MALFORMED_AUTH_HEADER. Header names themselves are not case-sensitive.
A missing header is 422 VALIDATION_FAILED, with one details[] entry per missing header whose
field is the header name. Present headers are then checked in this order, and the first failure
answers: the Bearer prefix (401), the key (401 INVALID_API_KEY), the clinic (403
CLINIC_NOT_AUTHORIZED), then the tenant (403 UNKNOWN_TENANT).
A 403 CLINIC_NOT_AUTHORIZED means your key is fine and the clinic is not on it. Do not rotate the
key; ask Mantys to add the clinic.
What happens to a request
990- test ID leaves at step 3 with a canned 201.The split between 422 and 400 is the useful one:
- 422 is about the request itself: a missing or unknown field, a wrong type, more than 5 IDs, a duplicate ID, malformed JSON. It is the same for every payer. Fix your client.
- 400 is about this payer: it does not take that
id_type, thatvisit_type, or it needs a doctor.detailslists every problem at once, so fix them all and resend.
Either way, nothing was started. You can resend the corrected request straight away.
Statuses
status | Meaning | Final? |
|---|---|---|
IN_QUEUE | Accepted, waiting to start | No |
NAVIGATING_WEBSITE, CHECKING_ELIGIBILITY, EXTRACTING_DATA, PROCESSING | Running at the payer | No |
COMPLETE | Finished. Read eligible_responses and other_responses | Yes |
FAILED | Could not get an answer from the payer | Yes |
CANCELLED | Cancelled at the payer | Yes |
COMPLETE does not mean eligible
COMPLETE means the check finished. A patient who is not covered, a member the payer cannot find,
and a payer whose login failed all come back COMPLETE, with the row in other_responses. Read
the rows, not the status.
Polling, in practice:
- Poll every 5 to 10 seconds. Faster does not make the payer answer sooner.
- Stop on
COMPLETE,FAILEDorCANCELLED. Keep polling on anything else, including a status you do not recognise. - Give up after a timeout you choose, and show the check as still running rather than as not eligible. Search-all checks ask every payer and take longest.
Errors
Every error, on every endpoint, has this shape:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request failed provider validation",
"details": [
{
"field": "identifiers[0].id_value",
"code": "INVALID_EMIRATES_ID",
"message": "'123-4567' is not a valid id_value; expected 784-YYYY-NNNNNNN-N"
}
],
"request_id": "req_a96aaecb3ab3"
}
}| Field | What it is |
|---|---|
error.code | A fixed code, such as CHECK_NOT_FOUND. Branch on this. |
error.message | A sentence for people. It may be reworded at any time; never parse it. |
error.details | One entry per problem, on 422 and 400. An empty list on the others. |
error.details[].field | Where the problem is: a body field (identifiers[0].id_value), a header name, or a query parameter. |
error.details[].code | What is wrong with that field, such as UNSUPPORTED_VISIT_TYPE. |
error.request_id | Quote it when you contact Mantys about the call. |
New codes may be added. Treat a code you do not know by its HTTP status. Every code is on
Error codes.
Times and dates
Times are ISO 8601 in UTC, without a Z or an offset, for example 2026-10-17T09:12:03. Some
carry fractional seconds, 2026-10-17T09:12:03.536000. Parse both, and treat them as UTC: a parser
that assumes local time for a timestamp with no offset will be four hours out in the UAE.
Dates in query strings are YYYY-MM-DD.
A complete client
Create, poll until final, then print each payer's answer. Point HOST at production and use a real
ID only once this works with test IDs.
import os
import time
import requests
HOST = os.environ.get("HOST", "https://uat.hrcm-api.themantys.com")
HEADERS = {
"X-Api-Key": f"Bearer {os.environ['MANTYS_API_KEY']}",
"X-Clinic-Id": os.environ["MANTYS_CLINIC_ID"],
"X-Client-ID": os.environ["MANTYS_CLIENT_ID"],
}
FINAL = {"COMPLETE", "FAILED", "CANCELLED"}
def run_check(body: dict, timeout_s: float = 600, every_s: float = 5) -> dict:
created = requests.post(f"{HOST}/v4/eligibility-checks", json=body, headers=HEADERS, timeout=30)
if created.status_code != 201:
error = created.json()["error"]
raise RuntimeError(f"{created.status_code} {error['code']}: {error.get('details')}")
check_id = created.json()["id"]
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
time.sleep(every_s)
check = requests.get(f"{HOST}/v4/eligibility-checks/{check_id}", headers=HEADERS, timeout=30)
check.raise_for_status()
result = check.json()
if result["status"] in FINAL:
return result
raise TimeoutError(f"check {check_id} still running after {timeout_s} s")
result = run_check({
"tpa_name": "TPA002",
"identifiers": [{"id_type": "EMIRATESID", "id_value": "990-01"}],
"visit_type": "OUTPATIENT",
})
print(result["status"])
for row in result["eligible_responses"] + result["other_responses"]:
print(row["tpa_id"], row["status"], row["is_eligible"])Next
- Create a check: every body field, search-all, and
extra_args. - Visit types by payer: the
visit_typevalues each payer accepts. - Get a check's result: the rows in
eligible_responsesandother_responses. - Test IDs: every canned case, for testing each branch of your client.
- Moving from v3: the checklist, if you already call v3.