Document control
- Title
- Authentication — Client secret with subject assertion
- Version
- 1.0
- Status
- Issued
- Date of issue
- 12 October 2026
- Owner
- NPS Central Engineering
- Classification
- Confidential — for registered partners
1About this document
This document is for engineers integrating a point of presence (POP) with the NPS Central Partner API using a client secret (client_secret_basic or client_secret_post). It covers the two credentials every integration needs: the partner access token, which identifies your organisation, and the subscriber session, which lets you act for one existing subscriber.
You open a subscriber session on your own subject assertion. You verify and authenticate the subscriber in your own application, then present a signed statement of who they are; NPS Central confirms with the CRA that the account is registered under your point of presence and opens the session. The subscriber's identity is verified at your end, not by NPS Central.
Once you can open a session, the account reads are described in Account Information, changes to a subscriber's details in Modifications, and encrypting request and response bodies in Payload Encryption.
1.1Notation
In field tables, the presence column shows M for mandatory, O for optional, and C for conditional — mandatory only in the situation the description gives. Paths are relative to the base URL for your environment. Examples use $BASE_URL for that value and shorten long tokens with an ellipsis. The API refers to an existing subscriber as the subject, which is why paths and fields carry names such as subjectRef and subject_ref.
2Before you begin
NPS Central sets up your partner account and gives you the following. Keep them to hand; every step in this document uses at least one of them.
| Item | Notes |
|---|---|
| Client ID | For example pop-acme. Not secret, but identifies you in every token request and is the iss of your assertions. |
| Client secret | Delivered once, out of band. Store it in a secrets manager; never in source control or client-side code. |
| Authentication method | Either client_secret_basic or client_secret_post. Your account accepts only the method it was registered for. |
| Encryption key and key ID | A 256-bit key, delivered once, out of band, as base64url text, with its key ID. It signs your subject assertions, and encrypts request and response bodies if your account is set up for payload encryption. |
| POP registrations | Your POP / POP-SP codes at each CRA, recorded against your account. Only accounts registered under them can be reached. |
| Token endpoint URL | The exact URL to use as aud in your assertions, e.g. https://uat.example.in/api/v1/auth/partner-token. |
| Base URL | One per environment (UAT and Production). Always ends in /api. |
| Scopes | The scopes your account holds (section 8). Your tokens can never carry more. |
3How authentication works
There are two credentials, and they answer different questions.
| Credential | Proves | How you get it | Lifetime |
|---|---|---|---|
| Partner access token | Who you are | Client credentials grant (section 5) | 15 minutes |
| Subscriber session token | That you verified and authenticated the subscriber, and the CRA confirms the account is under your POP | Subject assertion grant (section 6) | 15 minutes idle, 60 minutes maximum |
The partner token is used for work that is not about one existing subscriber: eligibility checks, onboarding, registration, and reading the status of contributions and modifications you submitted. Everything about one existing subscriber — account reads, contributions, modifications and documents — uses the subscriber session token.
4Conventions for every call
4.1Requests
| Aspect | Rule |
|---|---|
| Transport | HTTPS only. |
| Authorisation | Authorization: Bearer <token> on every API call. |
| Body format | application/json, or the NPSC envelope (application/vnd.npsc.envelope+json) if your account encrypts payloads. The token endpoint is the exception: always application/x-www-form-urlencoded, never encrypted. |
| Correlation | Every response carries an x-request-id header. If you send your own x-request-id, it is echoed back. Quote it in any support request. |
| Dates | YYYY-MM-DD. Timestamps are RFC 3339 in UTC with a Z suffix. |
| Mobile numbers | E.164: +91 followed by 10 digits, the first of which is 6–9. |
4.2Errors
Apart from the token endpoint, every error is an RFC 9457 problem document with content type application/problem+json.
{
"type": "https://api.npscentral.in/problems/validation-rejected",
"title": "The request was understood and refused.",
"status": 422,
"category": "VALIDATION_REJECTED",
"code": "MODIFICATION_REQUEST_INVALID",
"detail": "The request body does not match the modification type.",
"retryable": false,
"requestId": "3f6c1a9e-…",
"errors": [
{ "field": "pop[0].popRegNo", "code": "MODIFICATION_REQUEST_INVALID", "message": "…" }
]
}| Field | Use it for |
|---|---|
| category | Your error handling logic. One of VALIDATION_REJECTED, AUTH_FAILED, NOT_FOUND, CONFLICT, CAPABILITY_UNSUPPORTED, RATE_LIMITED, CRA_UNAVAILABLE, CRA_TIMEOUT, TRANSIENT. |
| code | A finer-grained reason within the category, for logs and specific messages. New codes may be added; do not fail on an unknown one. |
| retryable | Whether repeating the same request may succeed. |
| errors[] | On validation failures, one entry per field at fault (field, code, message). |
| requestId, diagnosticRef | Quote these to support. |
A 429 response carries a Retry-After header; wait at least that long before retrying. The token endpoint follows OAuth 2.0 instead and returns { "error": "<code>" } (section 5.4).
5Partner access token
The partner token identifies your organisation. You use it for calls that are not about one existing subscriber, and to follow contributions and modifications you submitted. Opening a subscriber session does not need it: the assertion grant authenticates you with your client credentials directly (section 6).
5.1Endpoint
/v1/auth/partner-tokenPartner access token- Grant
- OAuth 2.0 client credentials (RFC 6749 §4.4). The same endpoint takes the subject assertion grant (section 6).
- Content type
- application/x-www-form-urlencoded
- Encryption
- Never
- Rate limit
- 30 requests per 5 minutes, per client ID and per source address
5.2Request
Request parameters
grant_typeformRequiredclient_credentials
scopeformOptionalSpace-separated scopes to request, e.g. subject:read contribution. Omit to receive every scope you hold.
AuthorizationheaderConditionalclient_secret_basic only: Basic base64(urlencode(client_id) ":" urlencode(client_secret))
client_idformConditionalclient_secret_post only: your client ID
client_secretformConditionalclient_secret_post only: your client secret
Present the secret in one place only. A request with both a Basic header and a client_secret field is refused, as is any request that puts the secret in the query string.
curl -X POST "$BASE_URL/v1/auth/partner-token" \
-u "pop-acme:$CLIENT_SECRET" \
-d grant_type=client_credentials \
-d scope="subject:read contribution modification"5.3Response — 200 OK
Response body
access_tokenstringRequiredThe partner token. Treat it as opaque; do not decode or rely on its contents.
token_typestringRequiredAlways Bearer.
expires_inintegerRequiredSeconds until expiry. Currently 900 (15 minutes).
scopestringRequiredThe scopes this token carries, space-separated.
The response is sent with Cache-Control: no-store.
5.4Errors
| HTTP | error | Cause | What to do |
|---|---|---|---|
| 400 | unsupported_grant_type | grant_type missing or not one of the two grants | Fix the request. |
| 400 | invalid_request | No credentials; credentials in two places; malformed Basic header; secret in the query string | Fix the request. |
| 400 | invalid_scope | None of the requested scopes is one you hold | Request scopes you hold, or omit scope. |
| 401 | invalid_client | Any authentication failure: unknown client, wrong or revoked secret, wrong method, suspended account, disallowed source address | Check the client ID, secret, method and environment. If they are right, contact support with the x-request-id. |
| 429 | slow_down | Rate limit exceeded | Back off and cache tokens (section 5.5). |
invalid_client deliberately does not say which check failed, so that the response gives nothing away to someone guessing credentials.
5.5Handling the token
- Cache it. Reuse the token for its lifetime and request a new one when fewer than 60 seconds remain. Requesting a token per API call will hit the rate limit.
- Recover once. If an API returns 401 AUTH_FAILED, obtain a new token and retry the call once. If it fails again, stop and investigate rather than looping.
- Expect changes to take effect immediately. Your account is checked on every call, so a suspension or a removed scope applies to tokens you already hold.
6Opening a subscriber session
Every subscriber session is opened on your own subject assertion: a short JWT, signed with your encryption key, in which you state who the subscriber is and how you authenticated them. NPS Central sends no OTP. It asks the CRA holding each account which POP / POP-SP the account is under, and keeps only the accounts registered under your registrations at that CRA.
/v1/auth/partner-tokenOpen subscriber session (subject assertion)- Grant
- JWT bearer (urn:ietf:params:oauth:grant-type:jwt-bearer)
- Prerequisites
- The subject:assert grant; your POP registrations on file; a live encryption key, which signs the assertion
- Content type
- application/x-www-form-urlencoded
- Encryption
- Never
- Rate limit
- 600 per hour per partner, and 5 per hour per partner per subscriber
6.1Verifying the subscriber
NPS Central relies on your assertion and does not verify the subscriber itself. Before you present one, you must have:
- verified the subscriber's identity at your end, to the standard your agreement with NPS Central requires — for example against their KYC record and the account details you hold;
- authenticated them in this interaction, for example with an OTP to their registered mobile number, a password and OTP, or a biometric, no more than 15 minutes before the request; and
- obtained their consent to act for them.
The assertion records how you authenticated them (amr) and when (auth_time). Both must be accurate.
6.2Identifying the subscriber
| You assert | What NPS Central does | The session holds |
|---|---|---|
| Mobile number and date of birth | Finds the subscriber at every CRA, then asks the CRA holding each account which POP / POP-SP it is under. Add pran to narrow to one of the subscriber's accounts. | Every account of the subscriber's that is under your registrations. accounts_incomplete is true if a CRA did not answer. |
| PRAN alone | Finds the CRA holding the PRAN and asks it which POP / POP-SP the account is under. | That one account, with a subject_ref of its own that is new for each session. |
6.3The subject assertion
A compact JWS, signed HS256 with your encryption key: the 32 bytes you obtain by decoding the key's base64url value. Never sign with your client secret.
| Header | Req. | Value |
|---|---|---|
| alg | M | HS256. No other algorithm is accepted. |
| kid | M | The key ID of the encryption key you signed with. It must be a live key of yours. |
| typ | O | JWT |
Claims
issstringRequiredYour client ID.
substringRequiredYour own reference for the subscriber, such as your customer ID. Never your client ID. Recorded against the session.
audstringRequiredThe token endpoint URL exactly as given to you, character for character.
jtistringRequiredA unique value, such as a UUID. Each value is accepted once.
iatintegerRequiredIssued-at time, epoch seconds. Not in the future.
expintegerRequiredExpiry, epoch seconds. No more than 300 seconds after iat.
subjectobjectRequiredEither { mobile, dateOfBirth }, optionally with pran, or { pran } alone.
amrarrayRequiredHow you authenticated them, as RFC 8176 values: for example ["otp"], ["pwd","otp"], ["fpt"], ["face"].
auth_timeintegerRequiredWhen you authenticated them, epoch seconds. No more than 15 minutes before this request, and not in the future.
pop[]arrayOptionalLimits the check to particular registrations of yours. Each entry is { cra, popRegNo, popSpRegNo }. Omit pop to accept any of your registered pairs.
{
"iss": "pop-acme",
"sub": "cust-88213",
"aud": "https://uat.example.in/api/v1/auth/partner-token",
"jti": "a7d9e1c4-2b6f-4a83-8e15-0c3f9b7d2e61",
"iat": 1791547200,
"exp": 1791547320,
"subject": { "pran": "110012345678" },
"amr": ["otp"],
"auth_time": 1791547180
}6.4Request
Request parameters
grant_typeformRequiredurn:ietf:params:oauth:grant-type:jwt-bearer
assertionformRequiredThe subject assertion (section 6.3).
AuthorizationheaderConditionalclient_secret_basic only: your client credentials, as in section 5.2.
client_id, client_secretformConditionalclient_secret_post only: your client credentials, as in section 5.2.
scopeformOptionalNarrows the session: any of subject:read, contribution, modification and document.
6.5Response — 200 OK
Response body
access_tokenstringRequiredThe subscriber session token. Send it as Authorization: Bearer <access_token> on subscriber APIs.
token_typestringRequiredAlways Bearer.
expires_inintegerRequiredSeconds until the session's absolute end, 60 minutes after the assertion. Refreshing does not move it.
idle_expires_inintegerRequiredSeconds until the session ends if it is not refreshed, normally 900 (15 minutes). Call POST /v1/auth/sessions:refresh to extend it.
scopestringRequiredAt most subject:read contribution modification document, limited to the scopes you hold.
subject_refstringRequiredThe subscriber's subjectRef, used in /v1/subjects/{subjectRef}/… paths. On a session opened by PRAN alone it is new for each session; correlate on the PRAN.
accounts[]arrayRequiredThe accounts the session holds: pran, craRef, craCode, craName.
accounts_incompletebooleanConditionalPresent, and true, when a CRA did not answer; an account may be missing. Never present on a session opened by PRAN alone.
6.6Errors
| HTTP | error | Cause |
|---|---|---|
| 400 | invalid_grant | Anything about the subscriber or the subject assertion: no match for the mobile and date of birth; a PRAN no CRA holds; no account under your POP; a pop pair you do not hold; auth_time or amr missing or out of range; a bad signature, aud or lifetime; a replayed jti; your client ID in sub. |
| 400 | unauthorized_client | subject:assert is not on your registration, or kid does not name a live encryption key of yours. |
| 400 | invalid_scope | scope names nothing an asserted session can carry. |
| 400 | invalid_request | No assertion, or client credentials in more than one place. |
| 401 | invalid_client | Your client credentials failed (section 5.4). |
| 429 | slow_down | Assertion rate limit exceeded. |
| 503 | temporarily_unavailable | No account could be confirmed, or the PRAN could not be found, because a CRA did not answer. |
6.7Your responsibilities
Under your agreement with NPS Central, you are responsible for having verified and authenticated the subscriber, and obtained their consent, before presenting an assertion. Keep the jti, sub, amr and auth_time of every assertion you send; NPS Central records the same values, and they are what either side will refer to if a session is ever questioned. Because the same encryption key signs your assertions and encrypts your payloads, revoking it stops both.
7Managing the session
These calls take the subscriber session token.
| Request | Purpose |
|---|---|
| GET /v1/sessions/current | Check that the session is live and see what it holds. |
| POST /v1/auth/sessions:refresh | Extend the idle timeout (section 7.1). No request body. |
| POST /v1/auth/sessions:logout | End the session. No request body. |
7.1Session lifetime
| Idle timeout | Absolute lifetime |
|---|---|
| 15 minutes, extended only by sessions:refresh | 60 minutes from the assertion |
Ordinary API calls do not extend the idle timeout. Call refresh when the subscriber is active in your application, rather than on a timer. Refreshing never moves the session past its 60 minutes. A session also ends early if the subject:assert grant is withdrawn from your account.
7.2Fresh OTP for sensitive changes
CONTACT_UPDATE, BANK_UPDATE and TIER2_ACTIVATION move either the subscriber's money or the channel every later code is sent to, so each needs your own fresh OTP. Immediately before filing, authenticate the subscriber with an OTP that your application sends, and state the result in the request's verification, with a verifiedAt no more than 5 minutes old. This is separate from the authentication your assertion records. A missing or stale verification is refused with 422 PARTNER_VERIFICATION_REQUIRED.
8Scopes and credentials
8.1Scopes
| Scope | Grants |
|---|---|
| eligibility | PAN validation and duplicate checks |
| onboarding | Creating and completing onboarding applications |
| registration | Submitting registrations and reading their status |
| subject:read | Account reads on a subscriber session |
| contribution | Contributions on a subscriber session; their status on the partner token |
| modification | Modifications on a subscriber session; their status on the partner token |
| document | Document requests on a subscriber session |
| subject:assert | The subject assertion grant (section 6). Never carried on a token. |
A call outside your scopes returns 401 AUTH_FAILED, the same response as an invalid token. If a freshly issued token receives it, check the scope field returned with that token.
8.2Which credential each API takes
| API | Partner token | Subscriber session |
|---|---|---|
| /v1/eligibility/…, /v1/onboarding-applications/…, /v1/registrations… | Yes | – |
| GET /v1/sessions/current, …/sessions:refresh, …/sessions:logout | – | Yes |
| GET /v1/subjects/{subjectRef}/… (account reads) | – | Yes |
| POST /v1/contributions, …:retry | – | Yes |
| POST /v1/subjects/{subjectRef}/modification-requests, …:acknowledge, …:cancel | – | Yes |
| GET /v1/contributions/{id}, GET /v1/modification-requests/{id} | Yes (your own) | Yes |
| /v1/document-jobs/… | – | Yes |
| GET /v1/reference/… | Not required | Not required |
Records belong to the partner that created them. Another partner's record, a record the subscriber created elsewhere, and an identifier that does not exist all return the same 404 NOT_FOUND.