Displaying PAN and CVV securely
Let the cardholder view their full card number without your servers ever holding it in the clear.
Before you begin
- An
activecard. A card in any other status has no details to show: it is refused withcard_invalid_state, orcard_closedonce closed. - The
cards:secretsscope on your credential. It is not granted by default. - A place to keep a private key where card data is allowed, such as the cardholder's device (in the platform's secure key storage) or a hardware security module. The decision about where is covered below, because it is the one that decides your PCI DSS scope.
How it works
When the cardholder wants to see their card details, your app creates an RSA key pair on the device. The private key stays there. Your backend sends the public key to us in a Reveal card secrets call, and we return the card number and security code encrypted to that key.
Your backend passes the ciphertext on to the device, which decrypts it with the private key and shows the details. Your servers only ever hold ciphertext.
Steps
1. Create a key pair on the device
Generate an RSA key pair of at least 2048 bits each time the cardholder asks to see their details, or once per install if you prefer. Keep the private key in the platform's secure key storage and never send it anywhere. Only the public key leaves the device, in PEM form.
2. Ask for the secrets when the cardholder taps
Call Reveal card secrets with the public key. Don't request it when a screen loads: a number that nobody looked at was still read.
There's nothing to retry safely and nothing to cache. Each call returns a new
ciphertext, the response is never cached, and the call doesn't use
Idempotency-Key.
3. Decrypt on the device
The response carries encrypted_pan and encrypted_cvv, base64 encoded. Both are
encrypted with RSA-OAEP using SHA-256 and no label, which every common
cryptography library supports. last_four and expires_at come back in the
clear.
4. Show the details, then discard them
Display the number and security code for as long as the cardholder is looking. Don't write them to storage, logs, analytics events, crash reports or support tickets, and clear them from memory when the screen closes.
5. Gate the reveal
Showing a full card number is a sensitive action, so treat it that way:
- Step up authentication immediately before requesting it. Use a biometric or passcode rather than relying only on the existing session.
- Never reveal cards in bulk. Reveal one card at a time after a deliberate user action.
- Every read is audited on our side and tied to the request. Log the event on
your side too, using
card_id— never a decrypted value.
Where you decrypt decides your scope
Ciphertext on its own tells your systems nothing. The decision that matters is where the private key lives and where the decrypted number is handled.
| If you… | Card data on your servers |
|---|---|
| Decrypt on the cardholder's device, with the private key kept there | None. Your servers relay ciphertext they cannot read |
| Decrypt in a hardware security module and never expose the number | Depends on your assessor. The HSM and everything that touches the number is in scope |
| Decrypt on your servers | Yes. Your infrastructure handles card data and is in PCI DSS scope |
See PCI DSS scope for the reasoning. Make this choice while you design the feature, not when an auditor asks about it.
Verifying
- The response contains
encrypted_pan,encrypted_cvv,algorithmandlast_four, and no field holds a readable number. - Decrypting both values with your private key gives digits. A different key can't decrypt them.
- Two calls for the same card return different ciphertext, because each is encrypted afresh.
- A card that isn't
activereturnscard_invalid_state, and a closed cardcard_closed. - A credential without
cards:secretsreturnsinsufficient_scope. - Nothing sensitive shows up in your logs. Search them for a test card number and its security code and make sure both return zero results. Ideally, automate this check.
Common errors
| Status | code | Fix |
|---|---|---|
400 | missing_parameter | public_key wasn't sent |
400 | invalid_public_key | public_key isn't a PEM RSA key or certificate, or is shorter than 2048 bits |
403 | insufficient_scope | The credential doesn't have cards:secrets |
404 | not_found | This program can't return card details, so the route isn't available |
404 | card_not_found | The card doesn't exist, or belongs to another organization |
422 | card_closed | The card is closed. A closed card has no number to show |
422 | card_invalid_state | The card isn't active: it is pending, inactive, frozen or expired |
422 | program_not_configured | The program isn't set up to return card details. Raise it with your account contact |
502 | processor_permission_denied | The program isn't approved to read card details. Send us the request_id |
502 | processor_unavailable | The processor couldn't return the details, or its reply couldn't be verified. Try again |
Related
- Security overview
- PCI DSS scope
- Card lifecycle — which card states allow secret retrieval