Author Name
Emtiaz Hossain
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.
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.
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.

Select Set up manually
You’ll now see 5 steps:
After that, you'll go through:
At this point, you should have your:
Keep them safe for later.
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
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,
});
}
}
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;
}
src/lib/facebookCapi.jsThis 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;
}
};src/utils/getFbCookies.jsThis 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 };
};
src/lib/hash.jsUsed 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");
};
I created two components that call my custom API routes when events happen.
src/components/Tracking/PageViewTracker.jsUsed 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.jsxTriggers 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),
});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.
🎉 After implementation, here’s what I achieved:
PageView and Lead directly from server



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 💙
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.