# API

Read your numbers from scripts, BI tools and your own agents. The API answers exactly what the dashboard shows, because both use the same query layer. With the right key, you can also send events and manage alerts, goals, triggers and URL parameters.

**Guides:** [Keys and authentication](https://www.punchlineanalytics.com/docs/api-auth) · [Query cookbook](https://www.punchlineanalytics.com/docs/api-queries) · [Limits and conventions](https://www.punchlineanalytics.com/docs/api-conventions) · [TypeScript SDK](https://www.punchlineanalytics.com/docs/sdk) · [API reference](https://www.punchlineanalytics.com/docs/api-reference), with a real example response for every endpoint.

## Keys

Create a key in **Site settings → API and AI access**. It's shown once, so copy it then; revoke keys on the same page. [More on keys](https://www.punchlineanalytics.com/docs/api-auth).

- Send it as a Bearer token: `Authorization: Bearer pk_live_…`.
- One key works on one site, and only that site.
- Every key can read. Tick **Can send events** for `POST /v1/events`, and **Can manage** for the endpoints under [Managing your site](#managing-your-site).
- Rate limit: 120 requests a minute per key. Responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; a 429 includes `Retry-After`. See [Limits and conventions](https://www.punchlineanalytics.com/docs/api-conventions).

## Your first request

```bash
curl -s https://app.punchlineanalytics.com/v1/query \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "<your site id>",
    "date_range": { "from": "2026-09-01", "to": "2026-09-30" },
    "metrics": ["sessions", "agent_share"],
    "dimensions": ["day"]
  }'
```

The response echoes the query as it ran (with defaults filled in), then `results` and `meta`:

```json
{
  "query": { "site_id": "…", "metrics": ["sessions", "agent_share"], "dimensions": ["day"], "confidence": "likely", "…": "…" },
  "results": [{ "day": "2026-09-01", "sessions": 412, "agent_share": 0.31 }],
  "meta": { "audience": "external", "min_cohort": 5, "suppressed_rows": 0, "truncated": false, "rollups_refreshed_at": "…" }
}
```

## The query

`POST /v1/query` does almost everything: pick **metrics**, group by **dimensions**, narrow with **filters**.

| Metrics | |
|---|---|
| `sessions`, `pageviews` | Everyone |
| `human_sessions`, `agent_sessions` | Split by visitor type (agents include crawlers) |
| `agent_share` | `agent_sessions / sessions` |
| `thin_pageviews` | Successful responses under 2 KB (possibly JavaScript-only) |
| `events`, `event_sessions`, `event_value` | Custom events (with the `event_name` dimension) |
| `conversions`, `conversion_rate` | Goal conversions (with the `goal` dimension; `path` is the landing page) |
| `visitors` | Distinct visitors per day, summed over the range |
| `bounces`, `bounce_rate`, `avg_session_duration`, `pages_per_session`, `engaged_sessions`, `engagement_rate` | Session engagement (`path` is the landing page; also `exit_path`, `country`, `device_class`, `browser_family`, `os_family`) |
| `exits`, `exit_rate`, `avg_time_on_page` | Per-page engagement |

Dimensions (up to 4): `day`, `week`, `month`, `path`, `visitor_type`, `actor_class`, `operator`, `tier`, `referrer_host`, `assistant`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `agent`, `agent_purpose`, `delivery`, `content_type`, `topic`, `channel`, `partner`, `exit_path`, `country`, `device_class`, `browser_family`, `os_family`, plus `param:<name>` for each [URL parameter](https://www.punchlineanalytics.com/docs/url-params) the site allows (page and conversion metrics). See [Concepts](https://www.punchlineanalytics.com/docs/concepts) and [Reports](https://www.punchlineanalytics.com/docs/reports) for what each value means.

Filters (up to 10) are `[field, op, value]`, with ops `eq`, `neq`, `in`, `not_in`, `starts_with`, `is_null`, `not_null`. `["is_entry", "eq", true]` keeps only the first page of each session (landing pages):

```json query
{
  "date_range": { "from": "2026-09-01", "to": "2026-09-30" },
  "metrics": ["sessions"],
  "dimensions": ["assistant", "path"],
  "filters": [["actor_class", "eq", "ai_referred_human"], ["is_entry", "eq", true]],
  "order_by": [["sessions", "desc"]],
  "limit": 20
}
```

`confidence` (`confirmed`, `likely` (default), `all`) sets which agent labels count as agents.

The [query cookbook](https://www.punchlineanalytics.com/docs/api-queries) has a ready-made body for most questions.

## Other endpoints

| Endpoint | Returns |
|---|---|
| `POST /v1/events` | Send custom events from your servers ([Events and goals](https://www.punchlineanalytics.com/docs/events-goals)) |
| `GET /v1/sites`, `/v1/sites/{id}` | Your site and how it's installed |
| `GET /v1/operators` | Known agent operators and how each is verified |
| `GET /v1/sessions` | Agent and crawler sessions, newest first; page with `cursor` |
| `GET /v1/sessions/{id}`, `/v1/sessions/{id}/evidence` | One agent session, and why it was labelled an agent |
| `GET /v1/content/funnel`, `/v1/content/recommendations` | [Agent content](https://www.punchlineanalytics.com/docs/agent-content) |
| `GET /v1/reconciliation` | [GA4 comparison](https://www.punchlineanalytics.com/docs/ga4) |
| `GET /v1/accounts` | Companies whose agents and people researched you, by intent, with a signal strength ([Accounts](https://www.punchlineanalytics.com/docs/accounts)) |
| `POST /v1/handoffs`, `GET /v1/handoffs` | Mint a handoff link for an agent (events:write), and see links opened and converted ([Handoff links](https://www.punchlineanalytics.com/docs/handoffs)) |
| `GET /v1/ai-influence` | The post-conversion survey's estimate of AI-assisted conversions, against what links show ([Events and goals](https://www.punchlineanalytics.com/docs/events-goals)) |
| `GET /v1/resolution` | Where agent sessions ran (in someone's browser, the cloud, a local tool) and how many joined a person or a company |
| `GET /v1/reports` | Saved reports shared with everyone on the site, with their Explore settings ([Reports](https://www.punchlineanalytics.com/docs/reports)) |

## Exports

For pulls bigger than a query returns, `POST /v1/exports` takes the same body as `/v1/query` with a limit of up to 100,000 rows (the default), and keeps the result as CSV for 24 hours. Rows withheld for privacy stay withheld (`suppressed_rows` says how many).

```bash
curl https://app.punchlineanalytics.com/v1/exports -H "Authorization: Bearer $PUNCHLINE_KEY" \
  -d '{"date_range": {"from": "2026-09-01", "to": "2026-09-30"}, "metrics": ["pageviews"], "dimensions": ["day", "path", "operator"]}'
# → {"data": {"id": "…", "rows": 8412, "download_url": "https://…/v1/exports/…/download", …}}
curl --compressed -o september.csv -H "Authorization: Bearer $PUNCHLINE_KEY" "<download_url>"
```

Add `"format": "parquet"` for a Parquet file instead (typed columns, no gzip wrapper needed).

The CSV file is sent gzip-encoded, so use `--compressed` with curl (browsers and `fetch` decompress on their own). It's capped at 4 MB compressed; narrow the range or dimensions if you hit that. Cells that start with `=`, `+`, `-` or `@` get a leading `'`, so a page path crafted as a formula can't run in a spreadsheet.

## Managing your site

With a key that can manage (the `admin` scope):

| Endpoint | |
|---|---|
| `GET`, `POST /v1/alerts`; `GET`, `PATCH`, `DELETE /v1/alerts/{id}` | [Alerts](https://www.punchlineanalytics.com/docs/alerts) |
| `GET`, `POST /v1/goals`; `GET`, `PATCH`, `DELETE /v1/goals/{id}` | [Goals](https://www.punchlineanalytics.com/docs/events-goals) |
| `GET`, `POST /v1/triggers`; `GET`, `PATCH`, `DELETE /v1/triggers/{id}` | No-code [event triggers](https://www.punchlineanalytics.com/docs/events-goals); changing one gives it a new version |
| `GET`, `PATCH`, `DELETE /v1/url-parameters` | The [URL-parameter](https://www.punchlineanalytics.com/docs/url-params) allowlist and rules |

`PATCH` takes only the fields you're changing. Writes accept `Idempotency-Key`, so a retry never creates a second alert. Validation is the same as in the dashboard, with the reason in `error.message`:

```bash
curl https://app.punchlineanalytics.com/v1/alerts \
  -H "Authorization: Bearer $PUNCHLINE_KEY" -H "Idempotency-Key: agent-share-1" \
  -d '{"name": "Agent share spike", "kind": "anomaly", "metric": "agent_share", "op": "up", "value": 50}'
```

Every parameter and response is in the [API reference](https://www.punchlineanalytics.com/docs/api-reference), also available as OpenAPI 3.1 at `/v1/openapi.json`.

## TypeScript

`@punchline-analytics/sdk` is a typed client generated from the same OpenAPI spec, so its types always match the API. Install it with `npm install @punchline-analytics/sdk`; see [TypeScript SDK](https://www.punchlineanalytics.com/docs/sdk) for every method.

```ts
import { Punchline } from "@punchline-analytics/sdk";

const punchline = new Punchline({ apiKey: process.env.PUNCHLINE_API_KEY! });

const { results } = await punchline.query({
  site_id: "<your site id>",
  date_range: { from: "2026-09-01", to: "2026-09-30" },
  metrics: ["sessions", "agent_share"],
  dimensions: ["agent_purpose"],
});

// Every agent session, page by page:
for await (const session of punchline.allSessions({ from: "2026-09-24", to: "2026-09-30" })) {
  console.log(session.type, session.operator, session.paths);
}
```

Errors throw `PunchlineError` with `status`, `code` and `param`; a 429 is retried after its `Retry-After`.

## Errors

Every error has the same shape, with the parameter at fault when there is one:

```json
{ "error": { "type": "invalid_request_error", "code": "invalid_parameter", "message": "limit: must be at most 1000", "param": "limit", "docs_url": "…/v1/errors#invalid_parameter" } }
```

## Privacy

Outside your own dashboard, any group containing fewer than 5 people's page views is withheld and counted in `meta.suppressed_rows`. Groups of only agents and crawlers are always shown. Sessions of people are never returned; agent sessions are.

## Warehouse export

To get data into BigQuery or Snowflake every day without calling the API, set up **Warehouse export** in Site settings. Each morning it writes yesterday's traffic, conversions, events and agent sessions into tables in your dataset or schema. It replaces each day's rows, so reruns are safe. For BigQuery, you share one dataset with Punchline's service account as BigQuery Data Editor and label it with your site ID; Snowflake uses a key pair Punchline generates, plus a setup script to run once. The Snowflake private key is stored encrypted. The same privacy rules apply as for the API.
