Show card details and set a PIN
Show the full card number, CVV and PIN only on the cardholder's device, and let them change the PIN.
This recipe shows the full card number, CVV and PIN only on the cardholder's device, and lets the cardholder change the PIN.
Who it is for. This recipe is for a consumer app with a "show card details" button and a "change PIN" screen.
Status. Available.
Money example
No money moves. Ada taps "show details" to pay 9.99 USD for an app subscription, copies the number and CVV, and later sets her PIN to 4821 for a terminal.
Before you start
- The card is issued.
GET /v1/cards/{card_id}returns astatusthat is not"". - The cardholder's device can make an RSA key pair of 2048 bits or more, and decrypt RSA-OAEP with SHA-256 and AES-256-GCM.
- The API key stays on your server. These routes need no special key permission, so limit who can call them in your own backend.
Find out what is required
You do not need to look anything up first. Send the device's public key in the X-Client-Public-Key header. Encode the whole PEM text as standard base64, with padding. The response contains encrypted_key (an AES-256 key wrapped with RSA-OAEP SHA-256), nonce (12 bytes) and ciphertext (ending with the 16-byte GCM tag).
Steps
1. Make a key pair on the device
Send the public key (PEM, base64) to your server. Keep the private key on the device.
2. Ask for the 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: <base64 of the PEM public key>'
const response = await fetch("https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/sensitive-details", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"X-Client-Public-Key": "<base64 of the PEM public key>",
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/sensitive-details",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"X-Client-Public-Key": "<base64 of the PEM public key>",
},
)
print(response.status_code, response.json())
Pass the three values to the device unchanged, and do not log them.
3. Decrypt on the device
Unwrap encrypted_key with the private key (RSA-OAEP, SHA-256), then decrypt ciphertext with AES-256-GCM and nonce, taking the last 16 bytes as the tag. The plaintext is {"pan": "…", "cvv": "…"}. Take expiry from GET /v1/cards/{card_id}. For complete code, see Go, Python and JavaScript.
4. Show the PIN
Send the same header. The response has the same three values, and the plaintext is {"pin": "4821"}.
curl --request GET \
--url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/pin \
--header 'Api-Key: <API_KEY>' \
--header 'X-Client-Public-Key: <base64 of the PEM public key>'
const response = await fetch("https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/pin", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"X-Client-Public-Key": "<base64 of the PEM public key>",
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/pin",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"X-Client-Public-Key": "<base64 of the PEM public key>",
},
)
print(response.status_code, response.json())
5. Change the PIN
Send a PUT with no Idempotency-Key. Send the 4 digits as a string. The API refuses repeated or sequential digits (1111, 1234).
curl --request PUT \
--url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/pin \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--data '
{
"card_pin": "4821"
}
'
const response = await fetch("https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/pin", {
method: "PUT",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"card_pin": "4821"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.put(
"https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/pin",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Content-Type": "application/json",
},
json={
"card_pin": "4821",
},
)
print(response.status_code, response.json())
{
"data": "card pin updated successfully",
"error": null
}
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 |
You sent URL-safe base64, or the raw PEM | Send standard base64 of the whole PEM |
400 parse public key: x_client_public_key must be a base64-encoded RSA public key in PEM format: no PEM block found |
You sent base64 of the DER bytes | Encode the PEM text with its header lines |
400 internal server error |
The key is too small for OAEP with SHA-256 | Use a key of 2048 bits or more |
400 card does not exist, whatever key you send |
The API checks the card id first, so this error hides every key error | Check the card id |
| Decryption fails on the device | The library defaults to OAEP with SHA-1 | Set the OAEP hash to SHA-256 |
400 card sensitive details missing |
The card is not issued, or the details cannot be revealed now | Check the card, then retry later |
400 this card has no PIN set: set one with PUT /v1/cards/{card_id}/pin, or reissue — cards issued now are given a PIN at creation |
The card is an older card with no PIN | Set a PIN |
400 failed to get card pin from provider |
The PIN cannot be revealed now | Retry later |
400 card pin update failed |
The PIN is weak or invalid | Choose another 4-digit PIN |
400 json: cannot unmarshal number into Go struct field Body.card_pin of type string |
You sent the PIN as a number | Send it as a string, such as "4821" |
Webhooks you will receive
These routes send no webhooks.
Testing in sandbox
These routes work with sandbox cards, so use them to test your decryption code. The details decrypt to a 16-digit pan, whose last 4 digits match masked_pan, and a 3-digit cvv. A new card has a PIN at creation.