# Goals and funnels

Count the visitors who do what matters: reach pricing, start a signup, copy the install script. A goal is one step. A funnel is 2 to 6 steps completed in order on the same day (UTC).

Base URL: `https://totallytics.com`. [Read this page as Markdown](/docs/goals.md).

## Create a goal

Open a site, choose the **Goals** tab and click **New goal**. A site without goals suggests its top events and pages; pick one to start from it.

Every step matches either:

- **Page**: pageviews by path. `is` matches exactly, `starts with` matches a prefix, `contains` matches anywhere in the path.
- **Event**: a [custom event](/docs/events) by its exact name.

Add filters to narrow a step by page, source, country, device, browser, OS or UTM source, medium and campaign. Filters on different fields must all match. Two filters on the same field match either value. A step takes up to 6 filters and a site up to 20 goals.

Page values start with `/`, contain no `?` or `#`, and drop trailing slashes, so `/pricing/` is saved as `/pricing`. Event names use letters, digits and `_`. In a funnel, a page step with no filters matches any pageview.

## What the numbers mean

| Number            | Meaning                                                                                  |
| ----------------- | ---------------------------------------------------------------------------------------- |
| Visitors          | Unique visitors per day: someone active on 3 days counts 3 times                         |
| Converted visitors | Visitors with at least one matching pageview or event that day                           |
| Conversions       | Every matching pageview or event                                                         |
| Conversion rate   | Converted visitors divided by visitors, never above 100%                                 |
| Funnel step       | Visitors who completed this step after all earlier steps, in order, on the same UTC day |
| Time to convert   | Median and 90th percentile time between two consecutive funnel steps                    |

The Overview counts visits (entries into the site), so its numbers won't match the visitors on this tab. Dashboard filters apply to goals too: they narrow the pageviews and events that goals are counted over. Funnel bars show each step as a share of step 1, with the drop-off from the step before.

Click a goal to break it down by referrer, entry page, country, device, browser, OS or UTM parameter. Each visitor is counted under the value of their first pageview that day.

## Coverage

Goals need a visitor identity on every pageview. The tracking script sends one; older imported data has none. When less than 99% of the pageviews in a range carry an identity, the Goals tab says so:

> Goals cover 72% of pageviews in this range (older imported data has no visitor identity).

Changes against the previous period only show when that period is fully covered as well. A range without any identified pageviews shows **No visitor data in this range**.

## Access

All goal routes require the site owner's Firebase ID token, even for public sites. See [Authentication](/docs/authentication).

| Method | Route                                     | Result                                  |
| ------ | ----------------------------------------- | --------------------------------------- |
| GET    | `/api/sites/{hostname}/goals`             | List goals, oldest first                |
| POST   | `/api/sites/{hostname}/goals`             | Create a goal (`201`)                   |
| GET    | `/api/sites/{hostname}/goals/stats`       | Numbers for every goal in a date range  |
| GET    | `/api/sites/{hostname}/goals/{id}`        | Read one goal                           |
| PUT    | `/api/sites/{hostname}/goals/{id}`        | Replace a goal                          |
| DELETE | `/api/sites/{hostname}/goals/{id}`        | Delete a goal                           |
| GET    | `/api/sites/{hostname}/goals/{id}/stats`  | Breakdown and funnel timing for one goal |

## Goal object

```json
{
  "id": "0b7c3e52-6a0e-4f0d-9d51-3f1f5c2a9e10",
  "name": "Pricing to signup",
  "kind": "funnel",
  "steps": [
    {
      "name": "Pricing",
      "match": "pageview",
      "filters": [{ "key": "page", "op": "eq", "value": "/pricing" }]
    },
    {
      "name": "Signup completed",
      "match": "event",
      "filters": [{ "key": "event", "op": "eq", "value": "signup_completed" }]
    }
  ],
  "created_at": "2026-09-30T09:12:44.000Z",
  "updated_at": "2026-09-30T09:12:44.000Z"
}
```

| Field               | Rules                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------- |
| `name`              | 1 to 80 characters                                                                                       |
| `kind`              | `goal` (exactly 1 step) or `funnel` (2 to 6 steps)                                                       |
| `steps[].name`      | Optional, up to 40 characters, defaults to `Step n`                                                      |
| `steps[].match`     | `pageview` or `event`                                                                                    |
| `filters[].key`     | `page`, `referrer`, `country`, `device`, `browser`, `os`, `utm_source`, `utm_medium`, `utm_campaign`, `event` |
| `filters[].op`      | `eq`; `prefix` and `contains` only with `page`                                                           |
| `filters[].value`   | Page paths start with `/`; event names match `^[A-Za-z0-9_]{1,256}$`                                     |

An `event` step has exactly one `event` filter with `op: "eq"`; a `pageview` step cannot use the `event` key. A goal step needs at least one filter. Two identical steps in a row, or the same filter twice in a step, are rejected.

A goal that no longer passes validation is listed with `"invalid": true` and left out of stats until it is saved again.

## Create and change goals

```bash
curl -X POST https://totallytics.com/api/sites/example.com/goals \
  -H "Authorization: Bearer $TT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Signup completed","kind":"goal","steps":[{"match":"event","filters":[{"key":"event","op":"eq","value":"signup_completed"}]}]}'
```

`POST` returns the new goal with `201`. `PUT` takes the same body, replaces the whole goal and returns it. `DELETE` returns `{"deleted": "<id>"}`.

## Goal stats

`GET /api/sites/{hostname}/goals/stats?from=1790208000&to=1790812800&tz=Europe/Amsterdam`

`from` and `to` are unix seconds, `tz` sets the chart buckets, and `f` takes the same filters as the [statistics API](/docs/stats).

```json
{
  "granularity": "day",
  "visitors": 2016,
  "previous_visitors": 1874,
  "coverage": {
    "pageviews": 5230,
    "identified": 5230,
    "previous": { "pageviews": 4870, "identified": 4870 }
  },
  "goals": [
    {
      "id": "7d2a8c1e-3b4f-4e6a-8c9d-0e1f2a3b4c5d",
      "name": "Signup completed",
      "kind": "goal",
      "conversions": 143,
      "converting_visitors": 118,
      "rate": 0.0585,
      "previous": { "conversions": 126, "converting_visitors": 104, "rate": 0.0555 },
      "series": [
        { "t": 1790208000, "conversions": 19 },
        { "t": 1790294400, "conversions": 23 }
      ]
    }
  ],
  "funnels": [
    {
      "id": "0b7c3e52-6a0e-4f0d-9d51-3f1f5c2a9e10",
      "name": "Pricing to signup",
      "kind": "funnel",
      "steps": [
        { "name": "Pricing", "visitors": 412 },
        { "name": "Signup completed", "visitors": 61 }
      ],
      "previous": [388, 52]
    }
  ],
  "invalid": []
}
```

- `rate` is a fraction from 0 to 1, or `null` when the range has no visitors.
- `series` has one entry per hour for ranges up to 4 days, otherwise one per day, zero-filled. Funnels have no series.
- `previous_visitors` and every `previous` are `null` when the previous period is less than 99% covered.

## Goal breakdown

`GET /api/sites/{hostname}/goals/{id}/stats?from=1790208000&to=1790812800&dim=referrers&limit=10`

`dim` is one of `referrers`, `pages` (entry page), `countries`, `devices`, `browsers`, `os`, `utm_sources`, `utm_mediums` or `utm_campaigns`. `limit` is 1 to 100, default 10.

```json
{
  "dim": "referrers",
  "breakdown": [
    { "name": "google.com", "visitors": 880, "steps": [170, 46] },
    { "name": "Direct / none", "visitors": 640, "steps": [131, 22] }
  ],
  "timing": [{ "p50_s": 204, "p90_s": 3310, "n": 61 }]
}
```

`steps` has one count per step. `timing` has one entry per step transition, in seconds, and is `null` for goals.

## Errors

| Status | Error                                                     |
| ------ | --------------------------------------------------------- |
| 400    | `invalid json`, `invalid range`, `invalid filter`, `unknown dimension` or the validation message |
| 401    | `sign in required`                                        |
| 403    | `Install your tracking script to verify this site first`  |
| 404    | `unknown site`, `unknown goal`                            |
| 429    | `too many goals for this site`                            |
| 504    | `date range too large for goals, narrow it`               |
