--- title: Scanner API description: >- 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). api_version: 2026-04 api_name: pos-ui-extensions source_url: html: >- https://shopify.dev/docs/api/pos-ui-extensions/latest/target-apis/platform-apis/scanner-api md: >- https://shopify.dev/docs/api/pos-ui-extensions/latest/target-apis/platform-apis/scanner-api.md --- # 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). ### Use 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. ### Support Targets (10) ### Supported targets * [pos.​cart.​line-item-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/cart-details#cart-details-action-modal-) * [pos.​customer-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/customer-details#customer-details-action-modal-) * [pos.​draft-order-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/draft-order-details#draft-order-details-action-modal-) * [pos.​exchange.​post.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/post-exchange#post-exchange-action-modal-) * [pos.​home.​modal.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/home-screen#home-screen-action-modal-) * [pos.​order-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/order-details#order-details-action-modal-) * [pos.​product-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/product-details#product-details-action-modal-) * [pos.​purchase.​post.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/post-purchase#post-purchase-action-modal-) * [pos.​register-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/register-details#register-details-action-modal-) * [pos.​return.​post.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-04/targets/post-return#post-return-action-modal-) ### Properties The [`shopify` global object](https://shopify.dev/docs/api/pos-ui-extensions/2026-04#target-apis-define-what-your-extension-does) 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. * **hideCameraScanner** **() => void** **required** Hide the camera scanner. * **scannerData** **ScannerData** **required** Access current scan data and subscribe to new scan events. Use to receive real-time scan results. * **showCameraScanner** **() => void** **required** Show the camera scanner. * **sources** **ScannerSources** **required** 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. ```ts ReadonlySignalLike ``` ### 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. ```ts (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. ```ts 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. ```ts 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 ```ts ScannerSource ``` ### ScannerSource The scanner source the POS device supports. ```ts '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. ```ts ReadonlySignalLike ``` Examples ### 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 ```tsx import {render} from 'preact'; import {useState, useEffect, useRef} from 'preact/hooks'; export default async () => { render(, 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 ( ); } if (state === 'success') { return ( Scan next ); } if (state === 'error') { return ( Try again ); } // state === 'scanning' — camera overlay is shown by showCameraScanner() return ; }; ``` * #### ##### 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 ```tsx import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; export default async () => { render(, 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 ( Scanned data: {scanData} Scanned data source: {scanSource} {hasCameraScanner && ( Camera scanner is available )} {hasExternalScanner && ( External scanner is available )} ); }; ``` *** ## Best 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. Call `showCameraScanner()` 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. *** ## Limitations * 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. ***