# Serge partner tracking integration

Canonical API reference: https://www.serge.ai/docs/api
Current schemas: https://www.serge.ai/api/v1/openapi.json
Display contract: https://www.serge.ai/docs/displaying-analytics/llm.txt

## 1. Create an API key

In Serge, open Settings → API keys. For AgentSolo provisioning, choose the preset that grants exactly sites:write + traffic:read. The key belongs in your server-side secret store, never tenant HTML or a browser bundle.

## 2. Register each tenant at provisioning time

Call this from AgentSolo's backend after the tenant domain is known. The workspace always comes from the key; the request cannot select another one.

Safe to retry. The normalized domain is idempotent: a repeat returns HTTP 200, created: false, and the same site_id, public_token, and canonical storage-free snippet.

Monitor the total returned by GET /api/v1/sites and alert at 900. v1 has no self-service removal yet; contact hello@serge.ai before 1,000 sites or when tenant churn needs cleanup. Existing sites keep working if the ceiling is reached.

## 3. Deploy the returned snippet, then check it

Insert the exact snippet returned by registration into the tenant's shared head and deploy. Then call check-install and surface both status and next_action in your operator UI.

Poll after 2 seconds, 4 seconds, 8 seconds, then every 15 seconds for at most two minutes. Stop on receiving or rejected; never loop forever. If it is still awaiting or stale, load the deployed page once and inspect the script request in browser DevTools.

## 4. Reconcile the fleet

Page through GET /api/v1/sites until next_cursor is null. Reconcile existing tenants too: persist each returned public_token and snippet, update the shared page template, and redeploy when it changes. An existing site_id is not proof that the tag was installed. Do not gate token recovery on last_event_at: it is the latest rolled-up session start, not installation health.

## 5. Read measured traffic for the same domain

Use traffic/sources for acquisition sources, article entry pages and daily counts. Read the top-level total_sessions, ai_sessions and coverage fields; there is no data wrapper. Send an explicit domain and period. A receiving heartbeat proves an accepted request, not an AI session. no_events_yet means no recorded session starts in that window; show the installation state alongside it. The snippet measures browsers that execute it, not every HTML-only crawler. Ordinary human visits are generally excluded unless full-traffic recording is enabled.

## 6. Generate a typed client

Generate a REST client from the current OpenAPI document and regenerate it when you update the integration. The JavaScript tracking SDK is a separate browser component. The example below uses traffic:read, checks HTTP status, and bypasses application fetch caching. Keep the request ID for support; never turn an API error into a zero count.

## 7. Optional: record conversions

This runs on your server, never in a browser, and requires conversions:write, which the provisioning preset does not grant. Attribution reporting separately requires conversions:read and measures submitted conversions, not traffic sessions. Skip this step for a traffic-only integration. Capture supported ad parameters on the landing request, then post the completed order.

Safe to retry. Conversions deduplicate on your own order id, so resending never creates a second one or inflates your totals.

## Traffic read examples

```sh
curl 'https://www.serge.ai/api/v1/traffic/sources?domain=tenant.example&period=30d' \
  -H 'Authorization: Bearer sk_serge_…'
```

Generate ./serge.d.ts: npx openapi-typescript https://www.serge.ai/api/v1/openapi.json -o serge.d.ts

```ts
import type { paths } from './serge'

type TrafficSources =
  paths['/api/v1/traffic/sources']['get']['responses'][200]['content']['application/json']

const res = await fetch('https://www.serge.ai/api/v1/traffic/sources?domain=tenant.example&period=30d', {
  headers: { Authorization: `Bearer ${process.env.SERGE_API_KEY}` },
  cache: 'no-store',
})
if (!res.ok) {
  throw new Error(`Serge HTTP ${res.status}; request ${res.headers.get('x-request-id') ?? 'unknown'}`)
}
const sources: TrafficSources = await res.json()
```

## Recovery and acceptance checks

- Read sites[].public_token and sites[].snippet explicitly. site_id is an internal identifier, not the public installation token. Never construct a script URL from either identifier.
- Keep each token associated with its returned domain. Registering apex.example also accepts www.apex.example and other subdomains; paths such as /blog are not separate registered domains.
- Install on the templates whose traffic you want to measure. A blog-only installation cannot measure visits to pages outside the blog. Confirm the tag in deployed HTML and the rendered DOM, not only in the CMS or a successful build.
- GET /sites is fleet inventory, not a traffic feed. Its last_event_at is a rolled-up session start; /sites/check-install.last_verified_event_at is the accepted-request heartbeat. Neither is a raw event count.
- The browser init heartbeat is deliberately not stored as an analytics event. A normal human test visit can yield receiving with no AI sessions. Do not manufacture bot signals or conversions to test an installation.
- /traffic/sources uses the first recorded session_start per session in the requested window. Repeated starts do not assign the same session to multiple sources, entry pages or days. /traffic/overview reads rolled-up sessions, including sessions recovered from later events. Rollup runs every five minutes after five minutes of inactivity; the counts can differ. Use sources consistently for acquisition charts rather than interchanging totals.
- REST returns the documented JSON object directly. MCP tools return structuredContent; MCP list_sites is a compact orientation list, not the paginated REST provisioning contract. Use REST GET /sites for token recovery.
- On 401 check the current server-side key; on 403 check scopes; on 404 check workspace/domain; on 429 honor Retry-After. Preserve the last successful data as stale on errors, never overwrite it with zero.
- Key analytics caches by workspace, normalized domain, endpoint and exact time window. Keep retrieval time, period_start and period_end with the response. Invalidate local setup caches after token or snippet changes.
- For support retain only X-Request-Id, endpoint, domain, window, HTTP status, coverage and numeric result counts. Never log Authorization or secret API keys.
