---
title: "Show card details and set a PIN"
description: "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 a `status` that 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

<CodeGroup>
```bash title="cURL"
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>'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

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](/platform/cards/view-card-sensitive-details/golang), [Python](/platform/cards/view-card-sensitive-details/python) and [JavaScript](/platform/cards/view-card-sensitive-details/javascript).

### 4. Show the PIN

Send the same header. The response has the same three values, and the plaintext is `{"pin": "4821"}`.

<CodeGroup>
```bash title="cURL"
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>'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

### 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`).

<CodeGroup>
```bash title="cURL"
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"
}
'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

```json title="Response"
{
  "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.

## Reference

- [Retrieve sensitive card details](/platform/cards/view-card-sensitive-details/overview)
- [Manage card PIN](/platform/cards/manage-card-pin)
