flowsk.com
WordPress

WordPress & WooCommerce install guide

WordPress runs on your own server, in PHP you control. That is the best possible starting position for first-party attribution, and almost nobody uses it.

Aug 4, 2026· 3 min read ·Docs
Quick answer

init hook sets the cookie · wp_head loads the snippet · woocommerce_payment_complete confirms

WordPress can set a durable first-party cookie from PHP in three lines. Everything is same-origin, there is no cross-domain checkout, and order hooks give you an authoritative server-side purchase event. This is the easiest stack to do properly — the ecosystem just defaults to JavaScript tags.

The one real hazard is page caching, which can strip or misdeliver your Set-Cookie header.

WordPress is the stack where first-party attribution is genuinely easy, and where almost nobody does it — because every plugin in the ecosystem reaches for a JavaScript tag first.

You have PHP running on every request, on your own domain, with hooks for every meaningful event. Use them.

// wp-content/plugins/flowsk/flowsk.php
add_action('init', function () {
  if (empty($_COOKIE['_fsk_id'])) {
    $id = wp_generate_uuid4();
    setcookie('_fsk_id', $id, [
      'expires'  => time() + 63072000,  // 2 years
      'path'     => '/',
      'samesite' => 'Lax',
      'secure'   => is_ssl(),
      'httponly' => false,              // the snippet reads the same id
    ]);
    $_COOKIE['_fsk_id'] = $id;          // available on this request too
  }
});

Because PHP writes this in the HTTP response, Safari’s seven-day cap on JavaScript cookies does not apply. Three lines, and you are ahead of most instrumented sites on the internet.

2. Load the snippet

add_action('wp_head', function () {
  $key = defined('FLOWSK_WRITE_KEY') ? FLOWSK_WRITE_KEY : '';
  if (!$key) return;
  printf(
    '<script async src="https://flowsk.com/flowsk.js" data-write-key="%s"></script>' . "\n",
    esc_attr($key)
  );
}, 1);  // priority 1 — before other plugins' tags

3. Confirm the purchase (WooCommerce)

add_action('woocommerce_payment_complete', function ($order_id) {
  $order = wc_get_order($order_id);
  if (!$order) return;

  flowsk_server_event([
    'source'      => 'server',
    'category'    => 'purchase',
    'name'        => 'purchase',
    'email'       => $order->get_billing_email(),
    'value_cents' => (int) round($order->get_total() * 100),
    'dedup_key'   => 'wc_order_' . $order_id,
  ]);
});

function flowsk_server_event($event) {
  wp_remote_post('https://flowsk.com/api/events', [
    'timeout'  => 5,
    'blocking' => false,   // do not make the shopper wait
    'headers'  => ['Content-Type' => 'application/json'],
    'body'     => wp_json_encode([
      'write_key'    => FLOWSK_WRITE_KEY,
      'anonymous_id' => $_COOKIE['_fsk_id'] ?? null,
      'events'       => [$event],
    ]),
  ]);
}

'blocking' => false matters — a slow HTTP call inside a payment hook is a checkout you just made worse.

4. Identify on registration and forms

// Account creation
add_action('user_register', function ($user_id) {
  $user = get_userdata($user_id);
  flowsk_server_event([
    'source'    => 'server',
    'category'  => 'identification',
    'name'      => 'signup_started',
    'email'     => $user->user_email,
    'dedup_key' => 'wp_user_' . $user_id,
  ]);
});

// Contact Form 7 — on confirmed send, not on submit
add_action('wpcf7_mail_sent', function ($form) {
  $data = WPCF7_Submission::get_instance()->get_posted_data();
  flowsk_server_event([
    'source'   => 'server',
    'category' => 'identification',
    'name'     => 'lead_captured',
    'email'    => $data['your-email'] ?? null,
  ]);
});

Gravity Forms uses gform_after_submission; Ninja Forms uses ninja_forms_after_submission. The principle is the same everywhere: fire on the confirmed submission, never on focus or on submit-attempt.

The caching problem

This is the one that catches people. A full-page cache plugin (WP Rocket, W3 Total Cache, LiteSpeed, or your host’s edge cache) serves a stored response — including, potentially, a Set-Cookie header generated for a completely different visitor.

Three ways out, best first:

  1. Exclude the cookie from cache. Most plugins let you list cookies that bypass the cache or headers to strip.
  2. Set the id from a tiny uncached endpoint the snippet calls once (/wp-json/flowsk/v1/id).
  3. Set it at the edge — a Cloudflare Worker in front of WordPress, which never touches PHP.

Verify with the cookie inspector: load your site twice in private windows and confirm you get two different _fsk_id values. If you get the same one, your cache is serving someone else’s cookie.

Plugin hygiene

Fire at priority 1. Other plugins inject tags too; being first means you capture the landing URL before anything rewrites it.

Expect duplicate hooks. If two analytics plugins listen to woocommerce_payment_complete, both fire. The dedup key makes that harmless — which is a good reason to always set one.

Use a site-specific plugin, not functions.php. Theme changes should not remove your measurement.

Filter test orders before sending, or your first week will look implausibly good.

Frequently asked questions

Which hook should fire the purchase event?

woocommerce_payment_complete. It fires when payment is confirmed, not when the order is created, so you never credit abandoned or failed orders.

Does page caching break the cookie?

It can. A full-page cache may serve a response whose Set-Cookie was generated for another visitor, or strip it entirely. Exclude the cookie-setting path from cache, or set the id from a small uncached endpoint.

What about guest checkout?

The email exists on the order even without an account. Send it with the purchase event and the person record is complete.

Will multiple analytics plugins cause duplicates?

If several plugins hook the same order event, yes. The dedup key makes it harmless — the second event finds the first and merges instead of inserting.

Where do I put the PHP?

A small site-specific plugin is better than functions.php, because it survives a theme change. mu-plugins is better still if you control the host.

Check what your plugins are loading

Scan your site: every third-party script your plugins have injected, and whether anything sets a durable first-party id.

Scan my site

Stop guessing which ad made the sale.

Flowsk Signals stitches the anonymous click to the email to the purchase — first-party, server-side, de-duplicated. One snippet, $29/mo, and every conversion comes with a receipt you can inspect.

Keep reading