User tracking - GA4 Tutorial
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.
Good candidates are:
- your website builder
- your website-ticketing integrator
- your marketing partner
Architecture Overview
The flow works like this:
- A visitor lands on your website, where your analytics script assigns them a GA4 client_id.
- When the visitor opens a Ticketmatic widget or sales flow, you pass that client_id as the ref parameter.
- Ticketmatic records every user action and stores it in their eventstream API.
- Your server polls the Ticketmatic eventstream, picks up new events, and forwards them to GA4 via the Measurement Protocol.
Visitor on your site ──► Ticketmatic widget (?ref=GA_CLIENT_ID)
│
▼
Ticketmatic Eventstream API
│
(your server polls)
│
▼
GA4 Measurement Protocol API
│
▼
Google Analytics 4 ReportsPrerequisites
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.
- Open Google Analytics and navigate to your GA4 property.
- Go to Admin (gear icon) > Data Streams.
- Click on your web data stream.
- Scroll down and click Measurement Protocol API secrets.
- Click Create and give it a nickname (e.g. "Ticketmatic Server Events").
- Copy both the API Secret and the Measurement ID (format: G-XXXXXXXXXX). You'll need them later.
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:
// 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):
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" buttonThe ref parameter does not need to be included in the widget signature, so you can safely append it to any pre-generated link.
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 |
3c. Polling Logic
Here's a basic example using curl:
# 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:
- Call the endpoint.
- Process the results.
- If moreresults is true, immediately call again with nextid.
- If moreresults is false, wait 5-60 seconds, then call again.
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 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:
https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET5b. Request Body Structure
{
"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
/**
* 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();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.
* * * * * php /path/to/ticketmatic-ga4-bridge.php >> /var/log/tm-ga4.log 2>&1Step 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:
https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRETThis 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
- Open your GA4 property in Google Analytics.
- Go to Reports > Realtime.
- Trigger a test action in a Ticketmatic widget (make sure the ref parameter is set).
- 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:
- Go to Reports > Monetization > Ecommerce purchases.
- 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