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.
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
- Request an API key from Dyneti. Specify that your key should be enabled for this SDK.
- Configure npm to authenticate against Dyneti's package registry. Add this to your project's
.npmrc, using the username and password Dyneti gave you:The@dyneti:registry=https://nexus.dyneti.com/repository/dyscan-web/
//nexus.dyneti.com/repository/dyscan-web/:_auth=BASE64_OF_USERNAME:PASSWORD_authvalue isbase64(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). - Install the package:
npm install @dyneti/dyscan-browser. - 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 awindow.DyScanBrowsernamespace object (so the scanner class iswindow.DyScanBrowser.DyScanBrowser) for integrations that can't usetype="module". - 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 ownonResultcallback.euis a required boolean, andconfigure()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 UI | Primitive UI | |
|---|---|---|
| Component | DyScanController | DyScanBrowser |
| What it gives you | A ready-made modal: camera feed, scan frame, help button, result handling | A camera feed and scan frame rendered into a container element you control; you build the surrounding UI |
| Best for | Fastest integration, consistent Dyneti UX | Fully 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
scanInstruction | string | "Position your card within the frame" | The instruction text shown above the scan frame. |
showPlaceholderText | boolean | true | Shows 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. |
placeholderCardNumber | string | "4242 4242 4242 4242" | Placeholder card number displayed inside the scan frame. |
placeholderDate | string | "11/11" | Placeholder expiry date displayed inside the scan frame. |
showResultOverlay | boolean | false | Briefly overlays the recognized card number and date on the live camera feed for about a second after a successful scan, before closing. |
enableSidewaysScanning | boolean | false | Allows 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. |
localization | Partial<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.
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.
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:
showPlaceholderText defaults to true in the Default UI, but defaults to false in the Primitive UI.
| Parameter | Type | Default | Description |
|---|---|---|---|
container | HTMLElement | Required | The 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. |
showCloseButton | boolean | false | Shows a small built-in "✕" close button in the top-right corner of the browser window. Its accessible label comes from localization.actionClose. |
showLogo | boolean | true | Shows the Dyneti logo beneath the scan frame. |
skin | Partial<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,
},
});
| Field | Type | Default | Description |
|---|---|---|---|
overlayColor | string | "#000000" | Color of the mask outside the scan frame. Use a hex color (#rgb or #rrggbb). |
overlayOpacity | number | 0.7 | Opacity of that mask, 0-1. |
cornerInactiveColor | string | "#808080" | Color of the frame's corner brackets before any part of the card is detected. |
cornerActiveColor | string | "#e7e7e7" | Color of the corner brackets once the card is detected. |
cornerCompletedColor | string | "#00ff00" | Color of the corner brackets once a scan completes successfully. |
cornerThickness | number | 4 | Line thickness of the corner brackets, in pixels. |
cardTextColor | string | "#ffffff" | Color used for the placeholder text and (if enabled) the post-scan result overlay text. |
cardTextPlaceholderOpacity | number | 0.3 | Opacity of the placeholder card number/date text. |
preloadBackgroundColor | string | null | null | Fill 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",
};
| Field | Default |
|---|---|
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." |
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.