> ## 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.

# Funnels API

> Create, update, and run funnels over HTTP.

## Base URL and authentication

```
https://api.traceten.com/v1/funnels
```

Every endpoint takes `Authorization: Bearer <YOUR_API_KEY>` ([details](/api/authentication)) and a `site_id`. With an API key, `site_id` is the site's **snippet key**, the `ttid_…` value your install snippet carries as `data-site`.

For what a funnel is and how the numbers behave, read [Funnels](/goals/funnels) first. This page is the wire format.

## The step object

Every step is one of two shapes, discriminated by `kind`.

A **pageview** step:

```json theme={null}
{
  "kind": "pageview",
  "label": "Landed on pricing",
  "url_operator": "starts_with",
  "url_value": "/pricing",
  "negate": false
}
```

A **goal** step:

```json theme={null}
{
  "kind": "goal",
  "label": "Signed up",
  "goal_name": "signup",
  "negate": false
}
```

| Field          | Type    | Required | Notes                                                                                              |
| -------------- | ------- | -------- | -------------------------------------------------------------------------------------------------- |
| `kind`         | string  | yes      | `"pageview"` or `"goal"`.                                                                          |
| `label`        | string  | yes      | 1 to 120 characters. What the funnel chart shows.                                                  |
| `negate`       | boolean | no       | Defaults to `false`. Never allowed on the first step.                                              |
| `url_operator` | string  | pageview | One of `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `wildcard`. |
| `url_value`    | string  | pageview | 1 to 512 characters. Matched against the URL path only, with no query string and no fragment.      |
| `goal_name`    | string  | goal     | `^[a-z][a-z0-9_]*$`, 1 to 64 characters.                                                           |

`negate` lives on the step, not inside the matcher, so both kinds express it the same way. On a pageview step it composes with the operator: `not_contains` plus `negate: true` is a double negative and is applied as written.

## `GET /v1/funnels`

List every funnel on the site, oldest first, each with its steps in position order.

```bash theme={null}
curl "https://api.traceten.com/v1/funnels?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "funnels": [
    {
      "id": "9c1f0f2a-7b3e-4d51-8a12-4d5e6f708192",
      "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12",
      "name": "Pricing to signup",
      "slug": "pricing-to-signup",
      "is_active": true,
      "window_seconds": 604800,
      "created_at": "2026-08-01T09:12:44.120Z",
      "updated_at": "2026-08-01T09:12:44.120Z",
      "steps": [
        {
          "id": "3b6d2f81-…",
          "position": 1,
          "kind": "pageview",
          "label": "Landed on pricing",
          "url_operator": "starts_with",
          "url_value": "/pricing",
          "negate": false
        },
        {
          "id": "7e4a9c02-…",
          "position": 2,
          "kind": "goal",
          "label": "Signed up",
          "goal_name": "signup",
          "negate": false
        }
      ]
    }
  ]
}
```

## `POST /v1/funnels`

```bash theme={null}
curl -X POST https://api.traceten.com/v1/funnels \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12",
    "name": "Pricing to signup",
    "window_seconds": 604800,
    "steps": [
      { "kind": "pageview", "label": "Landed on pricing", "url_operator": "starts_with", "url_value": "/pricing" },
      { "kind": "goal", "label": "Read the pricing table", "goal_name": "scroll_to_pricing" },
      { "kind": "goal", "label": "Signed up", "goal_name": "signup" }
    ]
  }'
```

| Field            | Type    | Required | Notes                                                                            |
| ---------------- | ------- | -------- | -------------------------------------------------------------------------------- |
| `site_id`        | string  | yes      |                                                                                  |
| `name`           | string  | yes      | 1 to 120 characters. Unique per site.                                            |
| `slug`           | string  | no       | Lowercase words separated by single hyphens. Derived from the name when omitted. |
| `is_active`      | boolean | no       | Defaults to `true`.                                                              |
| `window_seconds` | integer | no       | 3600 to 7776000. Defaults to 604800 (7 days).                                    |
| `steps`          | array   | yes      | 2 to 8 steps.                                                                    |

`201` returns `{ "funnel": { … } }` in the list shape above.

| Status | Cause                                                                                   |
| ------ | --------------------------------------------------------------------------------------- |
| `400`  | Bad body: fewer than 2 or more than 8 steps, a negated first step, a malformed step.    |
| `409`  | A funnel with that name, or that slug, already exists. The response `field` says which. |

## `PATCH /v1/funnels/:id`

Accepts the same fields as create, all optional. **`steps` replaces the entire list** rather than merging into it, so send every step you want to keep.

```bash theme={null}
curl -X PATCH https://api.traceten.com/v1/funnels/9c1f0f2a-… \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12", "is_active": false}'
```

`200` returns `{ "funnel": { … } }`. An id belonging to another site is `404`.

## `DELETE /v1/funnels/:id`

```bash theme={null}
curl -X DELETE "https://api.traceten.com/v1/funnels/9c1f0f2a-…?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

`204` with no body. The steps go with it. Unlike a goal, a funnel is a definition you wrote, so deleting it deletes it.

## `GET /v1/funnels/:id/results`

Run the funnel and return the counts.

```bash theme={null}
curl "https://api.traceten.com/v1/funnels/9c1f0f2a-…/results?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12&from=2026-08-01&to=2026-08-31" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

| Parameter | Required | Notes                                                                                           |
| --------- | -------- | ----------------------------------------------------------------------------------------------- |
| `site_id` | yes      |                                                                                                 |
| `from`    | no       | `YYYY-MM-DD`. Defaults to six days before `to`.                                                 |
| `to`      | no       | `YYYY-MM-DD`. Defaults to today.                                                                |
| `filters` | no       | A JSON object, URL-encoded. Accepts `country_code`, `device_type`, `ai_source`, `utm_campaign`. |

The range may not span more than 730 days, the retention of the visitor step records a funnel reads, and `from` may not be after `to`. Either is a `422` naming the field.

`filters` must be JSON-encoded as a whole (`?filters=%7B%22country_code%22%3A%22US%22%7D`), not as bracketed keys. Malformed JSON is a `422` rather than a silently ignored filter. It is validated today; the date range is what currently narrows the run.

```json theme={null}
{
  "funnel_id": "9c1f0f2a-7b3e-4d51-8a12-4d5e6f708192",
  "funnel_name": "Pricing to signup",
  "slug": "pricing-to-signup",
  "from": "2026-08-01",
  "to": "2026-08-31",
  "window_seconds": 604800,
  "mode": "window_funnel",
  "pending_total": 0,
  "overall_conversion_rate": 0.061,
  "revenue_cents": 148200,
  "revenue_per_visitor_cents": 121,
  "steps": [
    {
      "position": 1,
      "label": "Landed on pricing",
      "kind": "pageview",
      "url_operator": "starts_with",
      "url_value": "/pricing",
      "negate": false,
      "visitors": 1220,
      "pending": 0,
      "conversion_from_start": 1,
      "conversion_from_previous": 1,
      "dropoff_from_previous": 0,
      "revenue_cents": 148200,
      "value_per_visitor_cents": 121,
      "top_sources": [{ "ai_source": "chatgpt", "visitors": 402 }],
      "top_countries": [{ "country_code": "US", "visitors": 511 }]
    }
  ]
}
```

| Field                      | Notes                                                                                                                                                                                                |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode`                     | `window_funnel`, or `array_fold` when any step is negated.                                                                                                                                           |
| `visitors`                 | Visitors who reached this step within the window.                                                                                                                                                    |
| `pending`                  | Visitors whose negation window is still open: neither converted nor dropped. Always 0 on a positive step and in `window_funnel` mode. A step's `visitors` only ever revises upward as these age out. |
| `pending_total`            | The sum of every step's `pending`.                                                                                                                                                                   |
| `conversion_from_start`    | Share of step-1 visitors who reached this step. 1 at position 1.                                                                                                                                     |
| `conversion_from_previous` | Share of the previous step's visitors. 1 at position 1.                                                                                                                                              |
| `dropoff_from_previous`    | Visitors lost since the previous step. 0 at position 1.                                                                                                                                              |
| `revenue_cents`            | Cumulative cohort revenue: every visitor who reached this step. It rises down the funnel.                                                                                                            |
| `value_per_visitor_cents`  | `revenue_cents / visitors`, to the cent. 0 when there are no visitors.                                                                                                                               |
| `top_sources`              | Up to 8.                                                                                                                                                                                             |
| `top_countries`            | Up to 8. `country_code` is `""` for the `other` bucket, which holds every country below the reporting floor.                                                                                         |

`top_countries` never names a country with too few visitors to report. The set of named countries is decided once for the whole run, so a country appears at every step or at none: step counts fall monotonically, and naming one at step 1 but not step 4 would reveal its step-4 count by subtraction.

## Errors

| Status | Meaning                                                                                  |
| ------ | ---------------------------------------------------------------------------------------- |
| `400`  | Malformed body on a write. `{ "error": "validation_failed", "message": …, "field": … }`. |
| `401`  | Missing, malformed, revoked, or out-of-scope key.                                        |
| `404`  | The funnel, or the site, is not reachable with this key.                                 |
| `409`  | A funnel with that name or slug already exists.                                          |
| `422`  | Malformed query string or path parameter.                                                |
| `429`  | Rate limited.                                                                            |
| `500`  | `{ "error": "Internal server error", "statusCode": 500 }`.                               |

Rate limits are per credential: 120 requests per minute for reads, including `results`, and 60 for writes. A second ceiling of 600 requests per minute applies per IP address.

## Next

* [Funnels](/goals/funnels): how to read the numbers.
* [Goals API](/api/goals)
