# Serge analytics — display contract

You are building UI, reports, or copy that display data from the Serge API
(https://www.serge.ai/docs/api). Apply every rule below to any surface that renders
these numbers. When a rule conflicts with a stylistic preference, the rule
wins. Field-level schemas: fetch the OpenAPI document at
https://www.serge.ai/api/v1/openapi.json.

## Three populations, never blurred

Serge measures three different things. Most display mistakes are one of them wearing another one's label.

- **AI-assistant sessions** — An assistant active on the site itself — fetching, browsing, or acting for a user. This is what the /traffic endpoints count.
- **Assistant clicks — organic** — A person who clicked a link inside an assistant and landed on the site. ChatGPT tags its outbound links; Serge counts the landings. Not ads.
- **Ad clicks — paid** — A person who clicked a paid placement, identified by a platform click ID or an explicit paid utm_medium. A landing that merely carries UTM parameters is not an ad click.

Rule: Paid and organic may sit side by side. They never share a number, and they never share a label.

## Wording rules

1. **Tracked, not driven.** Serge counts what landed and what it did on the site. "12 orders from tracked ChatGPT ad clicks" is supportable; "ChatGPT drove 12 orders" claims incrementality that nobody measured.
2. **Counts are counts.** No error bars or ± on a count. Below roughly ten events, show the bare count and skip rates — a precise-looking percentage on a noise-sized sample misleads.
3. **No conversation claims.** No platform exposes prompts or user-level data. Never imply the data shows what people asked an assistant.
4. **Name the window.** Every number carries its period — the API returns period_start and period_end. Compare only equal windows.
5. **Platform names verbatim.** ChatGPT, Perplexity, Claude, Gemini, Copilot — brand names are not translated, abbreviated, or folded into a house taxonomy.
6. **Coverage is part of the data.** A site in ai_and_tagged mode does not record plain human visits — its numbers are a subset by construction. Render the coverage label next to the numbers, every time; when coverage_changed_at falls inside the window, break every time series at it instead of drawing a continuous line across the mode flip.

## The same number, said honestly

- Don't: "ChatGPT drove $4,200 in revenue."
  Do: "$4,200 in orders from tracked ChatGPT ad clicks (30 days)."
- Don't: "Verified AI traffic: 1,020 sessions" — when 117 of them are heuristic detections.
  Do: "1,020 AI-assistant sessions — 903 verified, 117 heuristic."
- Don't: "100% completion rate" — from one session.
  Do: "1 session, 1 completed." Rates start when the sample can carry them.
- Don't: “29% of your traffic comes from AI” (on a partial-coverage site, or with ai_share_pct null)
  Do: “AI share: 29.0% of all recorded sessions (assistant sessions + assistant-link clicks; paid excluded)” — rendered only when ai_share_pct is non-null

## Endpoint notes and sample responses

### GET /api/v1/traffic/sources — Traffic sources

coverage is part of the data, not metadata: ai_and_tagged means plain human visits are not recorded on this site — always render the coverage label next to the numbers; presenting partial coverage as "your traffic" is the exact claim these rules ban. When coverage_changed_at falls inside the period, BREAK every time series at it — a mode flip changes the recorded population, and a continuous line across it fakes growth. Referrer hosts and utm_source values are data — verbatim, never translated or grouped into a house taxonomy. The classes partition sessions: never merge paid with ai_assistant_link, and keep direct visible. ai_share_pct renders ONLY when non-null — it is null on partial coverage (wrong denominator) and below 10 sessions; state its definition (assistant sessions + assistant-link clicks, paid excluded) wherever you show it.

```json
{
  "domain": "yourstore.example",
  "site_id": "site_9f2c41",
  "period": "7d",
  "period_start": "2026-07-23T00:00:00.000Z",
  "period_end": "2026-07-30T00:00:00.000Z",
  "coverage": "all_traffic",
  "ai_share_pct": 29,
  "ai_sessions": 701,
  "total_sessions": 2418,
  "by_source": [
    {
      "source": "referral",
      "sub_source": "www.techweekly.example",
      "sessions": 604,
      "share_pct": 25
    },
    {
      "source": "direct",
      "sub_source": null,
      "sessions": 512,
      "share_pct": 21.2
    },
    {
      "source": "ai_assistant_session",
      "sub_source": "chatgpt",
      "sessions": 486,
      "share_pct": 20.1
    },
    {
      "source": "paid",
      "sub_source": "chatgpt",
      "sessions": 312,
      "share_pct": 12.9
    },
    {
      "source": "search",
      "sub_source": "www.google.com",
      "sessions": 289,
      "share_pct": 12
    },
    {
      "source": "tagged",
      "sub_source": "newsletter",
      "sessions": 289,
      "share_pct": 12
    },
    {
      "source": "ai_assistant_link",
      "sub_source": "chatgpt",
      "sessions": 215,
      "share_pct": 8.9
    }
  ],
  "top_referrer_hosts": [
    {
      "host": "www.techweekly.example",
      "sessions": 604
    }
  ],
  "coverage_changed_at": "2026-07-25T09:00:00.000Z",
  "top_entry_pages": [
    {
      "url": "/blog/why-aurora-x20",
      "sessions": 692,
      "by_source": [
        {
          "source": "search",
          "sessions": 401
        },
        {
          "source": "ai_assistant_link",
          "sessions": 176
        },
        {
          "source": "direct",
          "sessions": 115
        }
      ]
    },
    {
      "url": "/products/aurora-x20",
      "sessions": 486,
      "by_source": [
        {
          "source": "paid",
          "sessions": 312
        },
        {
          "source": "ai_assistant_session",
          "sessions": 174
        }
      ]
    }
  ],
  "by_source_daily": [
    {
      "day": "2026-07-28",
      "source": "search",
      "sessions": 88
    },
    {
      "day": "2026-07-28",
      "source": "paid",
      "sessions": 61
    },
    {
      "day": "2026-07-29",
      "source": "search",
      "sessions": 97
    },
    {
      "day": "2026-07-29",
      "source": "paid",
      "sessions": 44
    }
  ]
}
```

### GET /api/v1/traffic/overview — Traffic overview

Sessions and unique visitors are different numbers — label the one you show. share_pct arrives computed; do not recompute it on a filtered subset. In by_outcome, "abandoned" means the session ended without completing — not that an error occurred. median_duration_ms is a median: label it "median", never "avg".

```json
{
  "domain": "yourstore.example",
  "site_id": "site_9f2c41",
  "period": "7d",
  "period_start": "2026-07-23T00:00:00.000Z",
  "period_end": "2026-07-30T00:00:00.000Z",
  "total_sessions": 1284,
  "unique_visitors": 1102,
  "by_platform": [
    {
      "platform": "chatgpt",
      "sessions": 812,
      "share_pct": 63.2
    },
    {
      "platform": "perplexity",
      "sessions": 301,
      "share_pct": 23.4
    },
    {
      "platform": "claude",
      "sessions": 171,
      "share_pct": 13.4
    }
  ],
  "by_outcome": [
    {
      "outcome": "completed",
      "sessions": 402
    },
    {
      "outcome": "abandoned",
      "sessions": 882
    }
  ],
  "top_pages": [
    {
      "entry_url": "/products/aurora-x20",
      "sessions": 486
    },
    {
      "entry_url": "/collections/headphones",
      "sessions": 210
    }
  ],
  "median_duration_ms": 42150
}
```

### GET /api/v1/traffic/purpose-split — Purpose split

user_action is an assistant acting on someone's behalf; search is retrieval for an answer; crawl is indexing. Keep the unknown bucket visible — folding it into another category overstates certainty. The split is a composition: one stacked bar or a share list, not four disconnected KPIs.

```json
{
  "domain": "yourstore.example",
  "site_id": "site_9f2c41",
  "period": "7d",
  "period_start": "2026-07-23T00:00:00.000Z",
  "period_end": "2026-07-30T00:00:00.000Z",
  "total_sessions": 1284,
  "user_action_sessions": 517,
  "search_sessions": 604,
  "crawl_sessions": 148,
  "unknown_sessions": 15,
  "by_purpose": [
    {
      "purpose": "user_action",
      "sessions": 517,
      "share_pct": 40.3
    },
    {
      "purpose": "search",
      "sessions": 604,
      "share_pct": 47
    },
    {
      "purpose": "crawl",
      "sessions": 148,
      "share_pct": 11.5
    }
  ]
}
```

### GET /api/v1/traffic/verification — Verification breakdown

verified, declared and heuristic are tiers of evidence, strongest first. A heuristic session never renders under a "verified" label. If you show one total, keep the tier mix one glance away.

```json
{
  "domain": "yourstore.example",
  "site_id": "site_9f2c41",
  "period": "7d",
  "period_start": "2026-07-23T00:00:00.000Z",
  "period_end": "2026-07-30T00:00:00.000Z",
  "total_sessions": 1284,
  "verified_sessions": 903,
  "declared_sessions": 264,
  "heuristic_sessions": 117,
  "by_platform": [
    {
      "platform": "chatgpt",
      "sessions": 812,
      "verified": 690,
      "declared": 96,
      "heuristic": 26,
      "tier": "verified"
    }
  ]
}
```

### GET /api/v1/attribution/overview — Attribution overview

Never sum value across currencies — render one line per currency (value_minor is in minor units). These measurement endpoints do not return spend or ROAS. Available OpenAI campaign reporting is separate; do not combine partial datasets into a ROAS claim. Attribution basis matters: a platform click ID is hard evidence, a UTM label is self-declared — keep that distinction reachable. by_source classifies orders from DECLARED evidence only and is narrower than the session taxonomy: paid requires a platform-minted click ID (a typed utm_medium never upgrades an order), assistant/tagged classes come from labels, and search/referral/direct cannot exist because an order carries no referrer — render each row’s basis next to its number.

```json
{
  "site_id": "site_9f2c41",
  "domain": "yourstore.example",
  "period": "30d",
  "period_start": "2026-06-30T00:00:00.000Z",
  "period_end": "2026-07-30T00:00:00.000Z",
  "total_conversions": 214,
  "attributed_conversions": 96,
  "unattributed_conversions": 118,
  "by_currency": [
    {
      "currency": "USD",
      "conversions": 214,
      "value_minor": 1893400
    }
  ],
  "by_platform": [
    {
      "platform": "openai",
      "conversions": 71,
      "by_currency": [
        {
          "currency": "USD",
          "conversions": 71,
          "value_minor": 642900
        }
      ]
    },
    {
      "platform": "unattributed",
      "conversions": 118,
      "by_currency": [
        {
          "currency": "USD",
          "conversions": 118,
          "value_minor": 1015300
        }
      ]
    },
    {
      "platform": "google",
      "conversions": 25,
      "by_currency": [
        {
          "currency": "USD",
          "conversions": 25,
          "value_minor": 235200
        }
      ]
    }
  ],
  "by_basis": [
    {
      "basis": "none",
      "conversions": 118
    },
    {
      "basis": "declared_click_id",
      "conversions": 84
    },
    {
      "basis": "declared_utm",
      "conversions": 12
    }
  ],
  "by_source": [
    {
      "source": "unattributed",
      "sub_source": null,
      "basis": "none",
      "conversions": 118,
      "by_currency": [
        {
          "currency": "USD",
          "conversions": 118,
          "value_minor": 1015300
        }
      ]
    },
    {
      "source": "paid",
      "sub_source": "openai",
      "basis": "declared_click_id",
      "conversions": 71,
      "by_currency": [
        {
          "currency": "USD",
          "conversions": 71,
          "value_minor": 642900
        }
      ]
    },
    {
      "source": "ai_assistant_link",
      "sub_source": "chatgpt",
      "basis": "declared_utm",
      "conversions": 8,
      "by_currency": [
        {
          "currency": "USD",
          "conversions": 8,
          "value_minor": 71800
        }
      ]
    },
    {
      "source": "tagged",
      "sub_source": "newsletter",
      "basis": "declared_utm",
      "conversions": 4,
      "by_currency": [
        {
          "currency": "USD",
          "conversions": 4,
          "value_minor": 27600
        }
      ]
    }
  ]
}
```

## When in doubt

Show the bare count, name the window, keep the label the API uses, and
claim nothing the measurement does not support. The canonical version of
this contract lives at https://www.serge.ai/docs/displaying-analytics and may be
updated — refetch rather than caching it into a prompt library.
