Skip to main content

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:

  1. Your backend requests a short-lived access token from BVNK.
  2. Your frontend mounts the SDK, which creates a sandboxed iframe pointing to BVNK's card vault.
  3. After a postMessage handshake, the SDK passes the token to the iframe.
  4. 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 ACTIVE status.
  • 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.

POST /card/v1/sensitive-card-details-token
{
"cardId": "123e4567-e89b-12d3-a456-426614174000",
"customerId": "4f2a76a4-0954-4999-b555-f9f2bec78c50"
}

BVNK validates card ownership and returns a one-time, short-lived token:

Response
{
"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

  1. Import the mount function and call it with a container element and the access token from the Request an access token step. The SDK creates a sandboxed iframe inside 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 backend
    onReady: () => console.log('Card details rendered'),
    onError: err => console.error('SDK error', err.code, err.message),
    onStatusChange: status => console.log('Status:', status),
    })
  2. Size the container via CSS. The iframe fills 100% of its parent's width and height.

Mount options

mount(options): CardDetailsHandle

PropertyTypeRequiredDescription
containerHTMLElementDOM node that will own the iframe. Size it via CSS: the iframe is width: 100%; height: 100%.
accessTokenstringShort-lived token obtained from your backend. Sent to the iframe over postMessage after the handshake.
handshakeTimeoutMsnumberHow long to wait for the iframe to complete the handshake before failing. Default: 10_000 (10s).
iframeTitlestring<iframe title> for accessibility. Default: "Sensitive card details".
onReady() => voidCalled once when the iframe has fully rendered the card details.
onError(error: SdkError) => voidCalled whenever the SDK encounters a terminal or recoverable protocol error. See Error codes.
onStatusChange(status: LifecycleStatus) => voidCalled 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.

MethodSignatureDescription
unmount() => voidRemoves the iframe, disposes the message channel, clears timers. Idempotent. If called before onReady, fires onError with UNMOUNTED.
status() => LifecycleStatusReturns the current lifecycle status synchronously.

Handle lifecycle and errors

The SDK transitions through these states:

StatusMeaning
mountingmount() was called; iframe element is being created.
awaiting-readyiframe attached to the DOM; waiting for the IFRAME_READY handshake message.
token-sentHandshake received; access token has been posted to the iframe.
readyiframe reported RENDER_COMPLETE — card details are visible to the user.
erroredA terminal error occurred. Inspect the SdkError from onError.
unmountedunmount() 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.

SdkError
interface SdkError {
code: ErrorCode | string
message: string
}

Error codes

CodeWhen it fires
HANDSHAKE_TIMEOUTiframe did not respond within handshakeTimeoutMs. Network or vault availability issue.
IFRAME_LOAD_FAILED<iframe> element fired its error event, or contentWindow was unavailable.
INVALID_ORIGINA message arrived from an unexpected origin. Filtered before reaching the host.
INVALID_MESSAGEThe vault sent a message that did not match the protocol schema.
TOKEN_REJECTEDThe vault rejected the supplied access token (expired, revoked, scope mismatch, etc.).
INVALID_TOKENToken is malformed.
RENDER_FAILEDVault failed to render the card details (downstream service error).
UNMOUNTEDunmount() 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'
Security requirements
  • 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.
Was this page helpful?