Scanner API
The Scanner API provides barcode and QR code scanning on POS devices. Use it to show the camera scanner, subscribe to scan events, or detect available scanner hardware (camera, external, or embedded).
Anchor to Use casesUse cases
- Barcode scanning: Implement barcode scanning for product lookup or inventory management.
- QR codes: Build QR code scanning for customer engagement or loyalty programs.
- Custom workflows: Create scanning workflows that process data and trigger business logic.
- Real-time feedback: Implement real-time scanning feedback with immediate processing.
Anchor to PropertiesProperties
The shopify global object provides barcode and QR code scanning capabilities. Access the following properties on shopify to read scan data, control the camera scanner, and detect available scanner hardware.
- Anchor to hideCameraScannerhide
Camera Scannerhide Camera Scanner () => void() => voidrequiredrequired Hide the camera scanner.
- Anchor to scannerDatascanner
Datascanner Data ScannerDataScannerDatarequiredrequired Access current scan data and subscribe to new scan events. Use to receive real-time scan results.
- Anchor to showCameraScannershow
Camera Scannershow Camera Scanner () => void() => voidrequiredrequired Show the camera scanner.
- Anchor to sourcessourcessourcesScannerSourcesScannerSourcesrequiredrequired
Access available scanner sources on the device. Use to check which scanners are available (camera, external, or embedded).
ScannerData
Represents the scanner interface for accessing scan events and subscription management. Provides real-time access to scanned data through a reactive signal pattern.
- current
Current available scanner sources with subscription support. The `value` property provides current sources, and `subscribe` listens for changes. Use to monitor which scanners are available.
ReadonlySignalLike<ScannerSubscriptionResult>
ReadonlySignalLike
Represents a reactive signal interface that provides both immediate value access and subscription-based updates. Enables real-time synchronization with changing data through the observer pattern.
- subscribe
Subscribes to value changes and calls the provided function whenever the value updates. Returns an unsubscribe function to clean up the subscription. Use to automatically react to changes in the signal's value.
(fn: (value: T) => void) => () => void - value
The current value of the signal. This property provides immediate access to the current value without requiring subscription setup. Use for one-time value checks or initial setup.
T
ScannerSubscriptionResult
Represents the data from a scanner event. Contains the scanned string data and the hardware source that captured the scan.
- data
The string data from the last scanner event received. Contains the scanned barcode, QR code, or other scannable data. Returns `undefined` when no scan data is available. Use to process scanned content and implement scan-based business logic.
string - source
The scanning source from which the scan event came. Returns one of the following scanner types: • `'camera'` - Built-in device camera used for scanning • `'external'` - External scanner hardware connected to the device • `'embedded'` - Embedded scanner hardware built into the device
ScannerSource
ScannerSource
The scanner source the POS device supports.
'camera' | 'external' | 'embedded'ScannerSources
Represents the available scanner hardware sources on the device. Provides reactive access to the list of scanners that can be used for scanning operations.
- current
Current available scanner sources with subscription support. The `value` property provides current sources, and `subscribe` listens for changes. Use to monitor which scanners are available.
ReadonlySignalLike<ScannerSource[]>
jsx
Examples
Description
Build a scan-and-verify workflow. Open the camera on mount, send scanned data to a backend for validation, and show loading, success, or error states. Hide the camera during verification and restore it when the user taps **Scan next**.
jsx
import {render} from 'preact'; import {useState, useEffect, useRef} from 'preact/hooks'; export default async () => { render(<Extension />, document.body); }; const Extension = () => { const [state, setState] = useState('scanning'); // scanning | loading | success | error const [errorMessage, setErrorMessage] = useState(''); const lastScannedRef = useRef(''); useEffect(() => { shopify.scanner.showCameraScanner(); const unsubscribe = shopify.scanner.scannerData.current.subscribe( async (scan) => { if (!scan.data || scan.data === lastScannedRef.current) { return; } lastScannedRef.current = scan.data; setState('loading'); shopify.scanner.hideCameraScanner(); try { // Replace with your actual backend verification call // const response = await fetch(`/api/verify?code=${scan.data}`); // const data = await response.json(); // const valid = data.valid; const valid = true; if (valid) { setState('success'); shopify.toast.show('Verified'); } else { setErrorMessage('Code is not valid'); setState('error'); } } catch { setErrorMessage('Network error. Try again.'); setState('error'); } }, ); return () => { unsubscribe(); shopify.scanner.hideCameraScanner(); }; }, []); const resetScanner = () => { lastScannedRef.current = ''; setErrorMessage(''); setState('scanning'); shopify.scanner.showCameraScanner(); }; if (state === 'loading') { return ( <s-page heading="Verifying..."> <s-stack direction="block" block-alignment="center"> <s-spinner /> </s-stack> </s-page> ); } if (state === 'success') { return ( <s-page heading="Verified"> <s-stack direction="block" gap="small"> <s-banner heading="Code accepted" tone="success" /> <s-button onClick={resetScanner}>Scan next</s-button> </s-stack> </s-page> ); } if (state === 'error') { return ( <s-page heading="Error"> <s-stack direction="block" gap="small"> <s-banner heading={errorMessage} tone="critical" /> <s-button onClick={resetScanner}>Try again</s-button> </s-stack> </s-page> ); } // state === 'scanning' — camera overlay is shown by showCameraScanner() return <s-page heading="Scan a code" />; };Description
Subscribe to `shopify.scanner.sources.current` to detect which scanner hardware is available (camera, external, or embedded) and to `shopify.scanner.scannerData.current` to receive scan results. By identifying the scanner type, you can customize handling based on the scanning method.
jsx
import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; export default async () => { render(<Extension />, document.body); }; const Extension = () => { const [scanData, setScanData] = useState(''); const [scanSource, setScanSource] = useState(''); const [hasCameraScanner, setHasCameraScanner] = useState(false); const [hasExternalScanner, setHasExternalScanner] = useState(false); useEffect(() => { // This example doesn't call showCameraScanner(). Scan data // arrives from external/embedded scanners, or from the camera // if opened elsewhere — scannerData is shared globally. const unsubscribeData = shopify.scanner.scannerData.current.subscribe((result) => { setScanData(result.data || ''); setScanSource(result.source || ''); }); const unsubscribeSources = shopify.scanner.sources.current.subscribe((sources) => { setHasCameraScanner(sources.includes('camera')); setHasExternalScanner(sources.includes('external')); }); return () => { unsubscribeData(); unsubscribeSources(); }; }, []); return ( <s-page heading="Scanner Example"> <s-stack direction="block"> <s-text>Scanned data: {scanData}</s-text> <s-text>Scanned data source: {scanSource}</s-text> {hasCameraScanner && ( <s-text>Camera scanner is available</s-text> )} {hasExternalScanner && ( <s-text>External scanner is available</s-text> )} </s-stack> </s-page> ); };
Anchor to Best practicesBest practices
- Deduplicate scan events: The subscription can fire multiple times for the same code, including stale data from a previous scan when re-subscribing after a component remount. Track the last processed value and skip duplicates.
- Manage camera lifecycle: Call
hideCameraScanner()before showing results or a loading state. CallshowCameraScanner()when the user is ready to scan again. - Clean up subscriptions: Call the unsubscribe function returned by
subscribe()in your cleanup or unmount handler to prevent memory leaks. - Validate scanned data: Check the format of scanned data before processing. Show clear feedback for invalid codes, network errors, and unsupported formats.
Anchor to LimitationsLimitations
- The Scanner API is only available in action (modal) targets.
showCameraScanner()displays a full-screen system camera overlay. It doesn't return a value or promise, so there's no way to detect if the camera activated successfully.- Calling
scannerData.current.subscribe()may immediately emit the value from a previous scan because unsubscribing does not clear the signal.