Validate emails in Shopify

Check the email address of each Shopify customer and save the result on the customer, so your segments, flows and exports can use it. Two small pieces do the work, and you can use either or both:

  • A webhook relay checks a customer within seconds when it is added, or when its address changes. It can also tag each new order with the status of its address (off by default).
  • A clean-up command checks every customer, or those a customer search finds, that has no result yet.

Both run in your own accounts with your own keys: the relay as a Cloudflare Worker, the command with Node. They talk to your store through a custom app that you create for your own store. There is no app to install from the Shopify App Store.

Before you start

  • An API key. Create one on the API keys page with the permission "Validate addresses". To clean more than 50 customers at once, also turn on "Bulk lists". A daily credit cap limits what a busy store can spend.
  • Your store's myshopify address, such as acme for acme.myshopify.com.
  • Node 22.18 or later for the command, and a Cloudflare account for the relay.
  • The files, which are built from our tested code:
    • relay.js: the webhook relay, one Cloudflare Worker module;
    • clean.mjs: the clean-up command and the setup, one Node module.

Create a custom app for your store

  1. In Shopify's Dev Dashboard, create an app for your store.

  2. Give its version these Admin API access scopes, then release the version:

    read_customers, write_customers
    read_orders, write_orders        (only to tag orders)
  3. Install the app on your store.

  4. In the app's settings, copy its Client ID and Client secret. Keep the secret safe: it lets the relay and the command get access tokens for your store, and Shopify signs every webhook with it.

The relay and the command each get a short-lived access token with the client ID and secret when they need one. Shopify gives this token only when the app and the store belong to the same Shopify organization. If the setup below stops with HTTP 400 and shop_not_permitted, create the app again from the organization that owns the store.

If your store already has a custom app that was created in the Shopify admin before 2026, you can use it instead: set SHOPIFY_ACCESS_TOKEN to its Admin API access token in place of SHOPIFY_CLIENT_ID, and SHOPIFY_CLIENT_SECRET to its API secret key. It needs the same scopes.

Where the result goes

Each checked customer gets four metafields in the namespace email_validation:

MetafieldValue
email_validation.statusone of the five result statuses: valid, invalid, catch_all, unknown or disposable
email_validation.reasonwhy, such as accepted or no_mailbox
email_validation.checked_atwhen the check ran (date and time, UTC)
email_validation.checked_emailthe address that was checked

A customer is not checked again while its address is still the checked one. When you also turn on tags, the customer gets one of the tags email-valid, email-invalid, email-catch_all, email-unknown or email-disposable, and its other tags stay as they are.

Check new and changed customers

  1. Deploy the relay to your Cloudflare account and give it its secrets:

    npx wrangler deploy relay.js --name ev-shopify-relay --compatibility-date 2026-10-01 --var SHOPIFY_SHOP:acme
    npx wrangler secret put EV_API_KEY --name ev-shopify-relay
    npx wrangler secret put SHOPIFY_CLIENT_ID --name ev-shopify-relay
    npx wrangler secret put SHOPIFY_CLIENT_SECRET --name ev-shopify-relay

    Your keys stay in the Worker's secret store. For EU data residency, add --var EV_REGION:eu to the deploy command. To tag customers as well, add --var SHOPIFY_CUSTOMER_TAGS:on.

  2. Create the metafield definitions and subscribe the relay to your store's customer webhooks:

    export SHOPIFY_SHOP=acme  SHOPIFY_CLIENT_ID=…  SHOPIFY_CLIENT_SECRET=…
    node clean.mjs --setup --relay-url https://ev-shopify-relay.<your-subdomain>.workers.dev/

    The definitions give the four metafields their names on the customer page. The command subscribes the relay to customer creation and customer updates, and leaves anything that is already there as it is.

  3. Add a test customer with the address no_mailbox@sandbox.email-validation.org, a test address that costs nothing. Within a few seconds its Email status metafield is invalid. Then delete the test customer.

The relay checks a customer when it is created, and when its address changes. It ignores every other change, including its own write to the metafields, so a result never starts a second check. Before it checks, it reads the customer, so it always checks the address as it is now. The relay refuses any request that Shopify did not sign with your app's client secret.

Shopify waits only 5 seconds for an answer, so the relay answers at once and does the check just after. If a check cannot run, for example because the key has no credits left, Shopify does not deliver the event again: run the clean-up command to catch those customers up.

Tag new orders (optional)

The relay can tag each new order with the status of its address, such as email-invalid, so your team or a flow can look at it before fulfilling. When the order's address is its customer's and the customer already has a result, the order gets that result without a new check. Otherwise the relay checks the address, and also saves the result on the customer when the addresses match.

  1. Give the app the two order scopes listed above too, and release that version.

  2. Add --var SHOPIFY_ORDER_TAGS:on to the relay's deploy command, and deploy it again.

  3. Run the setup again with --orders:

    node clean.mjs --setup --relay-url https://ev-shopify-relay.<your-subdomain>.workers.dev/ --orders

This tags orders after they are placed. It does not stop a checkout.

Clean your customers

export EV_API_KEY=…  SHOPIFY_SHOP=acme  SHOPIFY_CLIENT_ID=…  SHOPIFY_CLIENT_SECRET=…
node clean.mjs --dry-run    # counts what it would check
node clean.mjs              # checks and writes the results

The command skips customers without an address and customers that already have a status other than unknown for the address they have now (add --revalidate to check them again). To check only some customers, pass a Shopify customer search, such as --query "tag:newsletter". Up to 50 customers are checked one by one. More go into one bulk job: the command uploads the list, starts it, waits for it, and writes each result to its customer.

If a run stops for any reason, such as no credits or a closed laptop, run the same command again. It keeps the bulk job's id in a small state file (ids only, no addresses) and resumes that job, so a list is never paid for twice. Exit code 75 means the job was still running when the command stopped waiting: run it again later.

Other options: --region eu for EU data residency, --tags to tag customers as well, and --wait-minutes for how long to wait for a bulk job (default 240).

Costs and limits

  • Each checked customer is one check. Results with the status unknown are free, and so are the test addresses.
  • Shopify limits how much API work an app can do per second. The command and the relay follow the limit that Shopify reports with each answer, and wait when Shopify asks them to.
  • Our own rate limits apply to the relay as to any other client.

Updated · Maintained by the email-validation.org team