> ## Documentation Index
> Fetch the complete documentation index at: https://plainrouter.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Track events and consent

> Use the Signals browser API to manage consent, identify visitors, and record conversion events.

The Signals browser API is available as `window.signalq` after you install the generated v3 pixel. Calls made before the asynchronous script loads remain in the queue.

## Set consent

Connect `signalq` to your consent-management platform. Send the current advertising consent state after initialization and whenever the visitor changes it.

```js theme={null}
await signalq("consent", {
  consent_basis: "consent",
  consent: {
    ad_storage: "granted",
    ad_user_data: "granted",
    ad_personalization: "granted",
    captured_at: new Date().toISOString(),
    source: "your_cmp"
  }
});

// If the visitor later withdraws consent:
await signalq("consent", {
  consent_basis: "consent",
  withdrawn: true
});
```

Each permission accepts `granted`, `denied`, or `unknown`. Missing or unrecognized permissions remain `unknown`; PlainRouter never upgrades them to a grant.

You can provide one or more supported consent adapters:

| Input          | Use                                                    |
| -------------- | ------------------------------------------------------ |
| `consent`      | Send PlainRouter's explicit three-permission snapshot. |
| `consent_mode` | Map Google Consent Mode v2 fields.                     |
| `tcf`          | Map a TCF v2 string and optional capture time.         |

When you provide more than one adapter, their derived permission states must agree.

<Warning>
  Do not infer consent. Leave each permission `unknown` until your consent-management platform has an explicit choice.
</Warning>

PlainRouter sends browser events only when all three permissions are `granted`. Granting `ad_storage` alone can permit local visitor state, but no event is transmitted while either downstream advertising permission is denied or unknown.

A complete grant creates or refreshes the first-party identifiers used for session joins and eligible destination delivery. Denied storage, malformed input, or explicit withdrawal clears PlainRouter's visitor and click identifiers. Send the latest structured state whenever the visitor's choice changes.

On a returning page, initialization stays fail-closed while your consent platform restores its decision: PlainRouter neither reads identifiers nor sends measurement during that undecided period. It also does not erase identifiers left by the visitor's previous grant merely because the consent platform has not answered yet. An explicit denial, malformed consent update, or withdrawal still clears them.

## Track an event

Call `track` with an event name and an optional payload:

```js theme={null}
const eventId = await signalq("track", "purchase", {
  value: "49.00",
  currency: "EUR",
  order_id: "ORDER-1042",
  num_items: 2
});
```

The promise resolves to the collector-accepted event ID. It resolves to `null` if collection fails after one retry.

<Note>
  Send monetary `value` as a decimal string, such as `"49.00"`. This avoids floating-point ambiguity.
</Note>

### Standard event names

Common names are normalized to Meta's standard spelling. For example, `page_view` becomes `PageView` and `initiate_checkout` becomes `InitiateCheckout`.

Frequently used standard events include:

* `PageView`
* `ViewContent`
* `Search`
* `AddToCart`
* `AddToWishlist`
* `InitiateCheckout`
* `AddPaymentInfo`
* `Purchase`
* `Lead`
* `CompleteRegistration`
* `Contact`
* `Schedule`
* `Subscribe`

Custom event names are trimmed and otherwise preserved.

### Payload fields

These convenience fields move into the event's value data automatically:

| Field       | Purpose                                      | Example                          |
| ----------- | -------------------------------------------- | -------------------------------- |
| `value`     | Monetary value as a decimal string.          | `"49.00"`                        |
| `currency`  | ISO 4217 currency code.                      | `"EUR"`                          |
| `order_id`  | Your unique order or transaction reference.  | `"ORDER-1042"`                   |
| `contents`  | Products or items associated with the event. | `[{ id: "SKU-1", quantity: 2 }]` |
| `num_items` | Total item count.                            | `2`                              |

You can also provide a `value_data` object directly. Other custom fields remain on the event and are available to supported destinations.

## Identify a consenting visitor

Call `identify` only after your consent policy permits advertising identity use:

```js theme={null}
const identified = await signalq("identify", {
  email: " Buyer@Example.com ",
  phone: "+358 (40) 123-4567"
});
```

The browser normalizes and SHA-256 hashes the values before sending them. PlainRouter never sends the raw email or phone in the identity request.

* Email is trimmed and lowercased.
* International phone numbers must start with `+` and include 8–15 digits after normalization.
* Finnish local numbers require `defaultPhoneRegion: "FI"` in the initialization configuration.
* Identification returns `false` on an insecure page where browser cryptography is unavailable.

```html theme={null}
<script>
  window.signalq = window.signalq || function () {
    (window.signalq.q = window.signalq.q || []).push(arguments);
  };
  signalq("init", {
    key: "YOUR_PUBLISHABLE_KEY",
    defaultPhoneRegion: "FI"
  });
</script>
```

## Read the visitor ID

`getVisitorId` returns the current first-party visitor ID after consent is granted. It returns `null` while consent is pending or denied.

```js theme={null}
const visitorId = signalq("getVisitorId");
```

One use is attaching the value to a checkout session so a later server webhook can join the purchase to the browser visit:

```js theme={null}
await fetch("/create-checkout-session", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ visitor_id: signalq("getVisitorId") })
});
```

Your server must copy the value into the payment or checkout metadata used by its integration.

## Verify Stripe Checkout settlement

If PlainRouter is configured to receive Stripe webhooks for your Signal, copy the browser visitor ID into the Checkout Session's `metadata.visitor_id`. This lets the settled server-side purchase join the consenting browser visit.

Configure the Stripe webhook endpoint to receive:

* `checkout.session.completed`
* `checkout.session.async_payment_succeeded`
* `checkout.session.async_payment_failed`
* `refund.created` and `charge.refunded` when you track refunds

PlainRouter creates Stripe Checkout revenue only after Stripe confirms an exact `payment_status` of `paid`:

| Stripe state                                                          | Signals result                                                        |
| --------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `checkout.session.completed` with `payment_status=paid`               | Creates one canonical `Purchase`.                                     |
| `checkout.session.completed` with `payment_status=unpaid`             | Records no `Purchase` and waits for asynchronous success.             |
| `checkout.session.async_payment_succeeded` with `payment_status=paid` | Creates the canonical `Purchase` at the asynchronous settlement time. |
| `checkout.session.async_payment_failed`                               | Creates no `Purchase` or `Refund`.                                    |
| `payment_status=no_payment_required`                                  | Creates no revenue event.                                             |
| Missing, unknown, or malformed payment status                         | Fails closed and creates no `Purchase`.                               |

Duplicate or out-of-order paid webhooks for the same Checkout Session converge on one `Purchase`. PlainRouter uses the settlement-confirming webhook time as the event time. Until settlement is confirmed, the Checkout Session does not enter Signals rollups or Meta delivery.

Stripe refund webhooks create idempotent custom `Refund` events with a negative refunded value. PlainRouter links the refund to its known purchase when the order lineage is available.

## Reliability behavior

* Event requests use browser `keepalive` where available.
* A granted event still in flight during `pagehide` is also sent as a text beacon.
* Network failures and `503` responses receive one retry.
* The snippet captures supported landing click IDs in memory and persists them only after consent is granted.
* First-party browser identifiers can still have shorter lifetimes when a browser applies its own storage restrictions.

## Avoid duplicate Meta events

After you [connect Meta](/docs/signals/connect-meta), continue to track through `signalq`. The managed browser Pixel and PlainRouter server delivery share one event ID.

Remove other Pixel or CAPI integrations that send the same event to the same dataset after you confirm the managed flow is working.
