Retrieve sensitive card details
Show the full card number, CVV and PIN on the cardholder's device only, encrypted to a key the device holds.
This page shows how to read a card's full number, CVV and PIN so that only the cardholder's device can decrypt them. Gravv encrypts these sensitive card details (the card number pan, the cvv and the card PIN) with a one-time AES-256-GCM key. It then wraps that key with your Rivest-Shamir-Adleman (RSA) public key, using RSA-OAEP with SHA-256 (MGF1-SHA-256, no label).
A money example. No money moves. Ada taps "show details" in your app to pay 9.99 USD for a subscription online. Her phone makes a key pair. Your server asks for the details with the phone's public key. Only her phone can read the number and CVV.
Make the key pair on the cardholder's device (or another client you control), and keep the private key there. Your server only relays the encrypted response. Do not log it. Use an RSA key of 2048 bits or more. These routes need no special key permission, so limit who can call them in your own backend.
Generate RSA key pair
Generate an RSA key pair with OpenSSL. The pair has a private key and a public key:
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
This creates two files:
| private.pem | Keep this secure on your server and never transmit it |
| public.pem | You encode this and send it to Gravv in the API request header |
Encode the public key
Base64-encode the entire public.pem file, including the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- markers and the newlines between them. The result is a single-line base64 string, which you send in the API request header.
# Linux
base64 -w 0 public.pem > public.pem.b64
# macOS
base64 -i public.pem -o public.pem.b64
When Gravv base64-decodes the header value, the result must be the original PEM text. If you encode only the modulus, the DER bytes or any other part of the key, the request fails.
Request encrypted card details
Send the base64-encoded PEM public key in the X-Client-Public-Key header when you request sensitive card details:
curl --request GET \
--url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/sensitive-details \
--header 'Api-Key: <API_KEY>' \
--header 'X-Client-Public-Key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEuLi4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0='
Replace the header value with the full base64-encoded contents of your public.pem.
The response contains the encrypted card details:
{
"data": {
"ciphertext": "IjBkZlJMWUVXMzJ5MjdySzhWQlp1WHM5N1hzZ0szNGFSb1VScTJrbDhyazkzSmUrM0lTTXNiOCs4bmo2MW9LdDYxdkN0d0w1NSI=",
"encrypted_key": "IkUxdXhGa2c3ekpVaUJmWFlhcnVhM0dZRyt1amZJczBpMDJsak85QnIvVjkzSGxvWWxDNk1xaVlyYkNGQnJtTHFkVkwybmlFbkxDemliOUoweWI3RTg5YUFvTEZncmFkNFV0Z2cvV1ZUK29HbGZPZWRJVnREU1ZvSmNWaDMrSExnSUMybnBaTWJkelpualB6b21PcThCcGpNN09yWG1FeHUvd3pHblhpYXIyRT0i",
"nonce": "Img2M29JSVVESTZ2aS90S2si"
},
"error": null
}
Decrypt sensitive card details
To decrypt the response:
- Unwrap
encrypted_keywith your RSA private key (RSA-OAEP, SHA-256). The result is the AES key. - Decrypt
ciphertextwith that AES key andnonce(AES-256-GCM). The 16-byte tag is at the end of the ciphertext. - Parse the plaintext, which is JSON:
{"pan": "…", "cvv": "…"}. For the PIN endpoint, it is{"pin": "…"}.
The plaintext has no expiry. Read expiry from Get card.
If decryption fails on the device, check that the library uses SHA-256 for OAEP. Many libraries default to SHA-1.
These guides have complete decryption code for each language:
Decrypt card details with Go's crypto package, using RSA-OAEP and AES-GCM.
Decrypt card details with Python's cryptography library.
Decrypt card details with the Node.js crypto module, using RSA and AES-GCM.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 x_client_public_key is required |
The header is missing | Send X-Client-Public-Key |
400 x_client_public_key must be a base64-encoded RSA public key in PEM format: the header value is not valid base64 |
The header is URL-safe base64, or the raw PEM | Send standard base64 of the whole PEM text |
400 parse public key: x_client_public_key must be a base64-encoded RSA public key in PEM format: no PEM block found |
The header is base64 of the DER bytes | Encode the PEM text, with its header lines |
400 internal server error |
The key is too small for RSA-OAEP with SHA-256 | Use 2048 bits or more |
400 card does not exist, whatever key you send |
The API checks the card id before the header, so a wrong id hides every key error | Check the card id first |
400 card sensitive details missing |
The card is not issued yet, or the details cannot be revealed right now | Check that the card is issued, then retry later |