npm package: @riculum/meta-conversions-api
Description: Type-safe Meta (Facebook) Conversions API client for Node/Next.js. Build and send server-side events to the Graph API with dedupe support.
Latest version: 0.1.2

# `@riculum/meta-conversions-api`

Type-safe **Meta (Facebook) Conversions API** client for Node/Next.js.  
Build and send server-side events to the **Graph API** (`/{pixel_id}/events`) with dedupe support.

- TypeScript types for standard events (Purchase, AddToCart, ViewContent, ...)
- Minimal API: `buildEvent()`, `sendEvents()`, `sha256()` and `createEventId()`
- No dependencies (Node 18+ for native `fetch`)
- Works great with Next.js Route Handlers / API routes

---

## Install

```bash
pnpm add @riculum/meta-conversions-api
# npm i @riculum/meta-conversions-api
```

You also need a **Pixel ID** and a **Meta access token** (server-side secret).

---

## Quick start (Next.js Route Handler)

```ts
// app/api/meta-capi/route.ts
import { NextRequest, NextResponse } from "next/server";
import { buildEvent, sendEvents, sha256, createEventId } from "@riculum/meta-conversions-api";

const PIXEL_ID = process.env.META_PIXEL_ID!;
const ACCESS_TOKEN = process.env.META_ACCESS_TOKEN!;

export async function POST(req: NextRequest) {
  const { order, testEventCode } = await req.json();
  const eventId = createEventId("purchase");

  const evt = buildEvent({
    name: "Purchase",
    eventId,
    eventSourceUrl: order?.url ?? undefined,
    user: {
      em: await sha256(order?.email),
      client_ip_address: req.headers.get("x-forwarded-for")?.split(",")[0] ?? undefined,
      client_user_agent: req.headers.get("user-agent") ?? undefined,
    },
    data: {
      value: order.total,
      currency: order.currency,
      contents: order.items.map((i: any) => ({ id: i.id, quantity: i.qty, item_price: i.price })),
      num_items: order.items.reduce((a: number, i: any) => a + i.qty, 0),
    },
  });

  const res = await sendEvents({
    pixelId: PIXEL_ID,
    accessToken: ACCESS_TOKEN,
    events: [evt],
    testEventCode,             // optional: Events Manager → Test Events code
    graphVersion: "v19.0",
    debug: true,
  });

  return NextResponse.json(res);
}
```

### AddToCart example (server)

```ts
import { buildEvent, sendEvents } from "@riculum/meta-conversions-api";

const evt = buildEvent({
  name: "AddToCart",
  eventId: "cart_123",
  eventSourceUrl: "https://example.com/product/sku_123",
  data: {
    value: 29.9,
    currency: "EUR",
    content_ids: ["sku_123"],
    content_type: "product",
  },
});

await sendEvents({ pixelId, accessToken, events: [evt] });
```

---

## Testing with **Test Events** (test_event_code) and debug

To see your **server events** in Events Manager → **Test Events**, include your pixel’s **test_event_code** in `sendEvents()`.
Turn on `debug: true` to log both the request body and the Graph API response on the server.

1) Copy the code from Events Manager → Pixel → **Test Events** (looks like `TEST1234...`).  
2) Add an env var (e.g., `META_TEST_EVENT_CODE`) and pass it to `sendEvents`.

```ts
// app/api/meta-capi/route.ts
import { buildEvent, sendEvents, sha256 } from "@riculum/meta-conversions-api";

const PIXEL_ID = process.env.META_PIXEL_ID!;
const ACCESS_TOKEN = process.env.META_ACCESS_TOKEN!;
const TEST_EVENT_CODE = process.env.META_TEST_EVENT_CODE; // ← from Events Manager while testing

export async function POST(req: Request) {
  const body = await req.json();

  const evt = buildEvent({
    name: "AddToCart",
    eventId: `cart_${Date.now()}`,
    eventSourceUrl: body.url,
    user: {
      client_ip_address: req.headers.get("x-forwarded-for")?.split(",")[0] ?? undefined,
      client_user_agent: req.headers.get("user-agent") ?? undefined,
      em: await sha256(body.email) // only if consented
    },
    data: {
      value: body.value,
      currency: body.currency,
      content_ids: [body.productId],
      content_type: "product"
    }
  });

  const res = await sendEvents({
    pixelId: PIXEL_ID,
    accessToken: ACCESS_TOKEN,
    events: [evt],
    testEventCode: TEST_EVENT_CODE, // ← makes the event appear in Test Events
    debug: true                     // ← logs request & response server-side
  });

  return new Response(JSON.stringify(res), { status: res.ok ? 200 : 400 });
}
```

**Note:** Remove `testEventCode` and set `debug: false` in production traffic.

---

## API surface

```ts
/** API surface (documentation only)

buildEvent<E extends FBStandardEvent>(opts: {
  name: E;
  actionSource?: ActionSource;      // default "website"
  eventId?: string;
  eventSourceUrl?: string;
  user?: UserData;                  // hash PII with sha256()
  data?: FBEventMap[E];             // per-event params
  eventTime?: number;               // unix seconds
}): GraphEvent<E>;

sendEvents(opts: {
  pixelId: string;
  accessToken: string;
  events: GraphEvent[];
  testEventCode?: string;
  graphVersion?: string;            // default "v19.0"
  debug?: boolean;
}): Promise<{ ok: boolean; status: number; body: any }>

sha256(input?: string): Promise<string | undefined>
createEventId(prefix?: string): string

*/
```

Types included: `FBStandardEvent`, `FBEventMap`, `UserData`, `ContentItem`, `ActionSource`.

---

## Best practices

- Hash PII (`email`, `phone`, names, address fields) with `sha256()` **server-side**.
- Use a shared `event_id` to dedupe browser (`fbq`) and server (CAPI) events.
- Send `value` + `currency` for commerce events (`AddToCart`, `InitiateCheckout`, `Purchase`, ...).
- Respect consent—only send PII when you have the user’s permission.
- Debug in Events Manager → **Test Events** with a `test_event_code`.

---

## License

MIT © Riculum