How to Add Castlytics to a Server-Side Tag Manager
Server-side tag managers — most commonly Google Tag Manager's server container (sGTM) — proxy tracking requests through your own subdomain rather than running JavaScript directly in the visitor's browser. The result: better data quality, fewer ad blocker losses, and a single governed layer where you control what fires and when.
This guide shows you how to integrate Castlytics with a server-side tag manager setup so that your page views, conversion events, and promo code data flow through your server container instead of relying entirely on the client-side snippet.
How Castlytics Tracking Works (Quick Recap)
Before diving in, a quick recap of the two Castlytics endpoints you need to send data to:
| Endpoint | What it does |
|----------|--------------|
| POST /api/events | Records a page_view, campaign_click, or campaign_landing event |
| POST /api/conversions | Records a purchase, signup, or lead conversion |
Both endpoints are public (no authentication beyond your workspace key) and accept JSON. The visitor ID (cly_vid) is what ties events and conversions together — it must be consistent across calls for the same visitor session.
Architecture Overview
In a server-side setup you have two options:
Option A — Hybrid (recommended for most sites): Keep the Castlytics client script on the page for visitor ID management and vanity path detection, but route conversion events through your server container. The client script handles the cookie and link-click attribution; your server container handles purchase events where you have reliable order data.
Option B — Fully server-side: Skip the client script entirely. Your server container manages the visitor cookie, fires page view events, and sends conversions. This approach works well if you already have a robust sGTM setup and want zero client-side tracking JavaScript.
Most brands start with Option A. Option B is covered at the end of this guide.
Option A: Hybrid Setup
Step 1: Install the Castlytics Client Script
Add the standard Castlytics snippet to your site's <head>. This handles visitor ID assignment, vanity path detection, and link click attribution — things that inherently need to run in the browser.
<script
src="https://castlytics.app/tracker.js"
data-workspace-key="YOUR_WORKSPACE_KEY"
defer>
</script>
The script exposes window._cly_vid — the current visitor's ID — as a plain global you can read from any other tag or data layer push.
Step 2: Push the Visitor ID to the Data Layer
On every page load, push the Castlytics visitor ID into your GTM data layer so your server container can access it. Add this snippet after the Castlytics script:
<script>
// Wait for Castlytics to initialise, then push to data layer
(function poll() {
if (window._cly_vid) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ cly_vid: window._cly_vid });
} else {
setTimeout(poll, 50);
}
})();
</script>
In GTM, create a Data Layer Variable named cly_vid that reads cly_vid from the data layer. You'll use this variable in your server-side tags.
Step 3: Configure Your Server Container to Forward Conversions
In your sGTM server container, create a Custom Tag that fires on your conversion event (e.g. purchase). The tag should make an HTTP POST request to the Castlytics conversions endpoint.
Tag configuration:
- Tag type: HTTP Request (or a custom tag template)
- URL:
https://castlytics.app/api/conversions - Method: POST
- Headers:
Content-Type: application/json - Body:
{
"visitorId": "{{cly_vid}}",
"workspaceKey": "YOUR_WORKSPACE_KEY",
"type": "purchase",
"revenue": {{ecommerce.value}},
"currency": "{{ecommerce.currency}}",
"promoCode": "{{coupon}}"
}
Replace {{ecommerce.value}}, {{ecommerce.currency}}, and {{coupon}} with the data layer variables that carry your order data. These names match the standard GA4 / GTM ecommerce schema — adjust to match your own data layer if it differs.
Trigger: Fire this tag on your purchase event (the same event you use to fire GA4 or other analytics).
Step 4: Verify the Setup
- Place a test order on your staging site.
- In your sGTM preview/debug mode, confirm the Custom Tag fires and returns a 2xx response.
- In your Castlytics dashboard, verify the conversion appears attributed to the correct campaign within a minute or two.
If the conversion appears but attribution shows "unknown", the visitorId in the request didn't match any tracked visitor. Check that window._cly_vid is being pushed to the data layer correctly before the conversion event fires.
Option B: Fully Server-Side Setup
If you want no client-side Castlytics JavaScript at all, your server container must handle three responsibilities the client script normally handles: visitor ID management, page view tracking, and conversion tracking.
Step 1: Manage the Visitor Cookie Server-Side
Castlytics uses a first-party cookie named cly_vid (a UUID) to identify visitors across sessions. In your sGTM server container, read this cookie from the incoming request and set it in the outgoing response.
Create a Cookie variable in sGTM that reads the cly_vid cookie. Then, in your container's initialization or page view tag, if the cookie is absent, generate a new UUID and set it as a first-party cookie on your domain:
- Cookie name:
cly_vid - Expiry: 365 days
- Path:
/ - SameSite: Lax
Many sGTM setups use a custom tag template or Firestore lookup to generate and persist UUIDs. If you already have a visitor ID managed by another tool (e.g. a _ga cookie value), you can reuse that as the cly_vid value — it just needs to be a stable per-visitor identifier.
Step 2: Send Page View Events
Create a tag that fires on every page load and POSTs a page view to Castlytics:
POST https://castlytics.app/api/events
{
"type": "page_view",
"visitorId": "{{cly_vid}}",
"workspaceKey": "YOUR_WORKSPACE_KEY",
"url": "{{Page URL}}",
"referrer": "{{Referrer}}"
}
Set the trigger to All Pages (or your equivalent "page view" trigger).
Step 3: Handle Vanity Path Detection
The client-side script checks whether the current page path matches a campaign vanity path and fires a campaign_landing event if it does. Server-side, you replicate this by:
- Fetching the vanity path map from
GET https://castlytics.app/api/workspace/vanity-paths?key=YOUR_WORKSPACE_KEY(you can cache this response for a few minutes to avoid unnecessary lookups). - Comparing the incoming request path against the path map.
- If there's a match, POSTing a
campaign_landingevent alongside your page view tag.
This is best implemented as a lookup table or a custom tag template in sGTM that fires conditionally when the request path matches a known vanity path prefix.
Step 4: Send Conversions
Same as Option A Step 3 — create a tag that POSTs to /api/conversions on your purchase event, using the server-managed cly_vid cookie as the visitorId.
Sending Promo Code Data
If your ecommerce platform sends coupon/promo code data in the data layer (standard in GA4 ecommerce), include the promoCode field in your conversion payload:
{
"visitorId": "{{cly_vid}}",
"workspaceKey": "YOUR_WORKSPACE_KEY",
"type": "purchase",
"revenue": {{ecommerce.value}},
"promoCode": "{{coupon}}"
}
Castlytics will match the promo code to the campaign it belongs to and attribute the conversion accordingly, even if no tracking link was clicked and no vanity path was visited. This is especially useful for offline or email-driven promo code usage that bypasses your regular web tracking.
Shopify Considerations
If your store runs on Shopify, the recommended approach is to use the native Castlytics–Shopify integration (Settings → Integrations) rather than routing through a tag manager. The integration syncs orders and coupon data directly via the Shopify API and doesn't require you to manage event tags in GTM at all.
If you need sGTM for other reasons (e.g. you're sending data to multiple analytics destinations), you can run both in parallel. Just be sure your conversion tag deduplications — use a condition to only fire the Castlytics tag when the Shopify integration is not connected, or you'll double-count conversions.
Troubleshooting
Conversions appear without attribution
The visitorId in the conversion request didn't match a visitor who clicked a tracking link or visited a vanity path. Common causes:
- The
cly_vidcookie isn't being set or read correctly across subdomains - The data layer push is happening before
window._cly_vidis available (the poll-and-push approach in Step 2 solves this) - You're testing in an incognito window that also opened the tracking link in incognito — cookies persist within a session but not across separate incognito windows
Server container returns a 400 error
Check that your JSON payload includes all required fields: visitorId, workspaceKey, and type. The revenue field defaults to 0 if omitted, but workspaceKey and visitorId are required.
Conversion events are duplicated
The conversion tag is firing twice — once from the client script (if you left castlytics.conversion() in your page code) and once from your sGTM tag. Remove the castlytics.conversion() call from your confirmation page once the server-side tag is verified to be working.
Summary
| Approach | Visitor ID | Page Views | Conversions | Vanity Paths | |----------|-----------|-----------|-------------|-------------| | Client script only | Client JS | Client JS | Client JS | Client JS | | Hybrid (recommended) | Client JS | Client JS | sGTM tag | Client JS | | Fully server-side | sGTM cookie | sGTM tag | sGTM tag | sGTM lookup |
Start with the hybrid approach: install the Castlytics client script for visitor ID and vanity path detection, then route conversion events through your server container where you have reliable, ad-blocker-proof order data. This gives you the attribution accuracy of server-side tracking without the complexity of replicating cookie management from scratch.
Ready to track your podcast ad ROI?
Castlytics gives you per-campaign attribution, real-time ROI, and listener journey analytics — free to get started.
Start free — no credit card