> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traceten.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Goals

> Count the actions that matter: clicks, scrolls, signups, and server-side events.

## What a goal lets you do

A goal counts one action, so you can see how many visitors took it, which AI sources brought those visitors, and how many of them went on to pay.

A goal is a custom event. `traceten.track("demo_booked")`, the HTML attribute `data-traceten-goal="demo_booked"`, and the server SDK call `goal("demo_booked")` all send the same event, and it shows up on the Goals page either way. There is nothing to create in advance: the first time an event arrives with a new name, Traceten registers it as a goal automatically.

## The four ways to fire a goal

| Method                            | Use it when                                                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `data-traceten-goal` attribute    | An element gets clicked. No JavaScript, no deploy.                                                                       |
| `data-traceten-scroll` attribute  | A section comes into view. Fires at most once per element, per page load. See [scroll tracking](/goals/scroll-tracking). |
| `traceten.track()` in the browser | The moment is in your own JavaScript (a form validated, a modal closed).                                                 |
| `goal()` in a server SDK          | The moment happens on your backend (a payment settled, a trial provisioned).                                             |

### Click: `data-traceten-goal`

Put the goal name on the element:

```html theme={null}
<button data-traceten-goal="signup">Start free trial</button>
```

Every click fires the `signup` goal. Details:

* **Clicks on children count.** The snippet walks up to 6 ancestor levels from whatever was clicked, so a click on the `<span>` inside the button still fires the goal.
* **Repeat clicks within 1 second are deduplicated**, per element, so a double-click is one goal.
* **`data-traceten` and `data-traceten-goal` are independent.** An element can carry both, and a `data-traceten-goal` on an inner element does not suppress a `data-traceten` label on an outer one, or the reverse.
* **The nearest `data-traceten-goal` wins.** One click fires at most one goal. If that nearest name is invalid or reserved nothing fires, because an outer element's valid goal is not used as a fallback. This matches how the nearest `data-traceten` label wins.
* **A goal and a click label are found in the same pass, in either order.** One click on a `data-traceten-goal` button inside a `data-traceten` wrapper records both: the goal, and the click event carrying the wrapper's label. Two events, two different endpoints, both billable. If you only want one, do not put both attributes on the same path.

### Properties: `data-traceten-goal-*`

Any attribute starting with `data-traceten-goal-` becomes a property. The suffix is converted from kebab-case to snake\_case:

```html theme={null}
<button data-traceten-goal="signup" data-traceten-goal-plan-type="pro" data-traceten-goal-seats="5">
  Start free trial
</button>
```

sends `signup` with `{ "plan_type": "pro", "seats": "5" }`.

Properties work the same way on a [scroll goal](/goals/scroll-tracking), on the same element as the `data-traceten-scroll` attribute.

<Warning>
  Property keys and values are both stored and both are shown back to you, and the attribute name
  becomes the key. Never put an email address, a person's name, or a postal address in one. See
  [what happens to property keys and values](#what-happens-to-property-keys-and-values-exactly).
</Warning>

### Server SDK: `goal()`

```ts theme={null}
traceten.goal("trial_provisioned", {
  visitorId: "123e4567-e89b-42d3-a456-426614174000",
  properties: { plan: "pro" },
});
```

`goal()` exists in the [Node](/sdks/node), [Python](/sdks/python), and [Go](/sdks/go) SDKs. It takes the same arguments as `track()` and posts to the same endpoint. The one difference is that it rejects the reserved names below.

`visitorId` is required. A server has no Traceten cookie to read, so without it the goal cannot be connected to the visit that produced it.

## Goal name rules

A goal name must match `^[a-z][a-z0-9_]*$`:

| Rule                 | Detail                                        |
| -------------------- | --------------------------------------------- |
| Length               | 1 to 64 characters                            |
| First character      | A lowercase letter, not a digit or underscore |
| Remaining characters | Lowercase letters, digits, underscores        |

Valid: `signup`, `demo_booked`, `scroll_to_pricing`, `trial_started_v2`.
Invalid: `Signup` (uppercase), `demo-booked` (hyphen), `2fa_enabled` (leading digit), `_internal` (leading underscore).

**An invalid name is dropped silently in the browser.** The snippet checks the name before sending, so a typo in an attribute costs you the goal but never sends a bad request, and never affects any other event batched alongside it. In the server SDKs an invalid name throws immediately instead, because a server-side typo is a bug you want to see in development.

Names are case-sensitive and are never normalised. `Signup` is not a mis-spelled `signup`; it is rejected.

### Reserved names

These eleven names belong to the Stripe and Shopify integrations, which emit them for real payment and subscription events:

`payment`, `free_trial`, `trial_started`, `trial_converted`, `subscription_started`, `subscription_upgraded`, `subscription_downgraded`, `subscription_renewed`, `subscription_cancel_scheduled`, `subscription_reactivated`, `subscription_ended`.

`data-traceten-goal`, `data-traceten-scroll`, and the SDK `goal()` all refuse them. If your own `subscription_started` mixed into the one Stripe sends, your revenue funnel would be counting two different things under one name, and nothing would look broken.

`traceten.track()` and the SDK `track()` still accept them, because that is how those events are legitimately sent.

## Property rules

Properties are a flat object of strings, numbers, and booleans.

| Limit                | Browser (snippet)                   | Server SDKs                   |
| -------------------- | ----------------------------------- | ----------------------------- |
| Maximum properties   | 10, extra keys dropped              | 20, extra keys dropped        |
| Key length           | Keys over 40 characters are dropped | No limit beyond the key count |
| String value length  | Truncated to 200 characters         | Truncated to 256 characters   |
| Numbers and booleans | Sent unchanged                      | Sent unchanged                |

The browser limits are the snippet's own; the server limits are what ingestion enforces on every path, browser included. In other words a browser goal is clamped twice, at 10 keys and 200 characters.

**Fewer than 10 properties can survive.** The snippet takes the first 10 attributes in document order and then drops any whose key is longer than 40 characters, so ten long-keyed attributes can crowd out short-keyed ones that would otherwise have been kept. If a property matters, put it early and keep its name short.

For the HTML triggers, note that the `data-traceten-goal-` prefix is not part of the key: only the part after it counts toward the 40 characters.

The reserved property keys `value_cents` and `currency` count against the browser's 10-key budget. They are lifted out of the properties and recorded as the goal's revenue. See the [browser API reference](/sdks/browser#recording-revenue-value_cents-and-currency).

**An HTML attribute cannot record revenue.** Attribute values are always strings, and `value_cents` is only read as revenue when it is a number. `data-traceten-goal-value-cents="4900"` is stored as an ordinary property called `value_cents` holding the string `"4900"`, and the goal carries no revenue. To attach revenue, call `traceten.track()` with a number, or send it from a [server SDK](/sdks/overview).

`currency` behaves differently from `value_cents` here: a 3-character string is promoted to the top-level currency field and removed from the properties, so `data-traceten-goal-currency="usd"` does take effect. The code is uppercased at ingestion and stored as `USD`, so the case you write does not matter. Anything that is not exactly 3 characters stays an ordinary property, because the ingestion endpoint rejects a currency of any other length and the whole goal would be lost.

### What happens to property keys and values, exactly

**Goal property keys and values are both stored, and both are read back.** `GET /v1/goals/{name}/properties` returns every key you have sent with a goal and that key's most common values, and each goal completion on a visitor's timeline lists the properties sent with it. Anything you put in a property, on either side, is something you and your team will read later.

Before storage, Traceten does two things to a property:

1. **Drops the property outright when its key is exactly** `email`, `phone`, `name`, `password`, `token`, `ssn`, `credit_card` or `card_number`.
2. **Redacts patterns inside string values**: email addresses, phone numbers, card-shaped digit runs, and national ID numbers are replaced with a redaction marker.

That is the whole of it, and the gaps matter:

* **Keys are never scanned, only matched against those eight names.** A property key is stored and displayed exactly as sent. This is worth pausing on for the HTML triggers, where the key comes from the attribute name: `data-traceten-goal-alice-chen="1"` stores a property key of `alice_chen`.
* The key list is an **exact match**, and only that. `full_name`, `customer_name` and `billing_address` are not on it and are kept.
* There is **no pattern for a personal name or a postal address**, because no reliable one exists. A value of `"Alice Chen"` or `"14 Rue de Rivoli"` is stored and returned as written.

Both steps run at the ingestion edge, on the browser path and the server SDK path alike. One door is different: `POST /v1/ingest/conversions`, the authenticated account API used by the data tooling rather than by the snippet or the SDKs, applies the value redaction but does not drop the eight key names.

So: put plans, tiers, page names, categories and A/B variants in properties. Do not put anything that identifies a person, in a key or in a value. Traceten cannot detect that for you.

## When a goal is not recorded

Nothing is sent when any of the following is true:

* The visitor denied consent, or your site uses `consentDefault: "pending"` and consent is still unresolved. See the [consent guide](/install/consent).
* The visitor has Do Not Track enabled, or the `_traceten_optout` cookie set.
* The goal name is invalid or reserved.
* For scroll goals only: the browser has no `IntersectionObserver`. See [scroll tracking](/goals/scroll-tracking#browser-support).
* **For click goals only: the page has been restored from the browser's back/forward cache.** The click listener is removed when the page is hidden and is not reinstalled on restore, which is pre-existing behaviour of `data-traceten` click tracking that goal clicks inherit. Scroll goals are unaffected and keep working on a restored page.
* **For both HTML triggers: a consent tool on the page has not answered yet.** The click listener and the scroll observer are installed only once consent resolves, so a click or a scroll in the window before your CMP decides records nothing. A `traceten.track()` call in that same window is kept, because an explicit API call is treated as intentional while the decision is in flight. But "kept" means buffered in memory, not sent immediately: no cookie is written and nothing reaches the server until consent resolves. See [Consent behavior](/sdks/browser#when-a-track-call-is-dropped).

That last one is the divergence worth designing around: the window is short, but it covers exactly the moment a visitor lands and clicks a hero button. If a goal has to survive it, call `traceten.track()` from your own click handler instead of using the attribute.

There is also a ceiling on **automatic goal registration: 200 distinct names per site**. It exists because the goal name is a free-text field on a public endpoint, so `signup_<random>` would otherwise mint registry rows forever. Past 200, a newly seen name stops being registered automatically, though the events themselves are still recorded and still counted. Archive goals you no longer need to make room.

The ceiling applies to that automatic discovery only. Creating a goal explicitly through [`POST /v1/goals`](/api/goals) is not capped, so a site can hold more than 200 goals if you declare them deliberately.

## What is stored, and for how long

A goal completion is stored as a conversion record for 365 days: the goal name, the properties you attached, and the pseudonymous visitor and session identifiers. Each one also writes a narrow visitor step row, retained for 730 days, holding the goal name and the page path but no property values. Both are removed by a [deletion request](/privacy/data-deletion).

The full retention table, and exactly what is scanned or dropped on the way in, is in [data collected](/privacy/data-collected#data-retention).

## Verify it worked

1. Open your site with the browser devtools Network tab filtered to `conversions`.
2. Click the element, or scroll to it.
3. You should see one `POST` to `https://ingest.traceten.com/v1/conversions` with your goal name in the body.

If the request is missing, work through [when a goal is not recorded](#when-a-goal-is-not-recorded) above, then [troubleshooting](/install/troubleshooting).

Goals appear on the dashboard within a minute or two of the first one arriving.

## In the dashboard

Open **Goals** in the sidebar. The chart starts with the six goals that have the most completions. Add or remove any goal with the **Goals** menu above it; there is no limit. The legend shows each goal's emoji beside its line colour. The date picker reaches back up to 365 days.

Under the chart, a table lists your goals with their completions and the last day each one was completed in the date range, most completions first, fifty to a page. Search it by name, and switch between three filters. **Goals**, the default, lists your own goals. **System** lists every [reserved goal](#reserved-names), such as `payment`. System goals appear only under this filter, and they are read-only. **Archived** lists the goals you archived.

Select a row to put that goal on the chart and list the visitors who completed it below the table, 50 people a page, in the same rows as the Visitors page. Each row's menu has **View visitors**, and for your own goals **Edit** (rename it or give it an emoji, which is optional and must be a single emoji) and **Archive**, or **Restore** under **Archived**. A goal's position in the table has no effect on how it is recorded.

The visitor list has one row per person, while a goal's count counts every completion, so the list is usually shorter. Click a visitor to open their timeline, where each goal completion lists its properties.

## Next

* [Scroll tracking](/goals/scroll-tracking): fire a goal when a section comes into view.
* [Funnels](/goals/funnels): put goals in sequence and see where visitors drop off.
* [Goals API](/api/goals): rename, reorder, and archive goals over HTTP.
