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)
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

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
- Go to your Meta Events Manager
- Select your Pixel
- Click on Add Events → Choose Using the Conversions API

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:
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
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
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:
https://graph.facebook.com/v18.0/${pixelId}/events?access_token=${accessToken}
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.
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.
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.
"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.
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
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:
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
PageViewandLeaddirectly from server - ▸Tracked both cookie and user data (hashed)




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 💙
