Skip to main content

Display sensitive card details using SDK

Beta

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:

  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 rather than your app.

Requirements

Ensure the following before you begin:

  • 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).
  • 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/pay and https://checkout.acme.com/ are both rejected.
  • Subdomains https://acme.com and https://www.acme.com are 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 appId hidden.
  • Each application surface gets its own appId. Do not share one appId across, 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 same appId, 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.

POST /card/v1/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: document.getElementById('card-details-container'), // Required: target DOM element
    accessToken: 'eyJhbGciOi...', // short-lived token from your backend
    appId: '5b1e2c3a-8f9d-4e2b-b1a0-123456789abc', // assigned to you by BVNK
    onReady: () => 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 assigned appId as a required parameter in the mount() options (MountOptions).

    warning

    The SDK must be loaded from a web page served directly from one of your registered origins, for example https://checkout.acme.com. If you call mount() from an unapproved origin, the browser blocks the iframe for security reasons.

  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 owns the iframe. Set its size with CSS; the iframe is width: 100%; height: 100%.
accessTokenstringShort-lived token obtained from your backend. Sent to the iframe over postMessage after the handshake.
appIdstringThe 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'.
handshakeTimeoutMsnumberTime 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, 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.

MethodSignatureDescription
unmount() => voidRemoves the iframe, disposes the message channel, and 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
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_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.).
RENDER_FAILEDVault failed to render the card details (downstream service error).
UNMOUNTEDunmount() 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'
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?