Display sensitive card details using SDK
You can allow your cardholders to view their sensitive card details (PAN, CVC, expiry date, cardholder name) directly within your application. The BVNK Card Details SDK renders this data inside a sandboxed iframe served from BVNK's card vault; your application never touches the sensitive values.
How it works
The SDK uses origin isolation to keep sensitive data out of your application scope:
- Your backend requests a short-lived access token from BVNK.
- Your frontend mounts the SDK, which creates a sandboxed
iframepointing to BVNK's card vault. - After a
postMessagehandshake, the SDK passes the token to theiframe. - The vault retrieves and renders the card details directly inside the
iframe.
Your application only ever sees lifecycle events (mounting, ready, errored) and never the card data itself. This reduces your PCI scope because sensitive data is rendered by the vault origin, not your app.
Requirements
Before you begin, ensure the following:
- The card is in
ACTIVEstatus. - You have implemented step-up authentication (e.g. 2FA) in your app before revealing card details.
- Your frontend can install npm packages (
@bvnk/card-details-sdk).
Request an access token
From your backend, send the POST /card/v1/sensitive-card-details-token request with the card ID and customer ID.
{
"cardId": "123e4567-e89b-12d3-a456-426614174000",
"customerId": "4f2a76a4-0954-4999-b555-f9f2bec78c50"
}
BVNK validates card ownership and returns a one-time, short-lived token:
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 60
}
The token expires after the number of seconds specified in expiresIn. Pass this token to your frontend.
Install the SDK
Install the @bvnk/card-details-sdk package in your frontend project:
pnpm add @bvnk/card-details-sdk
# or
npm install @bvnk/card-details-sdk
# or
yarn add @bvnk/card-details-sdk
Mount the card details iframe
-
Import the
mountfunction and call it with a container element and the access token from the Request an access token step. The SDK creates a sandboxediframeinside the container and renders the card details once the vault handshake completes.import { mount } from '@bvnk/card-details-sdk'const container = document.getElementById('card-details')!const handle = mount({container,accessToken: 'eyJhbGciOi...', // short-lived token from your backendonReady: () => console.log('Card details rendered'),onError: err => console.error('SDK error', err.code, err.message),onStatusChange: status => console.log('Status:', status),}) -
Size the container via CSS. The
iframefills 100% of its parent's width and height.
Mount options
mount(options): CardDetailsHandle
| Property | Type | Required | Description |
|---|---|---|---|
container | HTMLElement | ✅ | DOM node that will own the iframe. Size it via CSS: the iframe is width: 100%; height: 100%. |
accessToken | string | ✅ | Short-lived token obtained from your backend. Sent to the iframe over postMessage after the handshake. |
handshakeTimeoutMs | number | ❌ | How long to wait for the iframe to complete the handshake before failing. Default: 10_000 (10s). |
iframeTitle | string | ❌ | <iframe title> for accessibility. Default: "Sensitive card details". |
onReady | () => void | ❌ | Called once when the iframe has fully rendered the card details. |
onError | (error: SdkError) => void | ❌ | Called whenever the SDK encounters a terminal or recoverable protocol error. See Error codes. |
onStatusChange | (status: LifecycleStatus) => void | ❌ | Called on every lifecycle transition. Useful for driving UI state. |
CardDetailsHandle
When the user navigates away or you want to tear down the view, call handle.unmount(). This removes the iframe, disposes the message channel, and clears all timers.
| Method | Signature | Description |
|---|---|---|
unmount | () => void | Removes the iframe, disposes the message channel, clears timers. Idempotent. If called before onReady, fires onError with UNMOUNTED. |
status | () => LifecycleStatus | Returns the current lifecycle status synchronously. |
Handle lifecycle and errors
The SDK transitions through these states:
| Status | Meaning |
|---|---|
mounting | mount() was called; iframe element is being created. |
awaiting-ready | iframe attached to the DOM; waiting for the IFRAME_READY handshake message. |
token-sent | Handshake received; access token has been posted to the iframe. |
ready | iframe reported RENDER_COMPLETE — card details are visible to the user. |
errored | A terminal error occurred. Inspect the SdkError from onError. |
unmounted | unmount() was called. |
Use onStatusChange to drive UI states (e.g., show a loading spinner until ready, display an error message on errored).
unmount() is callable from any state and is idempotent.
interface SdkError {
code: ErrorCode | string
message: string
}
Error codes
| Code | When it fires |
|---|---|
HANDSHAKE_TIMEOUT | iframe did not respond within handshakeTimeoutMs. Network or vault availability issue. |
IFRAME_LOAD_FAILED | <iframe> element fired its error event, or contentWindow was unavailable. |
INVALID_ORIGIN | A message arrived from an unexpected origin. Filtered before reaching the host. |
INVALID_MESSAGE | The vault sent a message that did not match the protocol schema. |
TOKEN_REJECTED | The vault rejected the supplied access token (expired, revoked, scope mismatch, etc.). |
INVALID_TOKEN | Token is malformed. |
RENDER_FAILED | Vault failed to render the card details (downstream service error). |
UNMOUNTED | unmount() was called before RENDER_COMPLETE. Treat as a cancellation, not a failure. |
React example
A drop-in component that mounts the iframe and surfaces lifecycle/error state:
import { useEffect, useRef, useState } from 'react'
import {
mount,
type CardDetailsHandle,
type LifecycleStatus,
type SdkError,
} from '@bvnk/card-details-sdk'
type Props = {
accessToken: string
onReady?: () => void
}
export function CardDetails({ accessToken, onReady }: Props) {
const containerRef = useRef<HTMLDivElement | null>(null)
const [status, setStatus] = useState<LifecycleStatus>('mounting')
const [error, setError] = useState<SdkError | null>(null)
useEffect(() => {
const container = containerRef.current
if (!container || !accessToken) return
let handle: CardDetailsHandle | null = null
try {
handle = mount({
container,
accessToken,
iframeTitle: 'Secure card details',
onStatusChange: setStatus,
onReady: () => {
setError(null)
onReady?.()
},
onError: setError,
})
} catch (err) {
setError({
code: 'MOUNT_FAILED',
message: err instanceof Error ? err.message : 'Failed to mount SDK',
})
}
return () => {
handle?.unmount()
}
}, [accessToken, onReady])
return (
<div>
<div ref={containerRef} style={{ width: '100%', height: 360, border: '1px solid #ddd' }} />
{error ? (
<p role="alert">Error <code>{error.code}</code>: {error.message}</p>
) : (
<p>Status: {status}</p>
)}
</div>
)
}
Tokens are short-lived and should be fetched on demand, for example when the user taps a "Reveal card" button. Pass the token as a prop once obtained from your backend.
Usage with TanStack Query
import { useMutation } from '@tanstack/react-query'
import { CardDetails } from './CardDetails'
export function RevealCard({ cardId }: { cardId: string }) {
const { mutate, data, isPending, error } = useMutation({
mutationFn: () => fetch(`/api/cards/${cardId}/reveal`).then(r => r.json()),
})
if (!data) {
return (
<button onClick={() => mutate()} disabled={isPending}>
{isPending ? 'Requesting…' : 'Reveal card'}
</button>
)
}
return <CardDetails accessToken={data.token} />
}
TypeScript
All public types are exported from the package root:
import type {
CardDetailsHandle,
IframeChannel,
IframeChannelHandlers,
IframeChannelOptions,
LifecycleStatus,
MountOptions,
SdkError,
} from '@bvnk/card-details-sdk'
- Your application must require strong authentication (step-up auth or 2FA) before requesting the access token.
- You never receive PAN or CVC in plaintext or encrypted form. All sensitive data stays within the vault
iframe. - You must confirm that you operate in a PCI-compliant manner.