# Goals and funnels

Clicktag goals count the visitors who view a page or fire a custom event, and funnels chain 2 to 6 steps completed in order on the same UTC day, with conversion by referrer, entry page or country. A site can have up to 20 goals and funnels combined.

Base URL: `https://clicktag.io`. [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 the path with or without a trailing slash, `starts with` matches the path and every path below it (`/blog` matches `/blog/post` but not `/blogging`, and `/` matches every page), `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 and funnels combined.

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                    |

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 collector derives one from each request; 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 at least 99% of that period's pageviews carry an identity too. A range without any identified pageviews shows **No visitor data in this range**.

Events sent from your server with `POST /events` get an identity built from your server's IP address, not the visitor's, so they can't follow the visitor's pageviews in a funnel or inherit their referrer and entry page.

## Access

Goal routes need the site owner's Firebase ID token or a [management key](/docs/authentication#management-keys), with `read` for `GET` and `manage` for changes, even for public sites. Every route, reads included, needs a verified site. Sites you don't own return `404` `unknown site`. 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; stats call an unnamed step `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://clicktag.io/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>"}`. Goals are computed at query time, so a new or changed goal applies to past data too.

## Goal stats

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

`from` and `to` are unix seconds with the same defaults and limits as the [statistics API](/docs/stats#date-ranges), `tz` sets the chart buckets and the previous period, and `f` takes the same [filters](/docs/stats#filters).

```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.
- The previous period is the range moved back by as many calendar days as it covers in `tz`, like `previous_range` in the [overview](/docs/stats#overview-response). `previous_visitors` and every `previous` are `null` when less than 99% of its pageviews carry an identity, or none do.

## 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. `from`, `to` and `f` work as in goal stats. Rows are sorted by visitors, most first.

```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 }]
}
```

`visitors` counts the visitors whose first pageview that day had this value, and `steps` how many of them reached each step. `timing` has one entry per step transition, in seconds, and is `null` for goals; `p50_s` and `p90_s` are `null` when `n` is 0.

## Errors

| Status | Error                                                     |
| ------ | --------------------------------------------------------- |
| 400    | `invalid json`, `invalid range`, `invalid filter`, `unknown dimension`, `this goal has too many filters to measure, remove some` or a validation message such as `step 2 filter 1: unknown key` |
| 401    | `sign in required`, or `invalid or revoked management key` |
| 403    | `Install your tracking script to verify this site first`; a management key without the needed scope gets `scope read required` or `scope manage required` |
| 404    | `unknown site`, `unknown goal`                            |
| 429    | `too many goals for this site`; a management key sending changes too fast gets `rate limited, retry in <n>s` with `Retry-After` |
| 504    | `date range too large for goals, narrow it`               |
