Emtiaz
ProblemsSolutionsProcessResultsPricingAboutBlog

Reading Progress

Technical Guide

0% read

How I Implemented Facebook Conversions API in My Next.js Website (Custom Server Integration)Our request URL is like this Step 1: Meta Pixel Setup with Google Tag Manager (GTM)Step 2: Set Up Conversions API in Events ManagerStep 3: Create Server-side API in Next.js📄 API Route: src/app/api/facebook-capi/route.jssrc\lib\utils.js📁 Library: src/lib/facebookCapi.js📁 Utility: src/utils/getFbCookies.js📁 Hash Utility: src/lib/hash.jsStep 4: Frontend ⇒ Trigger API Callssrc/components/Tracking/PageViewTracker.js🧾 src/components/contact/ContactForm.jsx🧾 src\components\book-appointment\CalendlyForm.jsxStep 5: Setup ENV VariablesStep 6: Final ResultStep 7: Conclusion
E

Emtiaz Hossain

Attribution & Conversion Tracking Specialist

Advanced Trackingfree20 min readPublished August 6, 2025

Meta CAPI Tracking Using Own Server (Coding Method) - Understand Server-Side Tracking Behind the Scenes

Learn how to implement Meta Conversions API (CAPI) using your own server in a Next.js project. This guide breaks down the server-side tracking process step-by-step with real code and structure , no third-party tools needed. Perfect for developers and tracking experts who want full control.

Table of contents▼
How I Implemented Facebook Conversions API in My Next.js Website (Custom Server Integration)Our request URL is like this Step 1: Meta Pixel Setup with Google Tag Manager (GTM)Step 2: Set Up Conversions API in Events ManagerStep 3: Create Server-side API in Next.js📄 API Route: src/app/api/facebook-capi/route.jssrc\lib\utils.js📁 Library: src/lib/facebookCapi.js📁 Utility: src/utils/getFbCookies.js📁 Hash Utility: src/lib/hash.jsStep 4: Frontend ⇒ Trigger API Callssrc/components/Tracking/PageViewTracker.js🧾 src/components/contact/ContactForm.jsx🧾 src\components\book-appointment\CalendlyForm.jsxStep 5: Setup ENV VariablesStep 6: Final ResultStep 7: Conclusion

How I Implemented Facebook Conversions API in My Next.js Website (Custom Server Integration)

Want to take your Meta Pixel tracking to the next level? I just implemented Facebook CAPI (Conversions API) directly from my Next.js custom website without using paid tools like Stape or third-party integrations.

Our request URL is like this

https://www.emtiaz-v2.com/api/facebook-capi/pageview

https://www.emtiaz-v2.com/api/facebook-capi/lead

Blog image

Here’s a step-by-step breakdown of how I did it, along with file structure and setup instructions.


Step 1: Meta Pixel Setup with Google Tag Manager (GTM)

This step is pretty standard. I already had Meta Pixel installed via GTM and used browser-side events like PageView, Lead, etc.

Not the focus of today’s post, but just to let you know the Pixel was already working using GTM.

Step 2: Set Up Conversions API in Events Manager

  1. Go to your Meta Events Manager
  2. Select your Pixel
  3. Click on Add Events → Choose Using the Conversions API
Blog image

Select Set up manually

You’ll now see 5 steps:

  • ▸Overview
  • ▸Select Events
  • ▸Select Event Details
  • ▸Review Setup
  • ▸See Instructions

After that, you'll go through:

  • ▸Getting Started
  • ▸Explore Integration
  • ▸Generate an Access Token ✅
  • ▸Set Up Events
  • ▸Managing Your Implementation
  • ▸Finish Implementation ✅

At this point, you should have your:

  • ▸Pixel ID
  • ▸Access Token

Keep them safe for later.


Step 3: Create Server-side API in Next.js

Here’s how I structured my files in a custom Next.js 13+ App Router setup:

javascript
src/
├── app/
│   └── api/
│       └── facebook-capi/
│           ├── route.js
├── lib/
│   ├── facebookCapi.js
│   └── hash.js
		└── src\lib\utils.js

├── utils/
│   └── getFbCookies.js
├── components/
    ├── Tracking/PageViewTracker.js
    └── contact/ContactForm.jsx

📄 API Route: src/app/api/facebook-capi/route.js

javascript

import { sendFacebookEvent } from "@/lib/facebookCapi";
import { getClientIp, getUserAgent, prepareUserData } from "@/lib/utils";

export async function POST(req) {
  let eventName = "pageview"; 
  try {
    const body = await req.json();

    const {
      eventName: receivedEventName, 
      eventSourceUrl,
      eventId,
      eventTime,
      ...restOfBody 
    } = body;

    if (!receivedEventName || !eventSourceUrl || !eventId || !eventTime) {
      return new Response(
        JSON.stringify({
          error: "Missing required event parameters.",
          received: body, 
        }),
        { status: 400 },
      );
    }

    eventName = receivedEventName; 

    const userAgent = getUserAgent(req);
    const clientIp = getClientIp(req);

    const userData = prepareUserData(restOfBody, clientIp, userAgent);

    const result = await sendFacebookEvent({
      eventName,
      eventId,
      eventTime,
      eventSourceUrl,
      userData,
    });

    return new Response(JSON.stringify(result), { status: 200 });
  } catch (err) {
    console.error(`Facebook CAPI API Error - ${eventName}]`, err);

    let errorMessage = "An unknown error occurred.";
    if (err instanceof SyntaxError && err.message.includes("JSON")) {
      errorMessage = "Invalid JSON in request body.";
    } else if (err instanceof Error) {
      errorMessage = err.message;
    }

    return new Response(JSON.stringify({ error: errorMessage }), {
      status: 500,
    });
  }
}

src\lib\utils.js

javascript
import { hash } from "./hash";

export function getClientIp(req) {
  return (
    req.headers.get("x-forwarded-for") ||
    req.headers.get("x-real-ip") ||
    "0.0.0.0"
  );
}

export function getUserAgent(req) {
  return req.headers.get("user-agent");
}

export function prepareUserData(body, clientIp, userAgent) {
  const {
    city,
    state,
    zip,
    country,
    externalId,
    fbc,
    fbp,
    firstName,
    lastName,
    email,
    phone,
  } = body;

  const userData = {
    client_ip_address: clientIp,
    client_user_agent: userAgent,
    fbc,
    fbp,
  };

  if (email) userData.em = [hash(email)];
  if (firstName) userData.fn = hash(firstName);
  if (lastName) userData.ln = hash(lastName);
  if (phone) userData.ph = [hash(phone)];
  if (city) userData.ct = hash(city);
  if (state) userData.st = hash(state);
  if (zip) userData.zp = hash(zip);
  if (country) userData.country = hash(country);
  if (externalId) userData.external_id = hash(externalId?.toString());

  return userData;
}

📁 Library: src/lib/facebookCapi.js

This is where I wrote the logic to send events to:

javascript
https://graph.facebook.com/v18.0/${pixelId}/events?access_token=${accessToken}
javascript

export const sendFacebookEvent = async ({
  eventName, 
  eventTime,
  eventId,
  eventSourceUrl,
  userData = {},
  testEventCode = "TEST1234",
}) => {
  const pixelId = process.env.FB_PIXEL_ID;
  const accessToken = process.env.FB_ACCESS_TOKEN;

  if (!pixelId || !accessToken) {
    console.error(
      "error",
    );
    throw new Error("credentials missing.");
  }

  const fbUrl = `https://graph.facebook.com/v18.0/${pixelId}/events?access_token=${accessToken}&test_event_code=${testEventCode}`;
  // const fbUrl = `https://graph.facebook.com/v18.0/${pixelId}/events?access_token=${accessToken}`;

  const payload = {
    data: [
      {
        event_name: eventName,
        event_time: eventTime,
        event_id: eventId,
        event_source_url: eventSourceUrl,
        action_source: "website",
        user_data: userData,
      },
    ],
  };

  try {
    const fbRes = await fetch(fbUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });

    const result = await fbRes.json();

    // Check for Facebook API errors
    if (!fbRes.ok || result.error) {
      console.error(
        `${eventName} API returned an error:`,
        result,
      );
      throw new Error(
        `Error: ${result.error?.message || "Unknown error"}`,
      );
    }

    return result;
  } catch (error) {
    console.error(`Error - ${eventName}]`, error);
    throw error;
  }
};

📁 Utility: src/utils/getFbCookies.js

This utility fetches first-party cookies like _fbp, _fbc, etc.

javascript

export const getFbCookies = () => {
  if (typeof document === "undefined") return { fbc: "", fbp: "" };

  const fbc =
    document.cookie
      .split("; ")
      .find((row) => row.startsWith("_fbc="))
      ?.split("=")[1] || "";

  const fbp =
    document.cookie
      .split("; ")
      .find((row) => row.startsWith("_fbp="))
      ?.split("=")[1] || "";

  return { fbc, fbp };
};

📁 Hash Utility: src/lib/hash.js

Used for hashing user data (email, phone) before sending to Meta, as required.

javascript
import crypto from "crypto";

export const hash = (val) => {
  if (!val) return undefined;
  return crypto
    .createHash("sha256")
    .update(val.trim().toLowerCase())
    .digest("hex");
};

Step 4: Frontend ⇒ Trigger API Calls

I created two components that call my custom API routes when events happen.

src/components/Tracking/PageViewTracker.js

Used to send PageView server-side event.

javascript
"use client";

import { getEventId, getEventTime, getExternalId } from "@/lib/customID";
import { getGeoFromLocal } from "@/lib/getGeoFromLocal";
import { getFbCookies } from "@/utils/getFbCookies";
import { useEffect } from "react";

export default function PageViewTracker() {
  useEffect(() => {
    const { fbc, fbp } = getFbCookies();
    const geo = getGeoFromLocal(); 

    const payload = {
      eventName: "PageView", 
      eventSourceUrl: window.location.href,
      eventId: getEventId("pageview"),
      eventTime: getEventTime(),
      externalId: getExternalId(),
      fbc,
      fbp,
      city: geo?.city || "",
      state: geo?.region || "",
      zip: geo?.postal || "",
      country: geo?.country || "",
    };

    // Facebook CAPI call
    fetch("/api/facebook-capi", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        ...payload,
      }),
    })
      .then((res) => {
        if (!res.ok) {
          console.error("Failed ", res.statusText);
        }
        return res.json();
      })
      .then((data) => {
        // console.log("CAPI success:", data);
      })
      .catch((error) => {
        console.error("Error :", error);
      });
  }, []);

  return null;
}

🧾 src/components/contact/ContactForm.jsx

Triggers Lead event from the form submission.

javascript
	
	const ContactPayload = {
      eventName: "Lead",
      eventSourceUrl: window.location.href,
      eventId: getEventId("lead"),
      eventTime: getEventTime(),
      externalId: getExternalId(),
      firstName,
      lastName,
      email, 
      phone,
      city: geo?.city || "Unknown",
      state: geo?.region || "Unknown",
      zip: geo?.postal || "Unknown",
      country: geo?.country || "Unknown",
      fbc,
      fbp,
    };
    
			// Send data to Facebook CAPI
      fetch("/api/facebook-capi", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          ...ContactPayload,
        }),
      });

🧾 src\components\book-appointment\CalendlyForm.jsx

javascript
				const SchedulePayload = {
            eventName: "Schedule",
            eventSourceUrl: window.location.href,
            eventId: getEventId("schedule"),
            eventTime: getEventTime(),
            externalId: getExternalId(),
            firstName,
            lastName,
            email: inviteeEmail,
            phone,
            city: geo?.city || "",
            state: geo?.region || "",
            zip: geo?.postal || "",
            country: geo?.country || "",
            fbc,
            fbp,
          };
          
          fetch("/api/facebook-capi", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(SchedulePayload),
          });

Step 5: Setup ENV Variables

Add the following to your .env.local file:

javascript
FB_PIXEL_ID=your_pixel_id
FB_ACCESS_TOKEN=your_long_lived_token

Keep your access token secret.


Step 6: Final Result

🎉 After implementation, here’s what I achieved:

  • ▸Event Match Quality (EMQ) score: 9+
  • ▸Bypassed iOS 14.5+ restrictions
  • ▸Sent PageView and Lead directly from server
  • ▸Tracked both cookie and user data (hashed)
Blog image
Blog image
Blog image
Blog image

Step 7: Conclusion

This method is developer-focused, but if you're a tracking expert or a web analyst, it's valuable to understand how server-side tracking works behind the scenes.

I built this in my own project, combining my skills as a former web developer and current tracking expert.

I might have made some mistakes, feel free to suggest corrections or improvements. Always happy to learn.

Thanks for reading 💙

#Facebook CAPI#Stape#web analytics#full stack web analytics#tracking#event deduplication#facebook pixel#tracking setup#conversion tracking#CustomTracking#AdvancedTracking
E
About Emtiaz Hossain

Conversion tracking specialist — server-side GTM, Meta CAPI, GA4. Building the measurement infrastructure that makes paid media profitable across 12 countries.

LinkedInUpwork

Stop guessing. Start Measuring.

Get a detailed audit of your tracking architecture. Find exactly where your data is lying and fix it — before you spend another dollar on ads.

Related Insights

View all
How I Built a Shopify Attribution Pipeline (UTM & Click IDs → Orders)
Markting Analytics & Attribution

How I Built a Shopify Attribution Pipeline (UTM & Click IDs → Orders)

A real-world case study showing how attribution data (UTM, gclid, fbclid) was captured, persisted, and sent to Shopify orders for accurate marketing attribution. This guide walks through the architecture, implementation, and tracking strategy used in a live TinyBot client project.

Emtiaz
  • Privacy Policy
  • Terms of Service
  • FAQ
  • Email
  • Whatsapp

© 2026 Emtiaz Hossain. All Rights Reserved.