---
title: "ABDM API — Error Handling Reference"
description: "How the Nice HMS ABDM integration APIs report errors: the JSON error envelope, the stable error-code table (code → HTTP → meaning), the per-route error reference, and the x-request-id correlation header."
date: 2026-08-26
lastModified: 2026-10-04
category: "developer"
author: "Dr. Umesh Bilagi"
beta: true
---

## Base URL

All endpoints are called with an `Authorization: Bearer <idToken>` header — except
`/auth_token`, which issues that token.

```
https://asia-south1-psychic-city-328609.cloudfunctions.net/<endpoint>
```

- Default method is **POST**.
- `/discharge_patient` is **PUT**.
- `/health_document` is **multipart/form-data** (POST).

---

## Error envelope (every non-2xx response)

Every error response uses the same JSON shape. There are no other error shapes.

```json
{
  "message": "Human-readable summary of the failure",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": [
      { "field": "patient.abhaAddress", "message": "The direct input of Abha Address or Abha number is not possible." }
    ]
  }
}
```

| Field | Always present? | Meaning |
|---|---|---|
| `message` | Yes | Top-level summary (kept for backward compatibility — safe to keep parsing it). |
| `error.code` | Yes | Stable, machine-readable code. **Match on this.** |
| `error.message` | Yes | Curated, human-readable message. |
| `error.details` | **No** — only on `VALIDATION_ERROR` | Array of `{ field, message }`, one entry per failed field. |

**Example — non-validation error**

```json
{
  "message": "Patient not found",
  "error": {
    "code": "PATIENT_NOT_FOUND",
    "message": "Patient not found"
  }
}
```

---

## Response headers

Every response — success or error — includes the same headers.

### `x-request-id`

- If your request included an `x-request-id` header, its value is **echoed back** unchanged.
- Otherwise a new UUID is generated.
- The value lives in the **header only** — never inside the JSON body.

Send an `x-request-id` on every request and log the request/response pair against it. When
escalating an issue to Nice HMS support, include this value — it correlates to the
server-side log of the full (unredacted) error.

### `Access-Control-Allow-Origin`

Every response includes **`Access-Control-Allow-Origin: *`**, so the API can be called
directly from browser-based clients (CORS is enabled for all origins). No per-origin
allow-list is required on the integrator's side.

---

## Error code taxonomy

Match on `error.code` — these are stable strings.

| `error.code` | HTTP | Meaning |
|---|---|---|
| `VALIDATION_ERROR` | 400 | Request body failed schema validation. See `error.details` for field-level reasons. Also used for an unsupported upload type, a missing file, or a PDF combined with images on `health_document`. |
| `VALIDATION_ERROR` | 413 | An uploaded file exceeds the 5 MB limit (`health_document`). |
| `INVALID_EMAIL_FORMAT` | 400 | An email string is malformed. |
| `INVALID_DOCTOR_GCP_FHIR_ID` | 400 | A `doctorGcpId` does not exist in the FHIR store (Practitioner). |
| `DOCTOR_DETAILS_REQUIRED` | 400 | `doctorDetails` is missing (`health_document`). |
| `INVALID_REQUEST_METHOD` | 400 | Wrong HTTP method for the endpoint. |
| `UNAUTHORIZED` | 401 | Missing/invalid/expired token, or the token is not mapped to an organization. |
| `SUBSCRIPTION_EXPIRED` | 403 | The organization's subscription is invalid or expired. |
| `PATIENT_NOT_FOUND` | 404 | No patient matches the supplied `patientId` / `abhaAddress` for this organization. |
| `ENCOUNTER_NOT_FOUND` | 404 | A supplied `encounterId` was not found for this patient/organization. |
| `NO_ACTIVE_ENCOUNTER` | 409 | A document endpoint resolved no encounter: the patient has no encounter at all (not even a finished one). Create a visit with `opd_patient` / `admit_patient` first. |
| `DOCTOR_NOT_FOUND` | 404 | The Practitioner FHIR id does not exist (`edit_doctor`). |
| `EMAIL_ALREADY_EXISTS` | 409 | Duplicate email when creating a doctor. |
| `ABDM_GATEWAY_ERROR` | 502 | Upstream ABDM gateway returned an error (e.g. OTP verification failed, auth mode unavailable). |
| `LINK_TOKEN_MISSING` | 502 | ABDM care-context link token was not found after the callback — a genuine failure, not a rate limit. |
| `LINK_TOKEN_RATE_LIMITED` | 429 | ABDM refused to issue a link token because its cap of **3 new link tokens per ABHA address per 24 hours** was reached. Reuse the existing token (valid ~6 months) and retry after 24 hours. |
| `DB_ERROR` | 500 | A database operation failed. Curated generic message. |
| `INTERNAL_ERROR` | 500 | Any unhandled / unknown error. Curated generic message. |

---

## HTTP status code summary

| Status | Category |
|---|---|
| 200 | Success |
| 400 | Bad request / validation |
| 401 | Unauthorized |
| 403 | Forbidden (subscription) |
| 404 | Not found |
| 409 | Conflict (duplicate email / no active encounter) |
| 413 | Payload too large (uploaded file exceeds the 5 MB limit) |
| 429 | Rate limited (ABDM link-token daily cap reached) |
| 500 | Internal server error |
| 502 | Bad gateway (ABDM upstream) |

---

## Validation semantics (important)

1. **Extra / unknown fields are ignored (stripped), not rejected.** You will not get an
   error for sending fields the API doesn't know about.
2. **Validation runs before auth.** A request that is both unauthenticated *and* has an
   invalid body returns `400 VALIDATION_ERROR` first (auth is checked after body
   validation).
3. **Field paths** in `details[].field` use dot notation, e.g. `patient.firstName`,
   `doctorDetails.0.doctorGcpId`, `patient.dob`.
4. **Curated messages only.** Responses never leak stack traces, SQL, DB error text, GCP
   FHIR paths/IDs, or raw ABDM payloads. The full unredacted cause is logged server-side
   against the `x-request-id`.

---

## Field enum values

Some `VALIDATION_ERROR` messages name the rejected field but not its accepted values —
e.g. `details[].message` is just `"Invalid category"`. The accepted values for each enum
field are listed in that endpoint's request table; see
[ABDM API — Documents](/docs/abdm-api-documents).

For example, `/diagnostic_report` `category` accepts:

> `Hematology` · `Biochemistry` · `Microbiology` · `Radiology` · `Others`

---

## Shared error codes (every authenticated route)

The following codes are present on every **authenticated** route:

> `VALIDATION_ERROR` (400) · `UNAUTHORIZED` (401) · `SUBSCRIPTION_EXPIRED` (403) ·
> `DB_ERROR` (500) · `INTERNAL_ERROR` (500)

Each endpoint's documentation lists **these shared codes + its own additional codes**.

---

## Per-route error reference

Where a route says **"patientNeeds"**, the body must include at least one of `abhaAddress`
(string) or `patientId` (number). Omitting both yields `400 VALIDATION_ERROR` with the
message *"Either abhaAddress or patientId is required"*.

The table lists each route's **additional** codes — the ones on top of the shared codes.
(`/auth_token` is public and skips the subscription check.)

| Endpoint | Additional error codes |
|---|---|
| `/auth_token` (public) | `VALIDATION_ERROR` (400) · `UNAUTHORIZED` (401) |
| `/create_patient` | — |
| `/create_doctor` | `INVALID_EMAIL_FORMAT` (400) · `EMAIL_ALREADY_EXISTS` (409) |
| `/edit_doctor` | `DOCTOR_NOT_FOUND` (404) |
| `/get_doctor_opd_patients_by_date` | — |
| `/get_doctor_ipd_patients_by_date` | — |
| `/patient_by_id` | `PATIENT_NOT_FOUND` (404) |
| `/patient_by_abhaAddress` | `PATIENT_NOT_FOUND` (404) |
| `/patient_register_abha_otp_init` | `ABDM_GATEWAY_ERROR` (502) |
| `/patient_register_abha_otp_confirm` | `ABDM_GATEWAY_ERROR` (502) |
| `/opd_patient` | `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) |
| `/admit_patient` | `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) |
| `/discharge_patient` | `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) · `ENCOUNTER_NOT_FOUND` (404) |
| `/discharge_summary` | `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) · `ENCOUNTER_NOT_FOUND` (404) · `NO_ACTIVE_ENCOUNTER` (409) · `ABDM_GATEWAY_ERROR` (502) · `LINK_TOKEN_MISSING` (502) · `LINK_TOKEN_RATE_LIMITED` (429) |
| `/diagnostic_report` | `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) · `ENCOUNTER_NOT_FOUND` (404) · `NO_ACTIVE_ENCOUNTER` (409) · `ABDM_GATEWAY_ERROR` (502) · `LINK_TOKEN_MISSING` (502) · `LINK_TOKEN_RATE_LIMITED` (429) |
| `/op_consultation` | `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) · `ENCOUNTER_NOT_FOUND` (404) · `NO_ACTIVE_ENCOUNTER` (409) · `ABDM_GATEWAY_ERROR` (502) · `LINK_TOKEN_MISSING` (502) · `LINK_TOKEN_RATE_LIMITED` (429) |
| `/procedure_ot_notes` | `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) · `ENCOUNTER_NOT_FOUND` (404) · `NO_ACTIVE_ENCOUNTER` (409) · `ABDM_GATEWAY_ERROR` (502) · `LINK_TOKEN_MISSING` (502) · `LINK_TOKEN_RATE_LIMITED` (429) |
| `/health_document` | `INVALID_REQUEST_METHOD` (400) · `DOCTOR_DETAILS_REQUIRED` (400) · `INVALID_DOCTOR_GCP_FHIR_ID` (400) · `PATIENT_NOT_FOUND` (404) · `ENCOUNTER_NOT_FOUND` (404) · `NO_ACTIVE_ENCOUNTER` (409) · `ABDM_GATEWAY_ERROR` (502) · `LINK_TOKEN_MISSING` (502) · `LINK_TOKEN_RATE_LIMITED` (429) |

---

## Not covered here

- **`/uhi`** and `/uhi/whatsapp` follow the **UHI protocol** (search/init/confirm/cancel/status),
  which has its own acknowledgement and error format — documented separately, not via this
  envelope.

---

## Quick reference — which code for which mistake

| You did this | You'll see |
|---|---|
| Missing/invalid token | `401 UNAUTHORIZED` |
| Expired subscription | `403 SUBSCRIPTION_EXPIRED` |
| Bad field / wrong enum / missing required field | `400 VALIDATION_ERROR` (+ `details`) |
| A file over 5 MB on `health_document` | `413 VALIDATION_ERROR` |
| An unsupported file type, no file, or a PDF combined with images on `health_document` | `400 VALIDATION_ERROR` |
| A file on `health_document` you expected to reach ABDM, but `status` isn't `final` | `200`, with `abdmNotified: false` |
| Wrong `doctorGcpId` | `400 INVALID_DOCTOR_GCP_FHIR_ID` |
| Unknown `patientId` / `abhaAddress` | `404 PATIENT_NOT_FOUND` |
| Unknown `encounterId`, or one belonging to another patient/org | `404 ENCOUNTER_NOT_FOUND` |
| A document endpoint where the patient has no encounter at all (not even finished) | `409 NO_ACTIVE_ENCOUNTER` |
| Duplicate email on `create_doctor` | `409 EMAIL_ALREADY_EXISTS` |
| ABDM OTP/search/verify failure — including `patient_register_abha_otp_init` (ABHA address search / OTP request) and `patient_register_abha_otp_confirm` (OTP verification) | `502 ABDM_GATEWAY_ERROR` (its `error.message` is ABDM's own reason) |
| ABDM link token missing after callback (genuine failure) | `502 LINK_TOKEN_MISSING` |
| A 4th+ link-token generate for the same ABHA within 24 hours | `429 LINK_TOKEN_RATE_LIMITED` |
| Anything else unexpected | `500 INTERNAL_ERROR` / `500 DB_ERROR` |

---

## Troubleshooting: `PATIENT_NOT_FOUND` (404)

`PATIENT_NOT_FOUND` means no patient matched the identifier you sent **for the organization
tied to your bearer token**. Common causes, in order of likelihood:

1. **`patientId` sent as a string, not a number.** `patientId` must be a JSON **number** —
   `1250`, not `"1250"`. A quoted value can fail the lookup even when the digits look right.
2. **`abhaAddress` is a placeholder, not a real address.** A literal value like
   `"{{abhaAddress}}"` (an unresolved template variable) matches no patient. Send the actual
   ABHA address (e.g. `savitribilagi@sbx`), or omit `abhaAddress` entirely when using
   `patientId`.
3. **Wrong identifier for the patient type.** A non-ABHA patient (created via
   `/create_patient`) has an **empty `abhaAddress`** — it can only be fetched by `patientId`,
   not by ABHA address. Conversely an ABHA patient is fetched by `abhaAddress`.
4. **Organization scoping.** A patient created under one organization is not visible to a
   different organization's token. Use the token of the org that registered the patient.
5. **Sending both identifiers.** Validation accepts either, but the lookup precedence when
   both are supplied is undefined — send **exactly one** (the one you intend to match) so a
   bad `abhaAddress` can't mask a valid `patientId`.

Confirm the identifier before creating a visit by fetching it with `/patient_by_id` or
`/patient_by_abhaAddress`. If it resolves there but a visit endpoint still returns
`PATIENT_NOT_FOUND`, escalate with the `x-request-id` — the server log holds the full
unredacted cause.

---

## Troubleshooting: the link token (`LINK_TOKEN_RATE_LIMITED` 429 / `LINK_TOKEN_MISSING` 502)

Document endpoints file a care context against an ABDM **link token** before pushing. ABDM
issues at most **3 new link tokens per ABHA address per 24 hours**; a token is valid about
**6 months** and supports unlimited document sends. So a token should be generated **once**
and reused, not re-created per document.

| What you see | What it means | What to do |
|---|---|---|
| `429 LINK_TOKEN_RATE_LIMITED` | The 3-per-24 h cap was reached for this ABHA. ABDM refused to issue another token. | **Do not retry within the same 24 h window.** Reuse the existing token — if you hold one, send it; otherwise wait and retry after 24 h. |
| `502 LINK_TOKEN_MISSING` | No token was found after the `generateToken` callback. A genuine failure (or a callback that had not yet landed). | Retry once after a short delay; if it persists, escalate with the `x-request-id`. |

Guidance:

1. **Generate once, reuse.** A link token lasts ~6 months — generate it at patient/ABHA
   linking time and keep it, rather than requesting a new one for each document.
2. **Don't retry a 429 in a loop.** Each retry counts against the same daily cap and keeps
   you blocked; wait out the 24 h window.
3. **A `429` is not a payload problem.** Nothing in your request body caused it, so changing
   fields won't help.

---

## Date and time fields

Timestamp fields use **ISO 8601** — e.g. `"2023-05-08T19:21:23.918Z"` (see the examples in
each endpoint). `patient.dob` is the exception: `YYYY-MM-DD` (date only, no time).

| Field | Endpoint(s) | Required |
|---|---|---|
| `date` | `/opd_patient`, `/discharge_summary`, `/op_consultation`, `/procedure_ot_notes` | Yes |
| `date` | `/diagnostic_report`, `/health_document` | No (defaults to request time) |
| `doa` | `/admit_patient` | Yes |
| `dod` | `/discharge_patient` | Yes |
| `patient.dob` | `/create_patient` | Yes |

A `VALIDATION_ERROR` of `{ "field": "date", "message": "Required" }` means you hit one of
the endpoints in the first row and omitted `date`.
