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:
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.
Matches ^[a-zA-Z0-9_.]{1,128}$.
Unix seconds, not milliseconds. A millisecond timestamp puts your event tens of thousands of years in the future.
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.
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 sendsession),started_at.
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:
revenueorvalue→ 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:
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.
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 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.
- 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.