Integrations

Next.js + Google Tag Manager Integration

Complete guide to integrate Checkpoint user identification with Next.js and Google Tag Manager

Overview

This guide shows how to integrate Checkpoint with a Next.js website using Google Tag Manager (GTM) to track user identification. When users log into your site, their userId will appear on the Activity page of your Checkpoint dashboard, matching your internal user system.

Goal: When a user logs in as userId: "user_123" on your site, Checkpoint will track them with the same userId: "user_123" so you can correlate your user data with AI agent detection.

How It Works

  1. User visits your site → GTM loads the Checkpoint pixel through a Custom HTML tag
  2. User logs in → Your Next.js app pushes user data to GTM dataLayer
  3. GTM detects login → Triggers Checkpoint.identify() with user data
  4. Checkpoint tracks → All subsequent activity shows with userId and email
  5. Activity page → Shows the same userId from your system

Prerequisites

  • Your Checkpoint Project ID, from Installations in your project dashboard (see Credentials).
  • A GTM container that allows Custom HTML tags. Both tags in this guide are Custom HTML.

Step 1.2 uses a Custom HTML tag because it can set every Pixel option. If your Project ID is the only option you set, the Checkpoint Pixel template from the GTM gallery can load the Pixel instead: it loads pixel.js?project-id=YOUR_PROJECT_ID and runs the Pixel with its defaults. It can't set any other data-* option: if you uncheck its Enable Fingerprinting box or set its API Endpoint to a custom URL, the tag stops without loading the Pixel. Earlier versions of the template sent ?project_id= and never started the Pixel (the browser console shows [Checkpoint] No project ID provided), so accept the template update under Templates in your GTM workspace and Submit → Publish the container before you rely on it.

Step 1: GTM Setup

1.1 Create dataLayer Variables

Go to Variables → User-Defined Variables → New and create these three variables. The User Identification tag (Step 1.3) reads from them.

Variable 1: User ID

  • Variable Type: Data Layer Variable
  • Data Layer Variable Name: userId
  • Variable Name: DLV - User ID

Variable 2: User Email

  • Variable Type: Data Layer Variable
  • Data Layer Variable Name: userEmail
  • Variable Name: DLV - User Email

Variable 3: User Name (Optional)

  • Variable Type: Data Layer Variable
  • Data Layer Variable Name: userName
  • Variable Name: DLV - User Name

1.2 Create the Checkpoint Pixel tag

Go to Tags → New:

  • Tag Type: Custom HTML
  • Tag Name: Checkpoint - Pixel Loader
  • HTML: the loader snippet the dashboard generates:
<script>
  (function () {
    var as = document.createElement('script');
    as.type = 'text/javascript';
    as.async = true;
    as.src = 'https://kya.vouched.id/pixel.js';
    as.setAttribute('data-project-id', 'YOUR_PROJECT_ID');
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(as, s);
  })();
</script>
  • Triggering: All Pages
  • Tag Firing Options: Once per page

Replace YOUR_PROJECT_ID with your actual Checkpoint Project ID before saving.

Every other Pixel option (session timeout, batch size, fingerprinting, Do Not Track, consent gating, IP anonymization, URL redaction) is a data-* attribute you set on the same script with another as.setAttribute(...) line. While you test, add as.setAttribute('data-debug', 'true'); and remove it before you publish. Duplicate loads are safe: pixel.js guards itself against double-initialization.

1.3 Create the User Identification tag

User identification uses a second Custom HTML tag, because it needs to read your dataLayer variables and call Checkpoint.identify() after login events. (The pixel registers its API on window.Checkpoint; the previous window.AgentShield global remains as a deprecated alias, so older identification tags keep working.)

Go to Tags → New:

  • Tag Type: Custom HTML
  • Tag Name: Checkpoint - User Identification
  • HTML:
<script>
  (function () {
    // Wait for the Checkpoint pixel to be available with retry logic
    function identifyUser(retries) {
      retries = retries || 0;

      if (window.Checkpoint) {
        var userId = {{DLV - User ID}};
        var userEmail = {{DLV - User Email}};
        var userName = {{DLV - User Name}};

        if (userId) {
          try {
            window.Checkpoint.identify(userId, {
              email: userEmail,
              name: userName,
            });
            console.log('[GTM] Checkpoint user identified:', userId);
          } catch (error) {
            console.error('[GTM] Failed to identify user:', error);
          }
        } else {
          console.warn('[GTM] No userId provided to identify');
        }
      } else if (retries < 50) {
        // Retry up to 50 times (5 seconds total)
        setTimeout(function () {
          identifyUser(retries + 1);
        }, 100);
      } else {
        console.error('[GTM] Checkpoint pixel failed to load after 5 seconds');
      }
    }

    identifyUser();
  })();
</script>
  • Triggering: Custom Event
  • Event Name: user_login
  • Tag Sequencing (optional but recommended): Set this tag to fire after "Checkpoint - Pixel Loader" so the retry loop usually completes on the first attempt.

Step 2: Next.js Integration

If you have Content Security Policy (CSP) configured, you must allowlist https://kya.vouched.id in both script-src and connect-src directives. See the CSP configuration example below.

2.1 Add GTM to Your Next.js App

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        {/* Google Tag Manager */}
        <Script id="gtm" strategy="afterInteractive">
          {`
            (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
            new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
            j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
            'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
            })(window,document,'script','dataLayer','GTM-XXXXXXX');
          `}
        </Script>
      </head>
      <body>
        {/* GTM NoScript */}
        <noscript>
          <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
            height="0" width="0" style={{display: 'none', visibility: 'hidden'}} />
        </noscript>

        {children}
      </body>
    </html>
  );
}
Replace GTM-XXXXXXX with your actual GTM container ID.

2.2 Configure CSP Headers (If Applicable)

If your Next.js site has Content Security Policy headers, add the Checkpoint domain:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: [
              "default-src 'self'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://kya.vouched.id https://www.googletagmanager.com",
              "connect-src 'self' https://kya.vouched.id https://www.google-analytics.com",
              // ... your other CSP directives
            ].join('; '),
          },
        ],
      },
    ];
  },
};

Most Next.js sites don't have CSP configured by default. Only add this if you're seeing CSP errors in your browser console.

2.3 Push User Data to GTM DataLayer

Add this code after successful user login:

// components/login-form.tsx or pages/api/auth/[...nextauth].ts
import { signIn, useSession } from 'next-auth/react';

function LoginForm() {
  const { data: session } = useSession();

  // Trigger identification when session is established
  useEffect(() => {
    if (session?.user) {
      // Push user data to GTM dataLayer
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: 'user_login',
        userId: session.user.id,
        userEmail: session.user.email,
        userName: session.user.name,
      });
    }
  }, [session]);

  return (
    <button onClick={() => signIn()}>
      Sign In
    </button>
  );
}

2.4 Handle User Logout

When users log out, reset their identification:

// utils/auth.ts or logout handler
function handleLogout() {
  // Reset Checkpoint identification first
  if (typeof window !== 'undefined' && window.Checkpoint) {
    window.Checkpoint.reset();
  }

  // Push logout event to GTM (optional)
  if (typeof window !== 'undefined') {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'user_logout',
    });
  }

  // Clear user session
  localStorage.removeItem('user');

  // Redirect to login
  router.push('/login');
}

Always call window.Checkpoint.reset() on logout to properly clear user identification and start a new anonymous session.

Step 3: Testing & Verification

3.1 Enable GTM Preview Mode

  1. In GTM, click Preview → Start debugging
  2. Enter your website URL
  3. Navigate to your site with the debug panel open

3.2 Test the Flow

  1. Visit your site → Check GTM debug:

    • ✅ "Checkpoint - Pixel Loader" should fire on page load
    • ✅ Browser console should show: [Checkpoint] Pixel initialized with config: … (only while the tag sets data-debug)
  2. Log into your site → Check GTM debug:

    • ✅ DataLayer should show user_login event with userId, userEmail
    • ✅ "Checkpoint - User Identification" tag should fire
    • ✅ Browser console should show: [GTM] Checkpoint user identified: user_123
  3. Navigate around the site → Generate activity while logged in

  4. Check the Checkpoint dashboard:

    • Go to your project's Activity page
    • Look for new detections
    • ✅ Rows should show your user: Activity resolves the email first, then falls back to userId

3.3 Browser Console Verification

Open browser DevTools and run:

// Check if the Checkpoint pixel is loaded
console.log('Checkpoint loaded:', !!window.Checkpoint);

// Check current user
console.log('Current user:', window.Checkpoint?.getUser());

// Check GTM dataLayer
console.log('DataLayer:', window.dataLayer);

Step 4: Production Deployment

4.1 Turn off Debug Mode

  1. In GTM: Open the "Checkpoint - Pixel Loader" tag and remove the data-debug line if you added one.
  2. In Next.js: Remove any debug console.log statements you added.
  3. Publish your GTM container changes.

4.2 Monitor in Production

  • Check the Activity page in your Checkpoint dashboard regularly
  • Verify userIds match your internal user system
  • Watch for any identification gaps or issues

Common Issues & Solutions

Issue: Pixel never starts

Cause: The tag doesn't pass your Project ID in a form the loader reads, and the browser console shows [Checkpoint] No project ID provided. Earlier versions of the GTM gallery template cause this: they send ?project_id=, and the loader reads only data-project-id or ?project-id=.

Solution: Accept the Checkpoint Pixel template update under Templates in your GTM workspace, then Submit → Publish the container, or use the Custom HTML tag from Step 1.2 with your Project ID in data-project-id and pause or delete the template tag.

Issue: "Checkpoint not defined" Error

Cause: The pixel script hasn't loaded yet when identify() is called.

Solution: The retry logic in the User Identification tag (above) already handles this. If you're still seeing this error:

  1. Check GTM tag firing order:

    • Pixel Loader tag should fire on "All Pages"
    • User Identification tag should fire on custom event user_login
    • Not both on the same trigger
    • Tag Sequencing on the Identification tag (run after Pixel Loader) helps
  2. Verify pixel is loading:

    // In browser console
    console.log('Pixel loaded:', typeof window.Checkpoint !== 'undefined');
  3. Check for CSP errors: see the Next.js CSP configuration above.

  4. Increase retry timeout if your site is slow:

    // Change from 50 retries (5s) to 100 retries (10s)
    } else if (retries < 100) {

Issue: DataLayer Variables Empty

Cause: User data not pushed to dataLayer before GTM tag fires.

Solution: Ensure dataLayer.push happens before navigation/page changes.

Issue: Multiple Identifications

Cause: GTM tag firing multiple times.

Solution: Use "Once per event" trigger setting or add a fired flag:

<script>
  if (!window._checkpointIdentified) {
    window._checkpointIdentified = true;
    // Your identification code here
  }
</script>

Issue: UserIds Not Showing in Dashboard

Troubleshooting Steps:

  1. Check GTM Preview: is the tag firing?
  2. Check browser console: any errors?
  3. Verify the data-project-id value in the Pixel Loader tag matches your dashboard's Project ID
  4. Check if identify() is actually being called
  5. Wait 1-2 minutes for data to appear

Advanced Configuration

Custom User Properties

Add more user data for richer tracking:

window.dataLayer.push({
  event: 'user_login',
  userId: user.id,
  userEmail: user.email,
  userName: user.name,
  userPlan: user.subscription_plan,
  userCompany: user.company,
  userRole: user.role,
  userSignupDate: user.created_at,
});

E-commerce Integration

For e-commerce sites, also identify users during checkout:

// On order completion
window.dataLayer.push({
  event: 'purchase',
  userId: customer.id,
  userEmail: customer.email,
  transactionId: order.id,
  value: order.total,
});

Mirror Identification into the DataLayer

The flow in this guide pushes login data into the dataLayer and lets GTM call Checkpoint.identify(). You can also run the sync in the other direction: listen for the checkpoint:identify event and forward it to the dataLayer, so other GTM tags can react to Checkpoint identification.

// Listen for Checkpoint events
window.addEventListener('checkpoint:identify', (event) => {
  window.dataLayer.push({
    event: 'checkpoint_user_identified',
    ...event.detail,
  });
});

If your app calls Checkpoint.identify() directly (instead of via the User Identification tag), push a matching event to the dataLayer so GTM still sees the login:

// Enhanced identification with GTM
function identifyUserWithGTM(user) {
  // Identify with Checkpoint
  if (window.Checkpoint) {
    window.Checkpoint.identify(user.id, {
      email: user.email,
      name: user.name,
      plan: user.plan,
    });
  }

  // Push to GTM data layer
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'user_identified',
    user_id: user.id,
    user_properties: {
      email: user.email,
      name: user.name,
      plan: user.plan,
      identified_via: 'checkpoint',
    },
  });
}

Summary

After completing this integration:

  1. ✅ Checkpoint Pixel Loader tag (Custom HTML) fires on every page
  2. ✅ Checkpoint pixel tracks all visitors to your Next.js site
  3. ✅ User identification happens automatically when users log in via GTM
  4. ✅ Same userId appears in both your system and your Checkpoint dashboard
  5. ✅ Activity shows user emails and IDs for easy correlation
  6. ✅ AI agent activity can be linked back to real users in your system

Your project's Activity page will now show entries like:

  • Human Activity: userId: "user_123" with email user@example.com
  • AI Agent Activity: userId: "user_123" with email user@example.com

This allows you to see when real users are browsing vs when AI agents visit on their behalf, all tied to the same user identity.