Python
This guide shows you how to request and decrypt sensitive card details using Python. The request flow 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. The decryption flow uses Rivest-Shamir-Adleman Optimal Asymmetric Encryption Padding (RSA-OAEP) to unwrap the Advanced Encryption Standard (AES) key. Then Advanced Encryption Standard 256-bit Galois/Counter Mode (AES-256-GCM) is used to decrypt the card data.
Prerequisites
Before you begin, you need:
- Python 3.8 or later installed on your system
- The
cryptographyandrequestslibraries installed:
pip install cryptography requests
- 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 using the cryptography library. Keep the private key in scope to decrypt the response:
from cryptography.hazmat.primitives.asymmetric import rsa
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
Encode the public key
Serialize the public key to PEM (SubjectPublicKeyInfo), then Base64-encode the entire PEM byte string. This is the value to send in the X-Client-Public-Key header:
import base64
from cryptography.hazmat.primitives import serialization
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
base64_public_key = base64.b64encode(public_key_pem).decode("ascii")
Caution
Don't send only the modulus (n from the 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. The header is rejected if it contains newlines or spaces, which is why raw PEM cannot be sent directly.
Request the encrypted card details
Send a GET request to the View card sensitive details endpoint with the Base64-encoded public key:
import requests
response = requests.get(
f"https://api.gravv.xyz/v1/cards/{card_id}/sensitive-details",
headers={
"Api-Key": api_key,
"X-Client-Public-Key": base64_public_key,
"Content-Type": "application/json",
},
)
response.raise_for_status()
body = response.json()
if body.get("error"):
raise RuntimeError(f"Gravv API error: {body['error']}")
payload = body["data"]
The payload contains the encrypted_key, nonce, and ciphertext fields needed to decrypt the card details. The remaining steps use the in-memory private_key you generated above.
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 (AES-GCM ciphertext + 16-byte authentication tag) |
Decode the Base64 values
Convert all three Base64-encoded values from the API response into bytes:
encrypted_key = base64.b64decode(payload["encrypted_key"])
nonce = base64.b64decode(payload["nonce"])
ciphertext = base64.b64decode(payload["ciphertext"])
Decrypt the AES key
Use your RSA private key to decrypt the AES key. The decryption uses RSA-OAEP padding with SHA-256:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
aes_key = private_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
Decrypt the card details
Create an AES-GCM cipher and decrypt the card details. The AESGCM.decrypt method automatically validates the authentication tag, which the server appends to the ciphertext:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(aes_key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
card_details = plaintext.decode("utf-8")
Complete end-to-end code
The following implementation generates an RSA key pair, encodes the public key, requests sensitive card details, and decrypts the response in a single function. The private key is held only in memory:
import base64
import json
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def get_card_sensitive_details(
api_key: str,
card_id: str,
base_url: str = "https://api.gravv.xyz",
) -> dict:
# Step 1: Generate RSA key pair in memory
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# Step 2: Base64-encode the PEM-formatted public key
public_key_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
base64_public_key = base64.b64encode(public_key_pem).decode("ascii")
# Step 3: Request the encrypted card details
response = requests.get(
f"{base_url}/v1/cards/{card_id}/sensitive-details",
headers={
"Api-Key": api_key,
"X-Client-Public-Key": base64_public_key,
"Content-Type": "application/json",
},
)
response.raise_for_status()
body = response.json()
if body.get("error"):
raise RuntimeError(f"Gravv API error: {body['error']}")
payload = body.get("data") or {}
if not all(k in payload for k in ("encrypted_key", "nonce", "ciphertext")):
raise RuntimeError(
"Card sensitive details are not available. Please try again in a moment."
)
# Step 4: Decode Base64 values
encrypted_key = base64.b64decode(payload["encrypted_key"])
nonce = base64.b64decode(payload["nonce"])
ciphertext = base64.b64decode(payload["ciphertext"])
# Step 5: Decrypt the AES key using RSA-OAEP
aes_key = private_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
# Step 6: Decrypt the card details using AES-256-GCM
aesgcm = AESGCM(aes_key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
return json.loads(plaintext.decode("utf-8"))
Usage example
The following example calls get_card_sensitive_details with an API key and card ID loaded from environment variables and prints the decrypted card details:
import os
card_details = get_card_sensitive_details(
api_key=os.environ["GRAVV_API_KEY"],
card_id="dce0192a-9b3d-440b-9500-33dc9ac8dc20",
)
print("Card details:", card_details)
# {"pan": "4111111111111111", "cvv": "123"}
If you already have the encrypted payload and only need to decrypt it, load your existing private key with serialization.load_pem_private_key(priv_pem, password=None) and reuse the Decrypt the AES key and Decrypt the card details snippets.