{"id":318548,"date":"2026-07-14T11:19:47","date_gmt":"2026-07-14T11:19:47","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/customer-match-list-for-google-ads\/"},"modified":"2026-10-20T19:48:32","modified_gmt":"2026-10-20T19:48:32","slug":"customer-match-list-google-ads","status":"publish","type":"plugin","link":"https:\/\/hr.wordpress.org\/plugins\/customer-match-list-google-ads\/","author":9137856,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"2.1.1","stable_tag":"2.1.1","tested":"7.1.4","requires":"6.0","requires_php":"7.4","requires_plugins":null,"header_name":"CMLS - Customer Match List for Google Ads & Meta Ads","header_author":"Uprise Team","header_description":"One Google Ads Customer Match CSV per billing country, built from WooCommerce orders.","assets_banners_color":"543721","last_updated":"2026-10-20 19:48:32","external_support_url":"","external_repository_url":"","donate_link":"https:\/\/www.paypal.me\/eduardvd","header_plugin_uri":"","header_author_uri":"https:\/\/uprise.ro","rating":0,"author_block_rating":0,"active_installs":20,"downloads":762,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.1.1":{"tag":"1.1.1","author":"rwky","date":"2026-07-17 20:23:23","revision":3611989},"1.1.2":{"tag":"1.1.2","author":"rwky","date":"2026-10-15 09:55:16","revision":3748118},"2.0.0":{"tag":"2.0.0","author":"rwky","date":"2026-10-20 16:10:26","revision":3757067},"2.1.0":{"tag":"2.1.0","author":"rwky","date":"2026-10-20 16:10:26","revision":3757067},"2.1.1":{"tag":"2.1.1","author":"rwky","date":"2026-10-20 19:48:32","revision":3757387}},"upgrade_notice":{"2.1.1":"<p>Readme and admin wording only; no functional changes. Feed URLs don&#039;t change.<\/p>","2.1.0":"<p>New Health tab and optional all-countries file, plus fixes for token resets, backfills, phone numbers and lost schedules. Feed URLs don&#039;t change.<\/p>","2.0.0":"<p>New audience window with a nightly rebuild, any order status, and value segments. Your files keep working exactly as before until you save the Settings tab once; then the nightly rebuild starts. Feed URLs don&#039;t change.<\/p>","1.1.1":"<p>Naming and documentation release; no functional changes.<\/p>","1.1.0":"<p>Feed URLs changed to end in <code>.csv<\/code> and hashing normalization was corrected \u2014 run &quot;Delete all files &amp; reset dedup&quot;, then re-run the backfill.<\/p>"},"ratings":[],"assets_icons":{"icon-256x256.png":{"filename":"icon-256x256.png","revision":3609357,"resolution":"256x256","location":"assets","locale":"","width":500,"height":500},"icon.svg":{"filename":"icon.svg","revision":3609357,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3609357,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3609357,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3757388,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"meta\":{\"title\":\"CMLS - Customer Match List for Google Ads & Meta Ads\",\"description\":\"A WooCommerce store with sample orders in four countries, the plugin active and its customer lists already built.\",\"author\":\"rwky\",\"categories\":[\"WooCommerce\",\"Marketing\"]},\"landingPage\":\"\\\/wp-admin\\\/admin.php?page=cmls-settings\",\"preferredVersions\":{\"php\":\"8.3\",\"wp\":\"latest\"},\"features\":{\"networking\":true},\"login\":true,\"steps\":[{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"woocommerce\"},\"options\":{\"activate\":true}},{\"step\":\"setSiteOptions\",\"options\":{\"blogname\":\"Demo Store\",\"woocommerce_default_country\":\"US:CA\",\"woocommerce_currency\":\"USD\",\"woocommerce_coming_soon\":\"no\",\"woocommerce_task_list_hidden\":\"yes\",\"woocommerce_onboarding_profile\":{\"skipped\":true}}},{\"step\":\"runPHP\",\"code\":\"<?php\\nrequire_once '\\\/wordpress\\\/wp-load.php';\\n\\nglobal $wp_rewrite;\\n$wp_rewrite->set_permalink_structure( '\\\/%postname%\\\/' );\\n$wp_rewrite->flush_rules( false );\\ndelete_transient( '_wc_activation_redirect' );\\nadd_filter( 'pre_wp_mail', '__return_false' );\\n\\n$orders = array(\\n\\tarray( 'emma.johnson@example.com', 'Emma', 'Johnson', '(415) 555-0132', 'US', '94103', 'completed', 12, 184.50 ),\\n\\tarray( 'emma.johnson@example.com', 'Emma', 'Johnson', '(415) 555-0132', 'US', '94103', 'completed', 200, 96.00 ),\\n\\tarray( 'liam.smith@example.com', 'Liam', 'Smith', '(212) 555-0198', 'US', '10001', 'completed', 45, 92.00 ),\\n\\tarray( 'olivia.brown@example.com', 'Olivia', 'Brown', '', 'US', '60601-1234', 'completed', 130, 310.00 ),\\n\\tarray( 'noah.davis@example.com', 'Noah', 'Davis', '(305) 555-0147', 'US', '33101', 'processing', 1, 58.90 ),\\n\\tarray( 'ava.miller@example.com', 'Ava', 'Miller', '(206) 555-0110', 'US', '98101', 'completed', 700, 140.00 ),\\n\\tarray( 'oliver.taylor@example.co.uk', 'Oliver', 'Taylor', '020 7946 0958', 'GB', 'SW1A 1AA', 'completed', 30, 75.00 ),\\n\\tarray( 'amelia.wilson@example.co.uk', 'Amelia', 'Wilson', '07700 900123', 'GB', 'M1 1AE', 'completed', 400, 220.00 ),\\n\\tarray( 'lukas.mueller@example.de', 'Lukas', 'M\\u00fcller', '030 901820', 'DE', '10115', 'completed', 20, 129.00 ),\\n\\tarray( 'sophie.schmidt@example.de', 'Sophie', 'Schmidt', '', 'DE', '80331', 'completed', 90, 64.50 ),\\n\\tarray( 'andrei.popescu@example.ro', 'Andrei', 'Popescu', '0721 234 567', 'RO', '010011', 'completed', 8, 260.00 ),\\n\\tarray( 'maria.ionescu@example.ro', 'Maria', 'Ionescu', '0745 678 901', 'RO', '400001', 'completed', 60, 88.00 ),\\n);\\n\\nforeach ( $orders as $o ) {\\n\\t$order = new WC_Order();\\n\\t$order->set_billing_email( $o[0] );\\n\\t$order->set_billing_first_name( $o[1] );\\n\\t$order->set_billing_last_name( $o[2] );\\n\\t$order->set_billing_phone( $o[3] );\\n\\t$order->set_billing_country( $o[4] );\\n\\t$order->set_billing_postcode( $o[5] );\\n\\t$order->set_date_created( time() - $o[7] * DAY_IN_SECONDS );\\n\\t$order->set_total( $o[8] );\\n\\t$order->set_status( $o[6] );\\n\\t$order->save();\\n}\\n\"},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"customer-match-list-google-ads\"},\"options\":{\"activate\":true}},{\"step\":\"wp-cli\",\"command\":\"wp cmls rebuild\"}]}"}},"all_blocks":[],"tagged_versions":["1.1.1","1.1.2","2.0.0","2.1.0","2.1.1"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3757067,"resolution":"1","location":"assets","locale":"","width":1241,"height":1063},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3757067,"resolution":"2","location":"assets","locale":"","width":1354,"height":970},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3757067,"resolution":"3","location":"assets","locale":"","width":1331,"height":1270},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3757067,"resolution":"4","location":"assets","locale":"","width":1441,"height":1206},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3757067,"resolution":"5","location":"assets","locale":"","width":1297,"height":822}},"screenshots":{"1":"Feeds: the status (audience window, last and next run, storage folder) and one feed URL per country for your Google Ads customer lists, plus the optional all-countries URL.","2":"Settings: how often new orders are added, the audience window, the order statuses to export and the all-countries file.","3":"Value segments: lists of your best customers by spend, number of orders and last order date, each with its own URL.","4":"Health: checks for the feed URLs, schedule, file writes, storage, directory listing, nightly rebuild, hashing and list sizes, with the rows counted in every file.","5":"Tools: Update now, Rebuild now, the one-time backfill, and the danger zone (reset the token, delete the files)."}},"plugin_section":[],"plugin_tags":[271878,271475,985,239143,286],"plugin_category":[45,55],"plugin_contributors":[97974],"plugin_business_model":[],"class_list":["post-318548","plugin","type-plugin","status-publish","hentry","plugin_tags-cmls","plugin_tags-customer-match","plugin_tags-google-ads","plugin_tags-meta-ads","plugin_tags-woocommerce","plugin_category-ecommerce","plugin_category-seo-and-marketing","plugin_contributors-rwky","plugin_committers-rwky"],"banners":{"banner":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/banner-772x250.png?rev=3609357","banner_2x":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/banner-1544x500.png?rev=3609357","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/icon.svg?rev=3609357","icon":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/icon.svg?rev=3609357","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/screenshot-1.png?rev=3757067","caption":"Feeds: the status (audience window, last and next run, storage folder) and one feed URL per country for your Google Ads customer lists, plus the optional all-countries URL."},{"src":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/screenshot-2.png?rev=3757067","caption":"Settings: how often new orders are added, the audience window, the order statuses to export and the all-countries file."},{"src":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/screenshot-3.png?rev=3757067","caption":"Value segments: lists of your best customers by spend, number of orders and last order date, each with its own URL."},{"src":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/screenshot-4.png?rev=3757067","caption":"Health: checks for the feed URLs, schedule, file writes, storage, directory listing, nightly rebuild, hashing and list sizes, with the rows counted in every file."},{"src":"https:\/\/ps.w.org\/customer-match-list-google-ads\/assets\/screenshot-5.png?rev=3757067","caption":"Tools: Update now, Rebuild now, the one-time backfill, and the danger zone (reset the token, delete the files)."}],"raw_content":"<!--section=description-->\n<p>Build <strong>Google Ads customer lists<\/strong> (Customer Match) from your <strong>WooCommerce<\/strong> orders: one CSV file per billing country, updated automatically on a schedule. Each country file has its own private, tokenized URL that Google Ads fetches straight from your WordPress site, with no third-party connector or API key \u2014 ideal for marketing teams that run per-country campaigns and want clean, single-market audience lists.<\/p>\n\n<p>The values are hashed with the same SHA-256 normalization used by both Google Ads Customer Match and Meta Ads Custom Audiences, so the same file can also be uploaded manually to Meta Ads Manager (see FAQ).<\/p>\n\n<p><strong>Why this export?<\/strong>\nNot all purchases are captured by Google Ads tracking; periodic uploads of <strong>Customer Match lists<\/strong> help targeting and campaign performance. Even Google's automatic <strong>conversion-based customer lists<\/strong> (auto-enabled for eligible accounts from August 2026) only contain tag-tracked conversions \u2014 this plugin exports from the order database itself, so ad-blocked, phone, offline and historical orders are included too (see FAQ). The export reflects your order database as-is \u2014 applying your store's consent choices and legal requirements to it is your responsibility as the merchant (see the GDPR \/ privacy FAQ).<\/p>\n\n<p><strong>Why per-country files?<\/strong>\nMarketing teams optimize Google Ads campaigns per country. Mixing a German audience into a Romanian campaign's match list pollutes the targeting. This plugin keeps each market in its own file.<\/p>\n\n<h4>How it works<\/h4>\n\n<ol>\n<li>You pick an <strong>audience window<\/strong> (all orders, the last 365 or 540 days, or any number of days) and the <strong>order statuses<\/strong> that count as a purchase.<\/li>\n<li>Every night the plugin <strong>rebuilds<\/strong> one CSV per billing country from the orders inside the window, hashing the customer fields. The new files replace the old ones in one step, so customers who stopped buying drop out and Google never fetches a half-written file. Feed URLs never change.<\/li>\n<li>Between rebuilds, a <strong>scheduled job<\/strong> (1, 6, 12, or 24 hours) appends new orders to the right country file. Orders that reach a selected status late (delayed payment confirmation, manual completion) are appended the moment they do, so they are never missed.<\/li>\n<li>Each country file is downloadable from a <strong>secret URL<\/strong> that you hand to your marketing team. They paste it into <strong>Google Ads Data Manager \u2192 HTTPS connector<\/strong> as a customer list, and Google fetches the latest version on its own schedule. A one-time manual upload works too.<\/li>\n<li>Optionally, <strong>value segments<\/strong> add extra files with only some of your customers, such as your top 10% by spend or big spenders who stopped buying, each with its own URL.<\/li>\n<\/ol>\n\n<h4>Features<\/h4>\n\n<ul>\n<li><strong>Per-country splitting<\/strong> \u2014 one CSV per ISO billing country, never mixed.<\/li>\n<li><strong>Optional all-countries file<\/strong> \u2014 one more URL with every country together, for a campaign that targets them all.<\/li>\n<li><strong>Audience window with a nightly rebuild<\/strong> \u2014 keep only customers who ordered in the last N days. The rebuild runs in the background and swaps the new files in at once; URLs stay the same.<\/li>\n<li><strong>Any order status counts<\/strong> \u2014 pick Completed, Processing, or any custom status added by another plugin, in one searchable list.<\/li>\n<li><strong>Value segments<\/strong> \u2014 up to 5 extra lists built from each customer's spend, order count and last order date (top X% by spend, a minimum spend, repeat buyers, lapsed customers). Multi-currency stores can leave other currencies out or convert them at their own rates.<\/li>\n<li><strong>Health tab<\/strong> \u2014 checks the schedule, the last file write, the storage folder, the nightly rebuild and your permalinks, and counts the rows in every file against what it should hold.<\/li>\n<li><strong>Background backfill and rebuild from the admin<\/strong> (no CLI needed) \u2014 runs via Action Scheduler.<\/li>\n<li><strong>One-time backfill via WP-CLI<\/strong> with status filter, date filter (<code>--days=N<\/code>, <code>--since<\/code>, <code>--until<\/code>), dry-run, and batch-size control.<\/li>\n<li><strong>Scheduled incremental appends<\/strong> every 1, 6, 12, or 24 hours between rebuilds.<\/li>\n<li><strong>Per-(email, country) deduplication<\/strong> via a dedicated indexed database table \u2014 no full-file scans, single indexed insert per row.<\/li>\n<li><strong>CSV column headers<\/strong>: <code>Email, Phone, First Name, Last Name, Country, Zip<\/code>.<\/li>\n<li><strong>Hashed per Google spec<\/strong>: SHA-256 of trimmed\/lowercased values, phone normalized to E.164 with the leading <code>+<\/code> \u2014 the same hashing Meta Ads Custom Audiences accept.<\/li>\n<li><strong>Secret tokenized URLs<\/strong> \u2014 one per country, all resettable from the admin or via CLI.<\/li>\n<li><strong>Storage hardening<\/strong> \u2014 files live under <code>wp-content\/uploads\/customer-match-list-google-ads\/<\/code> with filenames containing a 64-bit hash derived from the token, behind a deny-all <code>.htaccess<\/code> on Apache.<\/li>\n<li><strong>REST endpoint stream<\/strong> \u2014 the CSV is sent chunked with <code>nocache_headers()<\/code>.<\/li>\n<li><strong>HPOS-first SQL<\/strong> \u2014 direct <code>JOIN<\/code> on <code>wc_orders<\/code> + <code>wc_order_addresses<\/code> (or <code>posts<\/code>+<code>postmeta<\/code> on classic). <strong>Designed for stores with tens of millions of orders.<\/strong><\/li>\n<\/ul>\n\n<h4>Admin Dashboard Settings<\/h4>\n\n<p><strong>WooCommerce \u2192 Marketing \u2192 Customer Match List<\/strong>.<\/p>\n\n<p>From there you can:<\/p>\n\n<ul>\n<li><strong>Feeds:<\/strong> see per-country status (row count, file size, and the tokenized URL ready to copy) and the value segment URLs.<\/li>\n<li><strong>Settings:<\/strong> pick the update interval (1\/6\/12\/24 h), the audience window, the order statuses and whether to offer an all-countries file.<\/li>\n<li><strong>Value segments:<\/strong> switch segments on, choose how spend is counted, and add up to 5 segments from templates (VIP, High value, Valuable, Lapsed high value, Repeat buyers) or your own rules.<\/li>\n<li><strong>Health:<\/strong> a check list (schedule, last write, storage folder, directory listing, nightly rebuild, permalinks, list sizes) and every file's row count next to what it should hold, with a one-click rebuild when they differ.<\/li>\n<li><strong>Tools:<\/strong> run the update now, rebuild now, start a background backfill and watch its progress, reset the token (regenerates every URL, renames every file), or delete every file and reset the dedup table.<\/li>\n<li>An automated check confirms the webserver does not expose the CSV directory via a public file listing.<\/li>\n<\/ul>\n\n<h4>WP-CLI<\/h4>\n\n<ul>\n<li><code>wp cmls rebuild<\/code> (or <code>wp cmls rebuild-now<\/code>) \u2014 rebuilds every file from the orders inside the audience window and swaps them in, in the same process (no PHP time limit). Same result as the nightly rebuild and <strong>Tools \u2192 Rebuild now<\/strong>.<\/li>\n<li><code>wp cmls backfill<\/code> \u2014 one-time backfill of historical orders, split by country. Flags: <code>--statuses=processing,completed<\/code> (defaults to the statuses saved in the settings), <code>--all-statuses<\/code>, <code>--days=N<\/code>, <code>--since=YYYY-MM-DD<\/code>, <code>--until=YYYY-MM-DD<\/code>, <code>--dry-run<\/code>, <code>--batch-size=5000<\/code>, <code>--force<\/code>. The next rebuild goes back to the saved window and statuses.<\/li>\n<li><code>wp cmls update-now<\/code> (or <code>wp cmls run-now<\/code>) \u2014 appends orders placed since the last run, right away. Same code path as the scheduled cron and <strong>Tools \u2192 Update now<\/strong>.<\/li>\n<li><code>wp cmls status<\/code> \u2014 prints a table with one row per country (row count, file size, URL) and the all-countries URL when it's on, then the audience window, statuses and rebuild times.<\/li>\n<li><code>wp cmls reset-token<\/code> \u2014 regenerates the token and renames every file to match. If a file can't be renamed, nothing changes and the command fails.<\/li>\n<li><code>wp cmls delete-file<\/code> \u2014 deletes every country CSV and truncates the dedup table.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin folder to <code>wp-content\/plugins\/customer-match-list-google-ads<\/code> (or install from the WordPress.org repository).<\/li>\n<li>Activate <strong>CMLS - Customer Match List for Google Ads &amp; Meta Ads<\/strong>. WooCommerce must already be active.<\/li>\n<li>Go to <strong>WooCommerce \u2192 Marketing \u2192 Customer Match List<\/strong>.<\/li>\n<li>On the <strong>Settings<\/strong> tab, pick the update interval (1, 6, 12, or 24 hours), the audience window (new installs start at 540 days) and the order statuses.<\/li>\n<li>Build the files:\n\n<ul>\n<li><strong>From the admin<\/strong> \u2014 click <strong>Rebuild now<\/strong> on the <strong>Tools<\/strong> tab, or wait for the nightly rebuild. You can close the tab; it keeps running.<\/li>\n<li><strong>From the shell<\/strong> \u2014 <code>wp cmls rebuild<\/code>.<\/li>\n<\/ul><\/li>\n<li>When the rebuild finishes, the <strong>Feeds<\/strong> tab shows one URL per country. Copy each URL and paste it into Google Ads Data Manager \u2192 HTTPS connector for the corresponding country's Customer Match list.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"why%20one%20file%20per%20country%3F\"><h3>Why one file per country?<\/h3><\/dt>\n<dd><p>Marketing teams optimize Google Ads campaigns per country. Mixing audiences degrades match-rate signals and wastes budget on irrelevant impressions. Each country file is independent and can be wired into its own Customer Match list. Google's Data Manager pulls each URL on its own schedule, so you can refresh some markets without affecting others.<\/p><\/dd>\n<dt id=\"can%20i%20get%20one%20file%20with%20every%20country%3F\"><h3>Can I get one file with every country?<\/h3><\/dt>\n<dd><p>Yes. On the <strong>Settings<\/strong> tab, tick <strong>Also offer one file with every country<\/strong>. The <strong>Feeds<\/strong> tab then lists an extra URL ending in <code>\/all.csv<\/code>, after the country URLs. It holds the rows of every country file together, under one header, so it's always in step with them. A customer who ordered from two countries appears once per country, as in the country files.<\/p><\/dd>\n<dt id=\"how%20do%20i%20create%20a%20google%20ads%20customer%20list%20from%20a%20feed%20url%3F\"><h3>How do I create a Google Ads customer list from a feed URL?<\/h3><\/dt>\n<dd><p>In Google Ads \u2192 <strong>Tools<\/strong> \u2192 <strong>Shared library<\/strong> \u2192 <strong>Audience manager<\/strong>, click <strong>+ Create remarketing list<\/strong> \u2192 <strong>+ Customer list<\/strong> and connect the <strong>HTTPS<\/strong> data source (step by step under \"How do I add value segment URLs in Google Ads?\" below; country URLs work the same way). Paste the country-specific URL from the plugin's admin page. Google Ads fetches the file on its own cadence; our plugin keeps the file fresh per the schedule you picked.<\/p><\/dd>\n<dt id=\"google%20says%20it%27s%20turning%20on%20%22conversion-based%20customer%20lists%22%20%E2%80%94%20do%20i%20still%20need%20this%20plugin%3F\"><h3>Google says it's turning on \"conversion-based customer lists\" \u2014 do I still need this plugin?<\/h3><\/dt>\n<dd><p>Yes \u2014 they complement each other. In 2026 Google began automatically enrolling accounts that use both Enhanced Conversions and Customer Match into <strong>conversion-based customer lists<\/strong> (enrollment emails went out from June 2026; data processing starts August 18, 2026). Those lists are built solely from what the Google Ads tag captures on your site from that point on: customers using ad blockers or declining your consent banner never enter them, and neither do phone, imported, or historical orders.<\/p>\n\n<p>This plugin builds Customer Match lists from the <strong>WooCommerce order database<\/strong> instead \u2014 every completed order, including full order history via backfill, split into one file per country, with a mailing-address fallback when an order has no email or phone. Keep both: attach the automatic conversion-based lists and the per-country lists this plugin feeds, and Google deduplicates matched customers on its side.<\/p><\/dd>\n<dt id=\"does%20the%20file%20work%20with%20meta%20%28facebook%29%20ads%3F\"><h3>Does the file work with Meta (Facebook) Ads?<\/h3><\/dt>\n<dd><p>Yes. Meta Ads Custom Audiences use the same SHA-256 hashing of normalized (trimmed, lowercased) values, so a country CSV can be uploaded to Meta Ads Manager as a customer list \u2014 map the columns in Meta's upload screen. Note that Meta does not pull files from URLs; the pull-from-URL workflow is Google-specific, so on Meta use manual upload.<\/p><\/dd>\n<dt id=\"how%20long%20does%20the%20backfill%20take%3F\"><h3>How long does the backfill take?<\/h3><\/dt>\n<dd><p>Roughly 10 seconds per 150,000\u2013200,000 orders on typical hardware. The query path is direct SQL against <code>wc_orders<\/code> + <code>wc_order_addresses<\/code> (HPOS).\nFor 1M+ orders the CLI is still the most reliable option because no PHP\/web timeout applies; the admin Action-Scheduler-backed runner also handles 1M+ but each batch must complete within the host's PHP limit and if it does not then it won't finish.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20orders%20with%20no%20billing%20country%3F\"><h3>What happens to orders with no billing country?<\/h3><\/dt>\n<dd><p>It shouldn't happen, since WooCommerce billing and shipping addresses are country-based, but if it does, the order is skipped. There is no correct file to route them to, and Google's per-country Customer Match campaigns can't use them anyway. The same applies to orders with an invalid country code.<\/p><\/dd>\n<dt id=\"what%20about%20orders%20with%20no%20email%3F\"><h3>What about orders with no email?<\/h3><\/dt>\n<dd><p>They get written. Google Customer Match accepts a row with <strong>any one<\/strong> of: email, phone, or full mailing address (first name + last name + zip). The plugin includes a row as long as one of those identifiers is present alongside a valid country. Deduplication is still single-column: it uses email when available, otherwise phone, otherwise the mailing address.<\/p>\n\n<p>Google handles cross-identifier deduplication on its side after matching to a Google account \u2014 so a customer who orders once with email + phone and again with only their phone will appear as two rows in the CSV but as one user in your Customer Match audience.<\/p><\/dd>\n<dt id=\"will%20the%20same%20customer%20appear%20in%20multiple%20country%20files%3F\"><h3>Will the same customer appear in multiple country files?<\/h3><\/dt>\n<dd><p>Yes \u2014 but only if they actually ordered from multiple countries!<\/p><\/dd>\n<dt id=\"i%20ran%20the%20backfill%20twice%20%E2%80%94%20why%20didn%27t%20it%20write%20more%20rows%20the%20second%20time%3F\"><h3>I ran the backfill twice \u2014 why didn't it write more rows the second time?<\/h3><\/dt>\n<dd><p>This is by design, as every <code>(email, country)<\/code> pair is recorded the first time it's seen and never re-written.<\/p>\n\n<p>To regenerate everything from scratch (for example after toggling debug mode), click <strong>Tools \u2192 Rebuild now<\/strong> or run <code>wp cmls rebuild<\/code>. The rebuild starts from empty files and swaps them in when it's done; the URLs stay the same.<\/p><\/dd>\n<dt id=\"the%20feed%20url%20contains%20%22%3Frest_route%3D%22\"><h3>The feed URL contains \"?rest_route=\"<\/h3><\/dt>\n<dd><p>Your site uses plain permalinks (<strong>Settings \u2192 Permalinks \u2192 Plain<\/strong>), so WordPress builds REST URLs with a query string. Google Ads rejects feed URLs with a query string, and the plugin shows a warning at the top of its page and on the <strong>Health<\/strong> tab. Pick any other permalink structure; the URLs then end in <code>.csv<\/code>. Update the URLs in Google Ads afterwards.<\/p><\/dd>\n<dt id=\"i%20get%20a%20401%20%22rest%20api%20has%20been%20restricted%20to%20authenticated%20users%22%20when%20i%20open%20the%20url\"><h3>I get a 401 \"REST API has been restricted to authenticated users\" when I open the URL<\/h3><\/dt>\n<dd><p>Some security plugins (\"Admin and Site Enhancements\" \/ ASE has this option, Disable REST API, etc.) block all unauthenticated REST traffic. This plugin does <strong>not<\/strong> attempt to override those policies \u2014 that would be hostile to the security setting you deliberately chose.<\/p>\n\n<p>If you hit this error, whitelist the plugin's feed prefix in your security plugin's exclusion list: <code>\/wp-json\/cmls\/v1\/feed\/<\/code>. Google Ads (and any other configured consumer) needs unauthenticated GET access to that prefix \u2014 the tokenized URL is the sole authentication mechanism.<\/p><\/dd>\n<dt id=\"phone%20numbers%20%E2%80%94%20how%20reliable%20is%20the%20normalization%20without%20libphonenumber%3F\"><h3>Phone numbers \u2014 how reliable is the normalization without libphonenumber?<\/h3><\/dt>\n<dd><p>For most countries (RO, DE, UK, FR, ES, US, NL, BE, PT, IT mobile, \u2026) it's correct. The routine accepts <code>+<\/code>, <code>00<\/code>, the country's national trunk <code>0<\/code>, and bare national numbers (using the order's billing country to fill in the dialing code).<\/p>\n\n<p>A number typed without <code>+<\/code> or <code>00<\/code> but starting with the dialing code's digits is read as already international. For countries whose numbers have a fixed length (India, Brazil, Norway, Poland, Singapore, Kazakhstan and 29 more) the plugin checks the length first, so an Indian mobile like <code>9123456789<\/code> becomes <code>+919123456789<\/code>, not <code>+9123456789<\/code>. Countries with variable-length numbers (Germany, Austria, Italy, Finland, \u2026) keep the simpler rule.<\/p>\n\n<p>There is a known limitation for <strong>Italian landlines<\/strong>: in Italy the leading <code>0<\/code> is part of the national number, not a trunk prefix, so the routine may strip a digit it shouldn't. Italian mobile numbers (3xx) are fine.<\/p>\n\n<p><strong>If you need strict accuracy across all locales, swap <code>CMLS_Writer::normalize_phone()<\/code> for a libphonenumber-based implementation.<\/strong><\/p><\/dd>\n<dt id=\"is%20wp-cron%20reliable%20enough%20for%20the%20incremental%20updates%3F\"><h3>Is WP-Cron reliable enough for the incremental updates?<\/h3><\/dt>\n<dd><p>WordPress's built-in cron is request-triggered: it only fires if someone hits the site. On low-traffic stores it may drift. For predictable runs, configure a real system cron that calls <code>wp-cron.php<\/code> every minute \u2014 standard WordPress practice and recommendation.<\/p><\/dd>\n<dt id=\"can%20i%20disable%20hashing%20for%20debugging%3F\"><h3>Can I disable hashing for debugging?<\/h3><\/dt>\n<dd><p>Yes. Add the <code>CMLS_DISABLE_HASHING<\/code> define to <code>wp-config.php<\/code>:<\/p>\n\n<pre><code>define( 'CMLS_DISABLE_HASHING', true );\n<\/code><\/pre><\/dd>\n<dt id=\"does%20this%20support%20high-performance%20order%20storage%20%28hpos%29%20%2F%20legacy%20postmeta%3F\"><h3>Does this support High-Performance Order Storage (HPOS) \/ Legacy Postmeta?<\/h3><\/dt>\n<dd><p>Yes. The plugin declares <code>custom_order_tables<\/code> compatibility and queries <code>wc_orders<\/code> + <code>wc_order_addresses<\/code> directly when HPOS is enabled. On classic (postmeta) storage it falls back to <code>wp_posts<\/code> + <code>wp_postmeta<\/code> with a single conditional-aggregation query per batch.<\/p><\/dd>\n<dt id=\"does%20the%20plugin%20send%20any%20data%20outside%20my%20server%3F\"><h3>Does the plugin send any data outside my server?<\/h3><\/dt>\n<dd><p>The plugin makes <strong>no requests to third-party services<\/strong> \u2014 no analytics, no tracking pixels, no telemetry. All processing happens on your WordPress install. (The directory-listing safety check is a one-second request from your server to its own uploads URL \u2014 see the directory-listing FAQ.)<\/p>\n\n<p>However, the whole point of the workflow is that <strong>Google Ads (or any other consumer you configure) will pull the CSV from your site<\/strong> at the tokenized URL. Anyone in possession of that URL can retrieve a file containing:<\/p>\n\n<ul>\n<li>SHA-256 hashes of the customer's email, phone, first name, and last name;<\/li>\n<li>the customer's billing country and postal code in plaintext.<\/li>\n<\/ul>\n\n<p>A value segment URL also shows that each customer in it belongs to that spend group (for example your top 10% by spend).<\/p>\n\n<p>Treat the tokenized URLs as secrets. If one leaks, use <strong>Reset token<\/strong> in the admin (or <code>wp cmls reset-token<\/code>) to invalidate all existing URLs and rename the files on disk. See the site's Privacy Policy page \u2014 this plugin registers suggested policy text via WordPress's Privacy tool.<\/p><\/dd>\n<dt id=\"where%20are%20the%20files%20stored%3F\"><h3>Where are the files stored?<\/h3><\/dt>\n<dd><p>wp-content\/uploads\/customer-match-list-google-ads\/customer-match-{hash}-{country}.csv, and <code>customer-match-{hash}-seg-{id}.csv<\/code> for value segments. During a rebuild the new files are written next to them with a <code>.build<\/code> suffix. The <code>{hash}<\/code> is a 16-character hex derived from the secret token, so filenames are not guessable. An <code>index.php<\/code> placeholder prevents listing if your host happens to have it enabled, and a deny-all <code>.htaccess<\/code> stops Apache from serving the files directly: the tokenized URLs read them through WordPress instead. nginx ignores <code>.htaccess<\/code>; there the unguessable names protect the files, or add <code>location ^~ \/wp-content\/uploads\/customer-match-list-google-ads\/ { deny all; }<\/code> to your server block. The all-countries file isn't stored: it's put together from the country files when Google fetches it.<\/p><\/dd>\n<dt id=\"what%20about%20directory%20listing%3F\"><h3>What about directory listing?<\/h3><\/dt>\n<dd><p>The settings page does a one-second loopback HTTP request to the uploads directory URL (<code>\/wp-content\/uploads\/customer-match-list-google-ads\/<\/code>) when you open it, caches the result for an hour, and shows one of three banners:<\/p>\n\n<ul>\n<li><strong>Green \u2014 safe.<\/strong> The server returned 200\/401\/403\/404 with no listing markers (typically because the bundled <code>.htaccess<\/code> denies access, the bundled <code>index.php<\/code> silence file is served, or the webserver explicitly blocks directory listings).<\/li>\n<li><strong>Red \u2014 exposed.<\/strong> The server returned 200 with <code>Index of \u2026<\/code> or with our CSV filename prefix in the body. Anyone who guesses the directory URL can enumerate your country files. Fix it: Apache \u2192 <code>Options -Indexes<\/code> (or rely on the WordPress-shipped <code>.htaccess<\/code>); nginx \u2192 <code>autoindex off;<\/code> in the relevant <code>location<\/code> block.<\/li>\n<li><strong>Yellow \u2014 could not verify.<\/strong> The loopback request failed within the one-second budget (NAT \/ split-DNS \/ firewall blocking PHP self-calls). Open the URL shown in the notice in a private\/incognito browser window \u2014 if you see a listing, restrict it; otherwise you're fine.<\/li>\n<\/ul>\n\n<p>The check is cached for an hour and re-runs immediately if you click <strong>Re-check now<\/strong>.<\/p><\/dd>\n<dt id=\"what%20about%20google%27s%20540-day%20lookback%20window%3F\"><h3>What about Google's 540-day lookback window?<\/h3><\/dt>\n<dd><p>Google Ads keeps a Customer Match member for up to 540 days after that member was last uploaded or refreshed. Every time Google fetches your file, everyone in it is uploaded again, so a customer stays in the audience for as long as they are in the file, however old their orders are.<\/p>\n\n<p>That's what the <strong>audience window<\/strong> (Settings tab) is for. Every night the files are rebuilt from the orders inside the window, so a customer whose last order is older drops out of the file. Google then stops refreshing them, and they leave the audience at the latest 540 days after the last fetch that included them. New installs start at 540 days; pick \"All orders\" to keep everyone.<\/p>\n\n<p>The backfill date range only limits the initial history import; the audience window is what keeps the list trimmed afterwards.<\/p>\n\n<p>Sites upgraded from 1.x keep their current files, which only ever grow, until the settings are saved once. Saving turns on the nightly rebuild with the window you chose.<\/p><\/dd>\n<dt id=\"can%20i%20include%20orders%20that%20aren%27t%20%60completed%60%3F\"><h3>Can I include orders that aren't `completed`?<\/h3><\/dt>\n<dd><p>Yes. On the <strong>Settings<\/strong> tab, <strong>Order statuses<\/strong> lists every status, including custom ones added by other plugins (for example \"Shipped\"). Shortcuts pick Completed only, every paid status, or everything. The scheduled writer, the late-status hook, the admin backfill and the nightly rebuild all use your selection.<\/p>\n\n<p>The page warns you when you select a status where the customer hasn't paid (pending payment, on hold, failed, cancelled, refunded): those customers didn't buy, and failed orders often come from bots testing stolen cards.<\/p>\n\n<p>When an order leaves your selection (for example Completed \u2192 Refunded), the customer is removed at the next nightly rebuild, unless another of their orders still qualifies.<\/p>\n\n<p>For a one-off run with other statuses, use <code>wp cmls backfill --statuses=processing,completed,on-hold<\/code> or <code>--all-statuses<\/code>.<\/p><\/dd>\n<dt id=\"what%20are%20value%20segments%3F\"><h3>What are value segments?<\/h3><\/dt>\n<dd><p>Extra files that hold only some of your customers, each with its own URL to add in Google Ads as a separate Customer Match list. Turn them on under <strong>Value segments<\/strong>, then add up to 5 segments from a template or your own rules:<\/p>\n\n<ul>\n<li><strong>Minimum spend:<\/strong> a fixed amount in your store currency, or the top X% of customers by spend (follows your store as it grows).<\/li>\n<li><strong>Minimum orders:<\/strong> 2 or more for repeat buyers.<\/li>\n<li><strong>Last order:<\/strong> within the last N days, or not in the last N days (big spenders who stopped buying, for win-back campaigns).<\/li>\n<\/ul>\n\n<p>Spend is a customer's total across their paid orders, either the order total or net sales (without tax and shipping), with refunds subtracted. It's counted over the audience window or over all orders ever, but a customer always needs an order inside the window to be in any file. If your orders use more than one currency, you can leave the other currencies out of the totals or convert them at rates you enter.<\/p>\n\n<p>Segments are recalculated at every nightly rebuild. Google doesn't serve lists with fewer than about 100 members, so the admin flags small segments. By default each segment is one file for all countries; you can split them by country.<\/p><\/dd>\n<dt id=\"how%20do%20i%20add%20value%20segment%20urls%20in%20google%20ads%3F\"><h3>How do I add value segment URLs in Google Ads?<\/h3><\/dt>\n<dd><p>Each segment URL is its own Customer Match list in Google Ads. Segment files are created at the nightly rebuild; click <strong>Tools \u2192 Rebuild now<\/strong> after adding a segment if you don't want to wait. The URLs are on the <strong>Feeds<\/strong> tab under <strong>Value segment URLs<\/strong>: one per segment, or one per segment and country if you split them by country.<\/p>\n\n<p>For each URL:<\/p>\n\n<ol>\n<li>In Google Ads, open <strong>Tools \u2192 Shared library \u2192 Audience manager<\/strong>.<\/li>\n<li>Click <strong>+ Create remarketing list \u2192 + Customer list<\/strong>.<\/li>\n<li>Under <strong>Data source<\/strong>, choose <strong>Connect a new data source<\/strong>, pick <strong>HTTPS<\/strong>, accept the customer data policy and click <strong>Continue<\/strong>. Each URL needs its own connection.<\/li>\n<li>Paste the segment URL. Google requires a username and password, but the plugin doesn't use them (the token in the URL is the key), so enter any values.<\/li>\n<li>Choose customer contact information as the data type and map the columns. Email, Phone, First Name and Last Name are already SHA-256 hashed; Country and Zip are plain text, as Google expects. Don't add transformations.<\/li>\n<li>Name the connection and the list after the segment (for example \"CMLS VIP \u2013 all countries\"), confirm the policy and save.<\/li>\n<\/ol>\n\n<p>Google takes a day or two to process a new list, then shows its size in Audience manager. It only counts customers it can match to a Google account, so a segment needs a few hundred rows to stay above the ~100 minimum.<\/p>\n\n<p>On Search and Shopping campaigns, add the lists in <strong>Observation<\/strong> mode and raise bids for your best customers. On Performance Max, add them to the asset group as an <strong>Audience signal<\/strong>. A lapsed high-value segment suits win-back campaigns.<\/p>\n\n<p>Editing a segment keeps its URL. Deleting a segment stops its URL, so remove the list in Google Ads too. <strong>Reset token<\/strong> changes every URL, country files and segments alike, and each Google Ads connection then needs the new one.<\/p><\/dd>\n<dt id=\"how%20is%20the%20deduplication%20table%20sized%3F\"><h3>How is the deduplication table sized?<\/h3><\/dt>\n<dd><p>One row per unique <code>(email_hash, country)<\/code>. At ~110 bytes per row, 1M unique customers across all countries is roughly 100 MB. While a rebuild runs, a second copy is filled next to it (plus a per-customer totals table when value segments are on), so plan for about twice that during the rebuild. The spare copies are emptied when it finishes.<\/p><\/dd>\n<dt id=\"does%20it%20work%20on%20wordpress%20multisite%3F\"><h3>Does it work on WordPress multisite?<\/h3><\/dt>\n<dd><p>Yes, activated per site: each site gets its own token, files, tables and schedule. Activate it from each site's <strong>Plugins<\/strong> screen; network activation is refused, because the setup has to run on every site. If an older version was network-activated, network-deactivate it and activate it per site. Uninstalling cleans up every site.<\/p><\/dd>\n<dt id=\"will%20deactivating%20woocommerce%20break%20it%3F\"><h3>Will deactivating WooCommerce break it?<\/h3><\/dt>\n<dd><p>The plugin's admin page shows a notice and stops scheduling cron, but data isn't lost. Re-activate WC and the plugin resumes from the noted order.<\/p>\n\n<p>If you uninstall the plugin then the <code>uninstall.php<\/code> routine cleans up the option keys, database tables, and any generated files.<\/p><\/dd>\n<dt id=\"gdpr%20%2F%20privacy\"><h3>GDPR \/ privacy<\/h3><\/dt>\n<dd><p>The files contain hashed personal information (email, phone, name) and plaintext country + ZIP. Treat the directory and its access URLs as sensitive data!<\/p>\n\n<p>The plugin only creates files on your own server and does not read consent-management records: every order in the selected range is exported as-is. Whether a given customer may be included in an advertising audience \u2014 and whether Google or Meta may receive the file \u2014 depends on your store's consent setup and local legal requirements; applying them is your responsibility as the data controller.<\/p>\n\n<p>If a customer requests deletion under GDPR \/ similar:<\/p>\n\n<ol>\n<li>Delete or anonymize the WooCommerce order(s).<\/li>\n<li>Wait for the nightly rebuild, or click <strong>Tools \u2192 Rebuild now<\/strong> (<code>wp cmls rebuild<\/code>) \u2014 the deleted customer is gone from every file, value segments included.<\/li>\n<\/ol>\n\n<p>Targeted per-row deletion isn't supported on purpose: the SHA-256 hashing is one-way and rebuilding from source is fast.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>2.1.1<\/h4>\n\n<ul>\n<li>Readme and admin wording: clearer description of how the files become Google Ads customer lists, and the same terms in both (feed URLs, nightly rebuild, Update now). No functional changes.<\/li>\n<\/ul>\n\n<h4>2.1.0<\/h4>\n\n<ul>\n<li>New: Health tab. It checks the schedule, the last file write (and the last write error), the storage folder, directory listing, the nightly rebuild, permalinks, hashing and list sizes, and counts the rows in every file against what it should hold. A badge on the tab shows the number of problems.<\/li>\n<li>New: optional all-countries file, one more URL ending in <code>\/all.csv<\/code> with every country together (Settings tab).<\/li>\n<li>New: <code>wp cmls update-now<\/code> and <code>wp cmls rebuild-now<\/code>, matching the admin buttons.<\/li>\n<li>Fix: <code>wp cmls run-now<\/code>, <code>reset-token<\/code> and <code>delete-file<\/code> now work under the names the readme documents; the underscore names keep working.<\/li>\n<li>Fix: a token reset that can't rename every file now changes nothing and says so, instead of leaving files behind under the old name.<\/li>\n<li>Fix: a backfill no longer moves the scheduled writer's position, which could make it skip orders the backfill's status or date filter left out.<\/li>\n<li>Fix: Indian, Norwegian, Polish, Singaporean, Kazakh and other national numbers that start with the country's dialing code digits are no longer read as international.<\/li>\n<li>Fix: a lost update schedule is put back the next time someone opens wp-admin.<\/li>\n<li>Fix: a warning when plain permalinks make the feed URLs unusable for Google Ads.<\/li>\n<li>Fix: the first activation on a new site no longer logs a database error, and <code>wp plugin activate<\/code> no longer fails with WP_DEBUG display on.<\/li>\n<li>Fix: a postcode can no longer start with a spreadsheet formula character.<\/li>\n<li>Security: a deny-all <code>.htaccess<\/code> in the storage folder, a download name that doesn't reveal the file name on disk, and the feed routes are no longer listed in the REST API index.<\/li>\n<li>Multisite: network activation is refused (activate per site); uninstall cleans up every site.<\/li>\n<li>Value segment URLs no longer repeat the member count when a segment has a single country.<\/li>\n<li>The Danger zone buttons on the Tools tab are red.<\/li>\n<li>The suggested privacy-policy text now says \"no requests to third-party services\".<\/li>\n<\/ul>\n\n<h4>2.0.0<\/h4>\n\n<ul>\n<li>New: audience window (all orders, last 365 or 540 days, or custom). Every night the files are rebuilt from the orders inside the window and swapped in at once, so customers who stopped buying drop out. Feed URLs stay the same. Also available as \"Rebuild now\" in the admin and <code>wp cmls rebuild<\/code>.<\/li>\n<li>New installs start with a 540-day window. Sites updating from 1.x keep their append-only files, with no nightly rebuild, until the Settings tab is saved once.<\/li>\n<li>New: choose which order statuses count, including custom statuses from other plugins. The scheduled writer, the late-status hook, the admin backfill and the rebuild all use the selection.<\/li>\n<li>New: value segments \u2014 up to 5 extra lists by spend (top X% or a minimum amount), order count and last order date, with templates, net or total spend, refunds subtracted and multi-currency handling.<\/li>\n<li>New: the admin page is split into Feeds, Settings, Value segments and Tools tabs, with \"Run now\" and \"Rebuild now\" buttons.<\/li>\n<li>Fix: the scheduled writer and the late-status hook no longer add orders older than the window (backdated or imported orders).<\/li>\n<li>Fix: when a CSV write fails, those customers are no longer marked as exported; the next run writes them.<\/li>\n<li>Fix: a new country file can no longer lose rows written at the same moment by another process.<\/li>\n<li>Fix: saving the settings without changing the interval no longer reports \"Invalid interval\".<\/li>\n<li>WP-CLI: new <code>wp cmls rebuild<\/code>; <code>wp cmls backfill<\/code> now defaults to the statuses saved in the settings; <code>wp cmls status<\/code> also shows the audience window, statuses and rebuild times.<\/li>\n<li>The suggested privacy-policy text now covers the selected statuses, value segments and the nightly rebuild.<\/li>\n<li>Admin JavaScript no longer uses inline event handlers.<\/li>\n<li>Tested with WordPress 7.1 and WooCommerce 11.2.<\/li>\n<\/ul>\n\n<h4>1.1.2<\/h4>\n\n<ul>\n<li>Orders that turn \"completed\" late (delayed payments, manual completion) are now appended the moment they complete instead of being missed by the scheduled writer.<\/li>\n<li>\"Delete all files &amp; reset dedup\" no longer causes the next scheduled run to rebuild the entire order history \u2014 rebuilding stays explicit via backfill.<\/li>\n<li>New FAQ on Google's automatic conversion-based customer lists.<\/li>\n<\/ul>\n\n<h4>1.1.1<\/h4>\n\n<ul>\n<li>Consistent plugin name: \"Customer Match List for Google Ads\". The exported files also work for Meta Ads Custom Audiences (see FAQ) \u2014 same hashing.<\/li>\n<li>Confirmed compatibility with WooCommerce 10.9.<\/li>\n<li>Readme and documentation cleanup. No functional changes.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>Feed URLs now end in <code>.csv<\/code> so Google Ads' HTTPS connector accepts them.<\/li>\n<li>Hashing normalization aligned with the Google Customer Match spec (Unicode-aware lowercase, name punctuation stripping, US ZIP truncation, double-hash guard).<\/li>\n<li>Fresh activations no longer sweep historical orders \u2014 backfill is explicit (admin button or WP-CLI).<\/li>\n<li>Admin JS moved into enqueued, page-scoped files.<\/li>\n<li>Added suggested privacy-policy text, sub-100-row serving warning, CSV-injection hardening on zip values, and removed the REST authentication override.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release: per-country CSVs with <code>(identity, country)<\/code> dedup, WP-CLI and Action Scheduler backfills, tokenized REST download endpoint, HPOS-first SQL.<\/li>\n<\/ul>","raw_excerpt":"Build Google Ads customer lists (Customer Match) from WooCommerce orders: one auto-updated CSV per country on a secure URL. Works with Meta Ads.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/318548","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=318548"}],"author":[{"embeddable":true,"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/rwky"}],"wp:attachment":[{"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=318548"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=318548"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=318548"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=318548"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=318548"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/hr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=318548"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}