{"id":389876,"date":"2026-10-23T05:42:52","date_gmt":"2026-10-23T05:42:52","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/utm-attribution-tracker\/"},"modified":"2026-10-23T05:42:22","modified_gmt":"2026-10-23T05:42:22","slug":"utmatra-utm-attribution-and-tracking","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/utmatra-utm-attribution-and-tracking\/","author":8448958,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.0","stable_tag":"1.0.0","tested":"7.1.3","requires":"6.9","requires_php":"8.0","requires_plugins":null,"header_name":"Utmatra - UTM Attribution and Tracking","header_author":"Lindeni Mahlalela","header_description":"Track UTM-tagged links, click IDs, and first\/last-touch attribution across form leads and WooCommerce orders, with a live Chart.js dashboard.","assets_banners_color":"","last_updated":"2026-10-23 05:42:22","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/mahlamusa\/utmatra-utm-attribution-and-tracking","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":74,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.0":{"tag":"1.0.0","author":"mahlamusa","date":"2026-10-23 05:42:22","revision":3760966}},"upgrade_notice":{"1.0.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3760966,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3760966,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3760966,"resolution":false,"location":"assets","locale":false}},"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3760966,"resolution":"1","location":"assets","locale":"","width":1440,"height":1500},"screenshot-10.png":{"filename":"screenshot-10.png","revision":3760966,"resolution":"10","location":"assets","locale":"","width":1440,"height":900},"screenshot-11.png":{"filename":"screenshot-11.png","revision":3760966,"resolution":"11","location":"assets","locale":"","width":1440,"height":900},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3760966,"resolution":"2","location":"assets","locale":"","width":1440,"height":900},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3760966,"resolution":"3","location":"assets","locale":"","width":1440,"height":1000},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3760966,"resolution":"4","location":"assets","locale":"","width":1440,"height":900},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3760966,"resolution":"5","location":"assets","locale":"","width":1440,"height":1500},"screenshot-6.png":{"filename":"screenshot-6.png","revision":3760966,"resolution":"6","location":"assets","locale":"","width":1440,"height":927},"screenshot-7.png":{"filename":"screenshot-7.png","revision":3760966,"resolution":"7","location":"assets","locale":"","width":1440,"height":900},"screenshot-8.png":{"filename":"screenshot-8.png","revision":3760966,"resolution":"8","location":"assets","locale":"","width":1440,"height":900},"screenshot-9.png":{"filename":"screenshot-9.png","revision":3760966,"resolution":"9","location":"assets","locale":"","width":1440,"height":900}},"screenshots":{"1":"Overview: clicks over time against the previous period, conversions, attributed revenue, and breakdowns by source, campaign, medium, device, channel, landing page, referrer and ad platform.","2":"Links: tracked short links with their campaign, source and medium, click counts and redirect URLs.","3":"Journeys: recent orders and form leads with their first and last touch, expandable into the full touch timeline.","4":"Add tracked link: UTM fields with presets, the generated short link, and a build-a-URL-only mode.","5":"Settings: tracking, custom parameters, data retention, attribution window, consent, weekly report and webhooks.","6":"Tracking Doctor: local checks for caching, consent, cookies, form coverage and WooCommerce attribution.","7":"Short links and QR codes: each tracked link gets a pretty \/go\/ short link and a QR code generated in the browser, with a PNG download.","8":"UTM naming presets: a saved preset fills source and medium in one click, and values are lowercased with spaces replaced by hyphens.","9":"Build a URL only: get a tagged URL to paste anywhere, without creating a short link.","10":"Edit tracked link: the short link, the final tagged URL and the [utmatra_link] shortcode to embed it.","11":"The [utmatra_link] shortcode in the block editor, wrapping custom link text."}},"plugin_section":[262246],"plugin_tags":[232,9067,550,24188,286],"plugin_category":[36,45],"plugin_contributors":[80710],"plugin_business_model":[],"class_list":["post-389876","plugin","type-plugin","status-publish","hentry","plugin_section-dashboard-widgets","plugin_tags-analytics","plugin_tags-attribution","plugin_tags-tracking","plugin_tags-utm","plugin_tags-woocommerce","plugin_category-analytics","plugin_category-ecommerce","plugin_contributors-mahlamusa","plugin_committers-mahlamusa"],"banners":[],"icons":{"svg":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/icon.svg?rev=3760966","icon":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/icon.svg?rev=3760966","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-1.png?rev=3760966","caption":"Overview: clicks over time against the previous period, conversions, attributed revenue, and breakdowns by source, campaign, medium, device, channel, landing page, referrer and ad platform."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-2.png?rev=3760966","caption":"Links: tracked short links with their campaign, source and medium, click counts and redirect URLs."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-3.png?rev=3760966","caption":"Journeys: recent orders and form leads with their first and last touch, expandable into the full touch timeline."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-4.png?rev=3760966","caption":"Add tracked link: UTM fields with presets, the generated short link, and a build-a-URL-only mode."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-5.png?rev=3760966","caption":"Settings: tracking, custom parameters, data retention, attribution window, consent, weekly report and webhooks."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-6.png?rev=3760966","caption":"Tracking Doctor: local checks for caching, consent, cookies, form coverage and WooCommerce attribution."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-7.png?rev=3760966","caption":"Short links and QR codes: each tracked link gets a pretty \/go\/ short link and a QR code generated in the browser, with a PNG download."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-8.png?rev=3760966","caption":"UTM naming presets: a saved preset fills source and medium in one click, and values are lowercased with spaces replaced by hyphens."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-9.png?rev=3760966","caption":"Build a URL only: get a tagged URL to paste anywhere, without creating a short link."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-10.png?rev=3760966","caption":"Edit tracked link: the short link, the final tagged URL and the [utmatra_link] shortcode to embed it."},{"src":"https:\/\/ps.w.org\/utmatra-utm-attribution-and-tracking\/assets\/screenshot-11.png?rev=3760966","caption":"The [utmatra_link] shortcode in the block editor, wrapping custom link text."}],"raw_content":"<!--section=description-->\n<p>Utmatra gives you self-hosted, privacy-respecting campaign tracking for any UTM-tagged link on or off your WordPress site \u2014 no third-party analytics service required.<\/p>\n\n<p><strong>Tracking engine:<\/strong><\/p>\n\n<ul>\n<li><strong>Cache-proof tracking beacon<\/strong> \u2014 a tiny front-end beacon records the first page of every visit (with its referrer) and every UTM-tagged visit, even when your pages are served from a full-page cache, where PHP never runs. Later untagged page views in the same visit send nothing. \"Track the first page of every visit\" in Settings switches visit recording off.<\/li>\n<li><strong>Channel classification<\/strong> \u2014 every recorded visit is filed into a marketing channel at write time: direct, organic search, paid search, paid social, organic social, email, referral, AI assistant (ChatGPT, Perplexity, Claude, Gemini, Copilot and others), display and affiliate.<\/li>\n<li><strong>Passive UTM tracking (no-JS fallback)<\/strong> \u2014 on uncached pages, any request arriving with a <code>utm_*<\/code> parameter or an ad click ID (<code>gclid<\/code>, <code>fbclid<\/code>, <code>msclkid<\/code>, <code>ttclid<\/code>, <code>li_fat_id<\/code>, <code>twclid<\/code>) is also recorded server-side.<\/li>\n<li><strong>Custom parameters<\/strong> \u2014 name up to ten extra URL parameters of your own (a coupon code, an affiliate ID, a promo code) under Settings \u2192 Tracking, and from then on they are captured exactly like the built-in UTM and click-ID parameters: recorded on the event, carried in the attribution cookie, order data and webhook payloads, offered to forms under their own names, exported as CSV columns, and settable per tracked link (see the custom parameters FAQ).<\/li>\n<li><strong>Bot filtering<\/strong> \u2014 crawlers, link-preview unfurlers, and monitoring agents are recognised by user agent and never pollute your stats. Bots still receive the redirect, so social link previews keep working.<\/li>\n<li><strong>Click deduplication<\/strong> \u2014 repeat hits from the same visitor with identical parameters within a 30-minute window (filterable) are recorded once.<\/li>\n<li><strong>First\/last-touch attribution cookie<\/strong> \u2014 campaign parameters or, for visits from a search engine, social network, AI assistant or other site, the channel and referrer host are persisted in a first-party cookie (<code>utmatra_attr<\/code>, 90 days) with separate first-touch and last-touch sets. Direct visits never write it.<\/li>\n<\/ul>\n\n<p><strong>Links &amp; sharing:<\/strong><\/p>\n\n<ul>\n<li><strong>Pretty short links<\/strong> \u2014 every managed link gets a clean <code>\/go\/{slug}\/<\/code> URL (query-string form on plain permalinks). The click is counted before the visitor is forwarded to the destination with full UTM parameters appended.<\/li>\n<li><strong>QR code per link<\/strong> \u2014 generate a scannable QR code for any tracked link straight from the dashboard and download it as a PNG. Generated entirely in your browser \u2014 no external service is contacted.<\/li>\n<li><strong>[utmatra_link] shortcode<\/strong> \u2014 embed tracked links in posts, pages, or widgets; supports enclosing content: <code>[utmatra_link id=\"sale\"]Shop the &lt;strong&gt;sale&lt;\/strong&gt;[\/utmatra_link]<\/code>.<\/li>\n<li><strong>UTM naming conventions<\/strong> \u2014 the add-link form lowercases values and replaces spaces with <code>-<\/code> (with a heads-up notice), and suggests previously used sources, mediums, and campaigns \u2014 so <code>My Newsletter<\/code> and <code>my-newsletter<\/code> never fragment your reports into separate rows. Save a source\/medium\/term\/content combination as a named preset (e.g. \"Instagram Story\") to apply it to future links in one click.<\/li>\n<\/ul>\n\n<p><strong>Reporting:<\/strong><\/p>\n\n<ul>\n<li><strong>Chart.js dashboard<\/strong> \u2014 clicks over time, breakdowns by source, campaign, medium, device type, channel (direct, organic search, paid search, paid social, organic social, email, referral, AI assistant, display, affiliate), top landing pages, top referrers, and ad platform, filterable by date range (7\/30\/90 days) and campaign. The Conversions tile counts WooCommerce orders <strong>and<\/strong> attributed form leads.<\/li>\n<li><strong>WooCommerce revenue attribution<\/strong> \u2014 orders are stamped with the buyer's first- and last-touch campaign data at checkout (visible in a metabox on the order screen), the conversion is recorded once the order is paid, and a revenue-by-campaign chart shows what your campaigns actually earn. Revenue counts orders only; form leads are conversions without a total.<\/li>\n<li><strong>Customer journeys<\/strong> \u2014 a Journeys tab lists recent orders and form leads with every tagged visit that preceded them, built from a random first-party visitor id.<\/li>\n<li><strong>Lead webhooks<\/strong> \u2014 every attributed form submission is recorded as a conversion and can be POSTed, with its attribution data and submitted field values, to Zapier, Make, HubSpot or GoHighLevel (see the webhook FAQ).<\/li>\n<li><strong>CSV export<\/strong> \u2014 download the raw events for any period as a CSV (the newest 10,000 rows), each row with its channel. Conversion rows are included alongside clicks, so the export can hold more rows than the dashboard click total. Exports never include IP hashes or user agents, and cell values are guarded against spreadsheet formula injection.<\/li>\n<li><strong>Read-only REST API<\/strong> \u2014 every dashboard dataset (stats, links, events, journeys, export, diagnostics) is available over <code>utmatra\/v1<\/code> to administrators using WordPress Application Passwords (see the REST API section).<\/li>\n<li><strong>Weekly email summary<\/strong> \u2014 an optional (off by default) weekly email with total clicks, top campaigns, top sources, and revenue when WooCommerce is active.<\/li>\n<li><strong>Tracking Doctor diagnostics<\/strong> \u2014 a \"Run diagnostics\" panel on the settings card runs five rule-based local checks (caching, consent, cookies, per-form attribution coverage, WooCommerce order attribution) and reports pass\/warn\/fail\/skip with a plain-language explanation for each. No AI, no external calls \u2014 every check inspects local plugin\/site state only.<\/li>\n<\/ul>\n\n<p><strong>Forms &amp; integrations:<\/strong><\/p>\n\n<ul>\n<li><strong>Form attribution capture<\/strong> \u2014 add a hidden field named <code>utm_source<\/code> (or any other tracked parameter) to any form and it is auto-filled from the attribution cookie. Zero-config adapters for Contact Form 7, Gravity Forms, Ninja Forms, Elementor Pro Forms, WPForms, Fluent Forms, Formidable Forms, Forminator and Bricks forms.<\/li>\n<\/ul>\n\n<p><strong>Privacy &amp; housekeeping:<\/strong><\/p>\n\n<ul>\n<li><strong>Privacy-safe by design<\/strong> \u2014 IP addresses are stored only as salted SHA-256 hashes; the salt is unique per site.<\/li>\n<li><strong>WP Consent API integration<\/strong> \u2014 the attribution cookie is gated in the visitor's browser on a configurable consent category, deleted on withdrawal, and declared to the API so consent banners can list it.<\/li>\n<li><strong>Data retention<\/strong> \u2014 events are pruned automatically after a configurable retention window (180 days by default, 0 = keep forever) from the dashboard settings card.<\/li>\n<li><strong>Suggested privacy policy text<\/strong> \u2014 provided under Settings \u2192 Privacy \u2192 Policy Guide.<\/li>\n<li><strong>No external services by default<\/strong> \u2014 no CDN calls and no third-party analytics. Chart.js, the QR library and Action Scheduler ship bundled with the plugin. The only outbound requests are the optional conversion webhooks, which stay off until you enter a receiver URL (see External services).<\/li>\n<li><strong>Reliable background jobs<\/strong> \u2014 retention pruning and the weekly report run on Action Scheduler (the job queue WooCommerce uses), listed under Tools \u2192 Scheduled Actions in the <code>utmatra<\/code> group, and they reschedule themselves if they go missing (after a reactivation, or on a multisite subsite). Action Scheduler keeps its own <code>actionscheduler_*<\/code> tables and a once-a-minute WP-Cron queue runner; they are shared with any other plugin that uses it (such as WooCommerce) and are left in place on uninstall.<\/li>\n<\/ul>\n\n<h3>External services<\/h3>\n\n<p>The plugin contacts no external service on its own. Tracking, reports, QR codes and charts all run on your site. The one exception is the optional <strong>conversion webhook<\/strong>, which is off until an administrator saves a receiver URL under Settings \u2192 Webhooks. Once a URL is saved, the site sends a JSON POST request to that URL, and only to that URL:<\/p>\n\n<ul>\n<li><strong>When a tracked form is submitted<\/strong> (lead event): the site URL, the form plugin, form id and title, the submitted field values (these can include a name, email address and phone number; file uploads, passwords and hidden fields are never sent), the visitor's first- and last-touch campaign data (UTM parameters, ad click IDs, custom parameters, channel, referrer host and landing page), the random visitor id, the page URL and a timestamp.<\/li>\n<li><strong>When an attributed WooCommerce order is paid<\/strong> (order event): the site URL, the order id, total and currency, the first- and last-touch campaign data and a timestamp. No customer details are sent.<\/li>\n<li><strong>When an administrator clicks \"Send test webhook\"<\/strong>: a synthetic sample payload.<\/li>\n<\/ul>\n\n<p>Order and lead events can each be switched off. The presets only change the format of the request body; a service is contacted only if you paste one of its URLs. When you use one of these services, its terms and privacy policy apply to the data it receives:<\/p>\n\n<ul>\n<li><strong>Zapier<\/strong> (Catch Hook URL, <code>hooks.zapier.com<\/code>): <a href=\"https:\/\/zapier.com\/legal\">Terms of service<\/a>, <a href=\"https:\/\/zapier.com\/privacy\">Privacy policy<\/a>.<\/li>\n<li><strong>Make<\/strong> (custom webhook URL, <code>hook.*.make.com<\/code>): <a href=\"https:\/\/www.make.com\/en\/terms-and-conditions\">Terms and conditions<\/a>, <a href=\"https:\/\/www.make.com\/en\/privacy-notice\">Privacy notice<\/a>.<\/li>\n<li><strong>HubSpot<\/strong> (Forms API v3, <code>api.hsforms.com<\/code>): leads only, mapped onto your HubSpot form fields. When the visitor has HubSpot's own <code>hubspotutk<\/code> cookie, its value is included so HubSpot can link the submission to that visitor. <a href=\"https:\/\/legal.hubspot.com\/terms-of-service\">Terms of service<\/a>, <a href=\"https:\/\/legal.hubspot.com\/privacy-policy\">Privacy policy<\/a>.<\/li>\n<li><strong>GoHighLevel<\/strong> (Inbound Webhook trigger, <code>services.leadconnectorhq.com<\/code>): <a href=\"https:\/\/www.gohighlevel.com\/terms-of-service\">Terms of service<\/a>, <a href=\"https:\/\/www.gohighlevel.com\/privacy-policy\">Privacy policy<\/a>.<\/li>\n<\/ul>\n\n<h3>Source code and third-party libraries<\/h3>\n\n<p>The full, human-readable source of this plugin, including the bundled libraries, is public on GitHub: <a href=\"https:\/\/github.com\/mahlamusa\/utmatra-utm-attribution-and-tracking\">github.com\/mahlamusa\/utmatra-utm-attribution-and-tracking<\/a>.<\/p>\n\n<p>Bundled libraries (the two JavaScript files ship minified as published by their authors; their unminified source is linked below):<\/p>\n\n<ul>\n<li><strong>Chart.js<\/strong> 4.5.1 (<code>assets\/js\/vendor\/chart.umd.min.js<\/code>), MIT license. Source: <a href=\"https:\/\/github.com\/chartjs\/Chart.js\">github.com\/chartjs\/Chart.js<\/a>.<\/li>\n<li><strong>QR Code Generator<\/strong> by Kazuhiko Arase (<code>assets\/js\/vendor\/qrcode.min.js<\/code>), MIT license. Source: <a href=\"https:\/\/github.com\/kazuhikoarase\/qrcode-generator\">github.com\/kazuhikoarase\/qrcode-generator<\/a>.<\/li>\n<li><strong>Action Scheduler<\/strong> (<code>vendor\/woocommerce\/action-scheduler<\/code>), GPL-3.0, installed with Composer from <code>composer.json<\/code>. Source: <a href=\"https:\/\/github.com\/woocommerce\/action-scheduler\">github.com\/woocommerce\/action-scheduler<\/a>.<\/li>\n<\/ul>\n\n<h3>Filter and action reference<\/h3>\n\n<p>All filters shipped by the plugin:<\/p>\n\n<ul>\n<li><strong><code>utmatra_track_logged_in_users<\/code><\/strong> <em>(bool, default <code>false<\/code>)<\/em> \u2014 return <code>true<\/code> to record clicks from logged-in administrators (excluded by default to keep test traffic out of your stats).\n    add_filter( 'utmatra_track_logged_in_users', '__return_true' );<\/li>\n<li><strong><code>utmatra_is_bot<\/code><\/strong> <em>(bool, receives the user agent string)<\/em> \u2014 override the bot verdict for a user agent. Bots are redirected but never recorded.<\/li>\n<li><strong><code>utmatra_client_ip<\/code><\/strong> <em>(string)<\/em> \u2014 the client IP used for hashing, deduplication, and rate limiting. Defaults to <code>REMOTE_ADDR<\/code>; see the reverse proxy FAQ above.<\/li>\n<li><strong><code>utmatra_dedup_window<\/code><\/strong> <em>(int, seconds, default 1800)<\/em> \u2014 the window within which an identical click from the same visitor is recorded once. Return <code>0<\/code> to disable deduplication.<\/li>\n<li><strong><code>utmatra_rate_limit<\/code><\/strong> <em>(int, requests\/minute\/IP, default 15)<\/em> \u2014 the collect endpoint's rate limit. Raise it when many visitors share one IP (see the reverse proxy FAQ above).<\/li>\n<li><strong><code>utmatra_retention_days<\/code><\/strong> <em>(int, days, default from the settings card \/ 180)<\/em> \u2014 the event retention window. <code>0<\/code> keeps events forever.<\/li>\n<li><strong><code>utmatra_cookie_days<\/code><\/strong> <em>(int, days, default 90)<\/em> \u2014 the lifetime of both attribution cookies (<code>utmatra_attr<\/code>, <code>utmatra_vid<\/code>). Defaults to the Attribution window setting on the dashboard; the filter overrides it.<\/li>\n<li><strong><code>utmatra_enable_cookie<\/code><\/strong> <em>(bool, receives <code>true<\/code>)<\/em> \u2014 site-wide master switch for both attribution cookies (<code>utmatra_attr<\/code>, <code>utmatra_vid<\/code>); return <code>false<\/code> to never set them. Per-visitor consent is handled separately through the WP Consent API (see the consent FAQ).<\/li>\n<li><strong><code>utmatra_consent_gate_events<\/code><\/strong> <em>(bool, default <code>false<\/code>)<\/em> \u2014 return <code>true<\/code> to also withhold event recording for tagged visits and untagged landing visits (the collect request and the PHP fallback) until the visitor grants the configured consent category.<\/li>\n<li><strong><code>utmatra_report_recipient<\/code><\/strong> <em>(string, default the admin email)<\/em> \u2014 the recipient address of the weekly summary email.<\/li>\n<li><strong><code>utmatra_channel<\/code><\/strong> <em>(string, receives the computed channel slug and the event row)<\/em> \u2014 override the channel a visit, click or conversion is filed under; return one of <code>direct<\/code>, <code>organic_search<\/code>, <code>paid_search<\/code>, <code>paid_social<\/code>, <code>organic_social<\/code>, <code>email<\/code>, <code>referral<\/code>, <code>ai_assistant<\/code>, <code>display<\/code>, <code>affiliate<\/code>, <code>paid_other<\/code> or <code>other<\/code> (anything else keeps the computed value).<\/li>\n<li><strong><code>utmatra_search_engine_hosts<\/code><\/strong> <em>(string[])<\/em> \u2014 the referrer hosts classified as search engines. An entry with a dot matches the host and its subdomains; an entry without a dot (<code>google<\/code>) matches any label of the host.<\/li>\n<li><strong><code>utmatra_social_network_hosts<\/code><\/strong> <em>(string[])<\/em> \u2014 the referrer hosts classified as social networks, same matching rule.<\/li>\n<li><strong><code>utmatra_ai_assistant_hosts<\/code><\/strong> <em>(string[])<\/em> \u2014 the referrer hosts classified as AI assistants, same matching rule; checked before the search-engine list.<\/li>\n<\/ul>\n\n<p>Actions:<\/p>\n\n<ul>\n<li><strong><code>utmatra_lead_recorded<\/code><\/strong> <em>(receives the integration slug and the lead record)<\/em> \u2014 fires after an attributed form submission has been recorded as a lead conversion. The record holds <code>form_id<\/code>, <code>form_title<\/code>, <code>posted<\/code> (the attribution data), <code>created_at<\/code>, <code>page_url<\/code> and <code>fields<\/code> (the cleaned submitted values, which the plugin itself never stores). The lead webhook listens on this action.<\/li>\n<\/ul>\n\n<h3>REST API<\/h3>\n\n<p>Every dataset the dashboard shows is available read-only over the <code>utmatra\/v1<\/code> REST namespace. All routes are GET, require an administrator (<code>manage_options<\/code>), and work with <a href=\"https:\/\/make.wordpress.org\/core\/2020\/11\/05\/application-passwords-integration-guide\/\">Application Passwords<\/a>: create one under <strong>Users \u2192 Profile \u2192 Application Passwords<\/strong> (needs HTTPS, or <code>WP_ENVIRONMENT_TYPE=local<\/code> on a development site), then authenticate with it as the user:<\/p>\n\n<pre><code>curl -u \"admin:xxxx xxxx xxxx xxxx xxxx xxxx\" \"https:\/\/example.com\/wp-json\/utmatra\/v1\/events?period=30&amp;kind=order&amp;per_page=100\"\n<\/code><\/pre>\n\n<p>Logged-in administrators can also call the routes from the dashboard (cookie plus nonce authentication) \u2014 that is how the plugin's own JS loads its data.<\/p>\n\n<p><strong>Conventions.<\/strong> All timestamps are UTC <code>YYYY-MM-DD HH:MM:SS<\/code>. The <code>custom<\/code> and <code>meta<\/code> columns are always JSON objects (<code>{}<\/code> when empty). <code>ip_hash<\/code> and <code>user_agent<\/code> are never returned by any route. Invalid arguments return HTTP 400 with WordPress's standard envelope, e.g. <code>{ \"code\": \"rest_invalid_param\", \"message\": \"Invalid parameter(s): kind\", \"data\": { \"status\": 400, \"params\": { \"kind\": \"\u2026\" } } }<\/code>. Unauthenticated requests get 401; authenticated non-administrators get 403. <code>visit<\/code> rows appear only while \"Track the first page of every visit\" is on; <code>channel<\/code> values and <code>custom<\/code> parameter keys appear on rows recorded since those features were configured.<\/p>\n\n<p><strong><code>GET \/stats<\/code><\/strong> \u2014 args <code>period<\/code> (days, default 30), <code>campaign<\/code>, <code>since<\/code>\/<code>until<\/code> (<code>Y-m-d<\/code>, a custom range overriding <code>period<\/code>). Response: <code>clicks_by_day[]<\/code> (<code>date<\/code>, <code>count<\/code>, <code>conversions<\/code>), <code>by_source[]<\/code>, <code>by_campaign[]<\/code>, <code>by_medium[]<\/code>, <code>by_device[]<\/code>, <code>by_channel[]<\/code>, <code>by_landing[]<\/code>, <code>by_platform[]<\/code>, <code>by_referrer[]<\/code> (each <code>label<\/code> + <code>count<\/code>), <code>revenue_by_campaign[]<\/code> (<code>label<\/code>, <code>orders<\/code>, <code>revenue<\/code>), <code>total<\/code>, <code>previous_total<\/code>, <code>previous_clicks_by_day[]<\/code>, <code>conversions<\/code>, <code>all_time_total<\/code>, <code>campaigns[]<\/code>.<\/p>\n\n<p><strong><code>GET \/links<\/code><\/strong> \u2014 response <code>links[]<\/code> (<code>id<\/code>, <code>label<\/code>, <code>destination<\/code>, <code>destination_raw<\/code>, <code>redirect_url<\/code>, <code>clicks<\/code>, <code>utm_campaign<\/code>, <code>utm_source<\/code>, <code>utm_medium<\/code>, <code>utm_id<\/code>, <code>utm_term<\/code>, <code>utm_content<\/code>, <code>custom<\/code> object), <code>suggestions<\/code> (<code>sources<\/code>, <code>mediums<\/code>, <code>campaigns<\/code>), <code>presets[]<\/code>.<\/p>\n\n<p><strong><code>GET \/export<\/code><\/strong> \u2014 args <code>period<\/code>, <code>campaign<\/code>, <code>since<\/code>, <code>until<\/code>, <code>include_visits<\/code> (bool, default false). Response <code>rows[]<\/code> with the CSV column set listed in the export FAQ (plus one key per configured custom parameter), capped at the newest 10,000 rows.<\/p>\n\n<p><strong><code>GET \/diagnostics<\/code><\/strong> \u2014 response <code>checks[]<\/code>: <code>id<\/code>, <code>label<\/code>, <code>status<\/code> (one of pass\/warn\/fail\/skip), <code>message<\/code>, <code>details<\/code>.<\/p>\n\n<p><strong><code>GET \/events<\/code><\/strong> \u2014 args <code>page<\/code> (default 1), <code>per_page<\/code> (default 50, capped at 200), <code>period<\/code>, <code>campaign<\/code>, <code>since<\/code>, <code>until<\/code>, <code>channel<\/code> (matches the stored channel slug only), <code>kind<\/code> (one of click, visit, order, lead), <code>visitor_id<\/code> (UUID format; any version \u2014 a non-v4 value simply matches no rows). Response <code>events[]<\/code> (newest first), <code>total<\/code>, <code>page<\/code>, <code>per_page<\/code>, <code>total_pages<\/code>. Each event carries <code>id<\/code>, <code>link_id<\/code>, <code>visitor_id<\/code>, <code>clicked_at<\/code>, <code>utm_source<\/code>, <code>utm_medium<\/code>, <code>utm_campaign<\/code>, <code>utm_id<\/code>, <code>utm_term<\/code>, <code>utm_content<\/code>, <code>gclid<\/code>, <code>fbclid<\/code>, <code>msclkid<\/code>, <code>ttclid<\/code>, <code>li_fat_id<\/code>, <code>twclid<\/code>, <code>destination<\/code>, <code>referrer<\/code>, <code>is_conversion<\/code> (bool), <code>touch<\/code>, <code>order_total<\/code> (number or null), <code>device<\/code>, <code>channel<\/code>, <code>custom<\/code> (object), <code>meta<\/code> (object) and the derived <code>kind<\/code> (click, visit, order or lead \u2014 <code>link_id<\/code> <code>lead<\/code> is a form submission, <code>order<\/code> or any legacy conversion row is an order, <code>visit<\/code> is an untagged landing, everything else is a click).<\/p>\n\n<p><strong><code>GET \/journeys<\/code><\/strong> \u2014 args <code>page<\/code> (default 1), <code>per_page<\/code> (default 50, capped at 100), <code>period<\/code>, <code>since<\/code>, <code>until<\/code>. Response <code>journeys[]<\/code> (newest conversion first), <code>total<\/code>, <code>page<\/code>, <code>per_page<\/code>, <code>total_pages<\/code>, <code>without_visitor<\/code> (conversions in range with no visitor id). Each journey: <code>conversion<\/code> (an event object), <code>visitor_id<\/code>, <code>first_touch<\/code> and <code>last_touch<\/code> (event objects or null), <code>touch_count<\/code> (clicks and visits only), <code>touches[]<\/code> (the most recent 100 preceding events, ascending, earlier conversions included as steps) and <code>touches_truncated<\/code>.<\/p>\n\n<p><strong><code>GET \/journeys\/{visitor_id}<\/code><\/strong> \u2014 the path parameter is a UUID v4 (anything else misses the route and 404s at the router). Response <code>visitor_id<\/code>, <code>events[]<\/code> (ascending, at most 500), <code>conversions<\/code>, <code>first_seen<\/code>, <code>last_seen<\/code>, <code>truncated<\/code>. An unknown visitor id returns HTTP 404 with <code>code: utmatra_not_found<\/code>.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>utmatra-utm-attribution-and-tracking<\/code> folder to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate the plugin via <strong>Plugins &gt; Installed Plugins<\/strong>.<\/li>\n<li>Go to <strong>Utmatra<\/strong> in the admin sidebar.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20this%20plugin%20store%20personal%20data%3F\"><h3>Does this plugin store personal data?<\/h3><\/dt>\n<dd><p>Raw IP addresses are never stored. Each IP address is combined with a secret per-site salt and hashed with SHA-256 before storage, so the stored hashes cannot be correlated with hashes from other sites. User agents and referrer URLs are stored as-is. This applies to the first page of each visit (landing page and referrer, recorded while \"Track the first page of every visit\" is on under Settings \u2192 Tracking) and to every tagged visit. A form submission that carries attribution is recorded as a conversion event with the same data as a campaign visit (hashed IP, user agent, referrer, the page the form was on) \u2014 never the submitted field values. Events are pruned automatically after a retention period (180 days by default, adjustable from the dashboard settings card; 0 keeps events forever). CSV exports never include IP hashes or user agents. If you configure a webhook, attributed orders and form submissions, including the submitted form fields, are sent to that external service. Suggested privacy policy text is provided under Settings \u2192 Privacy \u2192 Policy Guide.<\/p><\/dd>\n<dt id=\"how%20does%20tracking%20work%20on%20a%20cached%20site%3F\"><h3>How does tracking work on a cached site?<\/h3><\/dt>\n<dd><p>Full-page caches serve HTML without running PHP, which breaks purely server-side trackers. Utmatra ships a small beacon script in every front-end page: when the visitor's URL carries a tracked parameter, or on the first page of a visit, the beacon reports it to a public REST endpoint. On uncached pages a server-side fallback records tagged visits even for visitors without JavaScript; it does not record untagged landings, because PHP has no per-session marker to tell a landing from the next page view and would count every page of a no-JS visit. Deduplication ensures a tagged visit recorded by both paths is only counted once.<\/p>\n\n<p>Contact Form 7, Gravity Forms, Ninja Forms and Elementor Pro fill their attribution fields when the page is generated, so a cached page carries the campaign of whoever loaded it first. When the form is submitted, the values the plugin filled in are replaced with the submitter's own attribution (or the field's default when there is none), so entries and notification emails do not show another visitor's campaign. Values you supply yourself are kept: a Gravity Forms parameter in the page URL or <code>field_values<\/code> (and Gravity Forms REST or API submissions are left as sent), an Elementor dynamic-tag default when the visitor has no attribution, and any Contact Form 7 form tag you added under an attribution field name. Bricks resolves its tags when the page is generated and is not corrected on submit.<\/p><\/dd>\n<dt id=\"how%20do%20i%20create%20a%20tracked%20short%20link%3F\"><h3>How do I create a tracked short link?<\/h3><\/dt>\n<dd><p>Add a link under <strong>Tracked Links<\/strong> on the dashboard. With pretty permalinks enabled the link is served at <code>\/go\/{slug}\/<\/code>; on plain permalinks it falls back to <code>\/?utmatra_track={slug}<\/code>. Either URL records the click and 302-redirects the visitor to the destination with the link's UTM parameters appended. You can copy the URL, embed it with <code>[utmatra_link id=\"my-link\" label=\"Click here\"]<\/code>, or use the QR button to download a scannable PNG for print, packaging, or slides.<\/p>\n\n<p>You can also use the drawer as a plain UTM builder: switch on <strong>Build a URL only<\/strong> and the live preview becomes a tagged URL you copy with the submit button \u2014 no short link is created and nothing is saved. Presets apply in both modes, and link-level custom parameters (see the next FAQ) are appended too.<\/p><\/dd>\n<dt id=\"can%20i%20track%20my%20own%20url%20parameters%20%28coupon%2C%20affiliate%20id%2C%20promo%20code%29%3F\"><h3>Can I track my own URL parameters (coupon, affiliate ID, promo code)?<\/h3><\/dt>\n<dd><p>Yes. Under <strong>Settings \u2192 Tracking \u2192 Custom parameters<\/strong>, enter up to ten names separated by commas (for example <code>coupon, aff, promo2<\/code>). Names must start with a letter and use lowercase letters, digits and underscores (up to 32 characters); input is lowercased on save. From then on a URL such as <code>\/?coupon=SPRING10<\/code> is treated exactly like a tagged visit: the value is recorded on the event (in a JSON column, one key per parameter), carried in the attribution cookie and in webhook payloads, offered to forms as <code>&lt;name&gt;<\/code> and <code>&lt;name&gt;_first<\/code> fields, exported as its own CSV column, and available as an optional per-link field on tracked links \u2014 a link with a coupon value set appends <code>?coupon=\u2026<\/code> to the destination on redirect. Values are stored as-is (case-sensitive, up to 255 characters), so <code>SPRING10<\/code> never becomes <code>spring10<\/code>.<\/p>\n\n<p>Some names are reserved and rejected with a message: the built-in parameters, anything starting with <code>utm_<\/code>, <code>utmatra_<\/code> or <code>first_<\/code>, anything ending in <code>_first<\/code>, the plugin's own field and touch names (<code>landing_page<\/code>, <code>channel<\/code>, <code>lp<\/code>, <code>params<\/code>, \u2026), events-table columns, common form and credential field names (<code>email<\/code>, <code>name<\/code>, <code>password<\/code>, \u2026) and WordPress public query variables (<code>s<\/code>, <code>p<\/code>, <code>page<\/code>, \u2026).<\/p>\n\n<p>Three things to know:<\/p>\n\n<ul>\n<li><strong>Do not name a parameter after a form field or after anything that identifies a person<\/strong> (an email address, a phone number, a name). Whatever the URL parameter carries is stored on the event, placed in the attribution cookie and written into any form input that shares the parameter's name \u2014 a visible input named like a custom parameter is pre-filled from the cookie on every page, which is the point for coupon boxes but the wrong tool for personal data. The reserved list blocks the most common collisions, but you are naming a field that lives in the same namespace as real HTML form fields.<\/li>\n<li><strong>Tracking Doctor may report forms as \"partial\".<\/strong> Every form adapter that does not inject fields itself (Gravity Forms, Ninja Forms, Elementor, Fluent Forms, Formidable, Forminator, Bricks) compares the form's fields against the full field list, so once you define a custom parameter those forms count as missing <code>coupon<\/code> and <code>coupon_first<\/code> until you add the fields. Contact Form 7 and WPForms inject the fields automatically and stay \"complete\". This is the check working as intended: the form really does not capture the parameter yet.<\/li>\n<li><strong>On cached sites the list reaches visitors when the page cache regenerates.<\/strong> The parameter list is baked into the page HTML, so a newly added parameter is captured on cached pages only after the cache is purged, and a removed parameter keeps being written into the attribution cookie by the stale page (the server ignores the value) until then. Adding and removing a parameter takes effect on uncached pages immediately.<\/li>\n<\/ul><\/dd>\n<dt id=\"are%20clicks%20from%20bots%20and%20link%20previews%20counted%3F\"><h3>Are clicks from bots and link previews counted?<\/h3><\/dt>\n<dd><p>No. Requests whose user agent looks like a crawler, link-preview unfurler (Slack, WhatsApp, Facebook, etc.), or monitoring tool are excluded from your stats. They still receive the redirect so previews resolve correctly. Adjust the verdict with the <code>utmatra_is_bot<\/code> filter.<\/p><\/dd>\n<dt id=\"what%20does%20the%20attribution%20cookie%20store%3F\"><h3>What does the attribution cookie store?<\/h3><\/dt>\n<dd><p>When a visitor arrives with a UTM parameter or ad click ID, or lands from another site (a search engine, social network, AI assistant or any other referrer), the plugin sets a first-party cookie named <code>utmatra_attr<\/code> containing two sets of campaign data: the visitor's <strong>first touch<\/strong> (preserved once written) and <strong>last touch<\/strong> (updated on every tagged or referred visit). Each set holds the whitelisted UTM\/click-ID parameters present on that visit, or the channel and referrer host of an untagged landing, plus the landing page URL and a timestamp. A direct visit without parameters never writes or updates the cookie. The cookie does not store names, email addresses, or IP addresses.<\/p>\n\n<p>The cookie lives for 90 days (<code>path=\/<\/code>, <code>SameSite=Lax<\/code>, <code>Secure<\/code> on https sites). Change the lifetime with <code>utmatra_cookie_days<\/code> or disable the cookie entirely with <code>utmatra_enable_cookie<\/code> (see the filter reference below).<\/p>\n\n<p>A second cookie, <code>utmatra_vid<\/code>, holds a random visitor id (UUID v4) generated in the browser \u2014 from the browser's cryptographically secure random source, falling back to JavaScript's <code>Math.random()<\/code> only in browsers that lack it \u2014 or by the server-side fallback for visitors without JavaScript. It is not derived from your IP address or any personal data, and it is written, refreshed and deleted under exactly the same conditions and lifetime as <code>utmatra_attr<\/code>. It links a visitor's tagged visits to their later orders and form submissions for the Journeys report. On sites with a consent banner, visits recorded before the visitor consents carry no visitor id, so a journey begins with the first tagged visit after consent.<\/p><\/dd>\n<dt id=\"how%20does%20the%20plugin%20handle%20cookie%20consent%3F\"><h3>How does the plugin handle cookie consent?<\/h3><\/dt>\n<dd><p>The plugin does not ship a consent banner. If a plugin implementing the <a href=\"https:\/\/github.com\/rlankhorst\/wp-consent-level-api\">WP Consent API<\/a> is active (Complianz, Real Cookie Banner, Borlabs Cookie, and others implement it), the attribution cookie is gated on the visitor's consent:<\/p>\n\n<ul>\n<li><strong>The decision is made in the browser.<\/strong> The front-end beacon asks the Consent API's JavaScript whether the visitor granted the configured category before it writes the cookie, so gating is correct even on pages served from a full-page cache. The PHP fallback writer (for visitors without JavaScript) applies the same rule per request.<\/li>\n<li><strong>Consent category.<\/strong> Choose which category gates the cookie under Settings \u2192 Data and privacy \u2192 Consent: <em>Marketing<\/em> (the default \u2014 the cookie carries advertising click IDs), <em>Statistics<\/em>, or <em>Marketing or statistics<\/em>.<\/li>\n<li><strong>Withdrawal deletes the cookie.<\/strong> When the visitor withdraws consent, the beacon deletes the attribution cookie immediately.<\/li>\n<li><strong>While the banner is undecided<\/strong> no cookie is written. The landing visit's campaign data is kept in page memory only and written to the cookie the moment consent is granted on that page; forms are not pre-filled from an existing cookie until the visitor decides. A visitor who navigates away before deciding and grants consent on a later page is attributed from their next tagged visit. The beacon's per-tab duplicate guards for the collect request (the <code>utmatra_sent<\/code> marker, which holds the URL's own query string, including any UTM values and click IDs, and the <code>utmatra_visit<\/code> flag that records that the tab is already inside the site, having reported its landing or been opened from an internal link) are kept in session storage, which lasts only for the life of the tab, and are unaffected by consent.<\/li>\n<li><strong>Events are recorded regardless of consent<\/strong> by default: a tagged-visit event and an untagged landing visit carry no cookie and only a salted IP hash. Return <code>true<\/code> from the <code>utmatra_consent_gate_events<\/code> filter to withhold events as well until the visitor consents.<\/li>\n<li>The plugin declares itself to the Consent API and describes the cookie (name, category, lifetime, purpose) so consent banners can list it.<\/li>\n<\/ul>\n\n<p>Without a Consent API plugin the cookie is written on every tagged visit and every landing from another site; gate it yourself by returning <code>false<\/code> from <code>utmatra_enable_cookie<\/code>, or install a consent plugin that implements the API.<\/p>\n\n<p>Two notes for sites with a full-page cache:<\/p>\n\n<ul>\n<li>On sites with a full-page cache, a change to the consent category reaches visitors after the cache is purged.<\/li>\n<li>After deactivating a Consent API plugin, purge the page cache: cached HTML that still advertises the API makes the beacon hold the touch until the visitor's next uncached load.<\/li>\n<\/ul><\/dd>\n<dt id=\"how%20does%20woocommerce%20order%20attribution%20work%3F\"><h3>How does WooCommerce order attribution work?<\/h3><\/dt>\n<dd><p>When a visitor who previously arrived through a tracked campaign or from another site places an order, the order is stamped at checkout with their first- and last-touch attribution sets (order meta, shown in a \"Campaign attribution\" metabox on the order edit screen). The conversion event with the order total is recorded, and the conversion webhook sent, once the order is paid: when the gateway completes payment, or when the order moves to a paid status such as processing or completed (cash on delivery, or bank transfer once marked paid). Declined, abandoned and unpaid orders are never counted, and each order is counted once. Each touch carries <code>ch<\/code> (the channel) and, when known, <code>rh<\/code> (the referrer host) alongside the campaign parameters, in the order meta and in the conversion webhook payload. The dashboard then shows a revenue-by-campaign chart, and the weekly email includes a revenue table. No configuration needed \u2014 it activates automatically when WooCommerce is present.<\/p><\/dd>\n<dt id=\"how%20are%20channels%20classified%3F\"><h3>How are channels classified?<\/h3><\/dt>\n<dd><p>Every recorded visit, click and conversion is filed into one channel when it is written, following the rule order of Google Analytics' default channel grouping with one addition for AI assistants: a <code>gclid<\/code> or <code>msclkid<\/code> is <strong>paid search<\/strong>; a source naming an AI assistant is <strong>AI assistant<\/strong>; a display medium (<code>display<\/code>, <code>banner<\/code>, <code>cpm<\/code>, \u2026) is <strong>display<\/strong>; a paid medium (<code>cpc<\/code>, <code>ppc<\/code>, <code>paid-social<\/code>, \u2026) is <strong>paid search<\/strong>, <strong>paid social<\/strong> or <strong>paid (other)<\/strong> depending on the source; a social medium or source is <strong>organic social<\/strong>; an <code>organic<\/code> medium or search-engine source is <strong>organic search<\/strong>; an <code>email<\/code> or <code>newsletter<\/code> source or medium is <strong>email<\/strong>; <code>affiliate<\/code> is <strong>affiliate<\/strong>; <code>referral<\/code>, <code>app<\/code> or <code>link<\/code> is <strong>referral<\/strong>; a Facebook, TikTok, LinkedIn or X click ID without a medium is <strong>paid social<\/strong>; otherwise the referrer host decides (AI assistant, search engine, social network, or <strong>referral<\/strong> for any other site); anything else with campaign data is <strong>other<\/strong> and a visit with neither is <strong>direct<\/strong>.<\/p>\n\n<p>Three host lists drive the referrer rules and can be extended with the <code>utmatra_ai_assistant_hosts<\/code>, <code>utmatra_search_engine_hosts<\/code> and <code>utmatra_social_network_hosts<\/code> filters. AI assistants include ChatGPT, Perplexity, Claude, Gemini, Copilot, You.com, Phind, Poe, Meta AI, Mistral, Grok, DeepSeek, Duck.ai and Kimi; ChatGPT and Perplexity also tag their outbound links with <code>utm_source<\/code>, which the source rules recognise. Brave Search counts as organic search, because its AI answers render on the normal results page and the referrer cannot tell them apart. Facebook appends <code>fbclid<\/code> to organic clicks too, so an untagged Facebook click reads as paid social; override any verdict with the <code>utmatra_channel<\/code> filter. The channel is derived from the visitor's own referrer, so a Google Mail or Google Docs link is filed as organic search, as in GA4.<\/p>\n\n<p>Untagged landings are reported by the front-end beacon only (no PHP fallback, see the cached-site FAQ) and share the collect endpoint's rate limit with tagged visits; raise it with <code>utmatra_rate_limit<\/code> when many visitors share one IP.<\/p><\/dd>\n<dt id=\"how%20do%20i%20capture%20attribution%20in%20my%20lead%20forms%3F\"><h3>How do I capture attribution in my lead forms?<\/h3><\/dt>\n<dd><p>Every tracked parameter is available to forms under its own standard name: <code>utm_source<\/code>, <code>utm_medium<\/code>, <code>utm_campaign<\/code>, <code>utm_id<\/code>, <code>utm_term<\/code>, <code>utm_content<\/code>, <code>gclid<\/code>, <code>fbclid<\/code>, <code>msclkid<\/code>, <code>ttclid<\/code>, <code>li_fat_id<\/code>, <code>twclid<\/code>, <code>landing_page<\/code> (the landing page URL), <code>channel<\/code> and <code>referrer_host<\/code> \u2014 plus every custom parameter you define, under its own name (for example <code>coupon<\/code> and <code>coupon_first<\/code>). These hold the visitor's <strong>last touch<\/strong>; append <code>_first<\/code> (e.g. <code>utm_source_first<\/code>, <code>landing_page_first<\/code>) for the <strong>first touch<\/strong>.<\/p>\n\n<p><strong>Any form plugin (generic):<\/strong> add a hidden input (or text field) with one of the names above and it is auto-filled from the attribution cookie on page load. Fields that already have a value are never overwritten. Forms injected into the page after load (some popup builders) are not filled automatically. The cookie carries <code>channel<\/code> only for visits without campaign parameters, so the generic fill populates it for organic, social, AI-assistant and referral visitors; the server-side adapters below always have it.<\/p>\n\n<p><strong>Contact Form 7:<\/strong> zero configuration \u2014 the last-touch fields are appended to every form as hidden fields automatically and arrive with each submission. Use special mail tags in your mail templates to include them in notification emails: <code>[_utm_source]<\/code>, <code>[_utm_campaign]<\/code>, <code>[_gclid]<\/code>, <code>[_landing_page]<\/code>, <code>[_channel]<\/code>, <code>[_referrer_host]<\/code>, etc. (any field name above with a leading underscore works, including <code>_first<\/code> variants such as <code>[_utm_source_first]<\/code>).<\/p>\n\n<p><strong>Gravity Forms:<\/strong> add a Hidden field, enable \"Allow field to be populated dynamically\" under the field's Advanced tab, and set the parameter name to e.g. <code>utm_source<\/code> or <code>utm_source_first<\/code>.<\/p>\n\n<p><strong>Ninja Forms:<\/strong> add a Hidden field to your form and set its default value to <code>{utmatra:utm_source}<\/code> (or any other tracked parameter, e.g. <code>{utmatra:gclid}<\/code>, <code>{utmatra:landing}<\/code>, <code>{utmatra:channel}<\/code>, <code>{utmatra:referrer_host}<\/code>, <code>{utmatra:utm_source_first}<\/code>) to capture attribution.<\/p>\n\n<p><strong>Elementor (Pro Forms):<\/strong> add a Hidden field to your form (Advanced tab), set its Field ID to <code>utm_source<\/code> (or any other tracked parameter name, e.g. <code>utm_source_first<\/code> for first touch), and it is auto-filled from attribution data on render. Requires Elementor Pro \u2014 free Elementor has no form builder.<\/p>\n\n<p><strong>Fluent Forms:<\/strong> add a Hidden Field and set its <em>Name Attribute<\/em> to e.g. <code>utm_source<\/code> (or <code>utm_source_first<\/code>). It is filled from the attribution cookie by the page script on load. Submissions are listed in the dashboard widget automatically.<\/p>\n\n<p><strong>WPForms (Lite and Pro):<\/strong> zero configuration \u2014 every attribution field above is added to each form as an empty hidden input and filled on page load, so the values arrive with each submission. To put them in notification emails, add the smart tags <code>{utm_source}<\/code>, <code>{utm_campaign}<\/code>, <code>{landing_page}<\/code> \u2026 (any field name above, including <code>_first<\/code> variants) to the notification subject or message. To store a value in an entry (Pro), set one of those tags as the default value of a Hidden Field (Lite: a Single Line Text field); it is filled on page load, not when the page is generated. If \"Optimize Email Sending\" is enabled, notification emails are sent later in the background, where the tags cannot be resolved \u2014 use a field default instead.<\/p>\n\n<p><strong>Formidable Forms:<\/strong> add a Hidden field and, under Advanced, set its <em>Field Key<\/em> to e.g. <code>utm_source<\/code> (Formidable may append a number on other forms, such as <code>utm_source2<\/code> \u2014 that still works). The field is filled on page load.<\/p>\n\n<p><strong>Forminator:<\/strong> add a Hidden Field, set <em>Default Value<\/em> to \"Query parameter\" and the <em>Query parameter<\/em> to e.g. <code>utm_source<\/code> (or <code>utm_source_first<\/code>). The field is filled on page load; a matching URL parameter on the current page takes precedence. Forms set to \"Load form using AJAX\" are inserted after page load and are not filled.<\/p>\n\n<p><strong>Bricks Builder:<\/strong> add a Hidden field to the Form element and set its value to the dynamic data tag <code>{utm_source}<\/code> (all field names above are listed under the \"Utmatra\" group of the dynamic data picker). To list the submission as a lead in the dashboard widget, add the \"Custom\" action under \"Actions after successful form submit\". The tag is resolved when the page is generated, so on a fully page-cached site the stored value may be stale. Bricks support is untested against a live Bricks install; please report problems.<\/p><\/dd>\n<dt id=\"can%20i%20send%20leads%20and%20orders%20to%20zapier%2C%20make%2C%20hubspot%20or%20gohighlevel%3F\"><h3>Can I send leads and orders to Zapier, Make, HubSpot or GoHighLevel?<\/h3><\/dt>\n<dd><p>Yes. Under <strong>Settings \u2192 Webhooks<\/strong>, paste your receiver's URL, pick a preset (Generic JSON, Zapier, Make, HubSpot Forms, GoHighLevel) and use the \"Send for\" switches to enable or disable WooCommerce orders and form leads independently (both are on by default; the HubSpot preset sends leads only). Because the payloads carry the submitted form fields, use an <code>https:\/\/<\/code> URL so the values are not sent in cleartext. Every request is a JSON POST signed with an <code>X-Utmatra-Signature<\/code> header (sha256 = HMAC of the body over a per-site secret shown on the same card) and tagged with an <code>X-Utmatra-Event<\/code> header (<code>conversion<\/code>, <code>lead<\/code> or <code>test<\/code>), so any receiver can verify and route it \u2014 compare the signature with a constant-time check (e.g. PHP's <code>hash_equals()<\/code>). <strong>Send test webhook<\/strong> posts a synthetic payload in the selected preset's shape before you rely on it. Lead webhooks are sent as the form is submitted; order webhooks are sent by the background queue (Action Scheduler) once the order is paid, usually within a minute.<\/p>\n\n<p>A lead payload carries the integration (e.g. <code>contact-form-7<\/code>), the form id and title, the <strong>submitted form field values keyed by label<\/strong> (up to 50 fields of 1,000 characters each; file uploads, passwords and hidden fields are never sent), the first- and last-touch attribution sets, the page URL and a timestamp. The plugin itself never stores the submitted field values \u2014 they travel only in the webhook request. One exception: when a receiver answers with an error, a short excerpt of its response is kept under \"Last delivery\" so the failure can be diagnosed, and such an error message may quote a submitted value (HubSpot, for instance, echoes an invalid email address back). Values that are arrays (checkbox groups) arrive comma-joined; deeply nested values arrive as a JSON string. Field forwarding for Gravity Forms and Elementor Pro is derived from the vendors' documentation, untested against a live install.<\/p>\n\n<p>The Generic JSON preset (also used by Zapier and Make, whose receivers parse nested JSON themselves) sends the payload as-is. The HubSpot preset converts it into a HubSpot Forms API v3 submission: it needs a HubSpot form carrying fields named <code>email<\/code>, <code>firstname<\/code>, <code>lastname<\/code>, <code>phone<\/code> and hidden <code>utm_source<\/code>, <code>utm_medium<\/code>, <code>utm_campaign<\/code>, <code>utm_term<\/code>, <code>utm_content<\/code> (HubSpot rejects submissions with fields the form does not have \u2014 the error shows under \"Last delivery\"); when the visitor carries HubSpot's own <code>hubspotutk<\/code> tracking cookie it is forwarded so the submission links to their visitor record. The GoHighLevel preset flattens the payload into one level of snake_case keys (<code>email<\/code>, <code>phone<\/code>, <code>first_name<\/code>, <code>last_name<\/code>, <code>utm_*<\/code>, <code>first_utm_*<\/code>, <code>landing_page<\/code>, <code>field_&lt;name&gt;<\/code>), as its Inbound Webhook trigger requires. The HubSpot and GoHighLevel request shapes follow the vendors' documentation and have not been verified against a live account.<\/p><\/dd>\n<dt id=\"how%20do%20i%20export%20my%20data%3F\"><h3>How do I export my data?<\/h3><\/dt>\n<dd><p>Click <strong>Export CSV<\/strong> on the dashboard to download the raw events for the selected period and campaign filter (the newest 10,000 rows are exported). The columns, in order: <code>clicked_at<\/code>, <code>link_id<\/code>, <code>channel<\/code>, <code>utm_source<\/code>, <code>utm_medium<\/code>, <code>utm_campaign<\/code>, <code>utm_id<\/code>, <code>utm_term<\/code>, <code>utm_content<\/code>, <code>gclid<\/code>, <code>fbclid<\/code>, <code>msclkid<\/code>, <code>ttclid<\/code>, <code>li_fat_id<\/code>, <code>twclid<\/code>, <code>destination<\/code>, <code>referrer<\/code>, <code>is_conversion<\/code>, <code>order_total<\/code>, <code>visitor_id<\/code>, then one column per configured custom parameter (in saved order). The <code>channel<\/code> column and the custom-parameter columns carry values only once those features are active; <code>visitor_id<\/code> is empty on events recorded before this version. The export never includes IP hashes or user agents. Because the export also includes conversion rows (flagged in the <code>is_conversion<\/code> column) \u2014 WooCommerce orders (<code>link_id<\/code> order) and form leads (<code>link_id<\/code> lead) \u2014 alongside click rows, its total row count can exceed the click total shown on the dashboard. Untagged landing visits are left out unless the REST call to <code>utmatra\/v1\/export<\/code> passes <code>include_visits=1<\/code>. Values that could be interpreted as spreadsheet formulas are prefixed with <code>'<\/code> for safety.<\/p><\/dd>\n<dt id=\"how%20do%20i%20get%20a%20weekly%20report%20by%20email%3F\"><h3>How do I get a weekly report by email?<\/h3><\/dt>\n<dd><p>Enable \"Send a weekly campaign summary email\" on the dashboard settings card (it is off by default \u2014 no unsolicited mail). Once a week the site admin email receives an HTML summary with total clicks, the top 5 campaigns and sources, and revenue by campaign when WooCommerce is active. Change the recipient with the <code>utmatra_report_recipient<\/code> filter. Delivery goes through wp_mail(), so any SMTP plugin you use applies.<\/p><\/dd>\n<dt id=\"my%20site%20is%20behind%20a%20reverse%20proxy%20or%20cdn%20%E2%80%94%20anything%20to%20configure%3F\"><h3>My site is behind a reverse proxy or CDN \u2014 anything to configure?<\/h3><\/dt>\n<dd><p>Yes. Behind a proxy (Cloudflare, Varnish, a load balancer), <code>REMOTE_ADDR<\/code> is the proxy's IP, not the visitor's. Click deduplication and the collect endpoint's rate limiting key on a hash of that IP \u2014 so with the default behaviour all visitors would share the proxy's identity, over-deduplicating clicks and tripping the rate limit for everyone. Use the <code>utmatra_client_ip<\/code> filter to return the real client IP from the header your proxy sets (e.g. <code>CF-Connecting-IP<\/code>, or the first entry of <code>X-Forwarded-For<\/code>). Only trust forwarded headers when every request passes through a proxy that overwrites them \u2014 they are client-spoofable otherwise. If configuring the real IP is not possible, raise the shared ceiling with the <code>utmatra_rate_limit<\/code> filter instead.<\/p><\/dd>\n<dt id=\"why%20are%20my%20utm%20values%20being%20changed%20when%20i%20save%20a%20link%3F\"><h3>Why are my UTM values being changed when I save a link?<\/h3><\/dt>\n<dd><p>Values are normalized for consistent reporting: lowercased, with whitespace replaced by <code>-<\/code>. Analytics tools treat <code>Newsletter<\/code>, <code>newsletter<\/code>, and <code>News letter<\/code> as three different sources, silently fragmenting reports. Normalization (applied in the form and again server-side) keeps every report row canonical.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<li>Cache-proof tracking beacon with a no-JS PHP fallback, bot filtering and click deduplication.<\/li>\n<li>Ad click-ID capture (gclid, fbclid, msclkid, ttclid, li_fat_id, twclid) alongside UTM tags.<\/li>\n<li>First\/last-touch attribution cookie, gated on the WP Consent API.<\/li>\n<li>Channel classification, including AI assistants, and untagged landing-visit recording.<\/li>\n<li>Pretty short links with QR codes, the [utmatra_link] shortcode, UTM naming presets and a build-a-URL mode.<\/li>\n<li>Custom URL parameters captured alongside UTM tags.<\/li>\n<li>WooCommerce revenue attribution with an attribution metabox on the order screen, and form lead capture for Contact Form 7, Gravity Forms, Ninja Forms, Elementor Pro, WPForms, Fluent Forms, Formidable, Forminator and Bricks.<\/li>\n<li>Lead and order webhooks for Zapier, Make, HubSpot and GoHighLevel.<\/li>\n<li>Customer journeys built from a consent-gated visitor id, and a read-only REST API.<\/li>\n<li>Tabbed Chart.js dashboard with breakdowns by source, campaign, medium, channel, device, landing page, referrer and ad platform, plus a revenue-by-campaign chart.<\/li>\n<li>Recent leads widget on the WordPress admin Dashboard.<\/li>\n<li>CSV export, weekly email summary, Tracking Doctor diagnostics and data retention.<\/li>\n<li>IP addresses stored only as salted SHA-256 hashes, and suggested privacy policy text.<\/li>\n<li>Background jobs on bundled Action Scheduler, and no external services or CDN calls by default.<\/li>\n<\/ul>","raw_excerpt":"Self-hosted UTM and campaign tracking with short links, QR codes, WooCommerce revenue attribution, and a live Chart.js dashboard.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/389876","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=389876"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/mahlamusa"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=389876"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=389876"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=389876"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=389876"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=389876"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=389876"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}