Safely Reveal Card Details to Customers
Sensitive card details such as the card number and security code are not returned in the responses of normal cards endpoints. Instead, Bridge provides a way for you to safely expose PANs to users in a way that keeps your servers out of PCI compliance scope.
The following is an overview of the steps you will need to take in order to implement this functionality:
- Your backend implements an endpoint to generate an ephemeral key for your customer.
- Your frontend will generate a random secret, and use it to generate a nonce, which will be passed through your backend and then to Bridge to generate a one-time ephemeral key.
- Your frontend will then directly call a special Bridge endpoint with this ephemeral key as the credential, and provide the original secret to prove ownership.
- Bridge will then reveal the card number, security code, and expiration date directly to the frontend, without any sensitive card details passing through your backend.
Once implemented, the flow between your frontend, your backend, and Bridge should look similar to the following:

Below, we'll discuss each of the steps in this flow in detail.
1. Client derives a Nonce and sends it to your backend
Your client generates a secret, and uses SHA-256
to generate a nonce from the secret, and a deterministic string nonce:{timestamp}
. As this needs to be done in a particular way, you can use the following snippets in your client as a reference:
async function generateClientNonce(clientSecret: string, clientTimestamp: number): Promise<string> {
const message = `nonce:${clientTimestamp}`;
// Convert secret and message to ArrayBuffer
const encoder = new TextEncoder();
const keyData = encoder.encode(clientSecret);
const messageData = encoder.encode(message);
// Import the key
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
// Sign the message
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
// Convert to base64
const signatureArray = new Uint8Array(signature);
return btoa(String.fromCharCode(...signatureArray));
}
// Usage example
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
const clientSecret = Array.from(randomBytes, byte => byte.toString(16).padStart(2, '0')).join('');
const clientTimestamp = Math.floor(Date.now() / 1000);
const nonce = await generateClientNonce(clientSecret, clientTimestamp);
// pass `nonce` in step 2
// pass `clientSecret` and `clientTimestamp` in step 4
import Foundation
import CommonCrypto
func generateHmacSignature(clientSecret: String, clientTimestamp: Int) -> String? {
let message = "nonce:\(clientTimestamp)"
guard let keyData = clientSecret.data(using: .utf8),
let messageData = message.data(using: .utf8) else {
return nil
}
var hmac = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
keyData.withUnsafeBytes { keyBytes in
messageData.withUnsafeBytes { messageBytes in
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256),
keyBytes.bindMemory(to: UInt8.self).baseAddress, keyData.count,
messageBytes.bindMemory(to: UInt8.self).baseAddress, messageData.count,
&hmac)
}
}
let hmacData = Data(hmac)
return hmacData.base64EncodedString()
}
// Usage
let clientSecret = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
let clientTimestamp = Int(Date().timeIntervalSince1970)
let nonce = generateHmacSignature(clientSecret: clientSecret, clientTimestamp: clientTimestamp)
// pass `nonce` to your backend and Bridge in step 2
// pass `clientSecret` and `clientTimestamp` to Bridge in step 4
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
fun generateHmacSignature(clientSecret: String, clientTimestamp: Long): String {
val message = "nonce:$clientTimestamp"
try {
val secretKeySpec = SecretKeySpec(clientSecret.toByteArray(), "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(secretKeySpec)
val hmacBytes = mac.doFinal(message.toByteArray())
return Base64.getEncoder().encodeToString(hmacBytes)
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException("HmacSHA256 algorithm not available", e)
} catch (e: InvalidKeyException) {
throw RuntimeException("Invalid key", e)
}
}
// Usage
fun main() {
val clientSecret = java.util.UUID.randomUUID().toString().replace("-", "")
val clientTimestamp = System.currentTimeMillis() / 1000
val nonce = generateHmacSignature(clientSecret, clientTimestamp)
// pass `nonce` to your backend and Bridge in step 2
// pass `clientSecret` and `clientTimestamp` to Bridge in step 4
}
The client will send just the final nonce
from each of these snippets to your backend.
It will then reserve the original clientSecret
and clientTimestamp
it used to generate the nonce, as it will need to be used validate the ephemeral key in step 4 below. The client must not store or reuse the secret or timestamp.
2. Your backend relays the Nonce to Bridge
Your backend will then send the derived nonce to Bridge by POST
ing to /v0/customers/{customerId}/card_accounts/{cardAccountId}/ephemeral_keys
. The request should contain just the client nonce itself, like so:
{
"client_nonce": "nNhJ3tP3Rqah2evIdlCx7HFdPuwd0BMZOZyz7a21ufI="
}
Note that Bridge has no ability to authenticate where the client_nonce
is actually from. The responsibility is on your backend to authenticate that the nonce was sent from the right customer to access the right card account.
3. Bridge generates a one-time Ephemeral Key associated to the Nonce
The response returned by Bridge in /v0/customers/{customerId}/card_accounts/{cardAccountId}/ephemeral_keys
will contain just one field, ephemeral_key
, which contains a token that can be used once to reveal the card details. This token expires in 5 minutes.
Here is an example of the response:
{
"ephemeral_key": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTE5MjMxNzIsIm5iZiI6MTc1MTkyMzE3MiwiZXhwIjoxNzUxOTIzNDcyLCJzdWIiOiJmMTg0M2IwNi0xZTgwLTQzMGUtOGUxMy01MTk5OWNmNjZmYjgiLCJhdWQiOiJzZGZzZmQiLCJpc3MiOiJicmlkZ2UtcGNpIn0.QBP7SKYildM9raQN_m3bijbaqO-JZ8I7OSkj2IFHvUQ84e3Fr5DdFt3peBLbsSY4CYVf4w951MCUDgdHMbHCgiD_niEzUS1KxHtT3otT1dbCg4DO0MY3siwjsV9UAWd4huLYvBACWs316Ydk_38Qy5Q_cudkvTKHBzMYm7LsKMwEiCGDeFw95_JJwmvWCoLPx2Xn7MXfYeFOMEYXBKSiUwNtoGVYJxPeO4C-Krj79QklRKcCMrhY-s-rGTPt-trPnanCPEMnQwX4AjyNjHWAYh-sX_cQgpiYX1GymPaYoZkhfxNL2pxNZJjBp1BgAUvGy1rHrTeDa4Pn4kPVNemr_w"
}
Do not have your backend directly call Bridge to retrieve card credentials using this ephemeral key. Instead, it should pass this ephemeral key directly back to your frontend to perform the next step.
4. Your frontend directly calls Bridge with the Ephemeral Key, Nonce, and Secret
To reveal the card credentials, your frontend will directly call a special Bridge endpoint. This endpoint does not require a Bridge API key, and instead requires just ephemeral key itself in the Authorization
header as the credential, like so:
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTE5MjMxNzIsIm5iZiI6MTc1MTkyMzE3MiwiZXhwIjoxNzUxOTIzNDcyLCJzdWIiOiJmMTg0M2IwNi0xZTgwLTQzMGUtOGUxMy01MTk5OWNmNjZmYjgiLCJhdWQiOiJzZGZzZmQiLCJpc3MiOiJicmlkZ2UtcGNpIn0.QBP7SKYildM9raQN_m3bijbaqO-JZ8I7OSkj2IFHvUQ84e3Fr5DdFt3peBLbsSY4CYVf4w951MCUDgdHMbHCgiD_niEzUS1KxHtT3otT1dbCg4DO0MY3siwjsV9UAWd4huLYvBACWs316Ydk_38Qy5Q_cudkvTKHBzMYm7LsKMwEiCGDeFw95_JJwmvWCoLPx2Xn7MXfYeFOMEYXBKSiUwNtoGVYJxPeO4C-Krj79QklRKcCMrhY-s-rGTPt-trPnanCPEMnQwX4AjyNjHWAYh-sX_cQgpiYX1GymPaYoZkhfxNL2pxNZJjBp1BgAUvGy1rHrTeDa4Pn4kPVNemr_w
The client will include the original clientSecret
and clientTimestamp
used to generate the nonce as query parameters in the requested URL, like so:
https://cards-pci.bridge.xyz/v0/card_details/?secret={clientSecret}×tamp={clientTimestamp}
Bridge will validate that the ephemeralKey
is associated with the nonce
derived from the same clientSecret
and clientTimestamp
. It will also validate that the ephemeral key has not already been used, and that the key hasn't expired yet.
This endpoint will return a response similar to the following:
{
"card_number": "4432528012345678",
"card_security_code": "123",
"expiry_date": "2030-12-12"
}
This ephemeral key can only be used once, so the client will need to generate a new secret and nonce in order to show the card details again. In order to comply with PCI DSS requirement 3.4, you must not store the card details returned from this endpoint.
Updated about 15 hours ago