NPS Zaruri Hai
Technical document

Payload Encryption – The NPSC envelope

Encrypting request bodies and decrypting response bodies

How a partner encrypts request bodies and decrypts response bodies with the NPSC envelope, with test vectors to check an implementation against.

Version
Version 1.0
Status
Final
Last updated
Updated 12/10/2026
Audience:
Registered partners (points of presence)
Prepared by:
NPS Central Engineering
Document control
Title
Payload Encryption — The NPSC envelope
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 whose partner account is set up to encrypt API payloads. It describes the NPSC envelope, a JSON wrapper that carries an encrypted request or response body, and the steps to seal a request and open a response.

The envelope needs only standard cryptography: HMAC-SHA256, SHA-256 and AES-256-GCM, all available in the Java and Node.js standard libraries. Apart from the body being sealed, nothing about the API changes: the endpoints, tokens and JSON bodies are those described in the Authentication document for your method, Account Information and Modifications.

1.1Notation

In field tables, the presence column shows M for mandatory, O for optional, and C for conditional. The concatenation symbol means byte concatenation. Hex is lowercase. Base64 is standard base64 with padding, not the URL-safe variant. All text is UTF-8.

2Before you begin

ItemNotes
Encryption keyA 256-bit key, delivered once, out of band, as base64url text. Decode it to its 32 bytes before use. Store it in a secrets manager or HSM.
Key IDIssued with the key and sent in every envelope as keyId. Use it exactly as issued.
Access tokenYour partner token or a subscriber session token. Each message's key is bound to the token the call carries.
ClockKeep your servers on NTP. An envelope more than 5 minutes from NPS Central's clock is refused.

UAT and Production issue separate keys. A key from one environment is refused in the other.

3How it works

Every message, in either direction, is encrypted under its own key. The sender picks a fresh random number, derives the message key from the issued key, that random number and the call's access token, and encrypts the JSON body with AES-256-GCM. The additional authenticated data (AAD) names the direction, HTTP method, path, key ID, random number and timestamp, so a body sealed for one call cannot be replayed to another endpoint or session, or presented as a response.

4The envelope

Requests and responses use the same structure, with the content type application/vnd.npsc.envelope+json.

EnvelopeJSON
{
  "version": "NPSC1",
  "keyId": "test-key-0001",
  "randomNo": "00112233445566778899aabbccddeeff",
  "timestamp": 1760261400,
  "encData": "oKGio6SlpqeoqaqrW6Zcn0RWG8LiZ5MNtPHvfg7HaiEQDE2Ubp2J9D2PUh3tsQFBXxIJadzXHAwQPRiS7A=="
}
Fields
  • version
    stringRequired

    Always NPSC1.

  • keyId
    stringRequired

    The key ID of the key the message was sealed under.

  • randomNo
    stringRequired

    16 random bytes as 32 lowercase hex characters, from a secure random generator. New for every message; a value already used is refused.

  • timestamp
    integerRequired

    When the message was sealed, in epoch seconds. Within 300 seconds of NPS Central's clock.

  • encData
    stringRequired

    base64 of IV (12 bytes) + ciphertext + tag (16 bytes).

5Sealing a request

You need the issued key and its key ID, the bearer token you are about to send, the HTTP method, the request path and the JSON body.

  1. Derive the message key. K = HKDF-SHA256(ikm = issued key, salt = hex-decode(randomNo), info = "NPSC1" + 0x00 + SHA-256(token), length = 32). The token is the bearer token exactly as sent after Bearer in this call's Authorization header.
  2. Build the AAD. AAD = "NPSC1|REQ|" METHOD "|" path "|" keyId "|" randomNo "|" timestamp. METHOD is upper case. The path starts at /api and has no query string. The timestamp is a decimal number.
  3. Encrypt. Generate a random 12-byte IV and encrypt the UTF-8 JSON body with AES-256-GCM under K, the IV and the AAD, with a 16-byte tag. Then encData = base64(IV + ciphertext + tag).
  4. Send, with the headers below.
Request headers
  • Authorization
    headerRequired

    Bearer <token>, the same token used in step 1.

  • Content-Type
    headerConditional

    application/vnd.npsc.envelope+json, on a call with a body.

  • Accept
    headerRequired

    application/vnd.npsc.envelope+json, on every call, with or without a body.

  • Idempotency-Key
    headerConditional

    Wherever the endpoint requires one.

A call without a body, such as a GET, has nothing to seal; send it with the Accept header and no Content-Type. The largest envelope accepted is 10 MB.

6Opening a response

  1. Read the HTTP status. It is not sealed.
  2. Check the content type. If it is application/vnd.npsc.envelope+json, continue. If it is application/json or application/problem+json, the body is plain and is read as usual.
  3. Pick the key named by the envelope's keyId, and check that timestamp is within a few minutes of your clock.
  4. Derive K as in step 1 of section 5, from the response's randomNo and the token your request carried.
  5. Build the AAD with RES in place of REQ, your request's method and path, and the response's keyId, randomNo and timestamp.
  6. Decrypt. The first 12 bytes of the base64-decoded encData are the IV and the last 16 the tag. If the tag does not verify, discard the response and treat the outcome of the call as unknown.

The plaintext is the JSON response documented for the endpoint; an error opens to the usual problem document.

7Errors

HTTPcodeCauseWhat to do
400ENCRYPTION_REQUIREDA plain JSON body was sent where your account requires encryptionSeal the body.
400ENVELOPE_INVALIDA missing or malformed field, a reused randomNo, a stale timestamp, a keyId that is not live, a body that does not decrypt, or a decrypted body that is not JSONCheck the inputs, then send a newly sealed request.
503ENVELOPE_UNAVAILABLENPS Central could not seal the responseRetry. For a change, use the same Idempotency-Key.

Seal every retry afresh, with a new randomNo, IV and timestamp. Idempotency is judged on the decrypted body, so a retry with the same Idempotency-Key and the same JSON receives the original response.

8Test vectors

Check your implementation against these values before calling NPS Central. They use a fixed random number, IV and timestamp so that the output can be reproduced; in your integration all three are new for every message.

Request — POST /api/v1/contributions
InputValue
Issued key (hex)000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
Access tokenpsub.example-token
Method, pathPOST, /api/v1/contributions
keyIdtest-key-0001
randomNo00112233445566778899aabbccddeeff
timestamp1760261400
IV (hex)a0a1a2a3a4a5a6a7a8a9aaab
Body{"clientReferenceId":"pop-ref-1"}
K (hex)17e1482ba0ef3637cefd455256cbd12dffe8799f12248678db840005040b5c5e
encDataoKGio6SlpqeoqaqrW6Zcn0RWG8LiZ5MNtPHvfg7HaiEQDE2Ubp2J9D2PUh3tsQFBXxIJadzXHAwQPRiS7A==
Response — to the request above, with the same key, token, method and path
InputValue
keyIdtest-key-0001
randomNoffeeddccbbaa99887766554433221100
timestamp1760261401
IV (hex)b0b1b2b3b4b5b6b7b8b9babb
K (hex)57ee69a4c6bd54c347d90a65fd7fc988f84d488a44ed479043889bf911526de3
encDatasLGys7S1tre4ubq7G0n8kLf8+L0h2Qt/LvLcdsiH0br6yq2SbvsIzxtt0Q==
Body{"result":"ok"} (illustrative)

Sealing the request inputs must give the request encData exactly, and opening the response encData must give exactly {"result":"ok"}.