Card Viewer SDK
Overview
The Highnote Card Viewer SDK lets you embed sensitive card data into your UI using iframes. This prevents PCI-scoped data from flowing through your servers or from being accessible to scripts running on your page.
Prerequisites
- A Highnote account
- A server-side integration using an API key
- Payment card ID
Installation
The Highnote Card Viewer SDK follows semver and can be installed using the following:
- JavaScript package manager:
npm,yarn, orpnpm - Content Delivery Network (CDN) via traditional
<script>tag.
This guide documents the version 2 API (createCardViewer). Version 2 is
currently published under the alpha dist-tag — the latest tag still
points at version 1, whose API differs. Install with the @alpha tag (or pin
the exact 2.0.0-alpha.x version) shown below so the examples on this page
resolve. Contact your Highnote representative if you need the version 1
integration guide.
Package manager install
The Card Viewer SDK supports the following JavaScript package managers: npm, yarn, and pnpm.
npm i @highnoteplatform/card-viewer@alpha
yarn add @highnoteplatform/card-viewer@alpha
pnpm add @highnoteplatform/card-viewer@alpha
CDN install
To ensure stability and avoid potential issues caused by updates, replace @alpha with the specific version currently published on the alpha tag — check it with npm view @highnoteplatform/card-viewer dist-tags.
You can load the Card Viewer SDK directly from a content delivery network (CDN) such as JSDelivr.
Version 2 is published as ES modules and CommonJS only — there is no browser-global (UMD) build, and the v1 lib/index.js path no longer exists. Load it with <script type="module"> and an import; a classic <script src="…"> tag will fail.
<script type="module">
import { createCardViewer } from "https://cdn.jsdelivr.net/npm/@highnoteplatform/card-viewer@alpha/+esm";
const viewer = createCardViewer({
/* options — see Initialize card viewer below */
});
await viewer.mount();
</script>
Usage
To render card details in your UI, you must set up elements to hold each field as follows:
- Prepare your HTML with card detail elements.
- Fetch a client token.
- Initialize card viewer.
Prepare your HTML
To use the Card Viewer SDK, provide an element for each field you want to render in an iframe. The following example uses Card number, CVV, Expiration date.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Payment Card</title>
</head>
<body>
<p>Card Number</p>
<div id="cardNumber">
<!-- An iframe will be injected here -->
</div>
<p>CVV</p>
<div id="cvv">
<!-- An iframe will be injected here -->
</div>
<p>Expiration Date</p>
<div id="expirationDate">
<!-- An iframe will be injected here -->
</div>
</body>
</html>
Fetch a client token
To obtain a client token from your server for the Card Viewer SDK, use the generatePaymentCardClientToken mutation.
Refer to the following guidelines when generating a payment card client token:
- Provide the
paymentCardIdof the payment card you are rendering. - The client token is only valid for 15 minutes, but can be used multiple times.
- Once a token expires, you must generate a new one to render payment card details.
Use the following mutation to generate a payment card client token:
GeneratePaymentCardClientToken
Query
mutation GeneratePaymentCardClientToken(
$input: GeneratePaymentCardClientTokenInput!
) {
generatePaymentCardClientToken(input: $input) {
... on ClientToken {
value
expirationDate
}
}
}
Variables
{ "input": { "paymentCardId": "<PAYMENT_CARD_ID>", "permissions": [ "READ_RESTRICTED_DETAILS" ] } }
Result
{
"data": {
"generatePaymentCardClientToken": {
"value": "TOKEN",
"expirationDate": "2022-02-07T20:04:50.633Z"
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}
Initialize card viewer
The card viewer library works by injecting an iframe into each provided element to render the appropriate data. You can style the content inside each iframe by passing any combination of styling options.
You can initialize the card viewer by using the createCardViewer function. This returns a CardViewer instance synchronously — register your event listeners on it, then call mount() to fetch the client token and inject the iframes.
mount() returns a Promise that resolves once every requested field has rendered. The CardViewer instance is useful for lifecycle management and interactions such as card number masking.
The following example shows how to initialize the card viewer:
import { createCardViewer } from "@highnoteplatform/card-viewer";
const viewer = createCardViewer({
// Called by the SDK at mount time — return a valid client token
getClientToken: async () => "client token from server",
// This is the same paymentCardId used to generate the token
paymentCardId: "<PAYMENT_CARD_ID>",
// Specify the individual fields to render data into
elements: {
cardNumber: {
selector: "#cardNumber",
},
cvv: {
selector: "#cvv",
},
expirationDate: {
selector: "#expirationDate",
},
},
});
// Register listeners before calling mount()
viewer.on("ready", () => {
// Called after data has been fetched and the sdk is ready to display information
// For eg, can be used to show a custom loading indicator
document.querySelector(".loading").classList.add("hidden");
document.querySelector(".cardFields").classList.remove("hidden");
});
viewer.on("error", (error) => {
// Handle field-level errors (these do not reject mount())
});
await viewer.mount();
Error handling
The card viewer doesn't render error messages or update your UI inside iframes when errors occur. You must introspect and handle errors accordingly.
Errors surface in two places, and you must handle both:
-
Field-level errors — register a
viewer.on("error", (err) => { ... })listener before callingmount(). This fires for errors scoped to an individual field (a missing container element, a broken postMessage channel, a token fetch failure, a card-data fetch failure, a failed handshake, or a relay failure). A field-level error does not itself rejectmount().when a field-level error does end in a rejectionAll requested fields share a single
getClientToken()call, so a failing token callback fails every field. When no field can signal ready,mount()waits out its internal timeout and then rejects withMOUNT_TIMEOUT— so aTOKEN_FETCH_FAILEDon theerrorevent is typically followed by amount()rejection. Always wrapmount()intry/catchas well as registering the listener. -
Whole-mount failures —
await viewer.mount()rejects when the mount as a whole cannot complete (for example, none of the requested fields could be rendered, or the SDK's internal mount timeout is exceeded). Wrapmount()in atry/catchto handle these.
Every error the SDK raises — from either surface — is a CardViewerError. CardViewerError extends the built-in Error, so err instanceof Error holds, and it carries a stable err.code you can branch on along with the standard err.message.
Error codes
err.code is typed as string, not a closed union — the iframe can surface codes over the protocol, so always include a default branch when switching on it. The codes below are the ones the SDK raises today:
| Code | Surface | Description |
|---|---|---|
| CONTAINER_NOT_FOUND | viewer.on("error") | The element referenced by a field's selector could not be found in the DOM. |
| CHANNEL_ERROR | viewer.on("error") | The postMessage channel between the host page and the iframe encountered an error. |
| TOKEN_FETCH_FAILED | viewer.on("error") | Client-side, before any request: your getClientToken callback rejected, or returned a token that was malformed or already expired. The SDK never obtained a usable token to send. |
| FETCH_ERROR | viewer.on("error") | Server-side, during the card-data request: the iframe sent the request but the Highnote API rejected the token/request or returned no card data. The most common data-retrieval failure. |
| HANDSHAKE_FAILED | viewer.on("error") | The iframe failed to complete its initial handshake with the host page. |
| RELAY_FAILED | viewer.on("error") | A field never received its share of the card data relayed from the source field's fetch. |
| NO_FIELDS_RENDERED | mount() rejection | Thrown as a NoFieldsRenderedError when none of the requested fields could be mounted. |
| MOUNT_TIMEOUT | mount() rejection | Thrown as a MountTimeoutError when the SDK's internal mount timeout is exceeded. |
| ALREADY_MOUNTED | mount() rejection | Thrown as an AlreadyMountedError when mount() is called more than once on the same instance. |
| CONFIGURE_FAILED | viewer.on("error") | A field's CONFIGURE message could not be delivered — for example a reloaded frame or a transferred-port edge case. |
| CONFIGURE_ERROR | viewer.on("error") | Defensive-only: the iframe rejected a malformed or forged CONFIGURE message. Not reachable from the documented API — handle it via your default branch. |
| FIELD_NOT_MOUNTED | viewer.on("error") | An operation targeted a field that was not mounted — reachable from the documented API by calling setMask("cvv", …) when cvv was not included in elements. |
| MOUNT_FAILED | mount() rejection | The mount failed with an unexpected non-CardViewerError throw (also surfaced by the React binding). |
Example errors
The following code sample provides examples of handling card viewer errors from both surfaces:
import {
type CardViewerError,
createCardViewer,
} from "@highnoteplatform/card-viewer";
const handleError = (err: CardViewerError) => {
switch (err.code) {
case "TOKEN_FETCH_FAILED":
// Fetch a new client token and re-initialize
console.error(err.message);
break;
case "FETCH_ERROR":
case "CONTAINER_NOT_FOUND":
case "HANDSHAKE_FAILED":
case "CHANNEL_ERROR":
case "RELAY_FAILED":
console.error(err.message);
break;
default:
console.error(err);
}
};
const viewer = createCardViewer({
getClientToken: async () => "client token from server",
// This is the same paymentCardId used to generate the token
paymentCardId: "<PAYMENT_CARD_ID>",
// Specify the individual fields to render data into
elements: {
cardNumber: {
selector: "#cardNumber",
},
cvv: {
selector: "#cvv",
},
expirationDate: {
selector: "#expirationDate",
},
},
});
// Field-level errors — do not reject mount()
viewer.on("error", handleError);
viewer.on("ready", () => {
// Called after data has been fetched and sdk is ready to display information
});
try {
// Whole-mount failures reject here
await viewer.mount();
} catch (err) {
handleError(err as CardViewerError);
}
Styling options
The following styling options are available for use in elements for your card viewer:
| Property | Examples | Docs |
|---|---|---|
color | #55f5a3, rgba(85,245,163,1), #springgreen | MDN Docs |
cursor | pointer, none | MDN Docs |
fontFamily | sans-serif, serif, monospace System fonts only | MDN Docs |
fontSize | 12px, 1em, 1.1rem | MDN Docs |
fontWeight | bold, normal | MDN Docs |
letterSpacing | normal, .2rem | MDN Docs |
lineHeight | normal, 150% | MDN Docs |
userSelect | none, auto, inherit | MDN Docs |
iframe defaults
Highnote injects card viewer iframes with the following defaults. Your CSS styling can override each of these:
border: nonewidth: 300px(browser default)height: 150px(browser default)
The document and body inside the frame will have transparent backgrounds and default to margin: 0, padding: 0.
Custom fonts
The Card Viewer SDK only supports system fonts.
Layout
The layout of payment card fields in the card viewer is customizable. Highnote injects iframes into the provided container elements, which will inherit the width of the container.
You can customize the width and height of the container to accommodate your UI as needed.
The following code sample provides an example of custom layout styling:
#cardNumber {
margin: 1em;
}
/* You can target the iframe with a child combinator. */
#cardNumber > iframe {
height: 140px;
}
Using the Live environment
By default, the card viewer library will make requests against the Test environment. When you are ready to switch to the Live environment, set the environment configuration option using the following function call:
const viewer = createCardViewer({
getClientToken: async () => "client token from server",
paymentCardId: "<PAYMENT_CARD_ID>",
// Set this to `live`
environment: "live",
elements,
});
viewer.on("error", handleError);
await viewer.mount();
Lifecycle
The Card Viewer SDK has a lifecycle that consists of a loading state, which resolves with payment card details. You can also destroy payment card fields as needed.
Loading state
When rendering card viewer fields, the library writes the required iframes into your UI and renders them in a loading state. While in the loading state, the fields will be populated with placeholder characters:
- Card number:
•••• •••• •••• •••• - CVV:
••• - Expiration Date:
••/••
You can't change these values. Once the configuration has been validated and the fields rendered, the library will request the card details from the Highnote API and replace the loading values with the corresponding data.
Destroy fields
If you need to remove payment card fields, call the destroy() method on the CardViewer instance.
This is useful when you need to "restart" the integration, or navigate to a new view client-side.
Using this will ensure the cleanup of any DOM and event handlers.
destroy() is synchronous and safe to call at any point in the lifecycle — before, during, or after mount().
The following code sample provides an example function to destroy fields:
import { createCardViewer } from "@highnoteplatform/card-viewer";
const viewer = createCardViewer({
// ...config
});
await viewer.mount();
// Later...destroy
viewer.destroy();
Interactions
The Card Viewer SDK supports the following end-user interactions:
- Account holders can toggle sensitive data via card number masking
- Account holders can copy their payment card details to their clipboard to complete payments
Card number masking
By default, the payment card number will be masked (•••• •••• •••• ••••).
Only the last four digits will be shown when the card number is available.
You can mask or reveal a field's value using the setMask method on the CardViewer instance. setMask takes the field name and an explicit boolean — true to mask, false to reveal — so your integration owns the masked/revealed state instead of blindly toggling it. Calling setMask with the current state is a no-op (idempotent). While this example focuses on cardNumber, setMask works for any rendered field.
The following code sample is an example of a card number masking configuration:
import { createCardViewer } from "@highnoteplatform/card-viewer";
const viewer = createCardViewer({
// ...config
});
await viewer.mount();
// Track masked state in your own UI, then call setMask explicitly
let masked = true;
const toggleCardNumberVisibility = () => {
masked = !masked;
viewer.setMask("cardNumber", masked);
};
Copy to clipboard
Users can select and copy payment card detail values to their clipboard.
You can use the enableClipboard configuration property to toggle the "copy to clipboard" feature on or off. enableClipboard defaults to false — pass enableClipboard: true to enable it.
When enabled, clicking the card number, expiration date, or CVV field will copy the underlying value to the account holder's clipboard. This results in a copy event.
In addition to the copy event, two CardViewer events allow customization when a user copies or fails to copy a value:
| event | description |
|---|---|
clipboard:success | Fires when an account holder successfully copies a value to their clipboard. The handler receives { field }. |
clipboard:error | Fires when the copy fails. The handler receives { field, message }. |
The following code sample is an example of a copy to clipboard configuration:
import { createCardViewer } from "@highnoteplatform/card-viewer";
const viewer = createCardViewer({
getClientToken: async () => "client token from server",
// This is the same paymentCardId used to generate the token
paymentCardId: "<PAYMENT_CARD_ID>",
// Defaults to false — set to true to enable copy to clipboard
enableClipboard: true,
// Specify the individual fields to render data into
elements: {
cardNumber: {
selector: "#cardNumber",
},
cvv: {
selector: "#cvv",
},
expirationDate: {
selector: "#expirationDate",
},
},
});
viewer.on("clipboard:success", ({ field }) => {
console.log(`${field} value copied!`); // cardNumber value copied!
});
viewer.on("clipboard:error", ({ field, message }) => {
console.error(`Failed to copy ${field}: ${message}`);
});
viewer.on("error", (error) => {
// Handle errors
});
viewer.on("ready", () => {
// Called after data has been fetched and sdk is ready to display information
});
await viewer.mount();
Content security policy
If your application enforces a content security policy, you must set the frame-src header to allow iframes from the Highnote domain:
Content-Security-Policy: frame-src https://cdn.highnote.com