PixelFox Docs
Installation

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/next

Add 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.app

Using 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>
  );
}
PropPurpose
apiKeyYour site's key
hostWhere 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
scriptSrcLoad 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);
}
FieldPurpose
orderId, value, currencyRequired. value is also sent as total
distinctIdThe visitor id from the pixelfox_did cookie. distinctIdFromCookies() reads it from Next's cookies() or anything with a get(name) returning { value }
email, phoneHashed server-side before any ad platform sees them. What raises match quality most
customerfirstName, lastName, city, state, zip, country, externalId - the remaining match keys
items{ productId, name?, quantity?, price? }[], sent with item_count
clientIp, clientUserAgentThe shopper's, so Meta can match the purchase to the browser session
uuidPass 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.
  • fetch from the server client uses cache: "no-store"; analytics is never served from a cached response.

On this page