Blog
Headers carry geo data, request signatures, and consent state. Here is how each one travels into your tags.
Custom Templates in sGTM read incoming request headers via the getRequestHeader API. Header names are case-insensitive (HTTP standard) but the API expects lowercase. Several common header sources have specific quirks worth knowing.
const userAgent = getRequestHeader('user-agent');
const referer = getRequestHeader('referer');
const acceptLanguage = getRequestHeader('accept-language');
All standard headers are accessible. Returns null if the header is not set.
If your client-side code adds custom headers to the request (via fetch options), those headers are visible to your template. The browser may strip some custom headers if CORS is not configured to allow them; check that your CORS Allow-Headers includes any custom headers you depend on.
App Engine adds x-appengine-country, x-appengine-region, and similar. Self-hosted on Cloud Run or other platforms have different headers; check the platform documentation.
When sGTM is behind a load balancer or CDN, x-forwarded-for is a comma-separated list of IPs. The leftmost is the original client; the rightmost is the most recent hop. To get the actual client IP:
const xff = getRequestHeader('x-forwarded-for') || '';
const clientIP = xff.split(',')[0].trim();
Cookies are not accessible via getRequestHeader (they are technically a single Cookie header but you should use the cookie API). Use getCookieValues('cookie_name') instead.
Some platforms strip headers that look like they could be spoofed: x-real-ip, x-original-ip, anything starting with x-google-. If you cannot read a header you know was sent, the platform may have removed it before your template runs.
For debugging, log all available headers with log(JSON.stringify(getAllEventData())). Not all event data is headers, but the dump is exhaustive enough to surface anything you might be missing.