Custom events and presets
Three ways to send an event
- Markup, no code –
data-x3-eventon any clickable element (Data attributes):<a href="/pricing" data-x3-event="pricing_click" data-x3-prop-position="hero">See pricing</a> - Queue / script tag – works before the script has loaded:
x3("track", "newsletter_signup", { list: "weekly" }); - npm –
track(name, props)(SDK reference).
Events from your server (payment confirmed, CRM stage changed) go through
POST /v1/projects/{project}/events with the browser's session id (Server helper).
Names and props
- Names: lowercase snake_case, ≤ 40 characters (
trial_started,pricing_toggle). - Up to 20 props per event; values string, number (finite, |x| ≤ 10¹²) or boolean.
- Privacy rule: string props of events without a registered schema must look like identifiers –
at most 80 characters of letters, digits, space and
. : / # - _, never anything resembling an e-mail address or phone number. Other strings are dropped silently. This keeps visitor free text out of your analytics by construction.
Event schemas
Register an event to give its props types, enums, ranges or (deliberately) free text, and to make it appear with a label in the dashboard's event catalog:
curl -X PUT https://api.3x.example/v1/projects/$PROJECT/event-schemas/plan_selected \
-H "Authorization: Bearer $X3_SECRET_KEY" -H "Content-Type: application/json" \
-d '{
"label": {"en": "Plan selected", "de": "Tarif gewählt"},
"props": {
"plan": {"type": "enum", "values": ["starter", "pro", "enterprise"]},
"seats": {"type": "int", "min": 1, "max": 500, "optional": true},
"yearly": {"type": "bool", "optional": true}
},
"oncePerPageview": false,
"dedupe": "name_props",
"flush": false
}'| Prop type | Fields |
|---|---|
string | max (≤ 200), pattern (regex), privacy: label (cleaned like click labels) or text (free text ≤ 200, still e-mail/phone-filtered) |
int, number | min, max |
bool | – |
enum | values |
Every prop may be optional. A required prop that is missing or invalid drops the event.
dedupe: none, name (repeats within 1 s dropped regardless of props) or name_props.
status: "blocked" drops the event at ingest. Validate a payload without sending it:
POST /v1/events/validate or the MCP tool events_validate.
Cardinality limits
| Limit | Value |
|---|---|
| Distinct unregistered event names per project | 200 – further new names are dropped and counted (projects.status.eventNames.droppedToday) |
Event names counted per session (event_counts) | 50 – built-ins first, then registered schemas, then by count |
| Props per event | 20 |
Unregistered names show up as observed in the event catalog, where you can register or block them.
Presets
Presets are ready-made events with helpers. Names and props match the cpi-website2 engine exactly, so projects using the CPI preset keep comparable history. Full list: Built-in event catalog.
| Preset | Events | Helpers |
|---|---|---|
| Quiz | quiz_view, quiz_answer {variant, q, value}, quiz_contact, quiz_submit | x3.quiz.view/answer/contact/submit |
| CTA | cta_click {label, section} | auto-detected via data-x3-cta and tracking.cta.selectors; x3.cta() |
| Messaging | whatsapp_click {section}, messaging_click {app, section} | auto-detected from wa.me, api.whatsapp.com, whatsapp:, and enabled messengers |
| Calculator | calculator_used | x3.calculator() |
| Booking | booking {via} | adapters for HubSpot meetings, Calendly, same-origin messages, window events, thank-you pages; x3.booking(via) |
| Funnel | funnel_step {funnel, step} | x3.funnelStep() |
| Forms | form_start, form_submit, form_abandon | automatic (Forms) |
| Video | video_start, video_complete | automatic (Video) |
Quiz example
import { x3 } from "@3x-engine/sdk";
x3.quiz.view("pricing-quiz");
x3.quiz.answer("pricing-quiz", "team_size", "11_50");
x3.quiz.contact("pricing-quiz");
x3.quiz.submit("pricing-quiz");Register the questionnaire (PUT …/questionnaires/pricing-quiz) to get labelled answer statistics
and, with tracking.questionnaires.strict, to reject answers that do not exist
(Goals and funnels).
Booking adapters
Configured in the tracking config (bookings), no code needed:
[
{ "type": "postmessage", "origin": "https://meetings.hubspot.com", "match": { "meetingBookSucceeded": true }, "via": "hubspot" },
{ "type": "calendly", "via": "calendly" },
{ "type": "window_event", "event": "demo:booked", "via": "demo" },
{ "type": "thank_you", "paths": ["/thank-you"], "windowFlag": null, "via": "thank_you" }
]Window-event bridges
Map your app's own CustomEvents onto 3x events without touching the code that fires them:
{ "bridges": [{ "event": "app:quiz-submitted", "emit": "quiz_submit", "props": { "variant": "variant" }, "flush": true }] }props maps prop names to dotted paths in event.detail.
CPI drop-in bridges
@3x-engine/sdk/presets exports bridgeFunnelStage, bridgeQuizAnswer and bridgeFunnelEvent –
the cpi-website2 funnelBeacon hooks with identical semantics (quiz_seen → quiz_view,
contact_reached → quiz_contact, cta_click with the last click's label and section, …).