JavaScript
Request and decrypt a card's full number and CVV with Node.js. The same decryption works for the PIN.
This guide shows how to request and decrypt sensitive card details with Node.js. The code generates an RSA key pair in memory, Base64-encodes the public key in PEM format, and sends it in the X-Client-Public-Key header. To decrypt the response, it unwraps the Advanced Encryption Standard (AES) key with Rivest-Shamir-Adleman Optimal Asymmetric Encryption Padding (RSA-OAEP). It then decrypts the card data with Advanced Encryption Standard 256-bit Galois/Counter Mode (AES-256-GCM).
Prerequisites
Before you begin, you need:
- Node.js 18 or later installed on your system. The code samples use the global
fetch, which Node.js has by default from version 18. - An API key for the Gravv API
- A card ID to retrieve sensitive details for
Generate the RSA key pair
Generate a 2048-bit RSA key pair in memory with the built-in Node.js crypto module. Keep the private key in scope, because you need it to decrypt the response:
import crypto from "crypto";
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
});
Encode the public key
Export the public key as PEM (SubjectPublicKeyInfo), then Base64-encode the entire PEM string. This is the value to send in the X-Client-Public-Key header:
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const base64PublicKey = Buffer.from(publicKeyPem, "utf8").toString("base64");
Do not send only the modulus (
nfrom a JSON Web Key), the raw DER bytes, or the PEM string without Base64-encoding it. The header value must Base64-decode to the full PEM text, including the-----BEGIN PUBLIC KEY-----and-----END PUBLIC KEY-----markers. Gravv rejects a header that contains newlines or spaces, so you cannot send raw PEM directly.
Request the encrypted card details
Send a GET request to the View card sensitive details endpoint with the Base64-encoded public key:
const response = await fetch(
`https://api.gravv.xyz/v1/cards/${cardId}/sensitive-details`,
{
method: "GET",
headers: {
"Api-Key": apiKey,
"X-Client-Public-Key": base64PublicKey,
"Content-Type": "application/json",
},
}
);
const { data: payload, error } = await response.json();
if (error) {
throw new Error(`Gravv API error: ${JSON.stringify(error)}`);
}
The payload contains the encrypted_key, nonce, and ciphertext fields that you need to decrypt the card details. The next steps use the in-memory privateKey that you generated in Generate the RSA key pair.
Understanding the encrypted response
The View card sensitive details and the Get card PIN endpoints return three Base64-encoded values:
| Field | Description |
|---|---|
| encrypted_key | AES-256 key encrypted with your RSA public key |
| nonce | 12-byte initialization vector for AES-GCM decryption |
| ciphertext | Encrypted card details |
Decode the Base64 values
Convert all three Base64-encoded values from the API response into buffers:
const encryptedKey = Buffer.from(payload.encrypted_key, "base64");
const nonce = Buffer.from(payload.nonce, "base64");
const ciphertext = Buffer.from(payload.ciphertext, "base64");
Decrypt the AES key
Use your RSA private key to decrypt the AES key. The decryption uses RSA-OAEP padding with SHA-256. The key option accepts a crypto.KeyObject (such as the in-memory privateKey from the first step) or a PEM-encoded string:
const aesKey = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
encryptedKey
);
Separate the authentication tag
The last 16 bytes of the ciphertext are the GCM authentication tag. Separate the tag from the encrypted data:
const authTag = ciphertext.slice(ciphertext.length - 16);
const encryptedData = ciphertext.slice(0, ciphertext.length - 16);
Decrypt the card details
Create an AES-256-GCM decipher, set the authentication tag, and decrypt the card details:
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, nonce);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData).toString("utf8");
decrypted += decipher.final("utf8");
Complete end-to-end code
This code generates an RSA key pair, encodes the public key, requests sensitive card details and decrypts the response in one function. It holds the private key only in memory:
import crypto from "crypto";
export async function getCardSensitiveDetails({ apiKey, cardId, baseUrl = "https://api.gravv.xyz" }) {
// Step 1: Generate RSA key pair in memory
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
});
// Step 2: Base64-encode the PEM-formatted public key
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const base64PublicKey = Buffer.from(publicKeyPem, "utf8").toString("base64");
// Step 3: Request the encrypted card details
const response = await fetch(`${baseUrl}/v1/cards/${cardId}/sensitive-details`, {
method: "GET",
headers: {
"Api-Key": apiKey,
"X-Client-Public-Key": base64PublicKey,
"Content-Type": "application/json",
},
});
const body = await response.json();
if (!response.ok || body.error) {
throw new Error(`Gravv API error: ${JSON.stringify(body.error ?? body)}`);
}
const payload = body.data;
if (!payload?.encrypted_key || !payload?.ciphertext || !payload?.nonce) {
throw new Error("Card sensitive details are not available. Please try again in a moment.");
}
// Step 4: Decode Base64 values
const encryptedKey = Buffer.from(payload.encrypted_key, "base64");
const nonce = Buffer.from(payload.nonce, "base64");
const ciphertext = Buffer.from(payload.ciphertext, "base64");
// Step 5: Decrypt the AES key using RSA-OAEP
const aesKey = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
encryptedKey
);
// Step 6: Separate authentication tag from ciphertext
const authTag = ciphertext.slice(ciphertext.length - 16);
const encryptedData = ciphertext.slice(0, ciphertext.length - 16);
// Step 7: Decrypt using AES-256-GCM
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, nonce);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData, null, "utf8");
decrypted += decipher.final("utf8");
return JSON.parse(decrypted);
}
Usage example
This example calls getCardSensitiveDetails with the API key from the GRAVV_API_KEY environment variable and a sample card ID. It then logs the decrypted card details:
const cardDetails = await getCardSensitiveDetails({
apiKey: process.env.GRAVV_API_KEY,
cardId: "dce0192a-9b3d-440b-9500-33dc9ac8dc20",
});
console.log("Card details:", cardDetails);
// { pan: "4111111111111111", cvv: "123" }
If you already have the encrypted payload and only need to decrypt it, call crypto.privateDecrypt and crypto.createDecipheriv as in Decrypt the AES key and Decrypt the card details. Pass your PEM string in the key option.