# Install on WordPress

Paste one script tag into your site's header, clear your cache, and open the site. You can do it with a header-snippet plugin (no code) or from a child theme.

Platform steps checked against the official documentation on 2026-11-03: [WPCode: Using the global Header & Footer settings](https://wpcode.com/docs/using-the-global-header-footer-settings/), [WPCode plugin page](https://wordpress.org/plugins/insert-headers-and-footers/), [WordPress: Child themes](https://developer.wordpress.org/themes/advanced-topics/child-themes/), [WordPress: wp_head](https://developer.wordpress.org/reference/hooks/wp_head/), [WordPress: get_current_user_id()](https://developer.wordpress.org/reference/functions/get_current_user_id/), [WordPress: is_user_logged_in()](https://developer.wordpress.org/reference/functions/is_user_logged_in/), [WP Rocket: Delay JavaScript execution exclusions](https://docs.wp-rocket.me/article/1560-delay-javascript-execution-compatibility-exclusion).

## Quick install

Settings › Channels in the Telltale dashboard shows this tag with your real key already filled in. Copy it from there.

```html
<script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY"></script>
```

### With a plugin (no code)

This uses WPCode ("Insert Headers and Footers"), a free plugin from the WordPress plugin directory. Any plugin that adds code to every page's `<head>` works the same way.

1. In your WordPress admin, go to Plugins › Add New Plugin, search for **WPCode**, then choose **Install Now** and **Activate**.
2. Go to Code Snippets › Header & Footer.
3. Paste the tag into the **Header** box.
4. Choose **Save Changes**.
5. If you use a caching plugin or your host caches pages, clear the cache (see [Common mistakes](#common-mistakes)).

If you switched WPCode to its simple "Headers & Footers mode", the same boxes are under Settings › Header & Footer.

### From a child theme

If you maintain a child theme, print the tag from its `functions.php`. Never edit the parent theme: a theme update overwrites it.

```php
// wp-content/themes/your-child-theme/functions.php
add_action( 'wp_head', function () {
	printf(
		'<script src="%s" data-telltale-key="%s"></script>' . "\n",
		esc_url( 'https://app.trytelltale.dev/sdk/v1.js' ),
		esc_attr( 'YOUR_SITE_PUBLISHABLE_KEY' )
	);
}, 1 );
```

The priority `1` prints it early in `<head>`. The publishable key (`ao_pk_...`) is meant for the browser. Never put the server key (`ao_sk_...`) in a theme, a snippet or a page.

## Consent

The tag records from page load. That is right for sites without a cookie or consent banner.

If your site shows a banner (for example from a cookie consent plugin), hold the tag: add `data-telltale-wait-for-consent`. A held tag sends nothing at all until you call `optIn()`.

```html
<script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY" data-telltale-wait-for-consent></script>
```

Then, from your consent plugin's "consent given" and "consent withdrawn" hooks or JavaScript events, call:

```javascript
// When the banner allows measurement:
window.Telltale.getClient().optIn();
// When the visitor refuses or withdraws:
window.Telltale.getClient().optOut();
```

Apply the saved decision on every page load, and again each time it changes. Signing in is not consent. For OneTrust, Cookiebot or Sourcepoint, use the [Consent bridge guide](/docs/consent) and the [OneTrust](/docs/consent-onetrust), [Cookiebot](/docs/consent-cookiebot) or [Sourcepoint](/docs/consent-sourcepoint) recipe.

Some consent plugins block scripts until consent by changing their `type`. If yours does that, do not also add `data-telltale-wait-for-consent`; pick one way to wait.

## Identify the signed-in user

On a membership, shop or learning site, link each session to the WordPress user. Send the user's numeric ID, which is stable and opaque. Never send the e-mail address, username, display name or phone number.

**From your server (recommended).** Your own backend links the session to the user it authenticated. This is the identity Telltale trusts. See [Quickstart step 2](/docs/quickstart#2-attach-identity-from-your-own-authentication) and [Server API › Associate identity](/docs/server#associate-identity).

**In the tag (a label).** With the child-theme install, add the signed-in user's ID to the tag, only when someone is logged in:

```php
// wp-content/themes/your-child-theme/functions.php
add_action( 'wp_head', function () {
	$user = '';
	if ( is_user_logged_in() ) {
		$user = sprintf( ' data-telltale-user-id="%s"', esc_attr( 'wp_' . get_current_user_id() ) );
	}
	printf(
		'<script src="%s" data-telltale-key="%s"%s></script>' . "\n",
		esc_url( 'https://app.trytelltale.dev/sdk/v1.js' ),
		esc_attr( 'YOUR_SITE_PUBLISHABLE_KEY' ),
		$user
	);
}, 1 );
```

If your site has organisations or teams, add `data-telltale-account-id` the same way with that group's ID. A held tag ignores these attributes until `optIn()`; after consent, call `window.Telltale.getClient().identify({ userId: 'wp_42' })` with the same ID. Calls to `identify()` reject while the tag is held, so wrap them in `try { ... } catch {}`.

A full-page caching plugin can serve one visitor's page to another. Make sure logged-in users get uncached pages (most caching plugins do this by default), or the wrong ID could be printed.

## Content Security Policy

Most WordPress sites send no Content Security Policy. If yours does (from a security plugin, your host or `.htaccess`), allow the Telltale server for scripts and for connections:

```http
Content-Security-Policy: script-src 'self' https://app.trytelltale.dev; connect-src 'self' https://app.trytelltale.dev
```

Keep any sources your policy already lists. Never add `'unsafe-inline'` or `'unsafe-eval'` for Telltale. The [Content Security Policy guide](/docs/csp) has the nonce option too.

## Check it worked

1. In the Telltale dashboard, open Settings › Channels and leave the **Is it working?** check open.
2. Choose **Open my site to test it**, or open your site in a new tab yourself. Browse a page or two.
3. Watch the check move through **Script loaded on** your address, **First session received** and, if you set up identify, **Signed-in user linked** (log in to your site to see it). When the first session arrives, it links to it.

You can also look in your browser's DevTools › Network: `sdk/v1.js` should answer 200, and so should `api/sdk/init`. If it stays at the first step, see [It's not working](/docs/troubleshooting).

## Common mistakes

- **Caching and optimisation plugins.** WP Rocket, LiteSpeed Cache, Autoptimize and similar plugins can delay, defer or combine scripts. Add `sdk/v1.js` to their exclusions: in WP Rocket, the **Excluded JavaScript Files** list and the **Delay JavaScript execution** exclusions; in LiteSpeed Cache, Page Optimization › Tuning, **JS Excludes** and **JS Deferred/Delayed Excludes**; in Autoptimize, **Exclude scripts from Autoptimize**. Then clear the cache.
- **Not clearing the cache.** Until you clear it, visitors get old pages without the tag.
- **Editing the parent theme.** The next theme update removes the tag. Use a plugin or a child theme.
- **`www` and the bare domain.** `https://www.example.com` and `https://example.com` are different addresses. Add every one your site is served from under Settings › Channels › Website addresses, and your staging site too (or give staging its own Site).
- **Two installs.** Do not add the tag with a plugin and from the theme, or with Google Tag Manager as well.
- **Ad blockers.** Some privacy extensions block third-party scripts. Test in a window without extensions.
