Skip to main content

Card Viewer SDK (v2 alpha)

You are viewing pre-release documentation for Card Viewer SDK version 2 (alpha) — install with @alpha. Version 1 (1.x) is the current release.

Loading SDK demo...

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​

  1. A Highnote account
  2. A server-side integration using an API key
  3. Payment card ID

Installation​

The Highnote Card Viewer SDK follows semver and can be installed using the following:

  • JavaScript package manager: npm, yarn, or pnpm
  • Content Delivery Network (CDN), imported as an ES module
Version 2 is pre-release

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 shown below so the examples on this page resolve.

Running version 1? See the Card Viewer SDK v1 reference.

Package manager install​

The Card Viewer SDK supports the following JavaScript package managers: npm, yarn, and pnpm.

npm
npm i @highnoteplatform/card-viewer@alpha
yarn
yarn add @highnoteplatform/card-viewer@alpha
pnpm
pnpm add @highnoteplatform/card-viewer@alpha

CDN install​

You can load the Card Viewer SDK directly from a content delivery network (CDN) such as JSDelivr.

v2 ships as a module

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.

Module script for Card Viewer SDK
<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:

  1. Prepare your HTML with card detail elements.
  2. Fetch a client token.
  3. 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.

Prepare HTML
<!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 paymentCardId of the payment card you are rendering.
  • Provide permissions. It is required — for the Card Viewer SDK the value is ["READ_RESTRICTED_DETAILS"]. Omitting it fails schema validation before the request reaches Highnote.
  • 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"
    ]
  }
}
⚠️ Please login to execute queries. Visit the dashboard to authenticate.
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:

Create 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();

React​

The SDK ships a React binding at the /react subpath. It is optional — createCardViewer works inside a React app as well — but the component owns mount and teardown for you, so you do not manage the lifecycle by hand.

React binding
import { CardViewer } from "@highnoteplatform/card-viewer/react";
import { useState } from "react";

function PaymentCard({ getClientToken }) {
const [masked, setMasked] = useState(true);

return (
<CardViewer
getClientToken={getClientToken}
paymentCardId="pc_456"
maskedOnLoad
onReady={() => console.log("ready")}
onError={(err) => console.error(err)}
onClipboardError={({ field, message }) => console.warn(field, message)}
>
{({ cardNumberRef, cvvRef, expirationDateRef, setMask, isReady }) => (
<div>
<p id="cardNumberLabel">Card Number</p>
<div ref={cardNumberRef} aria-labelledby="cardNumberLabel" />

<p id="cvvLabel">CVV</p>
<div ref={cvvRef} aria-labelledby="cvvLabel" />

<p id="expirationDateLabel">Expiration Date</p>
<div ref={expirationDateRef} aria-labelledby="expirationDateLabel" />

<button
disabled={!isReady}
aria-pressed={!masked}
onClick={() => {
setMask("cardNumber", !masked);
setMasked(!masked);
}}
>
{masked ? "Reveal" : "Hide"}
</button>
</div>
)}
</CardViewer>
);
}

Attach only the refs you want rendered — that is how you render a subset of the fields.

Which props take effect after mount

Props fall into three groups, and the difference matters — reaching for a remount you do not need re-fetches a client token and re-renders PCI iframes.

  • paymentCardId and environment remount the iframes automatically when they change.
  • The callbacks — getClientToken, onReady, onError, onClipboardSuccess, onClipboardError and onMetric — are held in refs refreshed on every render, so they always fire the latest function you passed. A changing function identity neither remounts nor leaves you with a stale closure.
  • Everything else — enableClipboard, maskedOnLoad, styles, and which refs you attach — is read once at mount. Changing one later takes effect on the next remount, so conditionally revealing a field requires a remount rather than a re-render.

onMetric carries one caveat: it is only wired up if it is defined at mount. Going from undefined to a function later will not start delivering metrics, so pass one from the first render if you want them.

Wrap getClientToken in useCallback. The component holds it in a ref, so a changing function identity will not remount the iframes, but it keeps your dependency arrays honest.

<CardViewer> accepts the same options as createCardViewer — environment, enableClipboard, maskedOnLoad and onMetric — and adds five props of its own.

Four of them replace the events you would register with viewer.on(...) in the imperative API: onReady, onError, onClipboardSuccess (receives { field }) and onClipboardError (receives { field, message }). There is no on() to call on the component, so subscribe through these props instead.

The fifth is styles, which takes the same styling options keyed by field.

Telemetry​

Pass onMetric to receive the card viewer's own mount-phase measurements. Telemetry is opt-in: when you don't supply a handler, the SDK creates no performance marks or measures at all, so leaving it off costs nothing.

const viewer = createCardViewer({
// ...
onMetric: (metric) => {
switch (metric.type) {
case "timing":
recordDuration(metric.name, metric.durationMs, metric.tags);
break;
case "count":
incrementCounter(metric.name, metric.count, metric.tags);
break;
default:
// New metric kinds are additive — ignore what you don't recognize
// rather than assuming a value field that may not be there.
break;
}
},
});

The Metric payload​

Metric is a discriminated union on type. The value field is named per variant rather than shared, so a handler written against durationMs cannot silently receive a count in it.

FieldTypeNotes
type"timing" | "count"The discriminator. Branch on it.
namestringDot-namespaced and stable.
durationMsnumbertype: "timing" only. Milliseconds.
countnumbertype: "count" only. The increment, defaulting to 1.
tagsRecord<string, string>Low-cardinality dimensions. Always carries sdk, sdkVersion, iframeHash.
timestampnumberEpoch milliseconds at emit.

Metric, MetricType, TimingMetric, and CountMetric are all exported for TypeScript consumers.

What gets emitted​

MetricKindMeaning
total.mounttimingEnd-to-end mount() duration.
iframe.load:<field>timingTime for that field's iframe to load.
handshake:<field>timingpostMessage handshake with that field.
token.fetch:<field>timingClient-token fetch attributed to that field.
configure:<field>timingTime to apply configuration to that field.
repaint.nudge:<reason>countA repaint prompt was issued for a field.
repaint.nudge.inert:<reason>:<cause>countA repaint prompt was issued but could not be verified as landing.

The field name is currently baked into the metric name after a colon. If you are building dashboards, split on the first colon and record the field as a separate dimension — that keeps one series per phase instead of one per phase × field.

Do not split the counter names: their <cause> suffix is meaningful on its own.

<cause>What it means
no-layout-boxRoutine and self-healing. Any field mounted inside a closed accordion or an unopened modal emits it.
unmeasurable / stale-baselineRare and transient.
unverifiedPermanent for that host — the repaint prompt never lands, so the field keeps the blank-until-repainted symptom. This is the one worth alerting on.
Allowlist your tag keys

New keys may be added to tags in a minor release. If you forward metrics to a third-party monitoring vendor, copy across the specific keys you want rather than spreading tags wholesale — otherwise a future SDK version can send a dimension to that vendor without anyone having decided it should.

Error handling​

info

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 calling mount(). 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 reject mount().

    when a field-level error does end in a rejection

    All 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 with MOUNT_TIMEOUT — so a TOKEN_FETCH_FAILED on the error event is typically followed by a mount() rejection. Always wrap mount() in try/catch as 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). Wrap mount() in a try/catch to 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:

CodeSurfaceDescription
CONTAINER_NOT_FOUNDviewer.on("error")The element referenced by a field's selector could not be found in the DOM.
CHANNEL_ERRORviewer.on("error")The postMessage channel between the host page and the iframe encountered an error.
TOKEN_FETCH_FAILEDviewer.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_ERRORviewer.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_FAILEDviewer.on("error")The iframe failed to complete its initial handshake with the host page.
RELAY_FAILEDviewer.on("error")A field never received its share of the card data relayed from the source field's fetch.
NO_FIELDS_RENDEREDmount() rejectionThrown as a NoFieldsRenderedError when none of the requested fields could be mounted.
MOUNT_TIMEOUTmount() rejectionThrown as a MountTimeoutError when the SDK's internal mount timeout is exceeded.
ALREADY_MOUNTEDmount() rejectionThrown as an AlreadyMountedError when mount() is called more than once on the same instance.
CONFIGURE_FAILEDviewer.on("error")A field's CONFIGURE message could not be delivered — for example a reloaded frame or a transferred-port edge case.
CONFIGURE_ERRORviewer.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_MOUNTEDviewer.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_FAILEDmount() rejectionThe 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:

Card Viewer Errors
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:

PropertyExamplesDocs
color#55f5a3, rgba(85,245,163,1), #springgreenMDN Docs
cursorpointer, noneMDN Docs
fontFamilysans-serif, serif, monospace System fonts onlyMDN Docs
fontSize12px, 1em, 1.1remMDN Docs
fontWeightbold, normalMDN Docs
letterSpacingnormal, .2remMDN Docs
lineHeightnormal, 150%MDN Docs
userSelectnone, auto, inheritMDN Docs

iframe defaults​

Highnote injects card viewer iframes with the following defaults. Your CSS styling can override each of these:

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:

Card 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:

Live Environment
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:

Destroying 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:

Card masking config
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 true — pass enableClipboard: false to disable it. Earlier 2.0.0-alpha.* builds defaulted it to false; 2.0.0-alpha.14 restored the version 1 default, so each field iframe carries allow="clipboard-write" unless you opt out.

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:

eventdescription
clipboard:successFires when an account holder successfully copies a value to their clipboard. The handler receives { field }.
clipboard:errorFires when the copy fails. The handler receives { field, message }.

The following code sample is an example of a copy to clipboard configuration:

Copy To Clipboard Config
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 true — set to false to disable 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
Content-Security-Policy: frame-src https://cdn.highnote.com