# Destination mapping: names, limits, identity, dedupe and time

Every number below was read from the vendor's live docs on 2026-10-17. Limits change. Before you approve a mapping, fetch the page listed in `sources.yaml` for each destination in the plan and confirm the value. If the live page differs, follow it and tell the user what changed.

## Contents

- Mapping rules that apply to every destination
- Collision check
- GA4
- Mixpanel
- Amplitude
- Segment
- RudderStack
- HubSpot
- Choosing the dedupe field per destination

## Mapping rules that apply to every destination

- Route an event to a destination only when a named use needs it there. "Send everything everywhere" multiplies cost, PII exposure and the number of places a rename must ship.
- Record the delivery path as `via` (`direct_sdk`, `segment`, `rudderstack`, `measurement_protocol`, `gtm`, `warehouse_sync`). Dedupe, identity and time mappings all depend on it: the same destination reached through a CDP and through its own SDK takes different fields.
- Record the final name each destination receives, after every connector transformation. The canonical name is the plan's; the destination name is a mapping.
- The strictest destination in the plan sets the naming budget. With GA4 in the plan, keep canonical names and property names at 40 characters or fewer.
- Map to a destination's standard or reserved event only when the meaning matches. A click on "Create account" is not GA4 `sign_up`. A billing page view is not `purchase`.
- Map identity explicitly: which field carries the anonymous ID, the user ID and the account. A shared naming convention without a shared identity mapping still fragments journeys.
- Map time explicitly: which field carries `occurred_at`, in the format the destination expects. An ISO string is not valid where an API expects epoch milliseconds.
- A custom `event_id` property does not switch on vendor deduplication. Put the derived `message_id` in the destination's own dedupe field and record the configured field path as `dedupe_mapping` (see the last section). Then inspect one received event per destination and write down the value its dedupe field holds; a CDP destination may not map the field by default.

## Collision check

Run before approval, over the final destination names, not the canonical ones. `scripts/check_plan.py` runs the mechanical parts over the two sheet tabs; the reserved-name list comes from the live vendor pages.

- Lowercase every name and replace spaces with underscores. Two different canonical events that become one string will merge in GA4 (case sensitive, no spaces) and in any connector that normalises names.
- Check that no custom name equals a destination's reserved or automatically collected name for the stream type (web and app lists differ in GA4).
- Group by destination plus final name across every `via`. Two producers (browser and server, or a CDP route plus a direct tag) must not send the same final name unless the contract has a reconciliation rule.
- Check property names the same way, and check that one property name has one type across all events.
- HubSpot: internal names are lowercase only, so two plan names that differ only by case become one HubSpot property. Check lowercase duplicates before anyone creates properties.
- GA4: count parameters per event including those the tag adds (take the count from one DebugView capture), against the 25 limit.

## GA4

Sources: Google's event naming rules, collection limits, custom dimension limits, recommended events reference, ecommerce measurement article, key events article, consent mode overview, Measurement Protocol guide and data freshness article (URLs in `sources.yaml`).

Naming:

- Event names start with a letter and use only letters, numbers and underscores; no spaces. Names are case sensitive.
- Reserved event names exist and differ between web and app streams. Parameter names cannot start with `_`, `firebase_`, `ga_`, `google_` or `gtag.`. Fetch the current reserved lists; do not rely on a copied list.

Limits for standard properties, checked 2026-10-17:

- Event name: 40 characters. Parameter name: 40 characters.
- Parameter value: 100 characters, except `page_title` (300), `page_referrer` (420) and `page_location` (1,000).
- Parameters per event: 25. User properties: 25 per property; name 24 characters, value 36 characters. User-ID value: 256 characters.
- Distinct event names: no limit for web streams, 500 per app user for app streams.
- Custom definitions: 50 event-scoped dimensions, 25 user-scoped, 10 item-scoped, 50 custom metrics (360 properties have higher limits). After deleting a dimension at the limit, a new one cannot be added for 48 hours.
- Google states that events, parameters and user properties over these limits are not logged. GA4 does not reject the request; the 26th parameter simply disappears. A key event whose name is longer than 40 characters is not reported as a key event.

Mapping rules:

- Count parameters per event, including the ones the tag adds, before approving. Twenty custom parameters plus the tag's own can cross 25 silently.
- Do not register IDs, session IDs or timestamps as custom dimensions. Google warns high-cardinality dimensions push rows into "(other)". Send `event_id` as a parameter only if the BigQuery export needs it for joins, and leave it unregistered.
- Use recommended events by meaning: `sign_up` and `login` take an optional `method`; `purchase` and `refund` require `transaction_id`, and `currency` is required whenever `value` is set; `purchase` requires `items`. Money events: `ecommerce-and-billing.md`.
- `transaction_id` deduplicates purchases only for the same user and only on web streams, not app streams. Never send an empty string: GA4 deduplicates every purchase that has `transaction_id=""`.
- GA4 has no general event dedupe. A browser companion sent through GTM must guard against repeats itself: push once per `event_id` and skip a repeat after a refresh or back navigation.
- With `via: gtm`, write the dataLayer push next to the mapping (event name, keys, when it is pushed) so the developer and the GTM owner implement the same thing. Tag configuration itself is a hand-off to the GTM owner.
- Key events: mark only the outcomes an acquisition decision uses, and keep their names at 40 characters or fewer. Mark a new event as a key event in Admin before its first release: Google states marking affects reports from the time it is made and does not change historic data, and a not-yet-collected event can be created and marked ahead of time (Admin, Events, create event, "Mark as key event"; Marketer role or above for a new event, Editor or above for an existing one, checked 2026-10-17).
- Consent mode: record `basic` or `advanced` in the contract. Under basic, tags do not load and nothing is sent to Google while consent is denied. Under advanced, tags send measurements without cookies while denied. Verify with Tag Assistant's consent view.
- PII: Google's policy forbids sending data Google could use or recognise as personally identifiable. Check URLs, page titles, search terms and every free-text parameter.
- Server events through the Measurement Protocol: up to 25 events per request; events can be backdated up to 72 hours (older ones are clamped to 72 hours ago or rejected, depending on `validation_behavior`); events meant to join tag or SDK events should arrive within 48 hours of the client timestamp; for web streams the `client_id` should match the one the Google tag set. Google positions the protocol for server-side or trusted environments and points browsers to the Google tag.
- Through Segment's GA4 Cloud destination, the Event Timestamp field can be backdated up to 3 calendar days in the property's time zone. Beyond these windows the canonical record comes from the warehouse and GA4 counts are documented as partial.
- Late events: GA4 ignores events that arrive more than 2 calendar days plus today after they were triggered (data freshness article). This matters for mobile offline queues.
- Read-only verification: Google's GA4 MCP server (labelled Experimental, read-only scope) can list custom dimensions and run reports by event name.

## Mixpanel

Sources: Mixpanel events and properties page, supported data types, event deduplication reference, Import Events API reference, Track Events API reference, Lexicon docs, Simplified ID Merge docs.

- Names are case sensitive (`sign_up_completed` and `Sign_Up_Completed` are two events). Mixpanel recommends object then verb in snake_case and warns against dynamic names.
- Up to 255 properties per event. Property names and values are capped at 255 characters and longer ones are truncated; string values have a 255-byte limit; list and object properties are capped at 8 KB on events.
- Soft limit of 5,000 distinct event names and 5,000 distinct event properties per project. Beyond that, data is ingested but not indexed or shown in menus.
- No event-name length limit was found on 2026-10-17; check live, and stay within the plan's naming budget anyway.
- Dedupe: two events are duplicates only when event name, `distinct_id`, `time` and `$insert_id` all match. Deduplication happens at query time and in a background compaction with no guaranteed schedule. Raw exports are not deduplicated, so apply the same four-field rule in the warehouse.
- `$insert_id` format: the Import API requires it on every event and accepts only 36 bytes or less of letters, digits and `-`. A plain UUID or a UUIDv5 `message_id` fits; a prefixed or concatenated key is rejected.
- Time: the `/track` endpoint accepts only events from the last 5 days; older events need `/import`. This bites mobile apps that flush a queue after a long offline period.
- Identity (Simplified ID Merge): send `$device_id` before login and both `$device_id` and `$user_id` after; two `$user_id` values cannot be merged and a `$user_id` cannot be changed. Mixpanel recommends a database ID over an email.
- Lexicon holds display names and descriptions; Mixpanel states that changing a display name does not modify the underlying stored data, so queries and integrations that read raw names are unaffected.

Example mapping, for a stack where Mixpanel replaces Amplitude for `user_registered` (server event, sent through `/import` by a backend job):

```yaml
mixpanel:
  name: user_registered
  via: direct_sdk                 # server-side import
  dedupe_mapping: $insert_id set from message_id; event name, distinct_id and time identical on every retry
  identity: $device_id from the browser anonymous_id sent in the signup request; $user_id from user_id
  time: occurred_at in milliseconds since epoch
```

## Amplitude

Sources: Amplitude HTTP V2 API, limits and quotas FAQ, data planning playbook, Amplitude Data settings, display names page, login identity engineering page, S3 export column definitions.

- Amplitude recommends a consistent pattern such as noun plus past-tense verb and consistent casing ("Song Played" and "song played" are two events). Default Amplitude events use Title Case. If the plan is snake_case, set the project's naming convention to snake_case in Amplitude Data so the plan check agrees with the convention.
- String values (including event type, user ID and property values) are limited to 1,024 characters.
- Per project: 2,000 event types, 2,000 event properties, 1,000 user properties. Past these limits Amplitude stops indexing new values and they cannot be queried in charts; the raw data stays reachable only through export. Property values are unlimited, but dropdowns show only the first 1,000 values of a property, another reason to use enumerations instead of free text.
- No limit on the number of event properties on one event.
- `user_id` and `device_id` must be at least 5 characters unless `min_id_length` is overridden. Short IDs are removed from the event and an event left with neither ID can be rejected. A numeric database ID such as `42` fails; prefix or pad it consistently (`usr_42`). A server event with a null `user_id` and no `device_id` cannot be sent.
- Dedupe: Amplitude ignores later events with the same `insert_id` on the same `device_id` within the past 7 days. The event type is not part of the check, so two different events sharing an `insert_id` on one device lose the second. Retries after 7 days are not deduplicated.
- Time: `time` is milliseconds since epoch; other formats return 400. Amplitude adjusts `event_time` by the gap between `server_received_time` and `client_upload_time` when that gap is 60 seconds or more, which corrects device clock skew.
- Throttling: more than 30 events per second for one user or device on average is throttled; batch backfills by user.
- Identity: changing a non-null `user_id` creates another user. Keep the anonymous `device_id` when assigning the user ID at login.
- Display names in Amplitude Data change only the label in the Amplitude UI, not the raw data, and apply to active planned events and user properties, not custom events.

## Segment

Sources: Segment Track spec, Identify spec, common fields spec, duplicate data guide, HTTP API source docs, GA4 Cloud destination docs, Amplitude (Actions) destination docs, MTU guide, Protocols docs.

- Segment's collection best practices recommend object-action event names in Title Case and snake_case property names, and advise against dynamic values in event names. This is guidance, not validation: a snake_case plan works, but record the final name each downstream destination receives. Do not migrate an established, consistent Title Case taxonomy only for style.
- Reserved Track properties keep their meaning: `revenue` (number, decimal), `currency` (string, ISO currency code; Segment assumes US dollars if omitted), `value` (abstract non-revenue value). Semantic events (ecommerce, video and others) carry special meaning in destinations; use them only when the meaning matches.
- Every call needs `userId` or `anonymousId`. Segment recommends database IDs as `userId` and sending email as a trait instead.
- `messageId` is the dedupe key, up to 100 characters. Segment stores at least 24 hours of `messageId` values and drops a repeat, not scoped to a source or workspace; warehouses run a second pass on `messageId` for duplicates further apart. Set `messageId` to the contract's derived `message_id`, never to `event_id`, or a browser companion or a renamed twin is dropped as a duplicate of the server event.
- Timestamps: `originalTimestamp` (client time when called), `sentAt` (client time when sent), `receivedAt` (server time), and `timestamp` corrected as `receivedAt - (sentAt - originalTimestamp)`. Server libraries pass the business time in `timestamp`.
- HTTP API: 32 KB per call; batch endpoint 500 KB per request with 32 KB per event. Historical timestamps only work for destinations that accept them. Segment's HTTP API page names Google Analytics as not accepting historical data; that refers to the legacy product. For GA4, the GA4 Cloud destination accepts timestamps up to 3 calendar days back and the Measurement Protocol up to 72 hours.
- GA4 Cloud destination: Segment replaces spaces with underscores and offers a Lowercase Event Name setting. `Plan Selected` can become `Plan_Selected` or `plan_selected` depending on that setting, so check collisions against the configured output.
- Amplitude (Actions) destination, Log Event mapping: the docs list no default source for Insert ID, so map it from `messageId` and confirm on one received event. Device ID defaults to `context.device.id` with `anonymousId` as the fallback. Timestamp has no listed default; without it Amplitude uses the upload time.
- MTU billing counts unique `userId` values plus unique `anonymousId` values never linked to a `userId` in the billing period. New anonymous IDs (other browsers, incognito sessions, cleared cookies, `reset()` calls, separate domains) that are never identified each add an MTU. Identify at signup and login, and pass the browser `anonymousId` to server calls.
- Protocols (tracking plans, validation, blocking) is an add-on for Business plans only. Details in `enforcement-handoffs.md`.

## RudderStack

Sources: RudderStack tracking plans docs, tracking plan settings page, deduplication page, Rudder CLI docs, RudderStack MCP docs.

- No RudderStack event-name length limit or enforced casing was found on the pages checked. The binding name limits come from the destinations it forwards to.
- Tracking plans validate `track`, `identify`, `page`, `screen` and `group` calls (not `alias`): allowed events, required properties and data types.
- Plan limits by tier, checked 2026-10-17: Free 1 plan with 5 events; Growth unlimited plans with 75 events per plan; Enterprise unlimited. A Free workspace cannot hold more than a five-event journey plan.
- Dedupe, checked 2026-10-17: RudderStack SDKs assign an immutable `messageId` that stays the same across retries. RudderStack drops an event whose `messageId` it has already processed within 7 days. It does not guarantee exactly-once delivery: events outside the window, a rare failure to store the ID, and node scaling can let duplicates through. RudderStack warns that custom `messageId` values must differ between events, or later events are dropped even when they come from different users. Set the derived `message_id` and keep the warehouse rule either way.
- Details of violation handling and plans as code are in `enforcement-handoffs.md`.

## HubSpot

Sources: HubSpot send event data guide, current and legacy event definition references, record deduplication article, companies batch upsert reference, property editing knowledge base article, remote MCP docs.

Decide first whether HubSpot needs the event at all:

- Default: send derived properties (first connected date, connected source count, last active date) written by a warehouse or CDP sync. A salesperson acts on account state, not on an event stream.
- Use a custom event only when a HubSpot workflow, list or report must react to each occurrence.

Derived properties, matching and roll-up:

- HubSpot's company domain deduplication does not apply to companies created through the API or through sync apps, and your `account_id` is not the HubSpot record ID. A sync that matches on domain merges two customers that share a domain, or creates a new company on every run.
- Create one unique-value custom company property that holds `account_id` (HubSpot allows up to ten unique-value properties per object) and upsert on it; the companies batch upsert endpoint takes the unique property as `idProperty`. Record it as `match_key` in the contract.
- Record the target `object` (company, or a custom workspace object) and a `rollup` rule when the plan counts a smaller unit than the record: for example "earliest across the account's workspaces" for a first-connected date, "sum" for a count.

Custom event constraints, checked 2026-10-17:

- An event must be defined before occurrences are sent. The current reference defines events with `POST /events/2026-09/event-definitions` (`label` and `propertyDefinitions` required) and sends occurrences with `POST /events/2026-09/send`. The fully qualified name used when sending is `pe{HubID}_{name}`.
- The legacy definition reference says the internal `name` is generated from the label when not supplied and that `primaryObject` defaults to contacts. The 2026-09 reference does not state either default, so set both explicitly and check the live page.
- Property internal names start with a letter and use only lowercase letters, numbers and underscores. Labels and internal names: 50 characters.
- Up to 50 properties per occurrence. Values: 256 characters, or 1,024 for URL and referrer properties. Exceeding a limit fails the request (unlike GA4, which drops silently).
- 500 event definitions per account and 30 million occurrences per month.
- `occurredAt` takes ISO 8601 or epoch milliseconds; omitted means request time.
- Dedupe: `uuid` must be unique within the event type; when several occurrences share an ID within a year, the first is accepted and the rest rejected. Set it to the derived `message_id`.
- Record matching, in priority order: a custom matching ID set up on the definition, `objectId`, then `email` or `utk` for contacts.
- API versions are date-based and subscription requirements differ between defining in the UI and through the API. Check the current guide for which subscriptions can define and send events.

CRM properties written by the plan:

- A property's internal name and object type cannot be edited after creation, and changing a field type can invalidate stored values. Agree internal names in the plan before anyone creates them. Prefix custom properties consistently so they are distinguishable from HubSpot defaults.

## Choosing the dedupe field per destination

Set every transport field from the contract's derived `message_id`, never from `event_id` itself. Windows checked 2026-10-17:

- Warehouse: drop transport duplicates on `message_id`, then one canonical row per event name plus `event_id`.
- Segment: `messageId`; at least 24 hours, global across sources and workspaces.
- RudderStack: `messageId`; 7 days.
- Mixpanel: `$insert_id`, 36 bytes or less, letters, digits and `-`; a duplicate also needs the same event name, `distinct_id` and `time`.
- Amplitude: `insert_id`; 7 days, per `device_id`, any event type.
- HubSpot custom events: `uuid`; within the event type, one year.
- GA4: no general event dedupe; `transaction_id` (the order ID) deduplicates purchases only (same user, web streams). Prevent repeats at the source.

Hard rule: no two emitted messages share a `messageId`, `$insert_id`, `insert_id` or `uuid` within a destination's window. The acceptance case that proves it sends the canonical event and its companion through the same CDP and expects both to arrive in every mapped destination.
