Skip to main content

Client-Side DyScan Web SDK

Overview​

Unlike DyScan Web, which securely streams video to Dyneti's servers for processing, the Client-Side DyScan Web SDK runs card recognition entirely on the end user's device.

info

Client-Side DyScan Web returns the full card PAN to your application, which brings your application into PCI scope. If you need to stay out of PCI scope while still populating your order form, talk to us about hosted alternatives.

Prerequisites​

  1. Request an API key from Dyneti. Specify that your key should be enabled for this SDK.
  2. Configure npm to authenticate against Dyneti's package registry. Add this to your project's .npmrc, using the username and password Dyneti gave you:
    @dyneti:registry=https://nexus.dyneti.com/repository/dyscan-web/
    //nexus.dyneti.com/repository/dyscan-web/:_auth=BASE64_OF_USERNAME:PASSWORD
    The _auth value is base64(username:password), e.g. echo -n "yourusername:yourpassword" | base64. Don't commit this file if it contains real credentials; add it to .gitignore, or reference an environment variable there instead if your build system supports expanding .npmrc (most CI providers do).
  3. Install the package: npm install @dyneti/dyscan-browser.
  4. Import the package as an ES module (import { DyScanBrowser } from "@dyneti/dyscan-browser", as shown below). A classic, non-module <script src="...dyscan-browser.umd.cjs"> tag is also supported, exposing the same exports under a window.DyScanBrowser namespace object (so the scanner class is window.DyScanBrowser.DyScanBrowser) for integrations that can't use type="module".
  5. Serve your page over HTTPS (or localhost). Without a secure context the camera API is unavailable, and scanning fails with "Camera API not available in this browser/context".

The TFLite models and WASM runtime are bundled in the package (dist/assets/) and loaded relative to the SDK's own script. If you use your own bundler, deploy that assets/ directory next to the SDK file.

Data handling​

Call DyScanBrowser.configure() once, anywhere in your app, before creating a scanner. Creating a DyScanBrowser or DyScanController before configure() throws.

import { DyScanBrowser } from "@dyneti/dyscan-browser";

DyScanBrowser.configure({
apiKey: "YOUR_API_KEY",
eu: false,
});
  • apiKey: your Dyneti API key.
  • eu: controls what Dyneti's backend logs about each scan. When false, a redacted scan image and scan metadata (including the card's BIN and last four digits) are sent to Dyneti. When true, only a limited amount of metadata is sent. The full card number recognized by the on-device model is never sent to Dyneti in either mode; it only ever reaches your own onResult callback. eu is a required boolean, and configure() throws if it is missing or not a boolean.

Quickstart guide​

Client-Side DyScan Web ships two ways to integrate, depending on how much control you want over the UI:

Default UIPrimitive UI
ComponentDyScanControllerDyScanBrowser
What it gives youA ready-made modal: camera feed, scan frame, help button, result handlingA camera feed and scan frame rendered into a container element you control; you build the surrounding UI
Best forFastest integration, consistent Dyneti UXFully custom checkout flows, matching your own design system

Default UI​

import { DyScanBrowser, DyScanController } from "@dyneti/dyscan-browser";

DyScanBrowser.configure({ apiKey: "YOUR_API_KEY", eu: false });

const controller = new DyScanController({
onResult: (result) => {
console.log(result.cardNumber, result.date);
},
onError: (error) => {
console.error("Scan failed:", error.message);
},
onCancel: () => {
console.log("User closed the scanner");
},
});

scanButton.addEventListener("click", () => {
controller.present();
});

controller.present() inserts a full-screen modal over your page, starts the camera, and shows the scan frame. The modal closes itself automatically on success, error, or cancel (which each also fire your corresponding callback). You can also close it programmatically:

controller.cancel(); // user-initiated style cancel — fires onCancel
controller.close(); // tears the modal down without firing any callback

Default UI options​

All options are optional unless noted.

ParameterTypeDefaultDescription
scanInstructionstring"Position your card within the frame"The instruction text shown above the scan frame.
showPlaceholderTextbooleantrueShows a faint sample card number and expiry date inside the scan frame before a card is detected, to hint that the credit card side with numbers should be aligned.
placeholderCardNumberstring"4242 4242 4242 4242"Placeholder card number displayed inside the scan frame.
placeholderDatestring"11/11"Placeholder expiry date displayed inside the scan frame.
showResultOverlaybooleanfalseBriefly overlays the recognized card number and date on the live camera feed for about a second after a successful scan, before closing.
enableSidewaysScanningbooleanfalseAllows the model to recognize a card held sideways (rotated 90°) relative to the frame.
onProgress(progress: { network: CardType; lastFourDigits: string }) => void—Called once the card number has been recognized, with the card network and last four digits. Fires before onResult.
onResult(result: { cardNumber: string; date?: string }) => void—Called once on a successful scan. date, if present, is formatted MM/YY.
onError(error: Error) => void—Called if scanning fails, most commonly due to an invalid API key, denied or unavailable camera access, or the camera track ending unexpectedly.
onCancel() => void—Called when the user closes the scanner before completing it.
localizationPartial<DyScanLocalizationStrings>—Overrides any subset of the UI copy. See Localization below.

Primitive UI​

Use this option when you want to build your own UI around the camera feed.

warning

We strongly recommend you use the default UI as it has been extensively studied and optimized for conversion and fraud prevention.

<div id="scanner-container" style="position: relative; width: 400px; height: 300px;"></div>

import { DyScanBrowser } from "@dyneti/dyscan-browser";

DyScanBrowser.configure({ apiKey: "YOUR_API_KEY", eu: false });

const scanner = new DyScanBrowser({
container: document.getElementById("scanner-container"),
onResult: (result) => {
console.log(result.cardNumber, result.date);
},
onError: (error) => {
console.error("Scan failed:", error.message);
},
});

await scanner.start();

// Later:
scanner.stop(); // tears the camera down, no callback fired
scanner.cancel(); // tears the camera down and fires onCancel

If scanning can't start (for example, camera permission is denied), start() doesn't throw. The error is passed to onError.

info

The container element must have an explicit, non-zero size and a CSS position other than static (e.g. position: relative) — the camera feed and scan frame are rendered as absolutely-positioned children filling it.

Primitive UI options (DyScanOptions)​

In addition to the options listed above for the Default UI (placeholderCardNumber, placeholderDate, showResultOverlay, enableSidewaysScanning, onProgress, onResult, onError, onCancel, localization), the primitive UI exposes:

info

showPlaceholderText defaults to true in the Default UI, but defaults to false in the Primitive UI.

ParameterTypeDefaultDescription
containerHTMLElementRequiredThe element the camera feed and scan frame are rendered into.
objectFit"cover" / "contain""cover"Controls how the camera feed fills the container. "cover" fills the container, cropping overflow; "contain" fits the entire feed inside, adding letterboxing if needed.
showCloseButtonbooleanfalseShows a small built-in "✕" close button in the top-right corner of the browser window. Its accessible label comes from localization.actionClose.
showLogobooleantrueShows the Dyneti logo beneath the scan frame.
skinPartial<DyScanSkin>—Customizes the colors of the scan overlay. See Skin reference below.

Positioning custom UI​

Since you control the surrounding layout in the primitive UI, you may want your own instructional text or buttons to track the scan frame's actual on-screen position (which depends on the container's aspect ratio and objectFit). Call getReticleRect() at any point after start():

const rect = scanner.getReticleRect();
// { x, y, width, height } in pixels, relative to the container's own top-left corner

This returns null when the scanner isn't running. It's safe to call every animation frame. This is exactly how the Default UI positions its own instruction text above the frame.

Skin reference​

Pass a partial DyScanSkin object to skin to override any subset of these defaults:

const scanner = new DyScanBrowser({
container: myContainer,
skin: {
overlayColor: "#1a1a1a",
overlayOpacity: 0.6,
},
});
FieldTypeDefaultDescription
overlayColorstring"#000000"Color of the mask outside the scan frame. Use a hex color (#rgb or #rrggbb).
overlayOpacitynumber0.7Opacity of that mask, 0-1.
cornerInactiveColorstring"#808080"Color of the frame's corner brackets before any part of the card is detected.
cornerActiveColorstring"#e7e7e7"Color of the corner brackets once the card is detected.
cornerCompletedColorstring"#00ff00"Color of the corner brackets once a scan completes successfully.
cornerThicknessnumber4Line thickness of the corner brackets, in pixels.
cardTextColorstring"#ffffff"Color used for the placeholder text and (if enabled) the post-scan result overlay text.
cardTextPlaceholderOpacitynumber0.3Opacity of the placeholder card number/date text.
preloadBackgroundColorstring | nullnullFill color for the inside of the scan frame before the camera has attached. null leaves it transparent, showing whatever's behind the container. Set this if your page background doesn't contrast well with the (light-colored) placeholder text.

Localization reference​

Pass a partial DyScanLocalizationStrings object to localization to override any subset of this copy:

const localization = {
scanInstruction: "Hold your card up to the camera",
actionClose: "Cancel",
};
FieldDefault
scanInstruction"Position your card within the frame"
actionClose"Close"
needHelpButton"Need help?"
helpModalHeading"Need help?"
helpModalBody"Here are the common issues that people run into"
helpModalShowCard"Make sure that you don't accidentally block any card information with your fingers."
helpModalClearImage"Avoid glare from bright lighting that may obscure your card information."
helpModalAlignCard"Hold your card so that the edges are aligned with the scanning frame."
helpModalNumbersFirst"Make sure to scan the side with the card number first. Then, flip the card to scan the other side."
info

scanInstruction and the needHelpButton/helpModal* strings are only used by the Default UI; primitive-UI integrations build their own instructional copy and help flow. actionClose is also used by the Default UI's help dialog, and as the accessible label for the Primitive UI's close button when showCloseButton is enabled.

Handling the scan result​

On success, onResult receives:

{
cardNumber: string; // digits grouped with spaces, e.g. "4242 4242 4242 4242"
date?: string; // "MM/YY", e.g. "11/29", if an expiration date was found
}

While scanning, onProgress receives the card network and last four digits once the card number has been recognized:

{
network: CardType;
lastFourDigits: string;
}

CardType is exported from the package. Expect values such as visa, mastercard, amex, discover, etc.