Mantys Eligibility API

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.

ValueHeaderNotes
API keyX-Api-KeyLong-lived; there is no token exchange. One key can cover several clinics.
Clinic idX-Clinic-IdThe clinic the check is for. It has to be one of your key's clinics, or you get 403.
Tenant idX-Client-IDYour 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

HostUse it for
https://hrcm-api.themantys.comProduction. Real checks, and test IDs.
https://uat.hrcm-api.themantys.comBuilding 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.com

The flow

EVERY CHECK, SINGLE PAYER OR SEARCH-ALL1 · CREATEPOST /v4/eligibility-checks201 · returns the check id2 · POLLGET /v4/eligibility-checks/{id}IN_QUEUE, NAVIGATING_WEBSITE, …3 · DONECOMPLETE · FAILED · CANCELLEDthe body is the answersame request, every 5 to 10 secondsSEARCH-ALL (tpa_name BOTH, DHPO or RIYATI)The 201 says is_search_all: true. Poll the same way: status reads COMPLETE once every payer has answered,with one row per payer across eligible_responses and other_responses.CANCEL (optional)POST /v4/eligibility-checks/{id}/cancellation202, then poll until CANCELLED
Two calls. The poll is the same request every time; when its 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:

IN_QUEUENAVIGATING_WEBSITEEXTRACTING_DATACOMPLETElists emptylists emptythe case's answer0 s3 s5 s10 s
Seconds since the create. The clock is the create's, not your polls': poll late and you see COMPLETE straight away.

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:

HeaderValueExample
X-Api-KeyBearer + your API keyBearer $MANTYS_API_KEY
X-Clinic-IdThe clinic the request is for. It must be one your key is allowed to use.$MANTYS_CLINIC_ID
X-Client-IDYour tenant id, as given by Mantys$MANTYS_CLIENT_ID
Content-Typeapplication/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

POST /v4/eligibility-checks, CHECKED IN THIS ORDER1 · HEADERSall three present422VALIDATION_FAILED2 · KEY & CLINICBearer, key, clinic, tenant401 / 403see Error codes3 · BODYknown fields, 1-5 IDs422VALIDATION_FAILED4 · PAYER RULESid_type, visit_type, doctor400every problem in details5 · ACCEPTED201 · IN_QUEUEFail labels: the HTTP status, then error.code. 400s list every payer problem at once, so one fix-and-resend covers them all.canned 201990- test ID: payer rules skipped, nothing stored
The first failing check answers and nothing is started. 422 means the request itself is malformed; 400 means it is well formed but this payer will not take it. A 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, that visit_type, or it needs a doctor. details lists every problem at once, so fix them all and resend.

Either way, nothing was started. You can resend the corrected request straight away.

Statuses

KEEP POLLINGIN_QUEUEaccepted, waiting to startRUNNING AT THE PAYER: any of these, in any orderNAVIGATING_WEBSITECHECKING_ELIGIBILITYEXTRACTING_DATAPROCESSINGBoth answer lists are empty, and policy_authority is null, while a check is running.STOP: BRANCH ON THESE THREECOMPLETEanswered: read both listsFAILEDno answer from the payerCANCELLEDcancellation confirmedDashed: after a cancellation, a COMPLETE orFAILED check moves to CANCELLED.
Test for the three final statuses, never against the running ones: a client that waits for "not IN_QUEUE and not NAVIGATING_WEBSITE" breaks the day a new running state appears.
statusMeaningFinal?
IN_QUEUEAccepted, waiting to startNo
NAVIGATING_WEBSITE, CHECKING_ELIGIBILITY, EXTRACTING_DATA, PROCESSINGRunning at the payerNo
COMPLETEFinished. Read eligible_responses and other_responsesYes
FAILEDCould not get an answer from the payerYes
CANCELLEDCancelled at the payerYes

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, FAILED or CANCELLED. 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"
  }
}
FieldWhat it is
error.codeA fixed code, such as CHECK_NOT_FOUND. Branch on this.
error.messageA sentence for people. It may be reworded at any time; never parse it.
error.detailsOne entry per problem, on 422 and 400. An empty list on the others.
error.details[].fieldWhere the problem is: a body field (identifiers[0].id_value), a header name, or a query parameter.
error.details[].codeWhat is wrong with that field, such as UNSUPPORTED_VISIT_TYPE.
error.request_idQuote 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

On this page