RevenueUI documentation

Custom events and goals

Send meaningful conversion events from your application and turn them into measurable goals in RevenueUI.

How custom event tracking works

Use a custom event for an action that matters to your business, such as a completed signup, a submitted lead form, or a started checkout. The browser tracker sends the event with the current page and visitor or session identity so RevenueUI can show which visitors completed a goal.

The browser must make the final track() call. Your server can decide whether an action succeeded, but server-side PHP, Python, Ruby, or Node.js code does not have access to window.revenueUI or the browser identity used for attribution.

Use this sequence:

  1. The visitor starts an action in the browser.
  2. Your application or server completes the action.
  3. The browser receives a confirmed success response or returns to a confirmed success page.
  4. The browser calls track() once.
  5. You create a RevenueUI goal with the exact same event name.

Do not record a completion from the initial button click. A visitor can cancel OAuth, fail validation, or receive a declined payment after clicking.

Create a reusable browser helper

The tracking script uses defer, so a component can run before the tracker is ready. This helper sends immediately when possible and otherwise waits for the revenueui:ready event:

export function trackRevenueUI(eventName) {
  const send = () => window.revenueUI?.track(eventName);

  if (window.revenueUI?.ready) {
    send();
    return;
  }

  window.addEventListener("revenueui:ready", send, { once: true });
}

Keep event names stable. They must begin with a letter, contain only lowercase letters, numbers, and underscores, and contain no more than 32 characters. page_view and page_engagement are reserved.

Good names describe completed outcomes:

signup_completed
lead_form_submitted
checkout_started
demo_booked

RevenueUI does not accept custom event properties. Do not put an email address, customer name, order number, or other personal data in an event name.

JavaScript

Call the helper only after the request confirms success:

import { trackRevenueUI } from "./track-revenueui.js";

async function createAccount(formData) {
  const response = await fetch("/api/signup", {
    method: "POST",
    body: formData,
  });

  if (!response.ok) {
    throw new Error("Signup failed");
  }

  trackRevenueUI("signup_completed");
}

Do not attach signup_completed directly to the submit button. Client-side validation, the network request, and account creation must all succeed first.

TypeScript

Add the browser API type once, for example in types/revenueui.d.ts:

export {};

declare global {
  interface Window {
    revenueUI?: {
      readonly ready: boolean;
      track(eventName: string): void;
    };
  }
}

The reusable helper can then validate event names at the call site:

type RevenueUIEvent =
  | "signup_completed"
  | "lead_form_submitted"
  | "checkout_started";

export function trackRevenueUI(eventName: RevenueUIEvent): void {
  const send = () => window.revenueUI?.track(eventName);

  if (window.revenueUI?.ready) {
    send();
    return;
  }

  window.addEventListener("revenueui:ready", send, { once: true });
}

React

For a form handled in the component, send the event after the API response succeeds:

import { useState } from "react";
import { trackRevenueUI } from "./track-revenueui";

export function SignupForm() {
  const [submitting, setSubmitting] = useState(false);

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setSubmitting(true);

    const formData = new FormData(event.currentTarget);
    const response = await fetch("/api/signup", {
      method: "POST",
      body: formData,
    });

    setSubmitting(false);

    if (response.ok) {
      trackRevenueUI("signup_completed");
    }
  }

  return <form onSubmit={handleSubmit}>{/* fields and submit button */}</form>;
}

Prefer the confirmed request result over a mount-only effect. React development mode can run effects more than once, and revisiting a success route can otherwise record another completion.

Next.js App Router

window.revenueUI is available only in a Client Component. A Server Action should return a success result, and the Client Component should track that result:

"use client";

import { createAccount } from "./actions";
import { trackRevenueUI } from "@/lib/track-revenueui";

export function SignupForm() {
  async function handleSubmit(formData: FormData) {
    const result = await createAccount(formData);

    if (result.success) {
      trackRevenueUI("signup_completed");
    }
  }

  return <form action={handleSubmit}>{/* fields and submit button */}</form>;
}

For OAuth, verify the callback on the server and redirect to a client-rendered completion route. That route can consume a short-lived success signal and call the helper once. Do not treat the OAuth button click as a completed signup.

Vue and Nuxt

Use the same browser helper after a successful request. In Nuxt, call it from a client-side component or handler:

<script setup lang="ts">
import { trackRevenueUI } from "~/utils/track-revenueui";

async function submitSignup(form: Record<string, string>) {
  const result = await $fetch<{ created: boolean }>("/api/signup", {
    method: "POST",
    body: form,
  });

  if (result.created) {
    trackRevenueUI("signup_completed");
  }
}
</script>

Do not call the tracker from a Nuxt server route. Return the success state to the browser instead.

Angular

Inject your application service as usual, then track only the confirmed result from the component:

import { Component, inject } from "@angular/core";
import { SignupService } from "./signup.service";
import { trackRevenueUI } from "./track-revenueui";

@Component({
  selector: "app-signup-form",
  template: `<form (ngSubmit)="submit()"><!-- fields and button --></form>`,
})
export class SignupFormComponent {
  private readonly signupService = inject(SignupService);

  submit(): void {
    this.signupService.createAccount().subscribe({
      next: (result) => {
        if (result.created) {
          trackRevenueUI("signup_completed");
        }
      },
    });
  }
}

Do not place the event in the button's (click) handler because that runs before the signup request is known to have succeeded.

SvelteKit

Track the result from a browser event handler after your endpoint or form action confirms success:

<script lang="ts">
  import { trackRevenueUI } from "$lib/track-revenueui";

  async function submitSignup(event: SubmitEvent) {
    event.preventDefault();

    const form = event.currentTarget as HTMLFormElement;
    const response = await fetch("/signup", {
      method: "POST",
      body: new FormData(form),
    });

    if (response.ok) {
      trackRevenueUI("signup_completed");
    }
  }
</script>

<form onsubmit={submitSignup}>
  <!-- fields and submit button -->
</form>

If the SvelteKit action redirects, use the one-time success-page pattern below.

Express and Node.js

An Express route should return a success result to browser JavaScript:

app.post("/api/signup", async (request, response) => {
  const account = await createAccount(request.body);
  response.status(201).json({ created: true, accountId: account.id });
});

The browser tracks the confirmed response:

const response = await fetch("/api/signup", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify(form),
});

if (response.status === 201) {
  trackRevenueUI("signup_completed");
}

Do not call the browser tracker from the Express process. A server-only event would not automatically carry the browser visitor or session identity used by RevenueUI attribution.

Laravel and PHP

Laravel should decide that the account was created, then redirect with a one-time session value:

return redirect()
    ->route('welcome')
    ->with('revenueui_event', 'signup_completed');

The Blade view makes the browser call. Use Js::from() so the value is safely encoded:

@if (session('revenueui_event'))
  <script>
    const eventName = {{ Js::from(session('revenueui_event')) }};
    const send = () => window.revenueUI?.track(eventName);

    if (window.revenueUI?.ready) {
      send();
    } else {
      window.addEventListener("revenueui:ready", send, { once: true });
    }
  </script>
@endif

Laravel flash data is consumed once, which prevents a normal refresh from sending the completion again. Apply the same pattern in plain PHP: set a short-lived success state on the server, render it safely into the response, and call the browser tracker from the resulting page.

Django and Python

After the server completes the action, store a one-time message or session flag and redirect:

request.session["revenueui_event"] = "signup_completed"
return redirect("welcome")

Pop the value while rendering the destination so a refresh does not repeat it:

event_name = request.session.pop("revenueui_event", None)
return render(request, "welcome.html", {"revenueui_event": event_name})

Then call the tracker in the Django template:

{% if revenueui_event %}
  {{ revenueui_event|json_script:"revenueui-event" }}
  <script>
    const eventName = JSON.parse(
      document.getElementById("revenueui-event").textContent
    );
    const send = () => window.revenueUI?.track(eventName);

    if (window.revenueUI?.ready) send();
    else window.addEventListener("revenueui:ready", send, { once: true });
  </script>
{% endif %}

Rails and Ruby

Set a one-time flash value only after the model transaction succeeds:

flash[:revenueui_event] = "signup_completed"
redirect_to welcome_path

Render the value safely and send it from the browser:

<% if flash[:revenueui_event] %>
  <script type="application/json" id="revenueui-event">
    <%= raw json_escape(flash[:revenueui_event].to_json) %>
  </script>
  <script>
    const eventName = JSON.parse(
      document.getElementById("revenueui-event").textContent
    );
    const send = () => window.revenueUI?.track(eventName);

    if (window.revenueUI?.ready) send();
    else window.addEventListener("revenueui:ready", send, { once: true });
  </script>
<% end %>

The flash value is one-time state, so a normal refresh does not create another completion.

Prevent duplicate events

Send an event from one authoritative success path. Do not track the same outcome in both the form handler and the success page.

For client-side routes that can be revisited, store a non-sensitive completion token in session storage:

export function trackRevenueUIOnce(eventName, completionId) {
  const storageKey = `revenueui:${eventName}:${completionId}`;

  if (sessionStorage.getItem(storageKey)) return;

  const send = () => {
    window.revenueUI?.track(eventName);
    sessionStorage.setItem(storageKey, "1");
  };

  if (window.revenueUI?.ready) send();
  else window.addEventListener("revenueui:ready", send, { once: true });
}

Use an opaque, short-lived completion identifier. Never use an email address or another personal identifier in the storage key or event name. Server-side idempotency is still required for the underlying signup or payment operation; browser deduplication protects analytics only.

Create the goal in RevenueUI

  1. Open the website detail page in RevenueUI.
  2. Open the goal visitors tab.
  3. Select Add goal.
  4. Enter a readable goal name, such as Completed signup.
  5. Enter the exact event name used by the application, such as signup_completed.
  6. Save the goal.

Event names are case-sensitive. signup_completed and Signup_Completed are not the same event. RevenueUI shows completions received after the goal is created; previously collected events remain available but do not count as goal completions.

Verify the implementation

Test one successful path and one failed path:

  1. Open the tracked site in a normal browser with Do Not Track and Global Privacy Control disabled.
  2. Complete the action successfully once.
  3. Confirm that the custom event reaches the website dashboard.
  4. Confirm that the visitor appears under the configured goal.
  5. Refresh the success page and verify that it does not send another completion.
  6. Submit invalid data or cancel the flow and verify that no completion event is sent.

If no event arrives, check that the tracker script is present, its public key belongs to the current website, the registered origin matches, consent permits the expected collection mode, and a content blocker is not preventing the request.