> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pivolio.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pivolio.com/_mcp/server.

# Events

Everything Pivolio ingests — SDK, raw HTTP, webhook, Shopify — becomes the same
structure: a **UnifiedEvent**. One shape means identity resolution, attribution
and destination mapping never have to care where an event came from.

## Required fields

A raw HTTP event must carry all four of these, or the collector returns `422`:

**`event_id`** `string` — required

A ULID. Doubles as the idempotency key — re-sending the same `event_id` for a
workspace returns `{"ok": true, "duplicate": true}` and does no further work.

---

**`event_name`** `string` — required

Matches `^[a-zA-Z0-9_.]{1,128}$`.

---

**`event_time`** `float` — required

Unix **seconds**, not milliseconds. A millisecond timestamp puts your event
tens of thousands of years in the future.

---

**`anonymous_id`** `string` — required

The device-level identifier. The SDK manages this for you.

---

Optional: `user_id`, `traits`, `context`, `consent`, `properties`, `test_mode`,
and `schema_version` (defaults to `"1"`).

## Traits and context

**`traits`** carry person-level data — `email`, `phone`, `first_name`,
`last_name`, `country`, `city`, `state`, `zip`, `gender`, `date_of_birth`,
`external_id`, plus any custom keys you add. These are what get normalized and
hashed before delivery, and what drive [match quality](/documentation/core-concepts/match-quality).

**`context`** carries the environment:

* **`context.click_ids`** — 14 supported: `fbclid`, `fbp`, `fbc`, `gclid`,
  `gbraid`, `wbraid`, `ttclid`, `ttp`, `li_fat_id`, `msclkid`, `epik`,
  `rdt_cid`, `sc_click_id`, `twclid`.
* **`context.campaign`** — `source`, `medium`, `name`, `term`, `content`.
* **`context.page`** — `url`, `path`, `referrer`, `title`.
* **`context.device`** — `type`, `os`, `os_version`, `browser`,
  `browser_version`.
* **`context.session`** — `id` (required if you send `session`), `started_at`.

> **Note**
>
> `context.ip` is **server-set**. Whatever a client sends is overwritten with the
> observed IP — you can't spoof geo or traffic-quality signals from the browser.

## Properties

`properties` is free-form, capped at **64,000 bytes** JSON-encoded. Four keys
are read by the pipeline rather than just stored:

* **`revenue`** or **`value`** → the event's revenue, rounded to cents.
* **`currency`** → a 3-character code, uppercased.
* **`order_id`** → the commerce order reference, used for deduplication at
  destinations that support it.

Properties are **nested**, not top-level:

```ts
track('order_completed', {
  properties: { revenue: 99.5, currency: 'USD', order_id: 'ORD-1041' },
});
```

The SDK's commerce helpers (`trackPurchase`, `trackAddToCart`,
`trackBeginCheckout`) exist mostly to get this mapping right for you — notably
`orderId` → `order_id`.

## Standard event names

Twenty-five names are recognized. Using one gets you destination mapping and
correct taxonomy for free; anything else is a custom event.

| Group               | Names                                                                                                                                 |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Browsing            | `page_view`, `view_content`, `search`                                                                                                 |
| Commerce            | `add_to_cart`, `add_to_wishlist`, `initiate_checkout`, `begin_checkout`, `add_payment_info`, `order_completed`, `purchase`            |
| Lead & registration | `lead_submitted`, `lead`, `complete_registration`, `sign_up`, `contact`, `subscribe`, `start_trial`, `submit_application`, `schedule` |
| Engagement          | `click_button`, `download`, `donate`, `find_location`, `key_page_view`                                                                |
| Identity            | `identify`                                                                                                                            |

> **Warning**
>
> `product_viewed` is **not** a standard name — the one you want is
> `view_content`. A near-miss name is silently treated as a custom event, so it
> maps to nothing at Meta, TikTok or Reddit and you lose the conversion.

## Custom events

Any name matching `^[a-zA-Z0-9_.]{1,128}$` is accepted. Custom events are
tracked, stored and reported normally. What they don't get is a standard
destination mapping, so by default no destination sends them. To deliver a
custom event, [map it explicitly](/documentation/destinations/event-mappings) on each
destination that should receive it.

## Taxonomy

Every event lands in one of three tiers. This is what decides whether an event
earns attribution credit and triggers [click backfill](/documentation/core-concepts/identity-resolution).

* **Conversion** — `order_completed`, `purchase`, `lead_submitted`, `lead`,
  `complete_registration`, `sign_up`, `subscribe`, `start_trial`,
  `submit_application`, `schedule`, `contact`, `donate`
* **Intent** — `add_to_cart`, `add_to_wishlist`, `initiate_checkout`,
  `begin_checkout`, `add_payment_info`, `view_content`, `search`
* **Browse** — everything else, including custom events

Two of those placements are deliberate and worth knowing:

**`initiate_checkout` and `begin_checkout` are intent, not conversion.** A
started checkout is a strong signal, but it isn't an outcome — treating it as
one inflates your conversion counts with abandoned carts.

**`add_payment_info` is intent for the same reason, only sharper.** Counting it
as a conversion double-counted checkouts: the same session produced a payment-info
event *and* a purchase, and both claimed credit.

#### [Identity resolution](/documentation/core-concepts/identity-resolution)

How an event becomes a person.

#### [Attribution](/documentation/core-concepts/attribution)

How a conversion gets assigned credit.