Next.js
The @pixelfox/next package - the tag for App Router pages and a server client for Route Handlers, Server Actions and webhooks.
@pixelfox/next has two entry points. @pixelfox/next renders the PixelFox tag and
gives client components a hook. @pixelfox/next/server sends events from your server,
where an order is a fact and no ad blocker can get in the way.
Install
npm install @pixelfox/next
# or: bun add @pixelfox/nextAdd to .env:
# Browser side (public by design - the key identifies your site, it does not authenticate it)
NEXT_PUBLIC_PIXELFOX_KEY=pfx_live_YOUR_KEY
NEXT_PUBLIC_PIXELFOX_HOST=https://api.pixelfox.app
# Server side - never NEXT_PUBLIC_
PIXELFOX_API_KEY=pfx_live_YOUR_KEY
PIXELFOX_HOST=https://api.pixelfox.appUsing a custom ingest endpoint? Set both hosts to it,
e.g. https://t.yourstore.com. The tag is then served from your own domain and events
never touch a third-party hostname.
Add the script (App Router)
Drop PixelFoxScript into your root layout - it renders a next/script tag with the
afterInteractive strategy:
// app/layout.tsx
import { PixelFoxScript } from "@pixelfox/next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<PixelFoxScript
apiKey={process.env.NEXT_PUBLIC_PIXELFOX_KEY!}
host={process.env.NEXT_PUBLIC_PIXELFOX_HOST}
/>
</body>
</html>
);
}| Prop | Purpose |
|---|---|
apiKey | Your site's key |
host | Where events go and where pixelfox.min.js is loaded from. Required unless scriptSrc is set; without either the component renders nothing and warns in development |
scriptSrc | Load the script from a different address than ${host}/pixelfox.min.js |
Client-side navigations are tracked automatically - the SDK wraps the History API, so
App Router route changes produce $pageview events without any extra code.
Track from components
Use the usePixelFox hook in client components. Calls made before the tag loads are
safely dropped (the hook is a no-op until window.pixelfox exists):
"use client";
import { usePixelFox } from "@pixelfox/next";
export function BuyButton({ productId, price }: { productId: string; price: number }) {
const { track } = usePixelFox();
return (
<button
onClick={() =>
track("product_added_to_cart", {
product_id: productId,
price,
currency: "USD",
})
}
>
Add to cart
</button>
);
}The hook exposes track, identify, reset, optOut, optIn and getDistinctId.
Anything else on the SDK API is reachable as window.pixelfox.
Identify the customer at login/checkout so anonymous history gets stitched:
const { identify } = usePixelFox();
identify(user.id, { email: user.email });Provider variant
If you prefer context over a layout-level script, wrap your app in PixelFoxProvider -
it injects the script itself and provides the same API through usePixelFoxContext():
<PixelFoxProvider
apiKey={process.env.NEXT_PUBLIC_PIXELFOX_KEY!}
host={process.env.NEXT_PUBLIC_PIXELFOX_HOST}
>
{children}
</PixelFoxProvider>Server side
@pixelfox/next/server has no React and no "use client", so it is safe in Route
Handlers, Server Actions and webhooks. Keep it out of client components: importing it
there would ship your server key to the browser.
Create a client
import { pixelfoxFromEnv, PixelFoxServer } from "@pixelfox/next/server";
// From PIXELFOX_API_KEY and PIXELFOX_HOST. Null when either is unset, so an app
// without PixelFox configured still builds and runs.
const pixelfox = pixelfoxFromEnv();
// Or explicitly:
const pixelfox = new PixelFoxServer({
apiKey: process.env.PIXELFOX_API_KEY!,
host: "https://api.pixelfox.app",
timeoutMs: 3000, // default; deliberately short so a checkout never hangs on analytics
});Record a purchase
Send it from wherever the order becomes real - a Server Action, a payment webhook, a Route Handler:
// app/checkout/actions.ts
"use server";
import { cookies } from "next/headers";
import { distinctIdFromCookies, pixelfoxFromEnv } from "@pixelfox/next/server";
export async function completeOrder(order: Order) {
// … save the order …
const pixelfox = pixelfoxFromEnv();
if (!pixelfox) return;
// cookies() is async in Next.js 15+.
const distinctId = distinctIdFromCookies(await cookies());
const result = await pixelfox.purchase({
orderId: order.id,
value: order.total,
currency: "USD",
distinctId, // joins the page views that led here; without it the sale is a stranger
email: order.email,
phone: order.phone,
items: order.lines.map((l) => ({ productId: l.sku, name: l.name, quantity: l.qty, price: l.price })),
customer: { firstName: order.firstName, lastName: order.lastName, city: order.city, country: "US", externalId: order.customerId },
clientIp: order.ip, // the shopper's, from the checkout request
clientUserAgent: order.userAgent,
});
if (!result.ok) console.warn("pixelfox:", result.error);
}| Field | Purpose |
|---|---|
orderId, value, currency | Required. value is also sent as total |
distinctId | The visitor id from the pixelfox_did cookie. distinctIdFromCookies() reads it from Next's cookies() or anything with a get(name) returning { value } |
email, phone | Hashed server-side before any ad platform sees them. What raises match quality most |
customer | firstName, lastName, city, state, zip, country, externalId - the remaining match keys |
items | { productId, name?, quantity?, price? }[], sent with item_count |
clientIp, clientUserAgent | The shopper's, so Meta can match the purchase to the browser session |
uuid | Pass the browser's event id when the pixel already fired this sale, so it is counted once |
Identify and custom events
await pixelfox.identify({ distinctId, email: user.email, phone: user.phone, properties: { plan: "pro" } });
await pixelfox.track("subscription_started", { plan: "pro", value: 29, currency: "USD" }, distinctId);
await pixelfox.send([{ event: "lead", distinctId, properties: { source: "demo-form" } }]);Every method returns { ok, accepted, error? } and never throws. A failed analytics
call must not take down the request that produced it, so log it and move on.
Gotchas
- The browser key is public by design (it identifies, not authenticates, your site) -
the backend enforces your allowed origins list from Settings. The server key
is the same kind of key; keep it out of
NEXT_PUBLIC_so it is not in the bundle. - This package is online mode only: a developer wiring up a code-level integration already has a dashboard.
fetchfrom the server client usescache: "no-store"; analytics is never served from a cached response.