Display sensitive card details using SDK
Cards is in beta and may change as we continue to improve it. Please review the documentation and endpoints carefully, test everything in the sandbox, and reach out to the Solutions team if you need help.
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 rather than your app.
Requirements
Ensure the following before you begin:
- 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). - BVNK has allowlisted your display origins and assigned you an
appId. See Origins and appId.
Origins and appId
Before the SDK can render card details, BVNK needs to know where you'll display them. Reach out to your BVNK Solutions contact with the list of origins your application mounts the SDK from. Allowed origins must include an exact scheme, host, and optional port, permitted to display sensitive card details. The following rules apply:
- Include the protocol and host, for example
https://checkout.acme.com. - Do not include a URL path or a trailing slash.
https://checkout.acme.com/payandhttps://checkout.acme.com/are both rejected. - Subdomains
https://acme.comandhttps://www.acme.comare treated separately, so register every origin you use.
If an origin you submit contains a typo, BVNK's security layer drops the invalid origin and card details fail to load on that page. Verify the spelling of every origin before you send it. Origin updates take effect as soon as the configuration deployment completes.
BVNK adds each origin to an allowed list and assigns you an appId—a public 36-character identifier tied to that specific application surface. You pass this appId to mount(). You cannot choose your own appId string. BVNK generates a random v4 UUID for each application to prevent naming collisions.
A few things to know about the appId:
- It is not a secret. It appears in browser network requests, and security is enforced server-side by validating the request origin, not by keeping the
appIdhidden. - Each application surface gets its own
appId. Do not share oneappIdacross, for example, a web checkout and a mobile webview. Request a separate one for each. - To add a new origin later, contact BVNK referencing your existing
appId. You keep the sameappId, because a new one would break your existing integration.
Request an access token
From your backend, send the POST /card/v1/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: document.getElementById('card-details-container'), // Required: target DOM elementaccessToken: 'eyJhbGciOi...', // short-lived token from your backendappId: '5b1e2c3a-8f9d-4e2b-b1a0-123456789abc', // assigned to you by BVNKonReady: () => console.log('Card details rendered'),onError: err => console.error('SDK error', err.code, err.message),onStatusChange: status => console.log('Status:', status),})When mounting the card details form using
@bvnk/card-details-sdk, supply the assignedappIdas a required parameter in themount()options (MountOptions).warningThe SDK must be loaded from a web page served directly from one of your registered origins, for example
https://checkout.acme.com. If you callmount()from an unapproved origin, the browser blocks theiframefor security reasons. -
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 owns the iframe. Set its size with 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. |
appId | string | ✅ | The identifier BVNK assigned to your application. Must match a registered origin. See Origins and appId. |
env | 'production' | 'sandbox' | ❌ | Target environment for the vault URL the iframe loads. Default: 'production'. |
handshakeTimeoutMs | number | ❌ | Time 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, including mounting, token-sent, ready, and errored. 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, and 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
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_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.). |
RENDER_FAILED | Vault failed to render the card details (downstream service error). |
UNMOUNTED | unmount() was called before RENDER_COMPLETE. Treat as cancellation, not 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
appId: string
onReady?: () => void
}
export function CardDetails({ accessToken, appId, 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,
appId,
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, appId, 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 after obtaining it from your backend.
Usage with TanStack Query
import { useMutation } from '@tanstack/react-query'
import { CardDetails } from './CardDetails'
export function RevealCard({ cardId, appId }: { cardId: string; appId: 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} appId={appId} />
}
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.