---
title: "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 `cryptography` and `requests` libraries installed:

  ```shell install dependencies
  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:

```python
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:

```python
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")
```

> **Do not** 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](/api-reference/cards/get-v1-cards-card-id-sensitive-details) endpoint with the Base64-encoded public key:

```python
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](/api-reference/cards/get-v1-cards-card-id-sensitive-details) and the [Get card PIN](/api-reference/cards/get-v1-cards-card-id-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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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](#decrypt-the-aes-key) and [Decrypt the card details](#decrypt-the-card-details) snippets.

