---
title: How does Meta event_id deduplication work?
description: Fix duplicate purchases and Meta conversions with matching Pixel eventID and CAPI event_id values. Includes code examples, retry rules, and the 48-hour deduplication window.
published_at: 2026-09-03
last_updated: 2026-09-17
format: Guide
---

# How does Meta event_id deduplication work?

[Meta deduplicates a browser Pixel event and a server Conversions API event](https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events) when both describe the same conversion, use the same event name and event ID, reach the same dataset, and are received within 48 hours of each other. The browser option is `eventID`; the server field is `event_id`.

Generate the value once, preserve it across both delivery paths and every retry, and deliver both copies promptly. A matching ID that arrives outside Meta's deduplication window can still be counted as another event.

If one purchase appears twice, first compare the two event names, IDs, and destination datasets. Adding email, `fbp`, or `fbc` cannot repair mismatched event IDs; those fields serve a different matching purpose.

## Key takeaways

- Use one event ID for one logical business event, not one ID per HTTP request.
- Match both the event name and event ID across the browser Pixel and server CAPI copies.
- Meta Pixel calls the option `eventID`; Conversions API calls the field `event_id`.
- The matching browser and server events must reach the same dataset within 48 hours of each other.
- Keep the same ID when retrying a failed or timed-out server request.
- A Plainrouter response with `duplicate: true` confirms an idempotent API replay. It does not prove that Meta deduplicated the browser and server copies.

## Two duplicate problems, one identifier

The same value can protect two separate boundaries:

| Boundary                   | Duplicate problem                                    | What the ID does                                               |
| -------------------------- | ---------------------------------------------------- | -------------------------------------------------------------- |
| Your server to Plainrouter | A queue retry or timeout repeats the API request.    | Reusing `event_id` makes the request idempotent.               |
| Meta Pixel and Meta CAPI   | Browser and server paths report the same conversion. | Matching `eventID` and `event_id` let Meta recognize the pair. |

These outcomes are related but not interchangeable. API idempotency prevents a repeated server request from creating another Plainrouter event. Meta deduplication concerns two eligible copies that reached the same Meta dataset through different paths.

## What should you use as a Meta event_id?

Choose a value that is stable, unique to the logical event, and available before either path sends. It should not contain an email address, phone number, name, or other personal data.

Good examples distinguish the event type as well as the underlying business record:

```text
purchase:ORDER-1042
refund:REFUND-2088
lead:01K2EXAMPLE7SRY4RY3J2P5M8T
```

Using only `ORDER-1042` for both a purchase and a refund can create an accidental collision in an ingestion system that keys idempotency on the ID alone, such as Plainrouter. Meta's browser/server matching also considers the event name. Generating a new UUID inside every retry creates the opposite problem: each attempt looks like a new event.

Plainrouter accepts a caller-supplied `event_id` of up to 128 characters. Persist or deterministically derive the value at the point where your application commits the business event.

## How browser and server deduplication works

For one purchase, the two paths should carry the same identity. The field names differ between Meta Pixel and Conversions API:

| Check       | Meta Pixel                                         | Meta Conversions API                        |
| ----------- | -------------------------------------------------- | ------------------------------------------- |
| Event name  | `Purchase`, passed to `fbq`                        | `event_name: "Purchase"`                    |
| Event ID    | `eventID`, in the fourth `fbq` argument            | `event_id`, on the server event             |
| Destination | The configured Pixel / dataset                     | The same dataset                            |
| Timing      | Both copies received within 48 hours of each other | Send promptly; preserve identity on retries |

```text
                         event name: Purchase
Browser Pixel ────────── eventID: purchase:ORDER-1042 ──┐
                                                        ├─ Meta dataset ─ one logical event
Server CAPI  ─────────── event_id: purchase:ORDER-1042 ─┘
```

The payloads do not need to be byte-for-byte identical. Browser and server events can contain different context because they were observed in different environments. The deduplication identity, however, must describe the same logical event, and both copies must be received within the 48-hour window documented by Meta.

## How do you set eventID in Meta Pixel and event_id in CAPI?

Use this pattern when your application owns both the direct Pixel call and the server CAPI request. Do not add it alongside Plainrouter's managed Pixel for the same event.

### 1. Assign the ID before either path sends

Derive the ID from a committed business identifier or generate it once and store it with the event job:

```js
const eventId = `purchase:${order.id}`;
```

Do not create the ID independently in browser and server code. Pass the assigned value through the checkout result or another trusted application boundary.

### 2. Send the browser event with eventID

Meta Pixel expects the camel-cased `eventID` option in the fourth argument:

```js
fbq(
    'track',
    'Purchase',
    {
        value: 49.9,
        currency: 'EUR',
    },
    {
        eventID: 'purchase:ORDER-1042',
    },
);
```

The value must match the server field exactly. Preserve case, separators, and prefixes.

### 3. Send the server event with event_id

The Plainrouter TypeScript SDK uses the server-side `event_id` spelling:

```ts
import { configurePlainrouter, createEvent } from '@plainrouter/sdk';

const signalSecret = process.env.PLAINROUTER_SIGNAL_SECRET;

if (!signalSecret) {
    throw new Error('PLAINROUTER_SIGNAL_SECRET is required');
}

configurePlainrouter({ signalTrackerSecret: signalSecret });

const { data, response } = await createEvent({
    body: {
        event_id: 'purchase:ORDER-1042',
        event_name: 'Purchase',
        event_source: 'https://shop.example/checkout/success',
        action_source: 'website',
        consent_basis: 'consent',
        consent: {
            ad_storage: 'granted',
            ad_user_data: 'granted',
            ad_personalization: 'granted',
            captured_at: new Date().toISOString(),
            source: 'checkout_cmp',
        },
        value_data: {
            value: '49.90',
            currency: 'EUR',
            order_id: 'ORDER-1042',
        },
    },
});

if (!data) {
    throw new Error(`Event request failed with HTTP ${response.status}`);
}

console.log(data.event_id, data.duplicate);
```

The example assumes the consent values came from the site's real consent workflow. Server-side transport does not create permission to process or send advertising data.

### 4. Retry with the same ID

If the request times out after leaving your application, retry the same logical event with `purchase:ORDER-1042`. Do not generate a replacement ID just because the response is unknown.

Plainrouter's public API returns:

| Result                                         | HTTP status | Response                        |
| ---------------------------------------------- | ----------- | ------------------------------- |
| New event accepted for processing              | `202`       | `duplicate: false`              |
| Previously accepted ID replayed                | `200`       | `duplicate: true`               |
| Body `event_id` and `Idempotency-Key` disagree | `422`       | Validation error for `event_id` |

You may supply `Idempotency-Key` instead of the body field. If you send both, they must match.

## How does Plainrouter coordinate Pixel and CAPI events?

When Meta is connected as a Plainrouter Signals destination, a single consented browser tracking call can coordinate the managed Pixel and server delivery:

```js
const eventId = await signalq('track', 'Purchase', {
    value: '49.90',
    currency: 'EUR',
    order_id: 'ORDER-1042',
});

console.log('Accepted event ID', eventId);
```

Plainrouter uses one event identity for the eligible browser and server paths. After validating that managed path, remove another Pixel or CAPI integration that reports the same event to the same dataset with unrelated IDs.

## How should n8n and Make preserve event IDs on retries?

Persist the event ID with the business record before the first send. Read that saved value on every retry, even if the automation platform starts a new execution. A workflow execution ID identifies a run, not a purchase.

For example, the purchase event for fictional order `ORDER-1042` can use a saved ID such as `purchase-ORDER-1042`. A retry keeps that value and the original event time. A different event, such as a refund, needs its own event identity. If a paired browser event is also sent, use the same purchase ID and event name there; do not create a second independent sender alongside Plainrouter's managed `signalq` purchase flow.

Test two executions using the same synthetic input and compare the outgoing IDs. Then change the fictional order and confirm that its purchase gets a different ID. This checks your mapping; only the destination's response establishes what it accepted. A Plainrouter duplicate acknowledgement does not re-deliver a failed destination request.

The [automation setup guide](/library/meta-capi-without-gtm#adapt-an-automation-workflow-for-a-real-website-event) explains consent gates, credential handling, and the separate delivery check. Recheck current permission before retrying; a stable ID is not permission to send after withdrawal.

## Common event_id mistakes

| Mistake                                                     | What happens                                              | Fix                                                        |
| ----------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- |
| Browser and server generate IDs independently.              | The two values can differ even for the same purchase.     | Assign the ID before either path sends.                    |
| Every retry gets a new UUID.                                | Retries appear to be new server events.                   | Store and reuse the original ID.                           |
| Event names differ by spelling or case.                     | The pair no longer has the same deduplication identity.   | Use one canonical event-name mapping.                      |
| Purchase and refund reuse one order ID.                     | Different lifecycle events can collide.                   | Include the event type or use distinct business-event IDs. |
| Two integrations send the same purchase with unrelated IDs. | Meta may retain more than one conversion.                 | Give one integration ownership of the paired path.         |
| `duplicate: true` is treated as Meta proof.                 | API retry behavior is confused with destination behavior. | Verify the browser/server pair in Meta separately.         |
| HTTP `202` is treated as final delivery.                    | A later destination failure can be missed.                | Read the event's delivery trace.                           |

## Can you reproduce an event-ID mismatch locally?

Yes. Save this JavaScript as `compare-pair.mjs` and run `node compare-pair.mjs`. It compares fictional observations without sending events or needing credentials. The objects are diagnostic summaries, not complete Pixel or CAPI requests.

```js
function comparePair(browser, server) {
    const fields = [
        ['dataset_id', browser.dataset_id, server.dataset_id],
        ['event_name', browser.event_name, server.event_name],
        ['event_id', browser.eventID, server.event_id],
    ];

    return fields.filter(([, left, right]) => typeof left !== 'string' || left.length === 0 || left !== right).map(([name]) => name);
}

const browser = {
    dataset_id: 'example-dataset',
    event_name: 'Purchase',
    eventID: 'purchase:ORDER-1042',
};

const server = {
    dataset_id: 'example-dataset',
    event_name: 'Purchase',
    event_id: 'retry:ORDER-1042',
};

console.log(JSON.stringify(comparePair(browser, server)));
server.event_id = browser.eventID;
console.log(JSON.stringify(comparePair(browser, server)));
```

Expected local output:

```text
["event_id"]
[]
```

The first result finds the mismatch. The second says only that these three fields match. It cannot prove that either event was delivered, arrived in time, or was deduplicated by Meta. An empty field is also reported as a problem, even when both observations omit it.

## How to debug Meta deduplication

1. Pick one test conversion and record its expected event name and event ID.
2. In browser developer tools, inspect the Pixel request and confirm the `eventID` value.
3. Inspect the server request before it is queued and confirm `event_name` and `event_id` use the same values.
4. Confirm both paths target the same Meta dataset.
5. Confirm Meta received both copies within 48 hours of each other; delayed jobs outside that window are not an eligible pair.
6. Retry the server request once and confirm Plainrouter returns the same ID with `duplicate: true`.
7. Retrieve the Plainrouter event and inspect its destination delivery state. Ingestion acceptance is not final Meta acceptance.
8. Use Meta Test Events and Events Manager diagnostics to inspect the browser and server copies at the destination.
9. Search the site, tag manager, commerce platform, plugins, and backend jobs for another sender reporting the same event.

Debug one known conversion end to end. Aggregate counts alone cannot tell you whether the failure is mismatched IDs, mismatched names, another sender, or a destination delivery problem.

## Frequently asked questions

### Why do purchases duplicate after a thank-you page reload?

If every page load creates a new purchase ID, the reload looks like another event. Tie the purchase to the committed order and reuse its event ID in an integration you control. Also check for a second Pixel, checkout plugin, or CAPI sender reporting the order with a different ID. A page view and a completed purchase should have separate event identities.

### Is event_id required for every Meta CAPI event?

An event can be sent without a browser counterpart, but a paired browser/server implementation needs a shared event ID so Meta can recognize the copies. Plainrouter can derive an ID when the public API body omits it, but an explicit stable ID is safer when your application controls retries or a manual Pixel pair.

### Can I use the order ID as event_id?

Yes, if it is stable, non-personal, and unique to that logical event. Distinguish separate lifecycle events such as `purchase:ORDER-1042` and `refund:REFUND-2088` instead of reusing one bare order identifier for everything.

### Should event_id be random?

It may be a random opaque value, but generate it once and persist it. A deterministic value based on a stable business-event identifier is also suitable when it cannot expose personal data or collide with another event type.

### Does duplicate true mean Meta removed a duplicate?

No. `duplicate: true` is Plainrouter's API result for an already accepted event ID. Meta's browser/server deduplication is a separate destination outcome.

### What is Meta's browser and server deduplication window?

Meta documents a 48-hour receipt window. The browser and server events must reach the same dataset with matching event names and event IDs within 48 hours of each other. Do not treat the window as a retry target: send both copies promptly and use it as a diagnostic boundary for delayed jobs.

### Does event_id replace fbp, fbc, or user_data?

No. `event_id` identifies duplicate event copies. `fbp`, `fbc`, and permitted customer information provide matching and attribution context. They solve different problems.

## Sources

- [Meta: Deduplicate Pixel and server events](https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events)
- [Meta Conversions API Direct Integration Playbook](https://storage.googleapis.com/lr-tech-docs-resources/PDFs/Conversions-API-Direct-Integration-Playbook_English.pdf)
- [Meta: Conversions API server event parameters](https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event)
- [Meta Business SDK for Node.js](https://github.com/facebook/facebook-nodejs-business-sdk#conversions-api)
- [Plainrouter Conversion API reference](https://plainrouter.com/docs/api/conversions)
- [Plainrouter TypeScript SDK createEvent operation](https://github.com/plainrouter/sdk/blob/v0.5.1/packages/sdk/src/generated/sdk.gen.ts)
- [Plainrouter event request and response types](https://github.com/plainrouter/sdk/blob/v0.5.1/packages/sdk/src/generated/types.gen.ts)

## Related guides

- [Troubleshoot missing Meta Pixel and CAPI events](/library/meta-capi-missing-duplicate-events)

- [Can you use Meta CAPI without Google Tag Manager?](/library/meta-capi-without-gtm)
- [Track events and consent](https://plainrouter.com/docs/signals/track-events)
- [Connect a Meta dataset](https://plainrouter.com/docs/signals/connect-meta)
- [What is fbclid? How it relates to fbp and fbc](/library/fbp-fbc)

## Check browser and server pairs

Use the interactive pairing check below to compare one dataset at a time. Supply a JSON array with `source` (`browser` or `server`), `event_name`, `event_id`, and optional `event_time` in Unix seconds. The local report identifies missing pairs, repeated IDs, and timestamp differences. It cannot verify delivery or Meta's deduplication result. The executable example above works without this browser interface.
