---
title: User tracking - GA4 Tutorial
slug: en/user-tracking-ga4-tutorial
createdAt: 2026-02-27T10:22:04.752Z
---

# Ticketmatic Server-Side Events to Google Analytics 4: Step-by-Step Tutorial

This tutorial walks you through sending Ticketmatic's server-side user tracking events to Google Analytics 4 (GA4) using the Measurement Protocol. By the end, you'll have a pipeline that captures every webshop action (add to cart, checkout, purchase, etc.) and forwards it to GA4 for reporting.

Setting this up requires technical knowledge and you might need the help of a partner to implement this for your specific situation.&#x20;

:::hint{type="success"}
Good candidates are:

- your website builder
- your website-ticketing integrator
- your marketing partner
:::

***

## Architecture Overview

The flow works like this:

1. A visitor lands on your website, where your analytics script assigns them a GA4 `client_id`.
2. When the visitor opens a Ticketmatic widget or sales flow, you pass that `client_id` as the `ref` parameter.
3. Ticketmatic records every user action and stores it in their eventstream API.
4. Your server polls the Ticketmatic eventstream, picks up new events, and forwards them to GA4 via the Measurement Protocol.

```markdown
Visitor on your site ──► Ticketmatic widget (?ref=GA_CLIENT_ID)
                                │
                                ▼
                     Ticketmatic Eventstream API
                                │
                         (your server polls)
                                │
                                ▼
                   GA4 Measurement Protocol API
                                │
                                ▼
                    Google Analytics 4 Reports
```

***

## Prerequisites

Before you begin, make sure you have:

- A **Ticketmatic account** with API access (access key + secret key)
- A **Google Analytics 4** property
- A server with **PHP 7.4+** and the `curl` extension enabled (standard on most hosting)
- Basic familiarity with REST APIs

***

## Step 1: Create a GA4 Measurement Protocol API Secret

The Measurement Protocol requires an API secret to authenticate requests.

1. Open [Google Analytics](https://analytics.google.com) and navigate to your GA4 property.
2. Go to **Admin** (gear icon) > **Data Streams**.
3. Click on your web data stream.
4. Scroll down and click **Measurement Protocol API secrets**.
5. Click **Create** and give it a nickname (e.g. "Ticketmatic Server Events").
6. Copy both the **API Secret** and the **Measurement ID** (format: `G-XXXXXXXXXX`). You'll need them later.

:::BlockQuote
**Important:** Keep the API secret private. Never expose it in client-side code.
:::

***

## Step 2: Pass the GA4 Client ID as the Ticketmatic `ref` Parameter

Ticketmatic's user tracking is activated by appending a `ref` parameter to any widget or sales flow URL. The `ref` value ties server-side events back to a specific visitor. We'll use the GA4 `client_id` for this purpose, which allows GA4 to stitch server-side events to the correct user session.

### 2a. Read the GA4 Client ID from the Browser

The GA4 client ID is stored in the `_ga` cookie. You can extract it with JavaScript on your website:

```javascript
// Returns { clientId, sessionId } for the given GA4 Measurement ID (G-XXXXXXXXXX).
// Uses the gtag get API — stable across GA4 cookie format changes (GS1 → GS2).
// Ensure that the GA tag is loaded before calling this function!
function getGa4Ids(measurementId) {
  return new Promise((resolve) => {
    const ids = {};
    const done = () => { if ('clientId' in ids && 'sessionId' in ids) resolve(ids); };
    gtag('get', measurementId, 'client_id',  (v) => { ids.clientId  = v || null; done(); });
    gtag('get', measurementId, 'session_id', (v) => { ids.sessionId = v || null; done(); });
  });
}
```

### 2b. Append `ref` to Your Ticketmatic Links

When generating links to Ticketmatic widgets or sales flows, append `?ref=<client_id>` (or `&ref=<client_id>` if there are already query parameters):

```javascript
const { clientId, sessionId } = await getGa4Ids('G-XXXXXXXXXX'); // replace XXX with you GA identifier
// The client_id contains a dot, so pack the two ids with a '|' delimiter and URL-encode.
const ref = encodeURIComponent(`${clientId}|${sessionId}`);
const ticketmaticUrl =
  `https://apps.ticketmatic.com/widgets/demo/flow/saleslink?event=130716474767&l=en&ref=${ref}`;
// Use this URL for your "Buy Tickets" button
```

The `ref` parameter does not need to be included in the widget signature, so you can safely append it to any pre-generated link.

:::BlockQuote
**Note:** If you don't pass a `ref` parameter, Ticketmatic will **not** track user actions for that session.
:::

***

## Step 3: Poll the Ticketmatic Eventstream API

Ticketmatic stores all tracked user actions in a server-side eventstream. Your job is to poll this endpoint periodically and process new events.

### 3a. Understand the Eventstream Response

Each call to the eventstream returns:

| Field         | Description                                |
| ------------- | ------------------------------------------ |
| `results`     | Array of event objects                     |
| `nextid`      | The ID to use on your next poll call       |
| `moreresults` | Boolean: `true` if more events are waiting |

Each event object contains:

| Field  | Description                             |
| ------ | --------------------------------------- |
| `id`   | Unique ascending event ID               |
| `ts`   | Timestamp in ISO 8601 (UTC)             |
| `type` | Event type string (see below)           |
| `data` | Event payload including the `ref` value |

### 3b. Available Event Types

| Ticketmatic Event                  | Description                            |
| ---------------------------------- | -------------------------------------- |
| `webshop:add_tickets`              | User adds tickets to basket            |
| `webshop:remove_tickets`           | User removes tickets from basket       |
| `webshop:add_products`             | User adds products to basket           |
| `webshop:remove_products`          | User removes products from basket      |
| `webshop:checkout_start`           | User begins checkout                   |
| `webshop:checkout_addcustomerinfo` | User enters contact details            |
| `webshop:checkout_adddeliveryinfo` | User selects delivery method           |
| `webshop:checkout_addpaymentinfo`  | User selects payment method            |
| `webshop:checkout_initiatepayment` | User is redirected to payment provider |
| `webshop:checkout_confirm`         | Payment confirmed, order complete      |
| `webshop:pageview`                 | User views a page in the widget        |

:::BlockQuote
For a detailed description of each event type and its payload, see the [Ticketmatic User Tracking documentation](https://docs.ticketmatic.com/en/user-tracking).
:::

### 3c. Polling Logic

Here's a basic example using `curl`:

```bash
# First call (no id parameter = returns oldest available events)
curl -u '<accesskey>:<secretkey>' \
  'https://apps.ticketmatic.com/api/1/<accountname>/eventstream'

# Subsequent calls (pass nextid from previous response)
curl -u '<accesskey>:<secretkey>' \
  'https://apps.ticketmatic.com/api/1/<accountname>/eventstream?id=<nextid>'
```

The polling loop should follow this pattern:

1. Call the endpoint.
2. Process the results.
3. If `moreresults` is `true`, immediately call again with `nextid`.
4. If `moreresults` is `false`, wait 5-60 seconds, then call again.

:::BlockQuote
**Important:** Ticketmatic stores eventstream items for **4 hours only**. Make sure your polling process runs frequently enough to avoid missing events.
:::

***

## Step 4: Map Ticketmatic Events to GA4 Events

Below is a recommended mapping from Ticketmatic event types to GA4 recommended ecommerce events:

| Ticketmatic Event                  | GA4 Event Name      | GA4 Event Parameters                           |
| ---------------------------------- | ------------------- | ---------------------------------------------- |
| `webshop:add_tickets`              | `add_to_cart`       | `items`, `value`, `currency`                   |
| `webshop:remove_tickets`           | `remove_from_cart`  | `items`, `value`, `currency`                   |
| `webshop:add_products`             | `add_to_cart`       | `items`, `value`, `currency`                   |
| `webshop:remove_products`          | `remove_from_cart`  | `items`, `value`, `currency`                   |
| `webshop:checkout_start`           | `begin_checkout`    | `items`, `value`, `currency`                   |
| `webshop:checkout_addcustomerinfo` | `add_shipping_info` | `items`, `value`, `currency`                   |
| `webshop:checkout_addpaymentinfo`  | `add_payment_info`  | `items`, `value`, `currency`, `payment_type`   |
| `webshop:checkout_confirm`         | `purchase`          | `transaction_id`, `items`, `value`, `currency` |
| `webshop:pageview`                 | `page_view`         | `page_title`, `page_location`                  |

You can also send `webshop:checkout_initiatepayment` and `webshop:checkout_adddeliveryinfo` as custom events if you want that level of granularity. Refer to the [Ticketmatic User Tracking documentation](https://docs.ticketmatic.com/en/user-tracking) for the full payload structure of each event type, so you can extract the right fields for your GA4 parameters.

***

## Step 5: Send Events to GA4 via the Measurement Protocol

### 5a. The GA4 Measurement Protocol Endpoint

Send `POST` requests to:

```bash
https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
```

### 5b. Request Body Structure

```json
{
  "client_id": "<the ref value from Ticketmatic>",
  "events": [
    {
      "name": "add_to_cart",
      "params": {
        "currency": "EUR",
        "value": 45.00,
        "items": [
          {
            "item_id": "EVT-12345",
            "item_name": "Concert - Main Hall - Cat 1",
            "item_category": "Tickets",
            "price": 45.00,
            "quantity": 1
          }
        ]
      }
    }
  ]
}
```

### 5c. Full Implementation Example (PHP)

```php
<?php

/**
 * Ticketmatic Eventstream → GA4 Measurement Protocol Bridge
 *
 * Run as a CLI script:   php ticketmatic-ga4-bridge.php
 * Or via cron job:        * * * * * php /path/to/ticketmatic-ga4-bridge.php
 */

// --- Configuration ---
define('TM_ACCESS_KEY',     'your_ticketmatic_access_key');
define('TM_SECRET_KEY',     'your_ticketmatic_secret_key');
define('TM_ACCOUNT',        'your_account_shortname');

define('GA4_MEASUREMENT_ID', 'G-XXXXXXXXXX');
define('GA4_API_SECRET',     'your_ga4_api_secret');

// File to persist the nextid between runs
define('STATE_FILE', __DIR__ . '/eventstream_state.json');

// ============================================================
// GA4 Measurement Protocol
// ============================================================

/**
 * Send a single event to GA4 via the Measurement Protocol.
 */
function sendToGA4(string $clientId, string $sessionId, string $eventName, array $eventParams): void
{
    $url = sprintf(
        'https://www.google-analytics.com/mp/collect?measurement_id=%s&api_secret=%s',
        GA4_MEASUREMENT_ID,
        GA4_API_SECRET
    );
    
    $params = array_merge([
        'engagement_time_msec' => '1',// Required for the event to appear in reports
    ], $eventParams);
    
    // Only send session_id when we actually have one; never fall back to client_id.
    if (!empty($sessionId)) {
         $params['session_id'] = $sessionId;
    }

    $payload = json_encode([
        'client_id' => $clientId,
        'events'    => [[
            'name'   => $eventName,
            'params' => $params,
        ]],
    ]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode < 200 || $httpCode >= 300) {
        error_log("GA4 error (HTTP $httpCode): $response");
    }
}

// ============================================================
// Event Mapping
// ============================================================

/**
 * Map a single Ticketmatic event to a GA4 event and send it.
 */
function mapAndSend(array $tmEvent): void
{
    $data = $tmEvent['data'] ?? [];
    $ref  = $data['ref'] ?? null;

    if (empty($ref)) {
        return; // No ref = can't link to GA4 user
    }

    // ref carries "clientId|sessionId". Older links may contain only the clientId.
    $parts     = explode('|', $ref, 2);
    $clientId  = $parts[0];
    $sessionId = $parts[1] ?? null; // null for legacy refs → user-level attribution only

    $type  = $tmEvent['type'] ?? '';
    $order = $data['order'] ?? [];

    $currency = $order['currency'] ?? 'EUR';

    switch ($type) {
        case 'webshop:add_tickets':
            sendToGA4($clientId, $sessionId, 'add_to_cart', [
                'currency' => $currency,
                'value'    => calculateValue($data['tickets'] ?? []),
                'items'    => mapTicketsToItems($data['tickets'] ?? []),
            ]);
            break;

        case 'webshop:remove_tickets':
            sendToGA4($clientId, $sessionId, 'remove_from_cart', [
                'currency' => $currency,
                'value'    => calculateValue($data['tickets'] ?? []),
                'items'    => mapTicketsToItems($data['tickets'] ?? []),
            ]);
            break;

        case 'webshop:add_products':
            sendToGA4($clientId, $sessionId, 'add_to_cart', [
                'currency' => $currency,
                'value'    => calculateValue($data['products'] ?? []),
                'items'    => mapProductsToItems($data['products'] ?? []),
            ]);
            break;

        case 'webshop:remove_products':
            sendToGA4($clientId, $sessionId, 'remove_from_cart', [
                'currency' => $currency,
                'value'    => calculateValue($data['products'] ?? []),
                'items'    => mapProductsToItems($data['products'] ?? []),
            ]);
            break;

        case 'webshop:checkout_start':
            sendToGA4($clientId, $sessionId, 'begin_checkout', [
                'currency' => $currency,
                'value'    => (float) ($order['totalamount'] ?? 0),
            ]);
            break;

        case 'webshop:checkout_addpaymentinfo':
            sendToGA4($clientId, $sessionId, 'add_payment_info', [
                'currency' => $currency,
                'value'    => (float) ($order['totalamount'] ?? 0),
            ]);
            break;

        case 'webshop:checkout_confirm':
            sendToGA4($clientId, $sessionId, 'purchase', [
                'transaction_id' => (string) ($order['orderid'] ?? $tmEvent['id'] ?? ''),
                'currency'       => $currency,
                'value'          => (float) ($order['totalamount'] ?? 0),
            ]);
            break;

        case 'webshop:pageview':
            sendToGA4($clientId, $sessionId, 'page_view', [
                'page_title' => $data['page'] ?? 'Ticketmatic',
            ]);
            break;

        default:
            error_log("Skipping unknown event type: $type");
    }
}

// ============================================================
// Helper Functions
// ============================================================

function calculateValue(array $items): float
{
    return array_reduce($items, function (float $sum, array $item) {
        return $sum + (float) ($item['price'] ?? 0);
    }, 0.0);
}

function mapTicketsToItems(array $tickets): array
{
    return array_map(function (array $t) {
        return [
            'item_id'       => (string) ($t['tickettypepriceid'] ?? $t['id'] ?? ''),
            'item_name'     => $t['eventname'] ?? $t['tickettypename'] ?? 'Ticket',
            'item_category' => 'Tickets',
            'price'         => (float) ($t['price'] ?? 0),
            'quantity'      => 1,
        ];
    }, $tickets);
}

function mapProductsToItems(array $products): array
{
    return array_map(function (array $p) {
        return [
            'item_id'       => (string) ($p['id'] ?? ''),
            'item_name'     => $p['name'] ?? 'Product',
            'item_category' => 'Products',
            'price'         => (float) ($p['price'] ?? 0),
            'quantity'      => 1,
        ];
    }, $products);
}

// ============================================================
// State Persistence
// ============================================================

function loadNextId(): ?string
{
    if (!file_exists(STATE_FILE)) {
        return null;
    }
    $state = json_decode(file_get_contents(STATE_FILE), true);
    return $state['nextid'] ?? null;
}

function saveNextId(string $nextId): void
{
    file_put_contents(STATE_FILE, json_encode(['nextid' => $nextId]));
}

// ============================================================
// Ticketmatic Eventstream Polling
// ============================================================

function pollEventstream(): void
{
    $nextId = loadNextId();

    // We loop as long as there are more results, then exit.
    // The cron job will re-invoke this script on the next interval.
    do {
        $url = sprintf(
            'https://apps.ticketmatic.com/api/1/%s/eventstream%s',
            TM_ACCOUNT,
            $nextId ? '?id=' . urlencode($nextId) : ''
        );

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_USERPWD        => TM_ACCESS_KEY . ':' . TM_SECRET_KEY,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 30,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            error_log("Ticketmatic API error (HTTP $httpCode): $response");
            return;
        }

        $data = json_decode($response, true);

        if (!is_array($data)) {
            error_log("Invalid JSON from Ticketmatic eventstream");
            return;
        }

        // Process each event
        foreach ($data['results'] ?? [] as $event) {
            mapAndSend($event);
        }

        // Persist the cursor
        $nextId = $data['nextid'] ?? null;
        if ($nextId) {
            saveNextId($nextId);
        }

        $moreResults = $data['moreresults'] ?? false;

    } while ($moreResults);

    echo "Poll complete. Processed " . count($data['results'] ?? []) . " event(s).\n";
}

// --- Run ---
pollEventstream();
```

:::BlockQuote
**Running the script:** You can either run this as a long-lived CLI process with a `sleep()` loop, or (recommended) schedule it as a **cron job** that runs every minute:

The script persists its cursor to `eventstream_state.json`, so each invocation picks up where the last one left off.
:::

```javascript
* * * * * php /path/to/ticketmatic-ga4-bridge.php >> /var/log/tm-ga4.log 2>&1
```

***

## Step 6: Validate Your Setup

Before going live, use the GA4 debug endpoint to validate your events.

### 6a. Use the Validation Endpoint

Replace `/mp/collect` with `/debug/mp/collect` in the URL:

```curl
https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
```

This returns a JSON response telling you whether the event was valid or what errors were found. No data is actually sent to your GA4 property.

### 6b. Check GA4 Realtime Reports

1. Open your GA4 property in Google Analytics.
2. Go to **Reports** > **Realtime**.
3. Trigger a test action in a Ticketmatic widget (make sure the `ref` parameter is set).
4. Within a few seconds, you should see the event appear in the Realtime report.

### 6c. Verify Ecommerce Reports

After events have been flowing for 24-48 hours:

1. Go to **Reports** > **Monetization** > **Ecommerce purchases**.
2. You should see transactions attributed to the `purchase` events.

***

## Step 7: Deploy to Production

### Hosting Options

Your polling script needs to run continuously. Good options include:

- **A cron job** on your existing PHP hosting (simplest option — run every minute)
- **A long-lived CLI process** with a `while(true)` / `sleep()` loop on a server
- **A cloud function on a schedule** (e.g. AWS Lambda with a PHP runtime layer, or Google Cloud Run)
- **A container** (Docker with PHP CLI) on any cloud platform

### Persistence

Store the `nextid` value between runs (e.g. in a database, a file, or an environment variable) so you don't reprocess old events or miss events after a restart.

### Error Handling Recommendations

- **Retry failed GA4 calls** with exponential backoff.
- **Log all events** for debugging and auditing.
- **Monitor your polling interval** to ensure you process events within the 4-hour window.
- **Handle unknown event types** gracefully since Ticketmatic may add new types in the future.

***

## GDPR and Privacy Considerations

Ticketmatic's user tracking is entirely opt-in. A few things to keep in mind:

- Only pass the `ref` parameter after obtaining valid consent from the user.
- Ticketmatic stores tracking data exclusively in their own systems and does not inject third-party trackers or non-essential cookies.
- Since you're forwarding data to Google Analytics, make sure your privacy policy covers this data flow.
- Consider using GA4's consent mode to respect user preferences on the Google side as well.

***

## Summary Checklist

- [ ] GA4 Measurement Protocol API secret created
- [ ] GA4 Measurement ID noted
- [ ] Website reads GA4 `client_id` from `_ga` cookie
- [ ] Ticketmatic widget/sales flow URLs include `?ref=<client_id>`
- [ ] Server-side script polls Ticketmatic eventstream
- [ ] Events are mapped to GA4 ecommerce events
- [ ] Events are sent to GA4 Measurement Protocol
- [ ] Validation endpoint confirms events are well-formed
- [ ] Realtime reports show incoming events
- [ ] `nextid` is persisted between polling runs
- [ ] GDPR consent is handled before tracking is activated

***

*References: [Ticketmatic User Tracking Docs](https://docs.ticketmatic.com/en/user-tracking), [GA4 Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events), [GA4 Ecommerce Events](https://developers.google.com/analytics/devguides/collection/ga4/set-up-ecommerce)*
