Shopify · Installation

Shopify — EUDI Wallet age verification

Install and configure the application for your own store.

Install EUDI Wallet age verification for Shopify

Application package: 0.1.8 · Guide updated: 30 October 2026

Add a verification button to your store and block checkout for selected 18+ products until the signed-in customer has presented a valid age proof. Follow these steps to deploy the application for your own store. You do not need access to our source repository or an Application API key.

Back to Wallet Connectors · Download the application source ZIP · Bridge integration guide · Contact

1 Before you start

You need:

A development store remains password protected and uses test payments only. The customer must be signed in to a Shopify customer account. An administrator session does not replace that account. Guest checkout for restricted products is not supported by this application.

Component Runs where Installation step
Merchant settings, OIDC callback and wallet redirect Your Python server Steps 4–5
EUDI Wallet button Shopify theme extension Steps 3, 6 and 9
Cart and Checkout Validation Function Shopify Steps 6 and 8
Wallet verification Our OIDC bridge Step 7

Deploying the Function alone is not sufficient. shopify app deploy publishes Shopify configuration and extensions; it does not host the Python application.

2 Download the application — development computer

Download the ZIP above. In the directory containing the downloaded file:

unzip eudi-wallet-age-shopify-0.1.8.zip
cd eudi-wallet-age-shopify
node --version
npm ci

The archive includes the backend, theme block, Function source and official GraphQL input schema, locked npm dependencies, deployment templates and this guide. It excludes private settings, installed dependencies, databases, compiled Wasm and our Shopify extension UIDs. Your app will receive its own UIDs.

3 Create and configure your Shopify application — development computer

  1. In the Shopify Dev Dashboard, select your own organization, then create an app for this integration. Record its Client ID and Client secret from the app settings. These are Shopify credentials, separate from the bridge credentials created in step 7. Keep the secret private.
  2. Link the downloaded project to this application:

bash npm run shopify -- app config link --file-name shopify.app.toml

Sign in when prompted. Select No, connect it to an existing app, then the application you just created in your organization. Do not select the Talao demonstration app. Linking does not deploy the extensions. 3. Open the generated shopify.app.toml. Keep the real Client ID obtained from Shopify and configure the values below. Replace YOUR_SHOPIFY_CLIENT_ID and age.example.com. The source ZIP also includes this configuration in shopify.app.example.toml.

client_id = "YOUR_SHOPIFY_CLIENT_ID"
name = "EUDI Wallet Age Verification"
application_url = "https://age.example.com"
embedded = false

[access_scopes]
scopes = "write_products,write_customers,write_validations,write_app_proxy"

[auth]
redirect_urls = ["https://age.example.com/oauth/callback"]

[app_proxy]
url = "https://age.example.com/proxy"
prefix = "apps"
subpath = "eudi-age"

[webhooks]
api_version = "2026-07"

[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks/uninstalled"

[[webhooks.subscriptions]]
compliance_topics = ["customers/data_request"]
uri = "/webhooks/data-request"

[[webhooks.subscriptions]]
compliance_topics = ["customers/redact"]
uri = "/webhooks/customer-redact"

[[webhooks.subscriptions]]
compliance_topics = ["shop/redact"]
uri = "/webhooks/shop-redact"

[build]
automatically_update_urls_on_dev = false

Do not add use_legacy_install_flow=true: it conflicts with the app-specific webhooks above. Keep the proxy path /apps/eudi-age; the backend uses this path to obtain the signed-in customer's identity from Shopify. Do not replace it with a URL containing an unsigned customer ID.

Keep handle = "eudi-age-validation" and target cart.validations.generate.run in the Function manifest. The activation code finds this Function by its handle. Do not copy extension UIDs from another app.

4 Install the Python backend — server

The following is a first installation on Debian/Ubuntu with systemd. It creates /opt/eudi-shopify-age; adapt paths and the process manager if your hosting uses a different deployment layout. Existing installations must be backed up and updated in place, preserving the private JSON and state database.

Transfer the original source ZIP to /tmp/eudi-wallet-age-shopify-0.1.8.zip on your server. No secrets are included in this transfer. Install Python 3.12, its venv module and unzip using your operating system's package manager, then run:

sudo bash <<'SH'
set -euo pipefail
umask 022
useradd --system --user-group --home-dir /opt/eudi-shopify-age --shell /usr/sbin/nologin eudi-shopify
install -d -m 0755 /opt/eudi-shopify-age
test ! -e /opt/eudi-shopify-age/app
unzip /tmp/eudi-wallet-age-shopify-0.1.8.zip -d /opt/eudi-shopify-age
mv /opt/eudi-shopify-age/eudi-wallet-age-shopify /opt/eudi-shopify-age/app
install -d -o eudi-shopify -g eudi-shopify -m 0700 /opt/eudi-shopify-age/private /opt/eudi-shopify-age/state
python3.12 -m venv /opt/eudi-shopify-age/.venv
/opt/eudi-shopify-age/.venv/bin/python -m pip install -r /opt/eudi-shopify-age/app/requirements.txt
SH

Run the configuration helper on the server, replacing the origin and shop with your actual values. Enter your Shopify Client ID and Client secret at its hidden prompts; do not put secrets in command arguments or commit them to Git.

sudo -u eudi-shopify /opt/eudi-shopify-age/.venv/bin/python \
  /opt/eudi-shopify-age/app/scripts/configure.py \
  --config /opt/eudi-shopify-age/private/shopify-secrets.json \
  --origin https://age.example.com \
  --shop YOUR-SHOP.myshopify.com

The helper creates a private 0600 JSON, generates the session/encryption keys and selects a private SQLite state path. It refuses to overwrite an existing configuration. Back up the JSON and database together securely: losing the encryption key prevents recovery of encrypted installation credentials.

Use the permanent *.myshopify.com hostname from Shopify, in lowercase, not a custom storefront domain. Do not use our demonstration hostname or credentials.

5 Start the backend and expose HTTPS — server

A complete systemd template is included in deploy/eudi-shopify-age.service.example. For the paths above:

sudo install -m 0644 /opt/eudi-shopify-age/app/deploy/eudi-shopify-age.service.example /etc/systemd/system/eudi-shopify-age.service
sudo systemctl daemon-reload
sudo systemctl enable --now eudi-shopify-age.service
sudo systemctl is-active eudi-shopify-age.service

The service must report active. It listens only on 127.0.0.1:5095. The runtime user needs read/execute access to the code and venv and write access only to its private state directory. A root-created venv must remain readable and executable by that user; do not run the service as root or use chmod 0777.

Configure your HTTPS reverse proxy to forward all paths, preserving the public Host header, to http://127.0.0.1:5095. The package includes deploy/nginx-https.conf.example: replace its hostname and certificate paths with yours, install it using your normal Nginx procedure, run nginx -t, then reload Nginx. This template requires an existing valid certificate. Obtain and configure it with your hosting provider or ACME client first.

Disable access logs for this application at every proxy/CDN and at Gunicorn: callback URLs, signed customer parameters and POST bodies must not be recorded. Do not cache responses. Permit HTTPS callbacks, /proxy, /webhooks/* and static assets. If you use a WAF, make sure these requests reach the application.

Open https://age.example.com/: the welcome page should load over verified HTTPS. A 502 means the reverse proxy cannot reach the running Python service. A 404 can indicate that the proxy forwarded a different Host than the configured origin. An HTTP 200 does not yet validate the Shopify or wallet integration.

6 Compile and deploy the extensions — development computer

From the extracted application directory:

npm run shopify -- app function schema --path extensions/age-validation --config shopify.app.toml
npm run shopify -- app build --config shopify.app.toml
npm run shopify -- app deploy --config shopify.app.toml

The build must succeed, including theme checks, GraphQL types and the Wasm Function. Review the app name and organization before confirming deployment. Shopify releases a new version containing both the theme extension and the validation Function. This does not activate the validation on a store or add the button to a theme automatically.

This deploy command does not publish an App Store listing and does not deploy or restart your Python backend. If you change Python files later, deploy those files on your server and restart its service separately.

7 Install the app, copy the callback and register the bridge client

  1. Open https://age.example.com/install?shop=YOUR-SHOP.myshopify.com in your browser. Sign in to Shopify and authorize the app for your store. Follow Shopify's Dev Dashboard installation/distribution procedure if required for your selected app distribution. The app opens EUDI Wallet configuration.
  2. Copy your return URL shown in that screen. It must be exactly https://age.example.com/oidc/callback, using your real origin.
  3. Select Create a free client. Choose Other OIDC application, Age verification (18+) and HTTP Basic. Paste your exact return URL, enter the developer/company contact, acknowledge the linked policies and verify the developer email with the six-digit code.
  4. Select Download configuration (JSON) on the confirmation screen. Keep it private: this confidential client JSON contains a secret.
  5. Return to the Shopify app, choose that JSON file and click Save. Confirm Client configured is displayed. Importing a file without saving it does not configure the client.
Setting Which registration?
Shopify Client ID / secret Shopify Dev Dashboard → your app settings; entered in the server helper
/oauth/callback Shopify merchant authorization; registered in the app TOML
Bridge Client ID / secret Bridge registration JSON; imported in the app's web settings
/oidc/callback Wallet age-verification return; registered with the bridge

Do not interchange these credentials or callbacks. No API key is required for the OIDC age check. Free registration quotas apply; multiple stores sharing one callback hostname share the Free daily budget. Obtain changes to quotas through your existing client rather than recreating its credentials.

8 Activate the checkout rule and select products

  1. In the application's configuration screen, select Activate 18+ validation. This creates the app-owned metafield definitions and the checkout rule using the deployed Function handle eudi-age-validation.
  2. In Shopify Settings → Checkout → Checkout rules, confirm the EUDI Wallet age verification rule is active. Its failure policy must block checkout; do not disable it to hide a verification error.
  3. Back in the app, tick each product that requires proof of majority and click its Save button. Leave unrestricted products unticked. The initial selection screen shows the first 50 products.

A deployed Function does not enforce anything until its rule is active. The rule allows unrestricted products without an age proof and blocks restricted checkout without a valid authorization on the signed-in customer account.

9 Add the button to the theme

  1. Open Online Store → Themes → Edit theme / Customize for the active theme.
  2. On the home page or another suitable page, select Add section → Apps → EUDI Wallet 18+.
  3. Set Application HTTPS origin to https://age.example.com using your real origin, without a trailing slash, then save the theme.
  4. Preview the storefront. Signed-out customers see Sign in; signed-in customers see Verify I am 18 or over. The block follows the storefront's French/English locale. Publish/configure your storefront languages in Shopify separately; the administrator interface language does not select storefront language or currency.

The button starts a wallet journey; the Function enforces checkout on the server. A green success page, browser parameter or cart attribute alone cannot grant access.

10 Test before enabling customer access

Use fictional products and a Shopify test payment method on a development store.

  1. Add an unrestricted product: the age rule does not block checkout.
  2. Add a restricted product without a proof: checkout is blocked.
  3. Sign in with a customer account distinct from the administrator account.
  4. Select the EUDI Wallet button and present an age attestation. On success, return to the cart: checkout is allowed for that account until the end of the store's calendar day. Do not enter a real payment card for a demo.
  5. Test cancellation, an expired request and a different customer account: no new 18+ authorization must be granted.
  6. Test again after the store's day changes: the old authorization is expired.
  7. Test your actual theme, checkout blocks, mobile flow and accelerated payment options individually. Do not assume every sales channel uses this rule.

The age flow stores no wallet name, birth date or identity credential in Shopify. It associates the daily authorization with the Shopify customer account. The wallet holder is not separately identified as the owner of that account.

Changing your application domain

Keep the HTTPS origin stable once merchants have installed the app. If you must change it:

  1. Configure DNS and a valid HTTPS certificate for the new origin, then update the backend's private origin setting.
  2. Update application_url, the /oauth/callback redirect and the app proxy URL in your Shopify app configuration. Deploy that configuration with Shopify CLI.
  3. Create a new bridge client for the new /oidc/callback URL and import its private JSON in the app settings. The bridge does not allow changing an existing client's callback hostname. Keep the previous client until you have validated the replacement.
  4. Set the new origin in the theme's EUDI Wallet block and save the theme.
  5. Restart your backend, authorize the app through a fresh /install flow and repeat the restricted/unrestricted checkout tests above.

Changing a domain does not transfer an existing browser session. Shopify app authorization and the bridge client registration are separate steps.

Troubleshooting and maintenance

Symptom Check
Function build fails Node version, npm ci, correct app configuration and current 2026-07 schema
Shopify rejects app-specific webhooks Remove use_legacy_install_flow=true and deploy the TOML again
App installation error Exact Shopify callback, app ID/secret, declared write scopes and a fresh /install flow in the same browser
JSON rejected Exact /oidc/callback, age purpose, openid age_verification, correct issuer and token authentication method
No products listed Products exist in the installed store; Shopify token has write_products
Rule cannot be activated Correct app/store, Function deployed, compatible plan/distribution and write_validations permission
Button absent Add the app block to the active theme and save it
Verification unavailable in the block Set the app origin in the block settings
Customer asked to sign in A Shopify customer session is required; an administrator login is insufficient
Age proof fails on return Same browser, unexpired transaction, exact bridge callback and unchanged registered client
Wallet succeeds but Shopify authorization fails Check protected customer-data access for customer identifiers and the age metafield, in addition to write_customers. A granted scope alone is insufficient. Do not request name, email, phone or address for this age flow

The backend requests expiring offline access tokens and stores their refresh tokens encrypted on the server. It renews access before API calls when needed; no user or bridge token is used for this operation. After updating an older installation that stored a permanent token, authorize once again through /install to obtain an expiring grant. Expired/revoked refresh access also requires merchant reauthorization; do not regenerate unrelated bridge credentials or delete shop settings. Back up configuration and state before updates. Keep existing extension UIDs after first linking your own app.

Local retention and manual rights requests

Version 0.1.3 includes scripts/privacy.py. Run it as the backend runtime user, with the backend's own private configuration. It never prints webhook bodies, customer identifiers, API tokens or client secrets to the terminal.

# Inspect pending request references, without their personal data.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json list

# Simulate local cleanup first. No Shopify API request is made.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json purge
# Apply after checking the simulation and the backup/retention procedure.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json purge --apply

# Export one request to a NEW file, readable only by its owner (0600).
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json export REFERENCE \
  --output /PRIVATE/request.json

# AFTER fulfilling the person's rights, simulate then record closure.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json fulfilled REFERENCE
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json fulfilled REFERENCE --apply

An export contains personal data. Keep it private, provide it only to the verified recipient through the agreed channel, and remove the working copy when the request is completed. The closure command does not export data to the person, delete their data, remove a Shopify metafield or revoke their credentials. Perform and verify those applicable actions before recording closure. Reference values come from list; never paste private request exports into support logs or review videos.

purge deletes expired local browser transactions and anti-abuse counters. It deletes a rights-request payload only 30 days after an operator records fulfillment; pending requests are never removed by age alone. It does not reset an active quota, alter installation records or delete Shopify grants.

To schedule this local cleanup on the documented AWS layout, inspect and copy deploy/eudi-shopify-age-maintenance.service.example and the matching timer into /etc/systemd/system/, removing .example from each destination name. Adjust paths/user first if your installation differs. Run the dry-run command as eudi-shopify, then systemctl daemon-reload and systemctl enable --now eudi-shopify-age-maintenance.timer. Start the oneshot service once and check its Result and ExecMainStatus. These files do nothing until installed and enabled by your operator.

Expiry is not erasure. The Shopify age metafield becomes unusable at the end of the store day, but this command does not remove it. Define and implement its removal, backup rotation, handling of restored data and the rights-request workflow before promising a complete retention policy. Privacy webhooks queue requests for manual fulfillment; an HTTP success only confirms receipt.

Shopify authorization content retention

Version 0.1.4 adds a separate per-shop operation. It reads the existing customer IDs and only the app-owned proof metafield, so authorizations written by older releases are included. It does not request names, email addresses, phone numbers or addresses, nor create a local customer registry.

# Simulation: no age authorization is modified.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json shopify-expired \
  --shop YOUR_STORE.myshopify.com
# Apply after checking the simulation.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json shopify-expired \
  --shop YOUR_STORE.myshopify.com --apply

Both modes make authenticated Shopify API calls and may renew an expired API access token using the existing refresh mechanism. Required customer-data access must already be granted. The shop must be installed in this backend; its Shopify ID is checked before processing. The operation uses Shopify's current shop timezone and touches only the known version-1 proof format whose valid_on is strictly earlier than today's shop date. Current, future, empty and customized values are preserved.

Content erasure, not field deletion. Expired content is replaced with {} using the read value's compareDigest. A renewal or deletion between reading and writing prevents the old update; there is no unguarded retry. This avoids clearing a fresh authorization. The empty metafield remains associated with the customer but contains neither the former age boolean nor its date. It grants no checkout access. Full removal of the metafield and removal of Shopify's own history/backups are not implemented or promised by this task.

The command reports only counts, never customer IDs, proof values or API response bodies. renewed counts values skipped after a concurrent change. complete: false with exit code 2 means the page limit was reached. Increase --max-pages if required (default 100 pages of 100 customers; maximum 10,000 pages). An API error stops the run with failure; previous successful updates remain applied, and retrying is safe. Do not claim full cleanup from an incomplete or failed run. Very large shops need a separately assessed schedule and request budget.

To automate on the documented AWS checkout layout, inspect/copy the deploy/eudi-shopify-age-retention@.service.example and matching timer to /etc/systemd/system/, removing .example. Run the simulation, reload systemd, then enable the timer for the exact permanent shop hostname, for example eudi-shopify-age-retention@talao-eudi-wallet-demo.myshopify.com.timer. Run the matching service once and verify its result and counts before relying on the hourly schedule. Enable one instance per installed shop, and disable it when removing a shop. Monitor API errors and incomplete scans. The existing local maintenance timer is unchanged and makes no Shopify API calls.

Before production, establish the merchant data-protection agreement and notice, actual retention/backup procedures and required protected customer-data access. The operator's draft checklist is not an adopted merchant agreement.

Official references: Shopify app configuration, linking a project, deploying versions and Functions availability.

Human review alternative

When the operator explicitly enables human_review_enabled: true, customers can request a genuine merchant review from the app's age-confirmation page before opening their wallet, or after a failed return with a valid browser context. Merchants handle the queue under Pending human reviews, provide a reasoned response and explicitly approve only after checking age using appropriate information. A request alone grants nothing. See human review setup and checks. This feature requires backend deployment and real merchant follow-up; it is not automatically enabled on AWS by downloading this guide.

In 0.1.8, each merchant must opt in under app settings → Allow requests for review by the merchant, and acknowledge the commitment to real checks and reasoned responses. The operator must also enable the module using the private boolean human_review_enabled=true. Existing shops do not opt in automatically on upgrade. Disabling new requests preserves received cases and follow-up. Do not declare the optional alternative as offered when it is disabled.