# API reference

Read your site's traffic split into people, AI agents and crawlers, with the evidence behind every label. Authenticate with an API key as a Bearer token. Each key is bound to one site. Outside your own dashboard, groups with fewer than 5 people's page views are withheld (meta.suppressed_rows), and sessions of people are never returned.

Base URL: `https://app.punchlineanalytics.com`. Authenticate with `Authorization: Bearer pk_live_…` ([keys and scopes](https://www.punchlineanalytics.com/docs/api-auth)). Generated from the [OpenAPI 3.1 spec](https://www.punchlineanalytics.com/v1/openapi.json) that the API is tested against; every example below is a real response, recorded by calling the endpoint in our test suite against a sample site, so they can't drift from what the API returns.

- **Query**: [POST /v1/query](#post-v1-query), [POST /v1/exports](#post-v1-exports), [GET /v1/exports/{id}](#get-v1-exports-id), [GET /v1/exports/{id}/download](#get-v1-exports-id-download). Metrics by dimensions, and exports for bigger pulls.
- **Your site and agents**: [GET /v1/sites](#get-v1-sites), [GET /v1/sites/{id}](#get-v1-sites-id), [GET /v1/operators](#get-v1-operators), [GET /v1/sessions](#get-v1-sessions), [GET /v1/sessions/{id}](#get-v1-sessions-id), [GET /v1/sessions/{id}/evidence](#get-v1-sessions-id-evidence). The site behind the key, known operators, and agent sessions with their evidence.
- **Reports**: [GET /v1/handoffs](#get-v1-handoffs), [GET /v1/content/funnel](#get-v1-content-funnel), [GET /v1/content/recommendations](#get-v1-content-recommendations), [GET /v1/accounts](#get-v1-accounts), [GET /v1/ai-influence](#get-v1-ai-influence), [GET /v1/resolution](#get-v1-resolution), [GET /v1/reconciliation](#get-v1-reconciliation), [GET /v1/reports](#get-v1-reports). The dashboard's reports as JSON.
- **Send data**: [POST /v1/events](#post-v1-events), [POST /v1/handoffs](#post-v1-handoffs). Events from your servers and handoff links for agents.
- **Manage**: [GET /v1/alerts](#get-v1-alerts), [POST /v1/alerts](#post-v1-alerts), [GET /v1/alerts/{id}](#get-v1-alerts-id), [PATCH /v1/alerts/{id}](#patch-v1-alerts-id), [DELETE /v1/alerts/{id}](#delete-v1-alerts-id), [GET /v1/goals](#get-v1-goals), [POST /v1/goals](#post-v1-goals), [GET /v1/goals/{id}](#get-v1-goals-id), [PATCH /v1/goals/{id}](#patch-v1-goals-id), [DELETE /v1/goals/{id}](#delete-v1-goals-id), [GET /v1/triggers](#get-v1-triggers), [POST /v1/triggers](#post-v1-triggers), [GET /v1/triggers/{id}](#get-v1-triggers-id), [PATCH /v1/triggers/{id}](#patch-v1-triggers-id), [DELETE /v1/triggers/{id}](#delete-v1-triggers-id), [GET /v1/url-parameters](#get-v1-url-parameters), [PATCH /v1/url-parameters](#patch-v1-url-parameters), [DELETE /v1/url-parameters](#delete-v1-url-parameters). Alerts, goals, event triggers and URL parameters. Needs the admin scope.
- [Errors](#errors)

## Query

Metrics by dimensions, and exports for bigger pulls.

### POST /v1/query

Metrics by dimensions, with filters. The main endpoint.

**Scope:** `read` (every key) · **SDK:** `punchline.query()`

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `site_id` | string (uuid) | yes |  |
| `date_range` | object | yes |  |
| `metrics` | list of `sessions`, `pageviews`, `human_sessions`, `agent_sessions`, `agent_share`, `thin_pageviews`, `events`, `event_sessions`, `event_value`, `conversions`, `conversion_rate`, `visitors`, `bounces`, `bounce_rate`, `avg_session_duration`, `pages_per_session`, `engaged_sessions`, `engagement_rate`, `exits`, `exit_rate`, `avg_time_on_page` | yes |  |
| `dimensions` | list of string |  |  |
| `filters` | list of any |  |  |
| `confidence` | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |
| `order_by` | list of any |  |  |
| `limit` | integer (1–1000) |  | Default 100 |

**Example**

```bash
curl -X POST "https://app.punchlineanalytics.com/v1/query" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "$PUNCHLINE_SITE_ID",
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "metrics": [
      "sessions",
      "agent_share"
    ],
    "dimensions": [
      "visitor_type"
    ]
  }'
```

Response `200`:

```json
{
  "query": {
    "site_id": "25cbb06c-e534-4f14-a740-785079ddc69c",
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "metrics": [
      "sessions",
      "agent_share"
    ],
    "dimensions": [
      "visitor_type"
    ],
    "filters": [],
    "confidence": "likely",
    "order_by": [
      [
        "sessions",
        "desc"
      ]
    ],
    "limit": 100
  },
  "results": [
    {
      "visitor_type": "human",
      "sessions": 9,
      "agent_share": 0
    },
    {
      "visitor_type": "ai_referred",
      "sessions": 6,
      "agent_share": 0
    },
    {
      "visitor_type": "crawler",
      "sessions": 1,
      "agent_share": 1
    },
    {
      "visitor_type": "agent",
      "sessions": 1,
      "agent_share": 1
    }
  ],
  "meta": {
    "audience": "external",
    "min_cohort": 5,
    "suppressed_rows": 0,
    "rows_before_limit": 4,
    "truncated": false,
    "rollups_refreshed_at": "2026-10-06T15:00:00.000Z"
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard?site=25cbb06c-e534-4f14-a740-785079ddc69c"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `413`: [`body_too_large`](#body-too-large)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### POST /v1/exports

Run a query (the /v1/query model, limit up to 100,000, default 100,000) and keep the result as CSV for 24 hours. Same privacy suppression as /v1/query.

**Scope:** `read` (every key) · **SDK:** `punchline.createExport()`

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `site_id` | string (uuid) | yes |  |
| `format` | `csv` |  |  |
| `date_range` | object | yes |  |
| `metrics` | list of `sessions`, `pageviews`, `human_sessions`, `agent_sessions`, `agent_share`, `thin_pageviews`, `events`, `event_sessions`, `event_value`, `conversions`, `conversion_rate`, `visitors`, `bounces`, `bounce_rate`, `avg_session_duration`, `pages_per_session`, `engaged_sessions`, `engagement_rate`, `exits`, `exit_rate`, `avg_time_on_page` | yes |  |
| `dimensions` | list of string |  |  |
| `filters` | list of any |  |  |
| `confidence` | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |
| `order_by` | list of any |  |  |
| `limit` | integer (1–100000) |  |  |

**Example**

```bash
curl -X POST "https://app.punchlineanalytics.com/v1/exports" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "$PUNCHLINE_SITE_ID",
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "metrics": [
      "pageviews"
    ],
    "dimensions": [
      "path",
      "visitor_type"
    ]
  }'
```

Response `201`:

```json
{
  "data": {
    "id": "9ce4690d-fbee-412d-af39-cb4722e35406",
    "status": "ready",
    "format": "csv",
    "query": {
      "date_range": {
        "from": "2026-10-06",
        "to": "2026-10-06"
      },
      "metrics": [
        "pageviews"
      ],
      "dimensions": [
        "path",
        "visitor_type"
      ],
      "filters": [],
      "confidence": "likely",
      "order_by": [
        [
          "pageviews",
          "desc"
        ]
      ],
      "limit": 100000
    },
    "rows": 11,
    "suppressed_rows": 0,
    "truncated": false,
    "bytes": 142,
    "created_at": "2026-10-06T15:00:00.000Z",
    "expires_at": "2026-10-07T15:00:00.000Z",
    "download_url": "https://app.punchlineanalytics.com/v1/exports/9ce4690d-fbee-412d-af39-cb4722e35406/download"
  }
}
```

**Responses**

- `201`: Created
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `413`: [`body_too_large`](#body-too-large)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/exports/{id}

An export's size and download link.

**Scope:** `read` (every key) · **SDK:** `punchline.getExport()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The export's ID. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/exports/9ce4690d-fbee-412d-af39-cb4722e35406" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "id": "9ce4690d-fbee-412d-af39-cb4722e35406",
    "status": "ready",
    "format": "csv",
    "query": {
      "date_range": {
        "from": "2026-10-06",
        "to": "2026-10-06"
      },
      "metrics": [
        "pageviews"
      ],
      "dimensions": [
        "path",
        "visitor_type"
      ],
      "filters": [],
      "confidence": "likely",
      "order_by": [
        [
          "pageviews",
          "desc"
        ]
      ],
      "limit": 100000
    },
    "rows": 11,
    "suppressed_rows": 0,
    "truncated": false,
    "bytes": 142,
    "created_at": "2026-10-06T15:00:00.000Z",
    "expires_at": "2026-10-07T15:00:00.000Z",
    "download_url": "https://app.punchlineanalytics.com/v1/exports/9ce4690d-fbee-412d-af39-cb4722e35406/download"
  }
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/exports/{id}/download

The CSV, gzip-encoded (Content-Encoding: gzip; with curl, use --compressed). Cells starting with = + - @ are prefixed with ' so spreadsheets don't run them.

**Scope:** `read` (every key) · **SDK:** `punchline.downloadExport()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The export's ID. |

**Responses**

- `200`: CSV
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

## Your site and agents

The site behind the key, known operators, and agent sessions with their evidence.

### GET /v1/sites

The site this key can read, with setup status.

**Scope:** `read` (every key) · **SDK:** `punchline.listSites()`

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/sites" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": [
    {
      "id": "25cbb06c-e534-4f14-a740-785079ddc69c",
      "name": "Shop",
      "domain": "shop.example",
      "setup": {
        "edge_install_reporting": true,
        "snippet_reporting": false,
        "browser_only": false,
        "ga4_connected": true
      },
      "url_params": []
    }
  ]
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/sites/{id}

One site.

**Scope:** `read` (every key) · **SDK:** `punchline.getSite()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes |  |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/sites/$PUNCHLINE_SITE_ID" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "id": "25cbb06c-e534-4f14-a740-785079ddc69c",
    "name": "Shop",
    "domain": "shop.example",
    "setup": {
      "edge_install_reporting": true,
      "snippet_reporting": false,
      "browser_only": false,
      "ga4_connected": true
    },
    "url_params": []
  }
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/operators

Known agent operators and how each is verified.

**Scope:** `read` (every key) · **SDK:** `punchline.listOperators()`

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/operators" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "registry_version": "2026-10-25.1",
  "data": [
    {
      "name": "GPTBot",
      "operator": "openai",
      "type": "crawler",
      "actor_class": "crawler",
      "verified_by": [
        "published_ip_ranges"
      ],
      "docs_url": "https://platform.openai.com/docs/bots"
    },
    {
      "name": "OAI-SearchBot",
      "operator": "openai",
      "type": "crawler",
      "actor_class": "crawler",
      "verified_by": [
        "published_ip_ranges"
      ],
      "docs_url": "https://platform.openai.com/docs/bots"
    }
  ]
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/sessions

Agent and crawler sessions, newest first. Page with cursor=next_cursor.

**Scope:** `read` (every key) · **SDK:** `punchline.listSessions()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |
| `actor_class` | query | `signed_agent`, `declared_agent`, `crawler`, `fetcher`, `suspected_agent`, `spoofed_bot` |  |  |
| `operator` | query | string |  |  |
| `tier` | query | `verified`, `declared`, `inferred_high`, `inferred_low` |  |  |
| `limit` | query | integer (1–100) |  | Default 25 |
| `cursor` | query | string |  |  |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/sessions?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06&limit=2" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": [
    {
      "session_id": "d9828ab0-ecf1-4046-a47f-dabd6a37e60e",
      "type": "declared agent",
      "actor_class": "declared_agent",
      "operator": "openai",
      "operator_docs": "https://platform.openai.com/docs/bots",
      "tier": "inferred_high",
      "started_at": "2026-10-06T13:30:00.000Z",
      "ended_at": "2026-10-06T13:32:00.000Z",
      "pageviews": 3,
      "paths": [
        "/pricing",
        "/security",
        "/compare/acme-vs-shop"
      ],
      "rules": [
        {
          "id": "R2u",
          "description": "Declares a known bot that couldn't be confirmed: no IP data, DNS failed, or an address outside the operator's published list on a network it's known to use"
        }
      ],
      "evidence": {
        "ua_claims": "ChatGPT-User",
        "claim_verification": "unavailable",
        "claim": "unverifiable",
        "asn": 16509
      }
    },
    {
      "session_id": "4d7e7cb2-df1a-40c3-a4a2-54c208b008f2",
      "type": "crawler",
      "actor_class": "crawler",
      "operator": "openai",
      "operator_docs": "https://platform.openai.com/docs/bots",
      "tier": "declared",
      "started_at": "2026-10-06T13:00:00.000Z",
      "ended_at": "2026-10-06T13:03:00.000Z",
      "pageviews": 4,
      "paths": [
        "/",
        "/pricing",
        "/docs/api",
        "/security"
      ],
      "rules": [
        {
          "id": "R2",
          "description": "Declares a known bot, and its IP matches the operator's published ranges or reverse DNS"
        }
      ],
      "evidence": {
        "ua_claims": "GPTBot",
        "ip_in_published_range": true,
        "claim": "confirmed"
      }
    }
  ],
  "next_cursor": null
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/sessions/{id}

One agent session as a request timeline.

**Scope:** `read` (every key) · **SDK:** `punchline.getSession()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes |  |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/sessions/d9828ab0-ecf1-4046-a47f-dabd6a37e60e?site_id=$PUNCHLINE_SITE_ID" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "session_id": "d9828ab0-ecf1-4046-a47f-dabd6a37e60e",
    "type": "declared agent",
    "actor_class": "declared_agent",
    "operator": "openai",
    "operator_docs": "https://platform.openai.com/docs/bots",
    "tier": "inferred_high",
    "started_at": "2026-10-06T13:30:00.000Z",
    "ended_at": "2026-10-06T13:32:00.000Z",
    "pageviews": 3,
    "paths": [
      "/pricing",
      "/security",
      "/compare/acme-vs-shop"
    ],
    "rules": [
      {
        "id": "R2u",
        "description": "Declares a known bot that couldn't be confirmed: no IP data, DNS failed, or an address outside the operator's published list on a network it's known to use"
      }
    ],
    "evidence": {
      "ua_claims": "ChatGPT-User",
      "claim_verification": "unavailable",
      "claim": "unverifiable",
      "asn": 16509
    }
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/agents/sessions?site=25cbb06c-e534-4f14-a740-785079ddc69c&operator=openai&class=declared_agent&tier=inferred_high"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/sessions/{id}/evidence

Why this session was labelled an agent: rules, evidence, tier.

**Scope:** `read` (every key) · **SDK:** `punchline.getSessionEvidence()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes |  |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/sessions/d9828ab0-ecf1-4046-a47f-dabd6a37e60e/evidence?site_id=$PUNCHLINE_SITE_ID" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "session_id": "d9828ab0-ecf1-4046-a47f-dabd6a37e60e",
    "type": "declared agent",
    "actor_class": "declared_agent",
    "operator": "openai",
    "tier": "inferred_high",
    "rules": [
      {
        "id": "R2u",
        "description": "Declares a known bot that couldn't be confirmed: no IP data, DNS failed, or an address outside the operator's published list on a network it's known to use"
      }
    ],
    "evidence": {
      "ua_claims": "ChatGPT-User",
      "claim_verification": "unavailable",
      "claim": "unverifiable",
      "asn": 16509
    }
  }
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

## Reports

The dashboard's reports as JSON.

### GET /v1/handoffs

Handoff links created in a date range: opened by a person, opened by an agent, and converted (the opener's session completed a goal), with values, per kind and agent operator.

**Scope:** `read` (every key) · **SDK:** `punchline.getHandoffs()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/handoffs?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "totals": {
      "created": 1,
      "opened_by_person": 0,
      "opened_by_agent": 0,
      "converted": 0,
      "value_created": 249,
      "value_opened": 0,
      "value_converted": 0
    },
    "by_kind": [
      {
        "kind": "cart",
        "created": 1,
        "opened_by_person": 0,
        "opened_by_agent": 0,
        "converted": 0,
        "value_created": 249,
        "value_opened": 0,
        "value_converted": 0
      }
    ],
    "by_operator": [
      {
        "operator": "openai",
        "created": 1,
        "opened_by_person": 0,
        "opened_by_agent": 0,
        "converted": 0,
        "value_created": 249,
        "value_opened": 0,
        "value_converted": 0
      }
    ]
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/handoffs?site=25cbb06c-e534-4f14-a740-785079ddc69c"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `409`: [`feature_unavailable`](#feature-unavailable)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/content/funnel

Agent content funnel: fetched (live vs crawl) → delivered → cited, per page, content type or topic.

**Scope:** `read` (every key) · **SDK:** `punchline.getContentFunnel()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |
| `confidence` | query | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |
| `group_by` | query | `path`, `content_type`, `topic` |  | Group pages by path (default), content type or topic. |
| `limit` | query | integer (1–200) |  | Default 25 |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/content/funnel?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06&limit=3" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "group_by": "path",
    "rows": [
      {
        "key": "/pricing",
        "fetches": {
          "total": 2,
          "live": 1,
          "crawl": 1,
          "other": 0
        },
        "delivery": {
          "known": 2,
          "ok": 2,
          "redirect": 0,
          "failed": 0,
          "not_found": 0,
          "blocked": 0,
          "thin": 0
        },
        "delivered_share": 1,
        "cited": 6,
        "citation_rate": 600,
        "converted": 6,
        "impostor_fetches": 0
      },
      {
        "key": "/security",
        "fetches": {
          "total": 2,
          "live": 1,
          "crawl": 1,
          "other": 0
        },
        "delivery": {
          "known": 2,
          "ok": 2,
          "redirect": 0,
          "failed": 0,
          "not_found": 0,
          "blocked": 0,
          "thin": 0
        },
        "delivered_share": 1,
        "cited": 0,
        "citation_rate": 0,
        "converted": 0,
        "impostor_fetches": 0
      },
      {
        "key": "/",
        "fetches": {
          "total": 1,
          "live": 0,
          "crawl": 1,
          "other": 0
        },
        "delivery": {
          "known": 1,
          "ok": 1,
          "redirect": 0,
          "failed": 0,
          "not_found": 0,
          "blocked": 0,
          "thin": 0
        },
        "delivered_share": 1,
        "cited": 0,
        "citation_rate": null,
        "converted": 0,
        "impostor_fetches": 0
      }
    ],
    "totals": {
      "key": "all",
      "fetches": {
        "total": 7,
        "live": 3,
        "crawl": 4,
        "other": 0
      },
      "delivery": {
        "known": 7,
        "ok": 7,
        "redirect": 0,
        "failed": 0,
        "not_found": 0,
        "blocked": 0,
        "thin": 0
      },
      "delivered_share": 1,
      "cited": 6,
      "citation_rate": 200,
      "converted": 6,
      "impostor_fetches": 0
    },
    "delivery_coverage": 1,
    "confidence": "likely"
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/content?site=25cbb06c-e534-4f14-a740-785079ddc69c&confidence=likely"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/content/recommendations

Evidence-based things to try so assistants can read and cite the site, with the previous period for comparison.

**Scope:** `read` (every key) · **SDK:** `punchline.getRecommendations()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |
| `confidence` | query | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/content/recommendations?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "previous_range": {
      "from": "2026-10-05",
      "to": "2026-10-05"
    },
    "delivery_coverage": 1,
    "findings": [],
    "confidence": "likely"
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/content?site=25cbb06c-e534-4f14-a740-785079ddc69c&confidence=likely"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/accounts

Agent intent by company: what each company's agents and people read, grouped by intent, with a strength and a summary. Account-level only.

**Scope:** `read` (every key) · **SDK:** `punchline.getAccounts()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |
| `confidence` | query | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |
| `company_domain` | query | string |  | One company, e.g. acme.com |
| `min_strength` | query | `light`, `moderate`, `strong` |  | Default light (all accounts) |
| `limit` | query | integer (1–200) |  | Default 25 |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/accounts?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "confidence": "likely",
    "accounts": [
      {
        "company_domain": "acme.com",
        "method": "form_domain",
        "strength": "moderate",
        "score": 9,
        "summary": "Agents read pricing, security and comparison pages 3 times",
        "agents": {
          "sessions": 1,
          "pageviews": 3,
          "operators": [
            "openai"
          ],
          "runtimes": [
            "cloud"
          ]
        },
        "people": null,
        "intents": [
          {
            "intent": "pricing",
            "label": "Pricing",
            "agent_pageviews": 1,
            "people_pageviews": null
          },
          {
            "intent": "security",
            "label": "Security & compliance",
            "agent_pageviews": 1,
            "people_pageviews": null
          },
          {
            "intent": "comparison",
            "label": "Comparisons",
            "agent_pageviews": 1,
            "people_pageviews": null
          }
        ],
        "previous_agent_sessions": 0,
        "first_seen": "2026-10-06",
        "last_seen": "2026-10-06"
      }
    ],
    "meta": {
      "withheld_people": 0
    }
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/accounts?site=25cbb06c-e534-4f14-a740-785079ddc69c&confidence=likely"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `409`: [`feature_unavailable`](#feature-unavailable)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/ai-influence

Estimated AI-assisted conversions from the post-conversion survey (ai_assist_yes / _no / _not_sure events), with a 95% interval, against conversions Punchline connects directly, and the multiplier.

**Scope:** `read` (every key) · **SDK:** `punchline.getAiInfluence()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |
| `confidence` | query | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |
| `goal` | query | string |  | One goal's conversions; default all goals |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/ai-influence?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "confidence": "likely",
    "responses": {
      "yes": 14,
      "no": 22,
      "not_sure": 6
    },
    "yes_share": 0.3888888888888889,
    "yes_share_interval": [
      0.24784655417822943,
      0.5513582970325652
    ],
    "enough_responses": true,
    "people_conversions": 6,
    "estimated_ai_assisted": {
      "low": 1.4870793250693768,
      "mid": 2.3333333333333335,
      "high": 3.3081497821953914
    },
    "observed": {
      "ai_referred": 6,
      "handoffs": 0,
      "total": 6
    },
    "multiplier": 0.3888888888888889
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/goals?site=25cbb06c-e534-4f14-a740-785079ddc69c&confidence=likely"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/resolution

Where agent sessions ran (in a person's browser, cloud, local tool) and how many joined a person's visit or a company the same day.

**Scope:** `read` (every key) · **SDK:** `punchline.getResolution()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |
| `confidence` | query | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/resolution?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "confidence": "likely",
    "agents": {
      "sessions": 1,
      "by_runtime": {
        "in_browser": 0,
        "cloud": 1,
        "local": 0,
        "unknown": 0
      },
      "in_browser_share": 0,
      "joined_person": 0,
      "joined_company": 1,
      "methods": {
        "same_day_visit": 0,
        "company_domain": 1,
        "company_ip": 0,
        "handoff": 0,
        "any": 1
      },
      "connected_by_runtime": {
        "in_browser": 0,
        "cloud": 1,
        "local": 0,
        "unknown": 0
      }
    },
    "crawlers": {
      "sessions": 1
    },
    "days": [
      {
        "day": "2026-10-06",
        "in_browser": 0,
        "cloud": 1,
        "local": 0,
        "unknown": 0
      }
    ],
    "operators": [
      {
        "operator": "openai",
        "sessions": 1,
        "by_runtime": {
          "in_browser": 0,
          "cloud": 1,
          "local": 0,
          "unknown": 0
        },
        "joined_person": 0
      }
    ]
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/resolution?site=25cbb06c-e534-4f14-a740-785079ddc69c&confidence=likely"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `409`: [`feature_unavailable`](#feature-unavailable)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/reconciliation

GA4 sessions vs Punchline's, with the gap explained. Needs GA4 connected.

**Scope:** `read` (every key) · **SDK:** `punchline.getReconciliation()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | query | string (uuid) |  | Optional; must be the API key's site. |
| `from` | query | string (date) |  | First UTC day, YYYY-MM-DD. Default: 6 days before `to`. |
| `to` | query | string (date) |  | Last UTC day, YYYY-MM-DD, inclusive. Default: today. |
| `confidence` | query | `confirmed`, `likely`, `all` |  | How strict to be about agent labels. confirmed: verified signatures and declared crawlers only. likely (default): adds high-confidence inferences. all: adds low-confidence patterns. Weaker labels count as human. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/reconciliation?site_id=$PUNCHLINE_SITE_ID&from=2026-10-06&to=2026-10-06" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "date_range": {
      "from": "2026-10-06",
      "to": "2026-10-06"
    },
    "confidence": "likely",
    "totals": {
      "ga4": 14,
      "ourHumans": 15,
      "agentsGa4NeverSees": 2,
      "agentsGa4CountsAsPeople": 0,
      "unexplained": -1,
      "daysWithGa4": 1
    },
    "days": [
      {
        "day": "2026-10-06",
        "ga4": 14,
        "ourHumans": 15,
        "agentsGa4NeverSees": 2,
        "agentsGa4CountsAsPeople": 0,
        "unexplained": -1
      }
    ],
    "estimate_note": "agentsGa4CountsAsPeople is an estimate: browser-based agents run GA4's script, so GA4 likely counted them."
  },
  "dashboard_url": "https://app.punchlineanalytics.com/dashboard/reconciliation?site=25cbb06c-e534-4f14-a740-785079ddc69c&confidence=likely"
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `409`: [`feature_unavailable`](#feature-unavailable)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/reports

Saved reports shared with this site (private reports are never listed), with their Explore parameters: tm metric, d breakdown, f filters, tt chart type, tg granularity, range, who, confidence.

**Scope:** `read` (every key) · **SDK:** `punchline.listReports()`

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/reports" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": [
    {
      "id": "1a85afd3-851d-48e9-a9ae-aca544ff6ea9",
      "name": "AI referrals by assistant",
      "params": {
        "tm": "sessions",
        "bd": "assistant"
      },
      "updated_at": "2026-10-06T15:00:00.000Z"
    }
  ]
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

## Send data

Events from your servers and handoff links for agents.

### POST /v1/events

Send custom events from your servers (up to 1,000). Needs the events:write scope. Send Idempotency-Key to make retries safe.

**Scope:** `events:write` (keys that can send events) · **SDK:** `punchline.sendEvents()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `site_id` | string (uuid) | yes |  |
| `events` | list of EventInput | yes |  |

**Example**

```bash
curl -X POST "https://app.punchlineanalytics.com/v1/events" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "$PUNCHLINE_SITE_ID",
    "events": [
      {
        "name": "signup",
        "value": 49,
        "currency": "USD",
        "props": {
          "plan": "team"
        }
      }
    ]
  }'
```

Response `202`:

```json
{
  "accepted": 1
}
```

**Responses**

- `202`: Accepted
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### POST /v1/handoffs

Mint a handoff link for an agent that built a cart, quote, configuration or booking. Give the agent's request (visitor) to tie the link to its session; when a person opens the link, the open is recorded. Needs the events:write scope. Only the token's hash is stored.

**Scope:** `events:write` (keys that can send events) · **SDK:** `punchline.createHandoff()`

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `site_id` | string (uuid) | yes |  |
| `kind` | `cart`, `quote`, `configuration`, `booking`, `other` |  |  |
| `value` | number (0–) |  | The cart or quote total |
| `currency` | string |  |  |
| `expires_in_days` | integer (1–90) |  |  |
| `url` | string |  | A path or https URL on your site; the response adds the token to it |
| `visitor` | object |  | The agent's request, used in memory only |

**Example**

```bash
curl -X POST "https://app.punchlineanalytics.com/v1/handoffs" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "$PUNCHLINE_SITE_ID",
    "kind": "cart",
    "value": 249,
    "currency": "USD",
    "url": "/cart/8431",
    "visitor": {
      "ip": "3.0.0.9",
      "user_agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot"
    }
  }'
```

Response `201`:

```json
{
  "data": {
    "token": "TLDqSZynF30ytN624lHQqPhX",
    "param": "pl_ho",
    "url": "https://shop.example/cart/8431?pl_ho=TLDqSZynF30ytN624lHQqPhX",
    "kind": "cart",
    "expires_at": "2026-11-05T15:00:00.000Z",
    "agent": {
      "session_found": false,
      "actor_class": "declared_agent",
      "operator": "openai"
    }
  }
}
```

**Responses**

- `201`: Created
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

## Manage

Alerts, goals, event triggers and URL parameters. Needs the admin scope.

### GET /v1/alerts

The site's alerts. Needs the admin scope.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.listAlerts()`

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/alerts" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": [
    {
      "id": "5620320e-d3c1-4405-a004-6c9b1cf8e994",
      "name": "Agent share spike",
      "kind": "anomaly",
      "metric": "agent_share",
      "filters": [],
      "confidence": "likely",
      "op": "up",
      "value": 50,
      "min_volume": 20,
      "active": true,
      "description": "Agent share rises by 50% or more on the usual, when the usual volume is at least 20.",
      "created_at": "2026-10-06T15:00:00.000Z"
    }
  ]
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### POST /v1/alerts

Create a alert. Needs the admin scope.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.createAlert()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  |  |
| `kind` | `threshold`, `anomaly`, `new_operator`, `account_signal` |  |  |
| `metric` | string |  | A /v1/query metric |
| `filters` | list of any |  |  |
| `confidence` | `confirmed`, `likely`, `all` |  |  |
| `op` | `above`, `below`, `up`, `down`, `either` |  |  |
| `value` | number |  |  |
| `min_volume` | integer (0–) |  |  |
| `active` | boolean |  |  |

**Example**

```bash
curl -X POST "https://app.punchlineanalytics.com/v1/alerts" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Agent share spike",
    "kind": "anomaly",
    "metric": "agent_share",
    "op": "up",
    "value": 50
  }'
```

Response `201`:

```json
{
  "data": {
    "id": "5620320e-d3c1-4405-a004-6c9b1cf8e994",
    "name": "Agent share spike",
    "kind": "anomaly",
    "metric": "agent_share",
    "filters": [],
    "confidence": "likely",
    "op": "up",
    "value": 50,
    "min_volume": 20,
    "active": true,
    "description": "Agent share rises by 50% or more on the usual, when the usual volume is at least 20.",
    "created_at": "2026-10-06T15:00:00.000Z"
  }
}
```

**Responses**

- `201`: Created
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/alerts/{id}

One alert.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.getAlert()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The alert's ID. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/alerts/5620320e-d3c1-4405-a004-6c9b1cf8e994" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "id": "5620320e-d3c1-4405-a004-6c9b1cf8e994",
    "name": "Agent share spike",
    "kind": "anomaly",
    "metric": "agent_share",
    "filters": [],
    "confidence": "likely",
    "op": "up",
    "value": 50,
    "min_volume": 20,
    "active": true,
    "description": "Agent share rises by 50% or more on the usual, when the usual volume is at least 20.",
    "created_at": "2026-10-06T15:00:00.000Z"
  }
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### PATCH /v1/alerts/{id}

Change a alert: send only the fields to change.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.updateAlert()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The alert's ID. |
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  |  |
| `kind` | `threshold`, `anomaly`, `new_operator`, `account_signal` |  |  |
| `metric` | string |  | A /v1/query metric |
| `filters` | list of any |  |  |
| `confidence` | `confirmed`, `likely`, `all` |  |  |
| `op` | `above`, `below`, `up`, `down`, `either` |  |  |
| `value` | number |  |  |
| `min_volume` | integer (0–) |  |  |
| `active` | boolean |  |  |

**Example**

```bash
curl -X PATCH "https://app.punchlineanalytics.com/v1/alerts/5620320e-d3c1-4405-a004-6c9b1cf8e994" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "value": 80
  }'
```

Response `200`:

```json
{
  "data": {
    "id": "5620320e-d3c1-4405-a004-6c9b1cf8e994",
    "name": "Agent share spike",
    "kind": "anomaly",
    "metric": "agent_share",
    "filters": [],
    "confidence": "likely",
    "op": "up",
    "value": 80,
    "min_volume": 20,
    "active": true,
    "description": "Agent share rises by 80% or more on the usual, when the usual volume is at least 20.",
    "created_at": "2026-10-06T15:00:00.000Z"
  }
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found), [`not_found`](#not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### DELETE /v1/alerts/{id}

Delete a alert.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.deleteAlert()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The alert's ID. |
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Example**

```bash
curl -X DELETE "https://app.punchlineanalytics.com/v1/alerts/5620320e-d3c1-4405-a004-6c9b1cf8e994" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `204`, no body.

**Responses**

- `204`: Deleted
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/goals

The site's goals. Needs the admin scope.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.listGoals()`

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/goals" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": [
    {
      "id": "242cd0db-2502-4eb9-a311-f9fb4bdc7e41",
      "name": "Purchase",
      "kind": "page",
      "match": "=/thanks"
    },
    {
      "id": "398a21ce-1622-4d5d-a136-527782fc885c",
      "name": "Signed up",
      "kind": "event",
      "match": "signup"
    }
  ]
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### POST /v1/goals

Create a goal. Needs the admin scope.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.createGoal()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  |  |
| `kind` | `event`, `page` |  |  |
| `match` | string |  | An event name, or a path (=/path for exactly that page) |

**Example**

```bash
curl -X POST "https://app.punchlineanalytics.com/v1/goals" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Signed up",
    "kind": "event",
    "match": "signup"
  }'
```

Response `201`:

```json
{
  "data": {
    "id": "398a21ce-1622-4d5d-a136-527782fc885c",
    "name": "Signed up",
    "kind": "event",
    "match": "signup"
  }
}
```

**Responses**

- `201`: Created
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/goals/{id}

One goal.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.getGoal()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The goal's ID. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/goals/398a21ce-1622-4d5d-a136-527782fc885c" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "id": "398a21ce-1622-4d5d-a136-527782fc885c",
    "name": "Signed up",
    "kind": "event",
    "match": "signup"
  }
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### PATCH /v1/goals/{id}

Change a goal: send only the fields to change.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.updateGoal()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The goal's ID. |
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  |  |
| `kind` | `event`, `page` |  |  |
| `match` | string |  | An event name, or a path (=/path for exactly that page) |

**Example**

```bash
curl -X PATCH "https://app.punchlineanalytics.com/v1/goals/398a21ce-1622-4d5d-a136-527782fc885c" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Signup"
  }'
```

Response `200`:

```json
{
  "data": {
    "id": "398a21ce-1622-4d5d-a136-527782fc885c",
    "name": "Signup",
    "kind": "event",
    "match": "signup"
  }
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found), [`not_found`](#not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### DELETE /v1/goals/{id}

Delete a goal.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.deleteGoal()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The goal's ID. |
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Example**

```bash
curl -X DELETE "https://app.punchlineanalytics.com/v1/goals/398a21ce-1622-4d5d-a136-527782fc885c" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `204`, no body.

**Responses**

- `204`: Deleted
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/triggers

The site's no-code event triggers. Needs the admin scope.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.listTriggers()`

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/triggers" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": [
    {
      "id": "92f7364b-89e3-43f0-aa7c-91954aa9b0af",
      "name": "Demo button",
      "kind": "click",
      "match": "#book-demo",
      "event_name": "demo_click",
      "version": 1,
      "active": true
    }
  ]
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### POST /v1/triggers

Create a trigger. Needs the admin scope.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.createTrigger()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  |  |
| `kind` | `page_view`, `url_match`, `click`, `form_submit` |  |  |
| `match` | string |  | A path (page_view, url_match with * wildcards) or a CSS selector (click, form_submit; needs the browser snippet) |
| `event_name` | string |  |  |
| `active` | boolean |  |  |

**Example**

```bash
curl -X POST "https://app.punchlineanalytics.com/v1/triggers" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Demo button",
    "kind": "click",
    "match": "#book-demo",
    "event_name": "demo_click"
  }'
```

Response `201`:

```json
{
  "data": {
    "id": "92f7364b-89e3-43f0-aa7c-91954aa9b0af",
    "name": "Demo button",
    "kind": "click",
    "match": "#book-demo",
    "event_name": "demo_click",
    "version": 1,
    "active": true
  }
}
```

**Responses**

- `201`: Created
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/triggers/{id}

One trigger.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.getTrigger()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The trigger's ID. |

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/triggers/92f7364b-89e3-43f0-aa7c-91954aa9b0af" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "id": "92f7364b-89e3-43f0-aa7c-91954aa9b0af",
    "name": "Demo button",
    "kind": "click",
    "match": "#book-demo",
    "event_name": "demo_click",
    "version": 1,
    "active": true
  }
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### PATCH /v1/triggers/{id}

Change a trigger: send only the fields to change.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.updateTrigger()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The trigger's ID. |
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  |  |
| `kind` | `page_view`, `url_match`, `click`, `form_submit` |  |  |
| `match` | string |  | A path (page_view, url_match with * wildcards) or a CSS selector (click, form_submit; needs the browser snippet) |
| `event_name` | string |  |  |
| `active` | boolean |  |  |

**Example**

```bash
curl -X PATCH "https://app.punchlineanalytics.com/v1/triggers/92f7364b-89e3-43f0-aa7c-91954aa9b0af" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "match": "a.book-demo"
  }'
```

Response `200`:

```json
{
  "data": {
    "id": "92f7364b-89e3-43f0-aa7c-91954aa9b0af",
    "name": "Demo button",
    "kind": "click",
    "match": "a.book-demo",
    "event_name": "demo_click",
    "version": 2,
    "active": true
  }
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found), [`not_found`](#not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### DELETE /v1/triggers/{id}

Delete a trigger.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.deleteTrigger()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | The trigger's ID. |
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Example**

```bash
curl -X DELETE "https://app.punchlineanalytics.com/v1/triggers/92f7364b-89e3-43f0-aa7c-91954aa9b0af" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `204`, no body.

**Responses**

- `204`: Deleted
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`not_found`](#not-found)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### GET /v1/url-parameters

The URL-parameter allowlist and rules. Needs the admin scope.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.getUrlParameters()`

**Example**

```bash
curl "https://app.punchlineanalytics.com/v1/url-parameters" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "params": [
      {
        "name": "ref",
        "mode": "value"
      },
      {
        "name": "gclid",
        "mode": "presence"
      }
    ],
    "rules": []
  }
}
```

**Responses**

- `200`: OK
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### PATCH /v1/url-parameters

Replace params and/or rules (send either or both). Rules may only use allowed parameters.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.updateUrlParameters()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `params` | list of object |  |  |
| `rules` | list of object |  |  |

**Example**

```bash
curl -X PATCH "https://app.punchlineanalytics.com/v1/url-parameters" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "params": [
      {
        "name": "ref",
        "mode": "value"
      },
      {
        "name": "gclid",
        "mode": "presence"
      }
    ]
  }'
```

Response `200`:

```json
{
  "data": {
    "params": [
      {
        "name": "ref",
        "mode": "value"
      },
      {
        "name": "gclid",
        "mode": "presence"
      }
    ],
    "rules": []
  }
}
```

**Responses**

- `200`: OK
- `400`: [`invalid_json`](#invalid-json), [`invalid_parameter`](#invalid-parameter)
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `404`: [`site_not_found`](#site-not-found)
- `413`: [`body_too_large`](#body-too-large)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

### DELETE /v1/url-parameters

Allow no parameters (except utm_*) and remove all rules.

**Scope:** `admin` (keys that can manage) · **SDK:** `punchline.deleteUrlParameters()`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | Retries with the same key and body return the first response. |

**Example**

```bash
curl -X DELETE "https://app.punchlineanalytics.com/v1/url-parameters" \
  -H "Authorization: Bearer $PUNCHLINE_API_KEY"
```

Response `204`, no body.

**Responses**

- `204`: Cleared
- `401`: [`invalid_api_key`](#invalid-api-key)
- `403`: [`insufficient_scope`](#insufficient-scope)
- `422`: [`idempotency_key_reused`](#idempotency-key-reused)
- `429`: [`rate_limited`](#rate-limited)
- `500`: [`internal_error`](#internal-error)

## Errors

Every error has one shape, and its `docs_url` links to the code's section below. `param` names the parameter at fault, when there is one; `message` never repeats what you sent.

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

| Code | HTTP | Type | Meaning | Retry? |
|---|---|---|---|---|
| `invalid_api_key` | 401 | `authentication_error` | Missing or invalid API key. Send it as: Authorization: Bearer pk_live_… | No: fix the request |
| `insufficient_scope` | 403 | `permission_error` | This API key doesn't have the scope this endpoint needs. | No: fix the request |
| `plan_upgrade_required` | 403 | `permission_error` | This site's plan doesn't include this. Upgrade in the dashboard under Billing. | No: fix the request |
| `not_found` | 404 | `invalid_request_error` | Not found. | No: fix the request |
| `site_not_found` | 404 | `invalid_request_error` | No site with that ID is available to this API key. | No: fix the request |
| `invalid_json` | 400 | `invalid_request_error` | The request body isn't valid JSON. | No: fix the request |
| `invalid_parameter` | 400 | `invalid_request_error` | A parameter is invalid. | No: fix the request |
| `body_too_large` | 413 | `invalid_request_error` | The request body is too large. | No: fix the request |
| `feature_unavailable` | 409 | `invalid_request_error` | This isn't available for this site. | No: fix the request |
| `idempotency_key_reused` | 422 | `invalid_request_error` | This Idempotency-Key was already used with a different request body. | No: fix the request |
| `rate_limited` | 429 | `rate_limit_error` | Too many requests. Retry after the time in the Retry-After header. | Yes, after `Retry-After` |
| `internal_error` | 500 | `api_error` | Something went wrong on our side. | Yes, with backoff |

### invalid_api_key

HTTP 401. Check the header is exactly `Authorization: Bearer pk_live_…`, with no quotes or extra spaces, and that the key hasn't been revoked in Site settings → API and AI access.

### insufficient_scope

HTTP 403. The endpoint needs a scope this key doesn't have (see **Scope** on each endpoint). Create a key with **Can send events** or **Can manage** ticked; scopes can't be added to an existing key.

### plan_upgrade_required

HTTP 403. The site's plan doesn't include this endpoint: the API and MCP need Growth, and `/v1/accounts` and `/v1/handoffs` need Business. The message names the plan. The site's owner can upgrade under **Billing** in the dashboard; it takes effect within a minute ([plans](https://www.punchlineanalytics.com/docs/billing)).

### not_found

HTTP 404. The ID doesn't exist on this site, or it was deleted. IDs from another site always return this.

### site_not_found

HTTP 404. `site_id` must be the site the key belongs to. Leave it out on GET endpoints to use the key's site; `GET /v1/sites` shows it.

### invalid_json

HTTP 400. The body didn't parse. Send `Content-Type: application/json` and a JSON object; check for trailing commas and unescaped quotes.

### invalid_parameter

HTTP 400. `param` names the field and `message` says what's wrong with it (allowed values, ranges or lengths). The same rules apply as in the dashboard.

### body_too_large

HTTP 413. Bodies are capped at 64 KB (1 MB for `POST /v1/events`). Send events in batches of up to 1,000, or narrow the query.

### feature_unavailable

HTTP 409. The report needs something this site hasn't set up yet: a connected GA4 property for `GET /v1/reconciliation`, or at least one goal for conversion metrics. The message says which; set it up in Site settings.

### idempotency_key_reused

HTTP 422. An `Idempotency-Key` can only be reused with the identical body. Use a new key for a different request.

### rate_limited

HTTP 429. 120 requests a minute per key. Wait for the `Retry-After` seconds; `RateLimit-Remaining` and `RateLimit-Reset` on every response let you pace before you hit it.

### internal_error

HTTP 500. Our side failed. Retry with exponential backoff (writes are safe to retry with an `Idempotency-Key`). If it persists, tell us the time and endpoint.
