Document control
- Document status
- For review by the NPS Central technology team, then for issue to each HBA
- Applies to
- NPS Central and every Health Benefit Administrator (HBA) that checks NPS Swasthya eligibility, reads the eligible balance or raises an NPS Swasthya withdrawal through NPS Central
- Prepared by
- NPS Central technology team
- Date
- 21-Oct-2026
1Common Integration Architecture and Principles
An NPS Swasthya subscriber pays hospital bills from the corpus of their NPS Swasthya scheme. The Health Benefit Administrator (HBA) named on the subscriber's cover services the claim. To do that it needs to know whether the subscriber can withdraw, how much is available, and to raise the withdrawal. Each of those is held by the Central Recordkeeping Agency (CRA) that keeps the subscriber's PRAN.
Each CRA publishes its own Swasthya APIs, with its own transport, keys, encryption and response codes. NPS Central has built an adapter for each. An HBA integrates once, with NPS Central. It sends a PRAN; NPS Central finds the CRA holding that PRAN, calls it, and returns its answer in one standard shape. The HBA never calls a CRA directly and never holds CRA credentials.
| Component | Responsibility |
|---|---|
| HBA | API consumer. Holds the claim (amount, OPD/IPD), relays the recordkeeper's OTPs from the subscriber, keeps the access_key safe, polls withdrawals to their outcome, and reconciles against the CRA acknowledgement. |
| NPS Central | API provider. Authenticates the HBA, finds the CRA holding the PRAN, runs the CRA's enrolment, eligibility, OTP and withdrawal calls, protects the CRA's balance credential, classifies outcomes and keeps the audit trail. |
| CRA | System of record for the PRAN, the NPS Swasthya corpus and the withdrawal. Sends every OTP to the mobile number registered against the PRAN, validates it, and acknowledges the withdrawal. |
| NPS Central HBA API Standard | Common interface, data, security and error requirements applicable to all HBAs (this document). |
1.1One contract, every recordkeeper
This document is a provider specification: NPS Central hosts every API in it and the HBA calls them. The paths, request and response bodies, status values and error codes are the same whichever CRA holds the PRAN. Differences between the CRAs (transport, encryption, response codes and wording) are absorbed by NPS Central. CRA names and CRA-native error codes never appear in a response; where a CRA's own sentence is passed through, its name is replaced with "the recordkeeper".
Field names are the CRAs' own. CAMS and KFintech use the same snake_case field names and values (for example eligibility: "Y", net_eligible_balance, category: "WDL_AUTH_PARTIAL", wdl_ack_id), and this contract uses them unchanged. The values are common to every CRA: a flag is always Y or N, an amount is always a JSON number, req_id is always a string and withdrawal_type is always WDL_AUTH_PARTIAL or WDL_AUTH_FULL, whichever CRA answered. Every value the HBA sends (category, patient_type) is likewise one set, translated by NPS Central for each CRA. The few fields NPS Central adds (id, status, checked_at, as_of, otp_expires_at, consent_expires_at, failure_reason) follow the same case. The CRA's OTP token and its raw access_key are never returned.
| API | Hosted by | Method and path (relative to the base URL) |
|---|---|---|
| Token (OAuth 2.0) | NPS Central | POST /api/v1/auth/partner-token |
| Eligibility | NPS Central | POST /api/v1/swasthya/eligibility |
| Balance – Consent Request | NPS Central | POST /api/v1/swasthya/balance-consents |
| Balance – Consent Verification | NPS Central | POST /api/v1/swasthya/balance-consents/{consentRequestId}/verify |
| Balance – Read | NPS Central | POST /api/v1/swasthya/balance |
| Withdrawal – Initiation | NPS Central | POST /api/v1/swasthya/withdrawals |
| Withdrawal – Submission | NPS Central | POST /api/v1/swasthya/withdrawals/{withdrawalId}/submit |
| Withdrawal – Status | NPS Central | GET /api/v1/swasthya/withdrawals/{withdrawalId} |
Recordkeepers served in this version: CAMS and KFintech. A PRAN held by Protean is answered with 501 SWASTHYA_WITHDRAWAL_UNSUPPORTED. Eligibility (Part I) and Withdrawal (Part III) for Protean will be served through the same APIs once NPS Central enables them, with no HBA change. Protean offers no balance confirmation (it has no AUTH OTP), so Balance (Part II) will go on answering 501 for a Protean PRAN.
1.2Three kinds of call, three levels of consent
The CRAs authorise each kind of call differently, and this contract follows them exactly. Every OTP in this document is the CRA's own. It is sent by the CRA to the mobile number registered against the PRAN, and checked by the CRA. NPS Central sends no OTP of its own, and the HBA does not need to log the subscriber in first.
| Call | Subscriber consent | Scope | Lasts |
|---|---|---|---|
| Eligibility | None | swasthya:eligibility | One answer per call |
| Balance | The CRA's AUTH OTP, once | swasthya:balance | An access_key, reusable for balance reads until it expires: as long as the CRA allows, up to 30 days |
| Withdrawal | The CRA's withdrawal OTP, every time, raised for that amount | swasthya:withdraw | One withdrawal. The OTP is single use and valid 15 minutes |
1.3Data held by NPS Central
- Balance consent request: the CRA's OTP token, held only for the CRA's 15-minute OTP window, then deleted.
- access_key: nothing. The CRA's access_key is sealed into the access_key value that the HBA holds, and NPS Central opens it only when the HBA presents it. NPS Central does not store it.
- Withdrawal: a working record (id, PRAN, amount, category, patient type, status, the CRA's req_id, withdrawal_type and wdl_ack_id), readable for 60 minutes after initiation. The CRA's OTP token is deleted the moment the withdrawal is submitted.
- Audit: a record of every eligibility check, consent and withdrawal status change, under NPS Central's retention policy. It never contains an OTP, a CRA token or an access_key.
- NPS Central does not keep the subscriber's corpus, balance or payout details. Every balance is a live read from the CRA.
2Common Journeys
2.1Eligibility (no OTP)
| Step | Actor | Action | API |
|---|---|---|---|
| 1 | HBA | Obtains a partner access token (cached for up to 15 minutes). | Token |
| 2 | HBA | Sends the PRAN, and the claim amount if known. | Eligibility |
| 3 | NPS Central | Finds the CRA holding the PRAN, runs the CRA's enrolment check and its eligibility check, and returns the answer. | – |
2.2Balance (AUTH OTP once, then reads for the consent's life)
| Step | Actor | Action | API |
|---|---|---|---|
| 1 | HBA | Requests a balance consent for the PRAN. | Balance – Consent Request |
| 2 | NPS Central | Runs the CRA's enrolment check and asks the CRA to send its AUTH OTP. | – |
| 3 | Subscriber | Receives the OTP from the CRA on their registered mobile and gives it to the HBA. | – |
| 4 | HBA | Submits the OTP. Receives the access_key and the first balance. | Balance – Consent Verification |
| 5 | HBA | Whenever it needs the balance again, until the access_key expires, presents it. No OTP. | Balance – Read |
2.3Withdrawal (an OTP every time)
| Step | Actor | Action | API |
|---|---|---|---|
| 1 | HBA | Initiates a withdrawal with the PRAN, the category (WDL_AUTH_PARTIAL / WDL_AUTH_FULL) and the claim amount. | Withdrawal – Initiation |
| 2 | NPS Central | Runs the CRA's enrolment and eligibility checks and asks the CRA to send the withdrawal OTP for that category and amount. | – |
| 3 | Subscriber | Receives the withdrawal OTP from the CRA (valid 15 minutes) and gives it to the HBA. | – |
| 4 | HBA | Submits the OTP and the patient type (OPD / IPD). NPS Central answers at once with SUBMITTING and calls the CRA in the background. | Withdrawal – Submission |
| 5 | HBA | Polls every 3–5 seconds until the status is COMPLETED, FAILED or UNCONFIRMED. | Withdrawal – Status |
| 6 | HBA | On COMPLETED, records the CRA's acknowledgement (wdl_ack_id) for reconciliation. The payout follows at the CRA. | – |
2.4Sequence
| # | HBA calls | NPS Central does | CRA does |
|---|---|---|---|
| 1 | Token | Issues a 15-minute access token | – |
| 2 | Eligibility | Enrolment and eligibility checks at the CRA | Answers; no OTP |
| 3 | Balance – Consent Request | Enrolment check; asks for the AUTH OTP | Sends the OTP to the subscriber |
| 4 | Balance – Consent Verification (OTP) | Validates the OTP; first balance read; returns the access_key sealed | Issues the access_key; answers the balance |
| 5 | Balance – Read (any number of times until expiry) | Balance read with the CRA's access_key | Answers the balance |
| 6 | Withdrawal – Initiation | Enrolment and eligibility; asks for the withdrawal OTP | Sends the OTP to the subscriber |
| 7 | Withdrawal – Submission (OTP) | Answers 202 SUBMITTING; submits the withdrawal | Acknowledges the withdrawal |
| 8 | Withdrawal – Status (poll) | Returns COMPLETED (wdl_ack_id), FAILED or UNCONFIRMED | – |
3Common API Standards
| Area | Standard Requirement |
|---|---|
| Protocol | HTTPS REST APIs. TLS 1.2 minimum, TLS 1.3 preferred. Plain HTTP is not permitted in any environment. |
| Payload | JSON (UTF-8). Field names are snake_case, as the CRAs name them (1.1), and case-sensitive. |
| Versioning | Major version in the path: /api/v1/… Adding an optional response field is not a breaking change; HBAs must ignore response fields they do not recognise. |
| Authentication | OAuth 2.0 client credentials. The HBA's access token is sent as a bearer token on every call (3.2). |
| Payload encryption | Per the HBA's registration: off, optional or required. When used, the NPSC envelope (3.3). |
| Request ID | Every response carries an X-Request-Id header. The HBA may send its own; otherwise NPS Central generates one (3.4). |
| Duplicate protection | A withdrawal OTP can be submitted exactly once. A withdrawal is never retried on the same id (3.7). |
| Asynchronous outcome | Withdrawal submission answers 202 at once; the outcome is read with the Status API (Part III). |
| Error handling | Errors are RFC 9457 problem documents (application/problem+json) with a stable category and code (3.8, Annexure A). |
| Audit | NPS Central audits every call in this document (1.3, 3.9). |
3.1Environments and network
| Environment | Base URL |
|---|---|
| UAT | https://uat.npscentral.in/nivruti |
| Production | https://npscentral.in/nivruti |
- All paths in this document are appended to the base URL unchanged. For example, Eligibility in UAT is POST https://uat.npscentral.in/nivruti/api/v1/swasthya/eligibility.
- Personal data (PRAN, OTP, amount) is sent only in request bodies, never in URLs or query strings. This is why eligibility and balance use POST. The ids in paths (swc_…, wdl_…) are opaque references.
3.2Authentication
The HBA authenticates with the OAuth 2.0 client credentials grant (RFC 6749 §4.4). Its client credential goes only to the token endpoint. Every API call carries the short-lived access token the endpoint returns.
| Item | HBA value |
|---|---|
| Client authentication | client_secret_basic or client_secret_post. NPS Central generates a 256-bit client secret and hands it over once, over an agreed secure channel. The HBA never chooses it. The method is fixed by the registration; the other is refused, even with valid credentials. |
| Scopes | Any of swasthya:eligibility, swasthya:balance, swasthya:withdraw (1.2). |
| Payload encryption | When used (3.3), a 256-bit encryption key with its keyId is issued alongside the client secret. |
The HBA receives its clientId, its client secret, and its encryption key if encryption is used.
/api/v1/auth/partner-tokenToken request- Content type
- application/x-www-form-urlencoded
- client_secret_basic
- Authorization: Basic base64(clientId:clientSecret). Body: grant_type=client_credentials&scope=swasthya:eligibility swasthya:balance swasthya:withdraw
- client_secret_post
- Body: grant_type=client_credentials&client_id={clientId}&client_secret={clientSecret}&scope=…
- scope
- Optional. The token carries the scopes asked for, intersected with the scopes registered. Omitted: every registered scope.
- Secrets
- Never in a query string. A request carrying one there is refused, because it would land in access logs.
{ "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 900,
"scope": "swasthya:eligibility swasthya:balance swasthya:withdraw" }- Lifetime: 15 minutes. There is no refresh token: request a new token shortly before expiry. Cache and reuse it; do not request one per call. The token endpoint is rate-limited per client id.
- API calls: Authorization: Bearer <access_token>.
- Live registration: the token proves who the HBA is, but whether it may still act is read from its registration on every call. If NPS Central suspends the HBA or narrows its scopes, live tokens stop working on their next request.
| HTTP | error | Meaning |
|---|---|---|
| 400 | unsupported_grant_type / invalid_request | The request is malformed. |
| 400 | invalid_scope | Only scopes the HBA does not hold were asked for. |
| 401 | invalid_client | Any authentication failure: unknown client, suspended, wrong method, wrong secret. Deliberately one answer. |
| 429 | slow_down | Too many attempts. Back off. |
3.3Payload encryption
TLS protects every call. On top of it, the HBA's registration sets a message encryption policy. No request header changes it.
| Policy | Requests | Responses |
|---|---|---|
| off | JSON | JSON |
| optional | JSON or NPSC envelope | In kind: an enveloped request is answered in the envelope |
| required | NPSC envelope only. A plain JSON body is refused: 400 ENCRYPTION_REQUIRED | Every response is in the envelope, error bodies included |
Encryption uses the NPSC envelope: a JSON envelope sealed with AES-256-GCM under the encryption key issued to the HBA, sent with Content-Type: application/vnd.npsc.envelope+json. No JOSE library is needed. A request with a body is answered in the envelope when it was sent in one. A request with no body (Withdrawal – Status) is answered in the envelope when its Accept header is application/vnd.npsc.envelope+json; under the required policy, send this header on that call. Not encrypted: the token endpoint (plain OAuth) and the X-Request-Id response header.
{
"version": "NPSC1",
"keyId": "jwe_3f9c…",
"randomNo": "00112233445566778899aabbccddeeff",
"timestamp": 1760261400,
"encData": "oKGio6SlpqeoqaqrW6Zcn0RWG8Li…"
}Envelope fields
versionStringRequiredRules: Only NPSC1
Envelope version.
keyIdStringRequiredRules: A live keyId issued to the HBA
Key the message is sealed under. NPS Central answers under the same key while it is live.
randomNoString 32RequiredRules: 16 random bytes from a secure generator, lowercase hex. New for every message; a reused value is refused
Makes the message's own key.
timestampIntegerRequiredRules: Epoch seconds; within 5 minutes of NPS Central's clock
When the message was sealed.
encDataString (base64)RequiredRules: base64( iv (12 bytes) + ciphertext + tag (16 bytes) )
The sealed JSON body.
Sealing a request:
- K = HKDF-SHA256( ikm = the issued encryption key (32 bytes), salt = randomNo as bytes, info = "NPSC1" + 0x00 + SHA-256(access token) ), 32 bytes. The access token is the bearer sent on this call, exactly as sent.
- AAD = "NPSC1|REQ|" + METHOD + "|" + path + "|" + keyId + "|" + randomNo + "|" + timestamp, UTF-8. path is the path from /api onwards, without the /nivruti prefix of the base URL and without the query string, e.g. /api/v1/swasthya/withdrawals.
- A random 12-byte IV; AES-256-GCM(K, iv, AAD) over the JSON body (UTF-8), 16-byte tag.
- encData = base64(iv + ciphertext + tag).
Opening a response is the same with RES in place of REQ, the response's own randomNo, timestamp and keyId, and the request's method and path. Because the key is bound to the access token and the AAD to the method and path, a body sealed for one call cannot be replayed to another route or under another token. Refusals: a bad shape, a stale timestamp, a reused randomNo, a keyId that is not live, or a body that does not open are all 400 ENVELOPE_INVALID. A test vector and Node and Java reference code are in Annexure C. Check the implementation against the test vector before the first UAT call.
3.4Common HTTP headers
Common HTTP headers
AuthorizationStringRequiredRules: Bearer <access_token>, on every API except the token endpoint.
Content-TypeStringConditionalRules: On every POST with a body: application/json, or application/vnd.npsc.envelope+json under encryption (3.3).
AcceptStringConditionalRules: application/vnd.npsc.envelope+json on Withdrawal – Status under encryption (3.3).
X-Request-IdString 1–64OptionalRules: The HBA's trace id for this HTTP call; a new UUID v4 on every attempt is recommended. Echoed in the response header and in the requestId of an error body. Generated by NPS Central when absent.
No Idempotency-Key header is needed on these APIs. Duplicate protection is built into the withdrawal flow (3.7).
3.5Data type conventions
| Type | Format | Example | Rule |
|---|---|---|---|
| Amount (request) | Number, or string of digits, at most 2 decimal places | 1500 or "1500.00" | INR. Up to 9 integer digits; a string matches ^[0-9]{1,9}(\.[0-9]{1,2})?$. No currency symbol, commas or negatives. A withdrawal amount must be greater than zero. |
| Amount (response) | Number | 18450.5 | INR. CRA balances (net_eligible_balance, total_corpus) are always a JSON number, at the precision the CRA gave. The withdrawal amount NPS Central echoes is a string with two decimals. |
| PRAN | String 12 digits | 900090067007 | ^[0-9]{12}$ |
| OTP | String 4–8 digits | "482913" | ^[0-9]{4,8}$. Sent as a string so leading zeros are kept. |
| Timestamp | String ISO 8601, UTC | 2026-10-13T09:30:00.000Z | UTC with Z suffix. |
| Flag | String Y / N | "Y" | The CRAs' flag values, always Y or N whichever CRA answered. A flag the CRA did not send is omitted. |
| Enum | String, UPPER_SNAKE_CASE | WDL_AUTH_PARTIAL | Case-sensitive. Values listed per field and in Annexure B. |
| Code | String | "1" | Codes are strings, never numbers. |
| Identifier | String with a prefix | wdl_2b7e… | Opaque. Store and echo it unchanged; do not parse it. |
Absent and empty are different: an optional response field with no value is omitted, never sent as an empty string. Send only the request fields defined for each API; a field not defined may be refused with 400 REQUEST_INVALID.
3.6Identifiers and correlation
| Identifier | Generated by | Purpose and life |
|---|---|---|
| clientId | NPS Central | The HBA's identity. Issued at onboarding. |
| access_token | NPS Central | Bearer credential for API calls. 15 minutes. Never logged. |
| consentRequestId (swc_…) | NPS Central | One per Balance – Consent Request. Key for Consent Verification. Lives for the CRA's 15-minute OTP window, visible only to the HBA that raised it. |
| access_key (swc1.…) | NPS Central, sealing the CRA's | The CRA's access_key, sealed so that only NPS Central can open it. Valid only for the HBA that obtained it and that PRAN, until consent_expires_at. A credential: stored encrypted, never logged, never parsed. |
| withdrawalId (wdl_…) | NPS Central | One per Withdrawal – Initiation. Key for Submission and Status. Readable for 60 minutes, by the HBA that raised it only. |
| req_id | CRA | The CRA's reference for the eligibility, balance or withdrawal request, always as a string. Quote it to the CRA or NPS Central support. |
| wdl_ack_id | CRA | The CRA's withdrawal acknowledgement number. Present only on COMPLETED. The HBA's reference for reconciliation with the CRA. |
| X-Request-Id / requestId | HBA or NPS Central | One per HTTP call. Tracing and support only; never used for business correlation. |
Ownership. Everything an HBA creates belongs to its clientId. Another HBA presenting the id receives the same 404 as for an id that does not exist.
3.7Duplicate protection and retries
- Eligibility and Balance – Read are reads and safe to repeat (eligibility is rate-limited per PRAN, 3.10).
- Balance – Consent Request is not idempotent: each call asks the CRA to send a new OTP. Do not repeat it on a timeout without checking with the subscriber.
- Balance – Consent Verification may be retried after a wrong OTP within the 15-minute window. Two verifications of one request at the same time are refused with 409 SWASTHYA_CONSENT_VERIFY_IN_PROGRESS.
- Withdrawal – Initiation is not idempotent: each call creates a new withdrawalId and asks the CRA for a new OTP.
- Withdrawal – Submission is accepted once per withdrawalId. Any later submission for the same id is refused with 409 SWASTHYA_WITHDRAWAL_ALREADY_SUBMITTED, whatever the outcome of the first.
- A FAILED withdrawal is never retried on the same id. To try again, start a new withdrawal; the subscriber receives a new OTP.
- An UNCONFIRMED withdrawal must not be re-initiated automatically. The CRA may have accepted it, and a new withdrawal could pay the claim twice. It is resolved through NPS Central support, quoting the withdrawalId.
- Withdrawal – Status is a read and safe to repeat.
- Under encryption, a retry re-sealed with a fresh randomNo is still the same request.
3.8Response structure and errors
A successful response body carries the API-specific fields described in each Part. Every error response (except the token endpoint's, 3.2.4) is a problem document with Content-Type: application/problem+json (encrypted under the required policy). HBAs branch on category and code, never on detail text.
Problem document fields
typeString (URI)RequiredProblem type, derived from category.
titleStringRequiredShort description of the category.
statusIntegerRequiredHTTP status code; equals the HTTP status.
categoryEnumRequiredClass of error; decides the client's handling (Annexure B.5).
codeString 1–60RequiredStable machine code within the category (Annexure A).
detailStringOptionalReadable explanation. Where the CRA gave a reason, its sentence with the CRA's name replaced by "the recordkeeper". Never names a CRA.
instanceStringOptionalThe request path.
requestIdStringOptionalSame value as the X-Request-Id header. Per-call trace id for support.
diagnosticRefStringOptionalOpaque handle NPS Central support uses to find the full CRA exchange. Quote it in a support ticket.
retryableBooleanRequiredWhether repeating the same request unchanged may succeed. true for RATE_LIMITED, CRA_UNAVAILABLE, CRA_TIMEOUT, TRANSIENT.
errorsArrayConditionalOne entry per invalid field, present on field validation failures.
errors[].fieldStringRequiredOffending field, e.g. amount.
errors[].codeStringRequiredField error code, e.g. FIELD_INVALID.
errors[].messageStringRequiredReadable field error.
HTTP status codes: 200/201/202 for a processed call; 400 for a body that could not be read or failed field validation; 401 for authentication (including a route outside the token's scopes, and an invalid or expired access_key); 404 for an id that does not exist or is not the HBA's (deliberately one answer); 409 for a state conflict; 422 for a request that was read and refused, including CRA refusals; 429 for rate limiting; 501 for an unsupported recordkeeper; 503 when the CRA could not be reached. A withdrawal the CRA refuses after submission is not an HTTP error: Withdrawal – Status answers 200 with status: FAILED and a failure_reason (Part III).
3.9Audit, logging and data protection
- Personal data is processed only to carry out the NPS Swasthya service requested by the subscriber, under the Digital Personal Data Protection Act, 2023 and applicable PFRDA and IRDAI regulations.
- NPS Central never logs or stores an OTP, a CRA token or the CRA's balance credential. PRANs are masked in operational logs.
- Eligibility discloses whether a PRAN holds NPS Swasthya, so every eligibility call is audited against the HBA and rate-limited per PRAN.
- The HBA:
- never logs the access token, the client secret, the encryption key, any OTP or the access_key;
- stores the access_key as a credential: encrypted, against the subscriber, and deleted when it expires or the subscriber withdraws consent;
- keeps its own record of each withdrawalId, wdl_ack_id, amount and final status for reconciliation;
- obtains and records the subscriber's consent to each balance read and withdrawal under its agreement with the subscriber and the insurer.
- Retention of NPS Central's audit trail: as per applicable regulation.
3.10Service levels and timings
| Item | Value | Notes |
|---|---|---|
| Access token lifetime | 15 minutes | expires_in = 900. No refresh token. |
| CRA OTP validity (AUTH and withdrawal) | 15 minutes from the request | otp_expires_at in the response. |
| access_key lifetime | As long as the CRA allows, up to 30 days | consent_expires_at in the verification response. Never longer than the CRA issued; may end early if the CRA ends it. |
| Withdrawal record read-back | 60 minutes from initiation | After that Withdrawal – Status answers 404. |
| CRA withdrawal call | Typically 20–40 seconds | Why submission is asynchronous. |
| No answer from the CRA after submission | 3 minutes | Reported as UNCONFIRMED (OUTCOME_UNKNOWN). |
| Recommended polling interval | 3–5 seconds | Stop at COMPLETED, FAILED or UNCONFIRMED. |
| Eligibility rate limit | 20 calls per hour per HBA per PRAN | 429 RATE_LIMITED beyond it. |
Planned maintenance on either side requires 48 hours' notice. Incidents are reported per the escalation matrix exchanged at onboarding.
4NPS Swasthya – Eligibility API Standard
The Eligibility API lets the HBA learn, without an OTP, whether a PRAN holds NPS Swasthya and whether the CRA considers it eligible for a withdrawal, optionally for a given claim amount.
| Party | Responsibility |
|---|---|
| HBA | Sends the PRAN of a subscriber it services, and the claim amount if known. |
| NPS Central | Finds the CRA holding the PRAN, runs the CRA's enrolment and eligibility checks, returns the answer, audits the call and applies the rate limit. |
| CRA | Answers enrolment and eligibility. |
/api/v1/swasthya/eligibilityEligibility- Direction
- HBA → NPS Central → CRA
- Scope
- swasthya:eligibility
- Subscriber consent
- None
- Response
- Synchronous, HTTP 200
What NPS Central does: applies the per-PRAN rate limit; finds the CRA holding the PRAN (after an inter-CRA shift the previous CRA still answers, the CRA where the account is active is used); runs the CRA's enrolment check, refusing (422) unless the PRAN exists, holds an NPS Swasthya scheme, has a balance and is not an NPS Vatsalya PRAN; runs the eligibility check and returns the CRA's eligibility fields. Balances are not returned here: they need the subscriber's consent (Part II).
4.5Request parameters
Request parameters
pranString 12RequiredRules: ^[0-9]{12}$
Subscriber's PRAN.
estimated_valueAmountOptionalRules: At most 2 decimals
The claim amount, if known.
4.6Response parameters
Response parameters
req_idStringOptionalRules: Always a string
The CRA's reference for the check.
pranString 12RequiredEcho of the PRAN.
eligibilityFlagConditionalRules: Y / N
Y: eligible. N: not eligible; reason in message. Absent: this CRA answers eligibility only under the subscriber's consent; take an access_key (Part II), whose answer carries it.
messageStringOptionalRules: Never names the CRA
The CRA's own words on eligibility.
eligibility_as_per_corpusFlagOptionalRules: Y / N
Whether the corpus allows a full withdrawal (category WDL_AUTH_FULL, Part III).
message_as_per_full_corpusStringOptionalRules: Never names the CRA
The CRA's own words on the full-corpus check.
checked_atTimestampRequiredWhen the check was made.
4.7Error parameters
| HTTP | code | When |
|---|---|---|
| 400 | REQUEST_INVALID | pran or estimated_value malformed (errors[] names the field) |
| 401 | AUTH_FAILED | Bad or expired token, suspended registration, or scope swasthya:eligibility not held |
| 422 | SWASTHYA_PRAN_NOT_FOUND | No recordkeeper holds this PRAN |
| 422 | SWASTHYA_NO_SWASTHYA_SCHEME | The PRAN has no NPS Swasthya scheme |
| 422 | SWASTHYA_NO_BALANCE | Nothing is available to withdraw |
| 422 | SWASTHYA_VATSALYA_NOT_ALLOWED | NPS Vatsalya PRANs cannot withdraw |
| 422 | SWASTHYA_NOT_ELIGIBLE | The CRA refused the eligibility check |
| 429 | RATE_LIMITED | Too many eligibility checks for this PRAN by this HBA |
| 501 | SWASTHYA_WITHDRAWAL_UNSUPPORTED | The PRAN's recordkeeper does not offer NPS Swasthya through NPS Central yet |
| 503 | SWASTHYA_CRA_UNAVAILABLE | The CRA did not answer, or no CRA has the account active while one did not answer. Retry shortly |
4.8Sample request and response
{ "pran": "900090067007", "estimated_value": 1500 }
// 200 OK
{
"req_id": "9541",
"pran": "900090067007",
"eligibility": "Y",
"message": "PRAN is eligible for withdrawal",
"eligibility_as_per_corpus": "N",
"message_as_per_full_corpus": "Full withdrawal is not allowed as per corpus",
"checked_at": "2026-10-13T09:30:00.000Z"
}5NPS Swasthya – Balance API Standard
The Balance API lets the HBA show the subscriber the balance eligible for withdrawal. The subscriber consents once, with the CRA's AUTH OTP. The HBA then holds the CRA's access_key, sealed by NPS Central, and reads the balance with it, without a new OTP, until it expires.
| Parameter | Consent Request | Consent Verification | Balance Read |
|---|---|---|---|
| Method | POST | POST | POST |
| Endpoint | /api/v1/swasthya/balance-consents | /api/v1/swasthya/balance-consents/{consentRequestId}/verify | /api/v1/swasthya/balance |
| Scope | swasthya:balance | swasthya:balance | swasthya:balance |
| Subscriber consent | Starts it: the CRA sends the AUTH OTP | The AUTH OTP | The access_key |
| Response | HTTP 201 | HTTP 200 | HTTP 200 |
5.4Balance – Consent Request
NPS Central finds the CRA holding the PRAN (the CRA where the account is active), runs the CRA's enrolment check, and asks the CRA to send its AUTH OTP to the mobile registered against the PRAN.
Request parameters
pranString 12RequiredRules: ^[0-9]{12}$
Subscriber's PRAN.
Response parameters
idStringRequiredRules: Prefix swc_
consentRequestId. Key for Consent Verification. Visible only to this HBA.
statusEnumRequiredRules: PENDING_OTP
The OTP has been sent.
categoryEnumRequiredRules: AUTH
The CRA's OTP category for a balance consent.
otp_expires_atTimestampRequiredRules: 15 minutes from now
The CRA's OTP window.
messageStringRequiredRules: Never names the CRA
The CRA's own words.
5.5Balance – Consent Verification
NPS Central validates the OTP at the CRA, receives the CRA's access_key, reads the first balance with it, and returns the access_key sealed.
Request parameters
consentRequestId (path)StringRequiredRules: Prefix swc_; from Consent Request
The consent request.
otp_valueString 4–8RequiredRules: ^[0-9]{4,8}$
The AUTH OTP the subscriber received.
estimated_valueAmountOptionalRules: At most 2 decimals
The claim amount, if known, for the first balance read.
Response parameters
idStringRequiredEcho of consentRequestId.
statusEnumRequiredRules: VERIFIED
The OTP was accepted.
access_keyStringRequiredRules: Prefix swc1.; opaque
The CRA's access_key, sealed (5.7). Valid only for this HBA and this PRAN.
consent_expires_atTimestampRequiredRules: At most 30 days from now
When the access_key ends.
balanceObjectConditionalThe first balance, as in 5.6.b. Absent when the first read could not be made; then call Balance – Read.
messageStringOptionalSays why balance is absent, when it is.
A wrong OTP is 422 SWASTHYA_OTP_INVALID; the subscriber may retype it within the window. A spent or expired CRA token is 409 SWASTHYA_OTP_EXPIRED, and a request whose 15 minutes have passed is 404 SWASTHYA_CONSENT_NOT_FOUND. Either way, start again with a new Consent Request.
5.6Balance – Read
NPS Central opens the access_key, checks that it was issued to this HBA for this PRAN, and reads the balance from the CRA with the credential inside. Each call is a live read; nothing is cached.
Request parameters
pranString 12RequiredRules: ^[0-9]{12}$; the PRAN the consent was issued for
Subscriber's PRAN.
access_keyStringRequiredRules: Exactly as received; ^swc1\.[A-Za-z0-9._-]{1,32}\.[A-Za-z0-9_-]+$
The access_key from Consent Verification.
estimated_valueAmountOptionalRules: At most 2 decimals
The claim amount, if known.
Response parameters (the balance object)
req_idStringOptionalRules: Always a string
The CRA's reference for the read.
pranString 12RequiredEcho of the PRAN.
eligibilityFlagOptionalRules: Y / N
Whether the PRAN is eligible for withdrawal.
messageStringOptionalRules: Never names the CRA
The CRA's own words on eligibility.
net_eligible_balanceNumberOptionalRules: INR, at the CRA's precision
What may be withdrawn now.
total_corpusNumberOptionalRules: INR, at the CRA's precision
Total NPS Swasthya corpus.
eligibility_as_per_corpusFlagOptionalRules: Y / N
Whether the corpus allows a full withdrawal.
message_as_per_full_corpusStringOptionalRules: Never names the CRA
The CRA's own words on the full-corpus check.
as_ofTimestampRequiredWhen the balance was read.
5.7The access_key
- The CRA's access_key, sealed. It has the CRA's field name but not the CRA's value: only NPS Central can open it, and the raw key never leaves NPS Central. Do not parse it.
- Bound to the HBA and the PRAN. Presented by another HBA, or with another PRAN, it is 401 SWASTHYA_CONSENT_INVALID.
- Lasts as long as the CRA allows, up to 30 days, never longer. If the CRA ends it early, the read answers 401 SWASTHYA_CONSENT_EXPIRED: take a new AUTH OTP with a new Consent Request.
- Read-only. It never authorises a withdrawal (Part III).
- Store it as a credential: encrypted, against the subscriber, deleted when it expires or the subscriber withdraws consent.
- Suspending the HBA's registration ends every one of its consents at once.
5.8Error parameters
| HTTP | code | When |
|---|---|---|
| 400 | REQUEST_INVALID | A field is malformed (errors[] names it) |
| 401 | AUTH_FAILED | Bad or expired token, suspended registration, or scope swasthya:balance not held |
| 401 | SWASTHYA_CONSENT_INVALID | Balance – Read: the access_key is malformed, not this HBA's, or for another PRAN |
| 401 | SWASTHYA_CONSENT_EXPIRED | Balance – Read: the access_key has expired or the CRA no longer accepts it. Take a new AUTH OTP |
| 404 | SWASTHYA_CONSENT_NOT_FOUND | Verification: the request is not this HBA's, does not exist, or its 15 minutes have passed |
| 409 | SWASTHYA_CONSENT_VERIFY_IN_PROGRESS | Verification: another verification of the same request is running |
| 409 | SWASTHYA_OTP_EXPIRED | Verification: the CRA's OTP token is spent or expired. Start again |
| 422 | SWASTHYA_OTP_INVALID | Verification: wrong OTP. May be retried within the window |
| 422 | SWASTHYA_PRAN_NOT_FOUND / … | Consent Request: the CRA's enrolment check refused (as Part I) |
| 422 | SWASTHYA_OTP_REFUSED | Consent Request: the CRA did not send an OTP |
| 422 | SWASTHYA_NOT_ELIGIBLE | The CRA refused the eligibility read |
| 501 | SWASTHYA_WITHDRAWAL_UNSUPPORTED | The PRAN is held by Protean, which offers no balance confirmation. Permanent for a Protean PRAN |
| 503 | SWASTHYA_CRA_UNAVAILABLE | The CRA did not answer. Retry shortly |
5.9Sample requests and responses
POST /api/v1/swasthya/balance-consents
{ "pran": "900090067007" }
// 201 Created
{ "id": "swc_6f1c0d2e9a7b4c55", "status": "PENDING_OTP", "category": "AUTH",
"otp_expires_at": "2026-10-13T09:45:00.000Z",
"message": "OTP Triggered successfully" }
POST /api/v1/swasthya/balance-consents/swc_6f1c0d2e9a7b4c55/verify
{ "otp_value": "123456" }
// 200 OK
{
"id": "swc_6f1c0d2e9a7b4c55",
"status": "VERIFIED",
"access_key": "swc1.k-2026-10.Qm9…",
"consent_expires_at": "2026-11-12T09:31:00.000Z",
"balance": {
"req_id": "9542", "pran": "900090067007", "eligibility": "Y",
"message": "PRAN is eligible for withdrawal",
"net_eligible_balance": 18450, "total_corpus": 24600,
"eligibility_as_per_corpus": "N",
"message_as_per_full_corpus": "Full withdrawal is not allowed as per corpus",
"as_of": "2026-10-13T09:31:00.000Z"
}
}POST /api/v1/swasthya/balance
{ "pran": "900090067007", "access_key": "swc1.k-2026-10.Qm9…", "estimated_value": 1500 }
// 200 OK
{ "req_id": "9560", "pran": "900090067007", "eligibility": "Y",
"message": "PRAN is eligible for withdrawal",
"net_eligible_balance": 18450, "total_corpus": 24600,
"eligibility_as_per_corpus": "N",
"message_as_per_full_corpus": "Full withdrawal is not allowed as per corpus",
"as_of": "2026-10-20T11:02:00.000Z" }6NPS Swasthya – Withdrawal API Standard
The Withdrawal API lets the HBA raise an NPS Swasthya withdrawal for a claim, authorised by the subscriber with the CRA's withdrawal OTP for that category and amount, and follow it to the CRA's acknowledgement.
| Parameter | Initiation | Submission | Status |
|---|---|---|---|
| Method | POST | POST | GET |
| Endpoint | /api/v1/swasthya/withdrawals | /api/v1/swasthya/withdrawals/{withdrawalId}/submit | /api/v1/swasthya/withdrawals/{withdrawalId} |
| Scope | swasthya:withdraw | swasthya:withdraw | swasthya:withdraw |
| Subscriber consent | Starts it: the CRA sends the withdrawal OTP | The withdrawal OTP | – |
| Response | Synchronous, HTTP 201 | Asynchronous, HTTP 202 | HTTP 200 |
6.4Withdrawal – Initiation
What NPS Central does: finds the CRA holding the PRAN; runs the enrolment check (refuses unless the PRAN exists, holds an NPS Swasthya scheme, has a balance and is not an NPS Vatsalya PRAN); runs the eligibility check for the amount (refuses only when the CRA says eligibility: N; if the check cannot be made, continues, since the CRA checks again); asks the CRA to send the withdrawal OTP for the category and amount the HBA sent. NPS Central does not derive or change the category; the CRA decides whether it allows it. The amount is fixed at this step.
Request parameters
pranString 12RequiredRules: ^[0-9]{12}$
Subscriber's PRAN.
categoryEnumRequiredRules: WDL_AUTH_PARTIAL / WDL_AUTH_FULL
The CRA's withdrawal OTP category. For an OPD claim the CRAs expect WDL_AUTH_PARTIAL; WDL_AUTH_FULL only where eligibility_as_per_corpus is Y.
amountAmountRequiredRules: > 0; at most 2 decimals; up to 9 integer digits
The claim amount. Fixed into the OTP.
Response parameters
idStringRequiredRules: Prefix wdl_
withdrawalId. Key for Submission and Status. Visible only to this HBA.
statusEnumRequiredRules: PENDING_OTP
The OTP has been sent.
pranString 12RequiredEcho of the PRAN.
categoryEnumRequiredRules: WDL_AUTH_PARTIAL / WDL_AUTH_FULL
The category the OTP was raised for.
amountStringRequiredRules: Two decimals
The amount fixed into the OTP.
otp_expires_atTimestampRequiredRules: 15 minutes from now
Submit before this.
messageStringOptionalRules: Never names the CRA
The CRA's own words.
6.5Withdrawal – Submission
Submits the subscriber's withdrawal OTP with the patient type, as the CRA's withdrawal request takes them. The CRA can take 20–40 seconds, so NPS Central answers at once with 202 SUBMITTING and calls the CRA in the background. The OTP is single use: the first submission claims the withdrawal (3.7).
Request parameters
withdrawalId (path)StringRequiredRules: Prefix wdl_; from Initiation
The withdrawal.
patient_typeEnumRequiredRules: OPD / IPD
Out-patient or in-patient claim.
otp_valueString 4–8RequiredRules: ^[0-9]{4,8}$
The withdrawal OTP the subscriber received.
Response: HTTP 202 with the withdrawal object (6.6.b), status: SUBMITTING.
6.6Withdrawal – Status
Poll every 3–5 seconds after submission until the status is terminal. Readable for 60 minutes after initiation. The request takes only withdrawalId (path), prefix wdl_.
Response parameters (the withdrawal object)
idStringRequiredRules: Prefix wdl_
withdrawalId.
statusEnumRequiredRules: Annexure B.3
Current status.
pranString 12RequiredEcho of the PRAN.
categoryEnumRequiredRules: WDL_AUTH_PARTIAL / WDL_AUTH_FULL
The category the OTP was raised for.
amountStringRequiredRules: Two decimals
The amount.
patient_typeEnumConditionalRules: OPD / IPD; once submitted
As sent at Submission.
otp_expires_atTimestampConditionalRules: While PENDING_OTP
OTP deadline.
req_idStringConditionalRules: Once the CRA answered
The CRA's reference for the withdrawal request.
withdrawal_typeEnumConditionalRules: WDL_AUTH_PARTIAL / WDL_AUTH_FULL; always on COMPLETED
The withdrawal type the CRA recorded; where it gave none, the category the OTP was raised for.
wdl_ack_idStringConditionalRules: On COMPLETED
The CRA's withdrawal acknowledgement. Reconcile against it.
failure_reasonEnumConditionalRules: On FAILED or UNCONFIRMED; Annexure B.3
Why the withdrawal did not complete.
messageStringOptionalRules: Never names the CRA
The CRA's own words where it gave them; otherwise NPS Central's.
| status | Meaning | What the HBA does |
|---|---|---|
| PENDING_OTP | The OTP has been sent; nothing submitted yet | Submit the OTP before otp_expires_at |
| SUBMITTING | Submitted; the CRA has not answered yet | Keep polling |
| COMPLETED | The CRA acknowledged the withdrawal; wdl_ack_id is its reference | Record wdl_ack_id and settle against it. The payout follows at the CRA |
| FAILED | The CRA refused it, or it could not be sent; see failure_reason and message | If appropriate, start a new withdrawal (new OTP) |
| UNCONFIRMED | No answer was received within 3 minutes; it may have been processed (failure_reason OUTCOME_UNKNOWN) | Do not retry. Raise it with NPS Central support quoting the withdrawalId |
6.7Error parameters
| HTTP | code | When |
|---|---|---|
| 400 | REQUEST_INVALID | A field is malformed (errors[] names it) |
| 401 | AUTH_FAILED | Bad or expired token, suspended registration, or scope swasthya:withdraw not held |
| 404 | SWASTHYA_WITHDRAWAL_NOT_FOUND | Not this HBA's, does not exist, or more than 60 minutes old (one answer) |
| 409 | SWASTHYA_WITHDRAWAL_ALREADY_SUBMITTED | Submission: the OTP was already submitted for this withdrawalId. Start a new withdrawal |
| 409 | SWASTHYA_OTP_EXPIRED | Submission: the 15-minute OTP window has passed. Start a new withdrawal |
| 422 | SWASTHYA_AMOUNT_INVALID | Initiation: amount is not greater than zero |
| 422 | SWASTHYA_PRAN_NOT_FOUND / … | Initiation: the CRA's enrolment check refused |
| 422 | SWASTHYA_NOT_ELIGIBLE | Initiation: the CRA says the PRAN is not eligible for this amount |
| 422 | SWASTHYA_OTP_REFUSED | Initiation: the CRA did not send an OTP |
| 501 | SWASTHYA_WITHDRAWAL_UNSUPPORTED | As Part I |
| 503 | SWASTHYA_CRA_UNAVAILABLE | Initiation: the CRA did not answer. Retry shortly |
6.8Sample requests and responses
POST /api/v1/swasthya/withdrawals
{ "pran": "900090067007", "category": "WDL_AUTH_PARTIAL", "amount": 1500 }
// 201 Created
{ "id": "wdl_2b7e5c1a9d3f4e60", "status": "PENDING_OTP", "pran": "900090067007",
"category": "WDL_AUTH_PARTIAL", "amount": "1500.00",
"otp_expires_at": "2026-10-13T09:47:00.000Z",
"message": "OTP Triggered successfully" }
POST /api/v1/swasthya/withdrawals/wdl_2b7e5c1a9d3f4e60/submit
{ "patient_type": "OPD", "otp_value": "654321" }
// 202 Accepted
{ "id": "wdl_2b7e5c1a9d3f4e60", "status": "SUBMITTING", "pran": "900090067007",
"category": "WDL_AUTH_PARTIAL", "amount": "1500.00", "patient_type": "OPD",
"message": "Your withdrawal request has been submitted and is being processed." }GET /api/v1/swasthya/withdrawals/wdl_2b7e5c1a9d3f4e60
// 200 OK
{ "id": "wdl_2b7e5c1a9d3f4e60", "status": "COMPLETED", "pran": "900090067007",
"category": "WDL_AUTH_PARTIAL", "amount": "1500.00", "patient_type": "OPD",
"req_id": "9543", "withdrawal_type": "WDL_AUTH_PARTIAL", "wdl_ack_id": "131000006952",
"message": "Withdrawal initiated successfully in the recordkeeper" }
// 200 OK (a refusal)
{ "id": "wdl_2b7e5c1a9d3f4e60", "status": "FAILED", "pran": "900090067007",
"category": "WDL_AUTH_PARTIAL", "amount": "1500.00", "patient_type": "OPD",
"failure_reason": "OTP_INVALID",
"message": "Invalid OTP" }AError Catalogue
Every code an HBA can receive on the APIs in this document, in addition to the token endpoint's OAuth errors (3.2.4). retryable is true only where repeating the same request unchanged may succeed. The category for each HTTP status is in Annexure B.5.
24 rows
| code | HTTP | Retry | Meaning and HBA action |
|---|---|---|---|
| REQUEST_INVALID | 400 | No | A field is malformed. Fix the request. |
| ENCRYPTION_REQUIRED | 400 | No | Plain JSON sent under the required policy. Use the NPSC envelope. |
| ENVELOPE_INVALID | 400 | No | Envelope malformed, stale, replayed, unknown key, or did not open. Check against the test vector. |
| AUTH_REQUIRED | 401 | No | No bearer token. |
| AUTH_FAILED | 401 | No | Bad, expired or revoked token; suspended registration; or route outside the token's scopes. Get a new token; if it persists, contact NPS Central. |
| SWASTHYA_CONSENT_INVALID | 401 | No | access_key malformed, not this HBA's, or for another PRAN. |
| SWASTHYA_CONSENT_EXPIRED | 401 | No | access_key expired or ended by the CRA. New Consent Request. |
| SWASTHYA_CONSENT_NOT_FOUND | 404 | No | Consent request unknown, not this HBA's, or past 15 minutes. New Consent Request. |
| SWASTHYA_WITHDRAWAL_NOT_FOUND | 404 | No | Withdrawal unknown, not this HBA's, or past 60 minutes. |
| SWASTHYA_CONSENT_VERIFY_IN_PROGRESS | 409 | No | A verification of the same request is running. Wait for it. |
| SWASTHYA_OTP_EXPIRED | 409 | No | OTP window passed or token spent. Start again. |
| SWASTHYA_WITHDRAWAL_ALREADY_SUBMITTED | 409 | No | OTP already submitted for this withdrawal. Poll its status; start a new withdrawal only if it FAILED. |
| SWASTHYA_PRAN_NOT_FOUND | 422 | No | No recordkeeper holds this PRAN. |
| SWASTHYA_NO_SWASTHYA_SCHEME | 422 | No | The PRAN has no NPS Swasthya scheme. |
| SWASTHYA_NO_BALANCE | 422 | No | Nothing available to withdraw. |
| SWASTHYA_VATSALYA_NOT_ALLOWED | 422 | No | NPS Vatsalya PRANs cannot withdraw. |
| SWASTHYA_NOT_ELIGIBLE | 422 | No | The CRA says not eligible; reason in detail. |
| SWASTHYA_OTP_REFUSED | 422 | No | The CRA did not send an OTP; reason in detail. |
| SWASTHYA_OTP_INVALID | 422 | No | Wrong OTP on a consent verification. Retype within the window. |
| SWASTHYA_AMOUNT_INVALID | 422 | No | Withdrawal amount not greater than zero. |
| RATE_LIMITED | 429 | Yes, later | Too many eligibility checks for one PRAN, or too many token requests. Back off. |
| SWASTHYA_WITHDRAWAL_UNSUPPORTED | 501 | No | The PRAN is held by Protean: not yet served for Eligibility and Withdrawal; never for Balance (1.1). |
| SWASTHYA_CRA_UNAVAILABLE | 503 | Yes | The CRA did not answer. Retry shortly (not for Submission, see 3.7). |
| PARTNER_REGISTRY_UNAVAILABLE | 503 | Yes | Transient fault at NPS Central. Retry. |
BCode Lists
| Value | Meaning |
|---|---|
| AUTH | Balance consent (Part II). Set by NPS Central |
| WDL_AUTH_PARTIAL | A partial withdrawal from the NPS Swasthya corpus. The CRAs expect it for every OPD claim |
| WDL_AUTH_FULL | A full withdrawal of the NPS Swasthya corpus, where eligibility_as_per_corpus is Y |
| Value | Meaning |
|---|---|
| OPD | Out-patient claim |
| IPD | In-patient claim |
| status | Terminal? |
|---|---|
| PENDING_OTP | No |
| SUBMITTING | No |
| COMPLETED | Yes |
| FAILED | Yes |
| UNCONFIRMED | Yes (resolved by support) |
| failure_reason | Meaning |
|---|---|
| OTP_INVALID | The CRA rejected the OTP |
| OTP_EXPIRED | The OTP window passed before submission |
| TOKEN_INVALID | The CRA's OTP token was no longer valid |
| INSUFFICIENT_BALANCE | The balance does not cover the amount |
| REJECTED | The CRA refused the withdrawal for another reason (see message) |
| UNAVAILABLE | The withdrawal certainly did not reach the CRA |
| OUTCOME_UNKNOWN | UNCONFIRMED only: no answer; it may have been processed |
| Value | Meaning |
|---|---|
| PENDING_OTP | The AUTH OTP has been sent |
| VERIFIED | The OTP was accepted and the access_key issued |
| category | retryable | Meaning |
|---|---|---|
| VALIDATION_REJECTED | false | The request was understood and refused |
| AUTH_FAILED | false | Not authenticated or not authorised |
| NOT_FOUND | false | No such resource for this caller |
| CONFLICT | false | The resource is not in a state that allows this |
| CAPABILITY_UNSUPPORTED | false | This operation is not available for this PRAN or registration |
| RATE_LIMITED | true | Too many requests |
| CRA_UNAVAILABLE | true | A recordkeeper could not be reached |
| CRA_TIMEOUT | true | A recordkeeper did not answer in time |
| TRANSIENT | true | The request could not be completed |
| Scope | Allows |
|---|---|
| swasthya:eligibility | Eligibility (Part I) |
| swasthya:balance | Balance – Consent Request, Consent Verification and Read (Part II) |
| swasthya:withdraw | Withdrawal – Initiation, Submission and Status (Part III) |
CNPSC Envelope Reference
An implementation must reproduce these bytes exactly before the first UAT call.
| Input | Value |
|---|---|
| encryption key (hex) | 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f |
| access token | psub.example-token |
| method, path | POST, /api/v1/contributions |
| keyId | jwe_test0000000001 |
| randomNo | 00112233445566778899aabbccddeeff |
| timestamp | 1760261400 |
| iv (hex) | a0a1a2a3a4a5a6a7a8a9aaab |
| body | {"clientReferenceId":"pop-ref-1"} |
| → K (hex) | 17e1482ba0ef3637cefd455256cbd12dffe8799f12248678db840005040b5c5e |
| → AAD | NPSC1|REQ|POST|/api/v1/contributions|jwe_test0000000001|00112233445566778899aabbccddeeff|1760261400 |
| → encData | oKGio6SlpqeoqaqrW6Zcn0RWG8LiZ5MNtPHvfg7HaiEQDE2Ubp2J9D2PUh3toHiTfEm5k5JOHlfxq7Ge1g== |
const crypto = require('node:crypto');
function seal(key /* Buffer(32) */, keyId, token, method, path, json) {
const randomNo = crypto.randomBytes(16).toString('hex');
const timestamp = Math.floor(Date.now() / 1000);
const info = Buffer.concat([Buffer.from('NPSC1\0'),
crypto.createHash('sha256').update(token).digest()]);
const k = Buffer.from(crypto.hkdfSync('sha256', key, Buffer.from(randomNo, 'hex'), info, 32));
const iv = crypto.randomBytes(12);
const c = crypto.createCipheriv('aes-256-gcm', k, iv);
c.setAAD(Buffer.from(['NPSC1', 'REQ', method, path, keyId, randomNo, timestamp].join('|')));
const ct = Buffer.concat([c.update(JSON.stringify(json), 'utf8'), c.final()]);
return { version: 'NPSC1', keyId, randomNo, timestamp,
encData: Buffer.concat([iv, ct, c.getAuthTag()]).toString('base64') };
}static byte[] hmac(byte[] key, byte[] data) throws Exception {
Mac m = Mac.getInstance("HmacSHA256"); m.init(new SecretKeySpec(key, "HmacSHA256"));
return m.doFinal(data);
}
static byte[] deriveKey(byte[] issuedKey, String randomNoHex, String token) throws Exception {
byte[] prk = hmac(HexFormat.of().parseHex(randomNoHex), issuedKey); // extract
byte[] tokenHash = MessageDigest.getInstance("SHA-256").digest(token.getBytes(UTF_8));
ByteBuffer info = ByteBuffer.allocate(6 + 32 + 1)
.put("NPSC1".getBytes(UTF_8)).put((byte) 0).put(tokenHash).put((byte) 1);
return hmac(prk, info.array()); // expand, one block
}
static String encData(byte[] k, String aad, String json) throws Exception {
byte[] iv = new byte[12]; new SecureRandom().nextBytes(iv);
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(k, "AES"), new GCMParameterSpec(128, iv));
c.updateAAD(aad.getBytes(UTF_8));
byte[] ctAndTag = c.doFinal(json.getBytes(UTF_8)); // tag appended
return Base64.getEncoder().encodeToString(ByteBuffer.allocate(12 + ctAndTag.length)
.put(iv).put(ctAndTag).array());
}DChange Log
| Version | Date | Changes |
|---|---|---|
| 1.0 | 21-Oct-2026 | First issue: eligibility, balance and withdrawal APIs for HBAs. |