Skip to main content

Track ticket sales from a website widget (V2)

C
Written by Customer Success

Use the ticket widget's browser events to measure shop openings, submitted details and completed orders. Give this guide and the widget's developer link to the person who maintains your website analytics.

Note: Website widgets V2 are scheduled to launch on 1 November 2026. These steps apply once V2 is available in your event.

Choose what to measure

Open Events on the shared Ticketing developer page for the event reference.

Widget event

What it means

How to use it

ticket:opened

The Get tickets dialog opened. It can happen again when the buyer reopens it.

Count shop openings. An inline shop does not emit this event just by being visible.

ticket:formSubmitted

An order was created after valid details were submitted, before checkout completes.

Count submissions, not sales. Payment can still be abandoned or fail.

ticket:purchased

The returning widget loaded an order whose status is completed.

Count a completed order once using purchaseId.

Ticketing developer reference showing three DOM events, when they occur and their event.detail fields

For example, 20 submitted forms and 12 distinct completed purchase IDs represent 12 completed orders. The remaining submissions cannot be counted as paid orders from these events.

A completed order may have no amount to pay. If you want a paid-sales metric, define and check that separately.

For the website developer

Listen on each widget container

The events are dispatched on #brella-widget. They do not bubble, so attaching a listener only to document will miss them.

Attach listeners after the widget containers exist and before loading the widget script. On an SPA, bind newly inserted containers before calling the widget renderer.

This example collects records in a local array. It sends nothing to an analytics service. Connect that array or replace the queue operation with your site's approved analytics call.

window.brellaTicketEvents = window.brellaTicketEvents || [];
window.brellaTrackedRoots = window.brellaTrackedRoots || new WeakSet();

window.bindBrellaTicketTracking = function () {
document.querySelectorAll('#brella-widget').forEach((root) => {
if (window.brellaTrackedRoots.has(root)) return;
window.brellaTrackedRoots.add(root);

['ticket:opened', 'ticket:formSubmitted', 'ticket:purchased']
.forEach((name) => {
root.addEventListener(name, (event) => {
const purchaseId = String(event.detail?.purchaseId ?? '');
const record = { event: name };
if (purchaseId) record.purchaseId = purchaseId;

if (name === 'ticket:purchased') {
if (!purchaseId) return;
const key = `brella-ticket-purchase:${purchaseId}`;
try {
if (sessionStorage.getItem(key)) return;
sessionStorage.setItem(key, 'seen');
} catch {
// Destination deduplication is still required if storage is blocked.
}
}

window.brellaTicketEvents.push(record);
});
});
});
};

window.bindBrellaTicketTracking();

The binding helper above is example site code, not a built-in Brella function. It binds each container once, including several widgets on one page. Call it again after an SPA adds new containers.

The browser can emit a completed-order event again after a reload. The example suppresses repeats in the same browser tab where session storage is available. Your analytics destination must also deduplicate completed orders by purchaseId across reloads, tabs and repeated delivery. A browser queue is not proof that the destination received a record.

The example omits buyer names, email addresses and ticket details. Do not forward the entire event.detail object just to count orders.

Understand the payload

The submitted and purchased events include purchaseId, discountCode, purchaser, tickets and total. The ticket details differ by event: submission carries the submitted ticket information; completion carries the returned invites. Do not assume that both arrays have the same structure.

total is the widget order total in Stripe amount units, not a formatted price. The payload has no separate top-level currency field. Confirm the order's currency from your ticket configuration or an appropriate order-data source before reporting revenue.

For EUR, an amount of 12500 means €125.00. For JPY, 12500 means ¥12,500. Do not divide every currency by 100. Check Stripe's currency amount rules, including special cases. Keep total out of your revenue metric until that conversion is defined and checked.

Keep the return page working

With Page after payment empty, the widget creates a return URL containing ticket_purchase_id and widget_id. The same widget must load there and retrieve the completed order before it emits ticket:purchased.

If you set a custom Page after payment, that address replaces the generated return URL. Design and verify its confirmation and tracking flow with the developer. A generic thank-you page does not automatically emit the widget's purchase event.

Keep the original widget props available for the return flow; its identity is derived from those props. Do not remove the return query parameters before the widget reads them.

Verify the integration

  1. On a permitted test page, open the Get tickets dialog. Confirm one opening record, then close and reopen it to check the next record.

  2. Check that the inline version does not create an opening record just by loading.

  3. Check each widget container on a page with more than one widget.

  4. Review the example's field allowlist and connect only the fields your metric needs.

  5. Before promoting your ticket shop, test checkout and the return page. Check submission, completed return, delayed confirmation and reload deduplication. The builder preview cannot prove a sale event because purchases are disabled there.

If completed sales are missing, first check the return page, its widget configuration and listener timing. If sales are counted twice, check purchaseId deduplication at the analytics destination.

Related articles

Did this answer your question?