Build a payment validation UI extension
This feature is available only to approved apps and is subject to change.
This feature is available only to approved apps and is subject to change.
Require staff to confirm that they counted cash payments of 100 or more. Shopify Point of Sale (POS) opens your confirmation interface before accepting the payment, then checks it again after staff tap Done.
Anchor to What you'll learnWhat you'll learn
In this tutorial, you'll learn how to do the following tasks:
- Generate a POS intercept extension.
- Configure a payment interceptor and its resolution target.
- Return a blocking finding for large cash payments.
- Save a confirmation from payment resolution that the interceptor reads.
- Test the payment validation flow in Shopify POS.
Requirements
Create a Shopify app, or use an existing app. Run the generation commands from the app root, which contains shopify.app.toml. Use Shopify CLI 4.8.0 or later. The template includes @shopify/ui-extensions@2026.10.0-rc.13, preact, and @preact/signals.
For an extension-only app, see Create an extension-only app.
Your app's API client must be approved for POS intercepts before you can test with shopify app dev. Creating a new app or installing Shopify CLI and SDK doesn't grant access.
Anchor to Generate the extensionGenerate the extension
Generate a payment intercept extension from the template. If you've already generated one, then skip this step and use its existing directory.
From your Shopify app root, download the payment intercept template catalog:
Terminal
Generate the extension using that catalog and template branch:
Terminal
The custom catalog applies only to this command. Don't pass --flavor; the template uses Preact automatically.
Shopify CLI creates extensions/cash-count-confirmation/ with a starter example in src/payment_intercept/ and the paired background and payment resolution targets. The following steps replace that starter example with the cash count confirmation example. All file paths in those steps are relative to this extension directory. Keep its generated uid.
Anchor to Configure interceptsConfigure intercepts
Update extensions/cash-count-confirmation/shopify.extension.toml, keeping its name, handle, and generated uid. Use api_version = "2026-10" and set runs_offline = true under [extensions.supported_features].
Configure pos.app.ready.data to use ./src/Background.js. Set blocking = true because the callback returns an ERROR finding, and set applies_to = ["cash"], the only supported payment method.
Add the matching payment resolution target, pointing it at ./src/PaymentResolution.jsx. Every intercepted event needs one.
Shopify CLI checks the TOML structure. When you deploy, Shopify also checks the event names, payment method values, API version, and matching resolution targets. For all supported settings, see Configuration rules.
The interceptor and the resolution interface use the same threshold, storage key, and confirmation format.
Create src/cashCount.js so that both files use one definition. The resolution interface saves the cash amount received and its currency so that the interceptor can recognize the confirmation when Shopify POS revalidates the payment.
cashAmountLabel() combines the amount and currency code, so a confirmation saved for one currency doesn't match a payment in another.
Anchor to Register an interceptorRegister an interceptor
Create src/Background.js. Register a callback for the paymentvalidations event from the target's default entry function. Shopify POS runs the callback when staff select or confirm cash, before Shopify POS adds the payment.
Read the storage key when the background target starts to begin loading it. Until it loads, its shopify.storage.current value is undefined, so the interceptor treats the payment as unconfirmed.
Clear the saved confirmation whenever the cart has no items. Check the cart when the background target starts and whenever it changes.
Keep confirmation unavailable until deletion succeeds.
Identical split cash payments within the same cart can reuse the confirmation until the cart empties.
Register the callback and return an empty operations array for cash amounts below the threshold. The amount field contains the cash amount received, not the amount charged, as a string.
The callback must return {operations: [...]} synchronously, not a Promise.
Read the saved confirmation from shopify.storage.current, which returns the stored value synchronously. When confirmation is available and matches the cash amount received, return no findings. A missing, unloaded, or mismatched confirmation leaves the payment unconfirmed.
Otherwise, return an ERROR finding targeting $.payment. After staff select the cash tendered amount, Shopify POS opens payment resolution, even if the store labels the finding Recommended. For the supported fields, see Validation operations.
Anchor to Build the resolution interfaceBuild the resolution interface
Payment resolution receives the payment method and cash amount received. It can read the cart but can't change it, so save the confirmation with the Storage API.
Create src/PaymentResolution.jsx with the form shown in the code panel. Store the form state in signals. Reset the confirmation and error at each launch so a previous count doesn't check the switch for a new payment. Keep those values during revalidation within the same flow. countMissing tracks whether staff tried to continue without confirming the count.
Render the cash amount received and a switch that staff turn on after they count the cash. When countMissing is set, the switch shows an error that says what's incomplete.
Register a save handler before rendering the interface. Shopify POS keeps only the most recently registered handler, so register it once, when the target starts, and don't unregister it.
Use shopify.resolution.onSave() to save the confirmation when staff tap Done, before Shopify POS checks the payment again. If staff haven't confirmed the count, then show the error and save nothing. If saving fails, show a retry toast. In either case, the finding remains unresolved.
After staff finish the resolution flow with no blocking findings remaining, Shopify POS closes the interface and resumes the original payment. Closing the interface or navigating back dismisses it without resuming the payment, even when the findings are Recommended.
Validation findings decide whether the payment continues, not the save handler's return value. See Revalidation.
After the new modules are in place, delete the template's src/payment_intercept/ directory. The configuration now points at src/Background.js and src/PaymentResolution.jsx, which use src/cashCount.js.
Anchor to Test your extensionTest your extension
Use a dev extension to test local changes. Use the dev-store app preview to test saved POS editor settings.
Anchor to Build the extensionBuild the extension
From your app root, run shopify app build. Shopify CLI bundles the extension and reports missing modules or imports. Unlike shopify app dev, this check doesn't require intercept preview approval.
Anchor to Test with a dev extensionTest with a dev extension
From your app root, run shopify app dev. Follow Connect your app and preview it in Shopify POS to open the Dev Console and load the dev extension on your device through the mobile preview link. Local code changes update the dev extension without a separate deploy.
- Start a sale and pay with less than 100 in cash. Check that the callback returns no finding.
- Start another sale and pay in full with 100 or more in cash. Check that Shopify POS opens the payment resolution interface with the cash amount received.
- Turn on the confirmation switch and tap Done. Check that the payment resumes and the sale completes. Change the form or callback and repeat with the same cash amount in a new sale to check your local changes and the confirmation reset.
Anchor to Configure the app preview in the POS editorConfigure the app preview in the POS editor
shopify app dev also builds and uploads an app preview on your dev store. This preview uses uploaded extension code rather than the device's live dev extension.
- Keep
shopify app devrunning, or rerun it from your app root. After your latest changes, wait for Shopify CLI to report that the dev preview is ready or updated. - On your device, open the POS Dev Console and select Remove dev extensions. Don't open the mobile preview link again during this pass: it reconnects the dev extension.
- Keep the dev-store app preview. Don't run
shopify app dev cleanor select Clean dev preview, which would remove it and restore the released app version. - From the Shopify admin, go to Sales channels > Point of Sale.
- Under Customize the in-store experience, click Edit Point of Sale. If this section isn't displayed, then use the alternate settings route.
- Click the apps icon in the editor menu bar. Under Apps for Point of Sale, find your app and select its payment validation extension. For navigation help, see Activate a POS app extension.
- Turn on Activate on. The extension lists Cash payments as its intercept surface.
- Turn on Allow app to block checkout. Despite its label, this setting also controls blocking for payment validations.
- Leave Block checkout if app experiences a problem off for the normal validation checks.
- Click Save, then close and reopen the extension's configuration to check that the settings persist.
Click Save after every editor change, and reopen the settings to check persistence before testing the result in Shopify POS.
Use these configurations for the Shopify POS checks below:
- Turn on Activate on and Allow app to block checkout to test blocking.
- Keep Activate on on and turn off Allow app to block checkout to test recommendations.
- Turn off Activate on to test the inactive extension.
After testing with the extension off, turn Activate on and Allow app to block checkout back on, then click Save.
Anchor to Test the app preview in Shopify POSTest the app preview in Shopify POS
Start with the extension active and allowed to block checkout. Return to Shopify POS with the device dev extension still disconnected, and run these checks:
- Start a sale and pay with less than 100 in cash. Shopify POS should add the payment without a finding.
- Start another sale and pay in full with 100 or more in cash. Shopify POS should open the payment resolution interface with the cash amount received.
- Tap Done without turning on the switch. The error should appear, and the interface should stay open.
- Turn on the switch and tap Done. Shopify POS should close the interface, resume the payment, and complete the sale. Check that the cart is empty.
- Start another sale and pay the same cash amount of 100 or more. Shopify POS should open payment resolution with the switch off. Tap Done without confirming again. The error should appear, and the interface should stay open.
- Close the interface without confirming. Shopify POS shouldn't add the payment.
- Start that unconfirmed payment again. The switch should still be off, and the previous error should be gone until staff tap Done.
- With Allow app to block checkout off, start an unconfirmed cash payment of 100 or more. The finding should be Recommended instead of Required, and staff should be able to proceed without confirming the count.
- With Activate on off, start another unconfirmed cash payment of 100 or more. This app shouldn't open payment resolution.
- With both settings back on, repeat steps 2–4.
Troubleshooting
Anchor to The event isn't supportedThe event isn't supported
Use cartvalidations or paymentvalidations.
Anchor to The event is supported but isn't configuredThe event is supported but isn't configured
Add the event under pos.app.ready.data and add its matching resolution target.
Anchor to An interceptor is already registeredAn interceptor is already registered
Register only one callback for that event in each extension instance, or call the existing callback's unregister function before replacing it.
Anchor to The interceptor returned a PromiseThe interceptor returned a Promise
PromiseReturn {operations: [...]} synchronously. Read stored values through shopify.storage.current instead of awaiting shopify.storage.get().
Anchor to Validation doesn't run again after a failed paymentValidation doesn't run again after a failed payment
Retrying a failed payment submission doesn't run payment validation again. Start the cash payment again to rerun the interceptor.
Anchor to Learn moreLearn more
Check a demo product serial number, and collect recommended inspection notes before checkout.Intercept API
Explore callback types, validation operations, configuration rules, and when validation runs.Resolution targets
Explore the cart and payment resolution targets, and the APIs and components they support.Storage API
Store values that other targets of your extension can read.