---
title: "How to add product analytics to a WordPress site"
description: "Track a WordPress pricing CTA and a form accepted by the server. Verify a saved acceptance record before counting the conversion."
canonical: "https://amplitude.com/en-us/build/wordpress-product-analytics"
---

# How to add product analytics to a WordPress site

Track a WordPress pricing CTA and a form accepted by the server. Verify a saved acceptance record before counting the conversion.

Source: https://amplitude.com/en-us/build/wordpress-product-analytics

---

# How to add product analytics to a WordPress site

Track a WordPress pricing CTA and a form accepted by the server. Verify a saved acceptance record before counting the conversion.

- WordPress 6.3+
- Custom plugin
- Browser SDK 2
- CDN
- 25 min

Chris Tam · Last updated 16 October 2026

A submit button shows an attempt; a server response tells you whether the form was accepted. [Product analytics](https://amplitude.com/amplitude-analytics) lets you keep those actions separate when you measure a WordPress funnel.

In this tutorial, I'll add a shortcode form, validate it in WordPress, save a private acceptance record, and track its successful response.

## What you'll build

`Pricing CTA Clicked` and `Demo Form Accepted` events, with the latter sent only after WordPress saves the example acceptance record.

## Prerequisites

- A staging WordPress site with permission to upload a custom plugin and edit `wp-config.php`. This tutorial uses a plugin you control. On managed hosting, confirm that your plan permits those changes.
- WordPress 6.3 or later and PHP 7.4 or later, plus a theme that calls `wp_head()` and `wp_footer()`. The executable example was tested on WordPress 6.9.10 and PHP 8.3.33.
- An Amplitude project API key: Settings > Projects > your project > General > Manage next to API Key. Managers and Admins can view it. The browser key is public and limited to the minimum scope needed for ingestion. Keep secret keys out of this plugin.
- The shared plugin from the [WordPress web analytics tutorial](/build/wordpress-web-analytics), plus public Home and Pricing pages.

A free Amplitude account covers this example within its product limits. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

## 1. Keep the existing SDK installation

Use the shared plugin and `wp-config.php` constants from the [WordPress web analytics tutorial](/build/wordpress-web-analytics). Keep one project loader and initializer. This page adds `demo.php` and `events.js`; the existing PHP bootstrap detects those files. Preserve replay privacy, engagement, and experiment consent additions.

Existing plugin folder, followed by this page's additions

```text
# Existing plugin folder, followed by this page's additions
wp-content/plugins/amplitude-build/amplitude-build.php
wp-content/plugins/amplitude-build/analytics.js
wp-content/plugins/amplitude-build/demo.php
wp-content/plugins/amplitude-build/events.js
```

## 2. Add the shortcode and its server handler

Create `demo.php` below. The [shortcode API](https://developer.wordpress.org/reference/functions/add_shortcode/) renders the form. The authenticated and anonymous `admin_post` hooks reach the same handler, which checks the nonce and email, then uses [wp\_insert\_post](https://developer.wordpress.org/reference/functions/wp_insert_post/) to save a private acceptance record.

wp-content/plugins/amplitude-build/demo.php

```php
<?php
// wp-content/plugins/amplitude-build/demo.php
defined('ABSPATH') || exit;

add_action('init', function () {
    register_post_type('amp_build_request', array(
        'label' => 'Build demo requests',
        'public' => false,
        'show_ui' => false,
        'supports' => array('title'),
    ));
});

add_shortcode('amplitude_demo_form', function () {
    ob_start();
    ?>
    <form id="demo" data-amp-demo-form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
        <input type="hidden" name="action" value="amp_build_demo">
        <?php wp_nonce_field('amp_build_demo', 'amp_build_nonce', false); ?>
        <label>Test email <input name="email" type="email" required autocomplete="off" data-replay-mask="true"></label>
        <button type="submit">Send demo request</button>
        <p role="status" aria-live="polite" data-amp-form-status></p>
    </form>
    <?php
    return ob_get_clean();
});

function amp_build_accept_demo() {
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        wp_send_json_error(array('message' => 'Use POST.'), 405);
    }
    $nonce = isset($_POST['amp_build_nonce']) && is_string($_POST['amp_build_nonce']) ? sanitize_text_field(wp_unslash($_POST['amp_build_nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'amp_build_demo')) {
        wp_send_json_error(array('message' => 'Refresh the page and try again.'), 403);
    }
    $email = isset($_POST['email']) && is_string($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
    if (!is_email($email)) {
        wp_send_json_error(array('message' => 'Enter a valid test email.'), 400);
    }
    // The example stores an acceptance record only. No email or form values persist.
    $saved = wp_insert_post(array(
        'post_type' => 'amp_build_request',
        'post_status' => 'private',
        'post_title' => 'Synthetic demo form accepted',
    ), true);
    if (is_wp_error($saved) || !$saved) {
        wp_send_json_error(array('message' => 'The request could not be saved.'), 500);
    }
    wp_send_json_success(array('accepted' => true));
}
add_action('admin_post_nopriv_amp_build_demo', 'amp_build_accept_demo');
add_action('admin_post_amp_build_demo', 'amp_build_accept_demo');
```

This is a synthetic example form, not a replacement for your lead-processing system. It validates the test email without storing it and sends no email. WordPress stores only the acceptance record. Before adapting this to public traffic, add your actual processing, abuse controls, and retention rules. A WordPress nonce is not authentication, rate limiting, or a single-use submission ID.

## 3. Track only the accepted response

Create `events.js` below. The text-only form uses URL-encoded fields. Read the endpoint with `getAttribute('action')`: WordPress's hidden input named `action` shadows the DOM `form.action` property. The handler disables repeat clicks while a request is pending, then checks the HTTP status and JSON result before tracking a conversion. The SDK is optional for form delivery: blocked analytics must not break submission.

wp-content/plugins/amplitude-build/events.js

```javascript
// wp-content/plugins/amplitude-build/events.js
// Keep this handler once; experiments and guides reuse its event names.
(() => {
  if (window.__ampBuildEvents) return;
  window.__ampBuildEvents = true;
  document.addEventListener('click', event => {
    if (!(event.target instanceof Element)) return;
    if (!event.target.closest('[data-analytics="pricing-cta"]')) return;
    window.amplitude?.track('Pricing CTA Clicked', {
      placement: 'pricing', page_path: location.pathname
    });
  document.querySelectorAll('[data-amp-demo-form]').forEach(form => {
    let pending = false;
    form.addEventListener('submit', async event => {
      event.preventDefault();
      if (pending || !form.reportValidity()) return;
      pending = true;
      const button = form.querySelector('[type="submit"]');
      const status = form.querySelector('[data-amp-form-status]');
      button.disabled = true;
      status.textContent = 'Sending test request...';
      try {
        // WordPress's input[name="action"] shadows form.action.
        const response = await fetch(form.getAttribute('action'), {
          method: 'POST', body: new URLSearchParams(new FormData(form)), credentials: 'same-origin'
        });
        const body = await response.json();
        if (!response.ok || body.success !== true || body.data?.accepted !== true) {
          throw new Error('The server did not accept this request.');
        }
        status.textContent = 'Test request accepted.';
        window.amplitude?.track('Demo Form Accepted', {
          form_name: 'demo', page_path: location.pathname
        });
      } catch {
        status.textContent = 'The request failed. Refresh and try again.';
      } finally {
        button.disabled = false;
        pending = false;
      }
    });
})();
```

On Pricing, add the CTA in a Custom HTML block. Put `[amplitude_demo_form]` in a separate Shortcode block. Keep the other data attributes when present.

Pricing page: Custom HTML block

```html
<!-- Pricing page: Custom HTML block -->
<a href="#demo" data-analytics="pricing-cta" data-experiment="pricing-cta" data-guide="pricing-help">
  <span>Request a demo</span>
</a>
```

Pricing page: Shortcode block

```text
# Pricing page: Shortcode block
[amplitude_demo_form]
```

## 4. Exercise rejection and acceptance

Publish, click the nested CTA text once, and try an empty or invalid email. No accepted conversion should follow. Submit a valid synthetic email and wait for `Test request accepted.` Then run:

Browser Console after WordPress accepts the form

```javascript
// Browser Console after WordPress accepts the form
window.amplitude.flush();
window.amplitude.getDeviceId();
```

Verify one `Pricing CTA Clicked` with `placement=pricing` and one `Demo Form Accepted` with `form_name=demo`. Both include `page_path`; neither custom event includes the email. Autocapture's submit event is a separate attempt signal. For another form plugin, use its documented successful-processing callback rather than reusing this shortcode handler.

WordPress nonces expire. If a page cache serves an old form, the server can reject an otherwise valid test with 403. Purge or exclude this synthetic form page from full-page caching so a reload receives a current nonce; see [WordPress nonce verification](https://developer.wordpress.org/reference/functions/wp_verify_nonce/).

**MCP:** Ask your coding agent to inspect the project's event taxonomy with the [Amplitude MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp) before adding events. Reuse existing names when they describe the same action.

## Verify it worked

1. **WordPress output:** Open the public staging page while logged out. Check that the project loader appears once and its inline initialization follows it. After deployment or configuration changes, purge the relevant page and CDN caches.
2. **Network:** Confirm an event POST receives HTTP 200 from `api2.amplitude.com` for US projects or `api.eu.amplitude.com` for EU projects. Large batches can be compressed; use this check for delivery status.
3. **Instrumentation Explorer:** Inspect event names, properties, counts, and `device_id` in the Chrome extension. Compare one action with one expected custom event.
4. **User Profiles:** Search that device ID in Amplitude and inspect its event stream. The docs describe a delay of ten seconds to one minute. Data > Sources > Ingestion Debugger shows request and event counts, rather than individual event payloads.

Confirm the server-saved acceptance and one custom conversion together. An HTTP 200 analytics response alone does not establish form processing.

## Troubleshooting

**A previously working form starts failing**

Refresh the page and inspect the form POST status. A cached nonce can expire; exclude the form page from a cache lifetime that outlives its nonce or refresh nonces through an appropriate server flow. Never cache the form-processing response.

**The browser shows Success but downstream delivery is absent**

This example saves an acceptance marker only. Add and verify your actual downstream processing before naming a conversion after that downstream outcome.

**The plugin is active but no SDK loads**

Check that `AMP_BUILD_API_KEY` is defined before `wp-settings.php`, then inspect the public page source. This plugin uses the front-end enqueue hook, so its SDK is absent from the WordPress login and administration pages.

**The script order differs from the example**

Check caching and script-optimization exclusions for `amp-build-sdk` and `amp-build-engagement`. The project loader and inline initialization must remain together. Check for an earlier installation in a theme, tag manager, or snippet plugin.

**Requests are blocked**

Check the key, region, browser extensions, and Console. Follow the relevant SDK's CSP and endpoint guidance while preserving your site's security policy.

**Autocapture settings differ from the file**

Review Data > Settings > Autocapture. When remote configuration is enabled, remote values take precedence for the features they specify.

## FAQ

### Is this a built-in WordPress contact form?

It is the example shortcode supplied by this tutorial. Other form plugins have their own validation and success callbacks.

### Does a valid nonce prevent duplicate submissions?

WordPress nonces can be reused during their validity window. The browser guard prevents concurrent clicks here; production retries need a server-side idempotency design when duplicate records matter.

## Full code reference

Unchanged from earlier pages; keep your versions: amplitude-build.php, analytics.js, engagement.php, experiment-consent.js. Merge the changes into existing files; preserve sibling additions named in the comments.

wp-content/plugins/amplitude-build/demo.php

```php
<?php
// wp-content/plugins/amplitude-build/demo.php
defined('ABSPATH') || exit;

add_action('init', function () {
    register_post_type('amp_build_request', array(
        'label' => 'Build demo requests',
        'public' => false,
        'show_ui' => false,
        'supports' => array('title'),
    ));
});

add_shortcode('amplitude_demo_form', function () {
    ob_start();
    ?>
    <form id="demo" data-amp-demo-form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
        <input type="hidden" name="action" value="amp_build_demo">
        <?php wp_nonce_field('amp_build_demo', 'amp_build_nonce', false); ?>
        <label>Test email <input name="email" type="email" required autocomplete="off" data-replay-mask="true"></label>
        <button type="submit">Send demo request</button>
        <p role="status" aria-live="polite" data-amp-form-status></p>
    </form>
    <?php
    return ob_get_clean();
});

function amp_build_accept_demo() {
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        wp_send_json_error(array('message' => 'Use POST.'), 405);
    }
    $nonce = isset($_POST['amp_build_nonce']) && is_string($_POST['amp_build_nonce']) ? sanitize_text_field(wp_unslash($_POST['amp_build_nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'amp_build_demo')) {
        wp_send_json_error(array('message' => 'Refresh the page and try again.'), 403);
    }
    $email = isset($_POST['email']) && is_string($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
    if (!is_email($email)) {
        wp_send_json_error(array('message' => 'Enter a valid test email.'), 400);
    }
    // The example stores an acceptance record only. No email or form values persist.
    $saved = wp_insert_post(array(
        'post_type' => 'amp_build_request',
        'post_status' => 'private',
        'post_title' => 'Synthetic demo form accepted',
    ), true);
    if (is_wp_error($saved) || !$saved) {
        wp_send_json_error(array('message' => 'The request could not be saved.'), 500);
    }
    wp_send_json_success(array('accepted' => true));
}
add_action('admin_post_nopriv_amp_build_demo', 'amp_build_accept_demo');
add_action('admin_post_amp_build_demo', 'amp_build_accept_demo');
```

wp-content/plugins/amplitude-build/events.js

```javascript
// wp-content/plugins/amplitude-build/events.js
// Keep this handler once; experiments and guides reuse its event names.
(() => {
  if (window.__ampBuildEvents) return;
  window.__ampBuildEvents = true;
  document.addEventListener('click', event => {
    if (!(event.target instanceof Element)) return;
    if (!event.target.closest('[data-analytics="pricing-cta"]')) return;
    window.amplitude?.track('Pricing CTA Clicked', {
      placement: 'pricing', page_path: location.pathname
    });
  document.querySelectorAll('[data-amp-demo-form]').forEach(form => {
    let pending = false;
    form.addEventListener('submit', async event => {
      event.preventDefault();
      if (pending || !form.reportValidity()) return;
      pending = true;
      const button = form.querySelector('[type="submit"]');
      const status = form.querySelector('[data-amp-form-status]');
      button.disabled = true;
      status.textContent = 'Sending test request...';
      try {
        // WordPress's input[name="action"] shadows form.action.
        const response = await fetch(form.getAttribute('action'), {
          method: 'POST', body: new URLSearchParams(new FormData(form)), credentials: 'same-origin'
        });
        const body = await response.json();
        if (!response.ok || body.success !== true || body.data?.accepted !== true) {
          throw new Error('The server did not accept this request.');
        }
        status.textContent = 'Test request accepted.';
        window.amplitude?.track('Demo Form Accepted', {
          form_name: 'demo', page_path: location.pathname
        });
      } catch {
        status.textContent = 'The request failed. Refresh and try again.';
      } finally {
        button.disabled = false;
        pending = false;
      }
    });
})();
```

## Hand it to your agent

Point your agent at https\://amplitude.com/build/wordpress-product-analytics.

Agent prompt

```text
Agent task: WordPress product analytics.
Preserve the shared plugin, analytics init, replay privacy, guides, and consent configuration. Add the documented shortcode and anonymous/authenticated WordPress handlers. Track Demo Form Accepted only after a successful JSON response following a saved record. Test invalid nonce, invalid email, actual persistence, duplicate script evaluation, blocked SDK, and mobile submission. Send no form field values to custom events. Report hosted and local evidence separately.
```

## What this unlocks

[Product analytics](https://amplitude.com/amplitude-analytics) measures the accepted-form outcome when `Demo Form Accepted` is installed.

[Session replay](https://amplitude.com/session-replay) shows the interaction around the test form after capture and masking are verified.

[Web Experiment](https://amplitude.com/web-experimentation) can use `Demo Form Accepted` as a conversion metric.

[Guides and surveys](https://amplitude.com/guides-and-surveys) can ask a visitor what they need on Pricing after targeting is configured.

## Next steps

[WordPressHow to add web analytics to a WordPress siteAdd Amplitude web analytics to WordPress through a small plugin. Check page views, campaign attribution, and visitor continuity.](/build/wordpress-web-analytics)[WordPressHow to add session replay to a WordPress siteConfigure replay masking in the WordPress plugin. Record a synthetic visit and verify its content and analytics association.](/build/wordpress-session-replay)[WordPressHow to add Web Experiment to a WordPress siteConfigure a WordPress CTA copy test with Amplitude Web Experiment. Preview variants and check impressions and accepted form conversions.](/build/wordpress-web-experiment)

Reference docs

[Amplitude quickstart →](https://amplitude.com/docs/get-started/amplitude-quickstart)[Browser sdk 2 →](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2)[Debug analytics →](https://amplitude.com/docs/analytics/debug-analytics)[Keys and tokens →](https://amplitude.com/docs/apis/keys-and-tokens)[WordPress script queue →](https://developer.wordpress.org/reference/functions/wp_enqueue_script/)[WordPress inline scripts →](https://developer.wordpress.org/reference/functions/wp_add_inline_script/)

For agents

[View raw .md](/build/wordpress-product-analytics.md)[llms.txt](/llms.txt)

## Add Amplitude to your WordPress site

Start with one shared plugin and follow the tutorials for each product.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
