Skip to main content

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 active card. A card in any other status has no details to show: it is refused with card_invalid_state, or card_closed once closed.
  • The cards:secrets scope 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 thereNone. Your servers relay ciphertext they cannot read
Decrypt in a hardware security module and never expose the numberDepends on your assessor. The HSM and everything that touches the number is in scope
Decrypt on your serversYes. 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, algorithm and last_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 active returns card_invalid_state, and a closed card card_closed.
  • A credential without cards:secrets returns insufficient_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​

StatuscodeFix
400missing_parameterpublic_key wasn't sent
400invalid_public_keypublic_key isn't a PEM RSA key or certificate, or is shorter than 2048 bits
403insufficient_scopeThe credential doesn't have cards:secrets
404not_foundThis program can't return card details, so the route isn't available
404card_not_foundThe card doesn't exist, or belongs to another organization
422card_closedThe card is closed. A closed card has no number to show
422card_invalid_stateThe card isn't active: it is pending, inactive, frozen or expired
422program_not_configuredThe program isn't set up to return card details. Raise it with your account contact
502processor_permission_deniedThe program isn't approved to read card details. Send us the request_id
502processor_unavailableThe processor couldn't return the details, or its reply couldn't be verified. Try again