---
title: "Get Campaign Visitors"
method: GET
path: "/analytics/campaign-visitors"
tags: ["Analytics"]
---

# Get Campaign Visitors

`GET /analytics/campaign-visitors`

## Request

- Base URL: `https://be.graph8.com/api/v1`
- URL: `https://be.graph8.com/api/v1/analytics/campaign-visitors`
- Auth: HTTP bearer

Campaign Visitors report for the authenticated org (analytics report a23).

Returns, for the resolved window:

  * **summary** — KPI tiles (page events, campaign clicks, unique contacts /
    steps, identified visitors, and the encrypted/plaintext/unparseable
    diagnostic split). Click counters are email-security-scanner filtered
    (humans only) so they match the in-app report — raw scanner traffic
    (Microsoft Defender / SafeLinks) is excluded.
  * **top_campaign_steps** — per sequencer step: clicks, unique contacts, a
    human-readable label, and send-side context (emails sent / with link).
  * **link_engagement** — per clicked destination URL: human clicks, unique
    contacts, the emails-with-link denominator, and CTR% (clamped 0–100).
  * **top_contacts** — who clicked, with resolved name/email (PII; omit via
    `include_contacts=false`).

Enrichment (labels, email, send counts, CTR) is best-effort: a field is
``null`` when it couldn't be resolved, never a fabricated zero. An org with
no visitor-tracking pipeline returns an empty report with
``summary.jitsu_wired = false``.

Agency keys: pass ``X-Target-Org-Id`` to run the report for a specific client
org within your authorized set.

## Query parameters

- `start_date` string, date, nullable: Window start (inclusive), YYYY-MM-DD. If omitted, derived from `days`.
- `end_date` string, date, nullable: Window end (inclusive), YYYY-MM-DD. If omitted, today.
- `days` integer, nullable: Rolling 'last N days from today'. Ignored when start_date/end_date are supplied.
- `limit` integer: Row cap for the steps + contacts tables
- `include_contacts` boolean: Include the Top contacts table (contact name + email — PII). Set false to omit it entirely for a privacy-lean response.

## Headers

- `Authorization` string, nullable
- `X-Target-Org-Id` string, nullable

## Response `200`

Successful Response

- ApiResponseCampaignVisitorsReport
  - `data` CampaignVisitorsReport, required: Composite Campaign Visitors report for a single org (analytics report a23).
    - `window` CampaignVisitorsWindow, required: The resolved reporting window echoed back to the caller.
      - `start_date` string, required: Window start (inclusive), YYYY-MM-DD
      - `end_date` string, required: Window end (inclusive), YYYY-MM-DD
      - `days` integer, required: Inclusive calendar-day span of the window
      - `mode` string, required: 'anchored' (explicit start/end) or 'rolling' (last-N-days)
    - `summary` CampaignVisitorsSummary, required: KPI tiles for the org's campaign-click activity in the window. All click-derived counters are email-security-scanner filtered (humans only) when ``SEQUENCER_VISITOR_SCANNER_FILTER=enforce`` — the same honest counts the in-app report shows. ``total_page_events`` is all page volume (not filtered).
      - `total_page_events` integer, required: All page events in the window (campaign + non-campaign)
      - `pageview_events` integer, required: Parseable campaign-attributed click events
      - `unique_contacts` integer, required: Distinct contacts who clicked a campaign link
      - `unique_campaign_steps` integer, required: Distinct sequencer steps that drew clicks
      - `identified_visitors` integer, required: Distinct identified web sessions on campaign clicks
      - `jitsu_wired` boolean, required: False when the org has no visitor-tracking pipeline / no page events
      - `unparseable_pageviews` integer, required: Campaign clicks whose tracking hash could not be decoded
      - `encrypted_pageviews` integer, required: Diagnostic: campaign clicks with an encrypted (#c=e1.) hash
      - `plaintext_pageviews` integer, required: Diagnostic: campaign clicks with a plaintext hash
      - `contact_rows` integer, required: Total (contact, step) click groups before the `limit` cap
      - `step_rows` integer, required: Total distinct campaign steps before the `limit` cap
      - `link_rows` integer, required: Total distinct clicked (host, path) before the ceiling
      - `metrics` CampaignVisitorsMetric[]: The counters above with friendly labels, plain-language definitions and a scope, in display order. Same values as the individual keys; use this to present the summary.
        - `key` string, required: The `summary` field this entry describes (same value)
        - `label` string, required: Human-readable metric name
        - `value` union, required: Same value as `summary.<key>`
          - integer
          - boolean
        - `definition` string, required: What is counted, in plain language
        - `scope` string, required: 'site' = all tracked page views (context only), 'campaign' = email-link clicks, 'diagnostic' = tracking-health detail, 'table' = row counts behind the tables
    - `top_campaign_steps` CampaignVisitorStep[], required
      - `step_id` string, required: Sequencer step id (globally-unique CUID)
      - `events` integer, required: Campaign click events attributed to this step
      - `unique_contacts` integer, required: Distinct contacts who clicked from this step
      - `first_seen` string, nullable: Earliest click timestamp (ISO)
      - `last_seen` string, nullable: Latest click timestamp (ISO)
      - `step_label` string, nullable: Human-readable '{sequence} / Step {n}' when resolvable
      - `emails_sent` integer, nullable: EMAIL sends dispatched from this step in the window
      - `emails_with_link` integer, nullable: Of those sends, how many carried a link
    - `link_engagement` CampaignVisitorLink[], required
      - `host` string, required: Clicked destination host
      - `path` string, required: Clicked destination path
      - `events` integer, required: Human click events on this URL (scanner-filtered)
      - `unique_contacts` integer, required: Distinct contacts who clicked this URL
      - `last_seen` string, nullable: Latest click timestamp (ISO)
      - `step_ids` string[]: Sequencer steps whose links drove these clicks
      - `emails_with_link` integer, nullable: Sends whose template embedded this URL (CTR denominator)
      - `ctr_percent` number, nullable: events ÷ emails_with_link × 100, clamped [0,100]; null when no denominator
      - `step_labels` string[], nullable: Human-readable labels for step_ids when resolvable
      - `ctr_unavailable_reason` string, nullable: Why `ctr_percent` is null: 'no_matching_send' = no email sent in the window contained this exact link (e.g. the click came from an email sent before the window, or from a link shared outside a sequence); 'send_lookup_unavailable' = the send counts could not be loaded this time. Null whenever `ctr_percent` has a value.
    - `top_contacts` CampaignVisitorContact[], required
      - `contact_id` integer, required: Per-org contact id
      - `step_id` string, nullable: Sequencer step the click was attributed to
      - `visits` integer, required: Click events by this contact in the window
      - `last_seen` string, nullable: Latest click timestamp (ISO)
      - `last_path` string, nullable: Most recent path this contact clicked to
      - `name` string, nullable: Contact full name when resolvable (PII)
      - `email` string, nullable: Contact work email when resolvable (PII)
      - `step_label` string, nullable: Human-readable step label when resolvable
  - `pagination` PaginationMeta
    - `page` integer, required: Current page number (1-indexed)
    - `limit` integer, required: Items per page
    - `total` integer, required: Total number of items
    - `has_next` boolean, required: Whether there are more pages
    - `next_cursor` string, nullable: Opaque cursor for the next page; null when there are no more items. Pass it back as the `cursor` query param to fetch the following page without computing page numbers.
    - `total_available` integer, nullable: Search endpoints only: how many records really match, when known. Can be larger than `total`, which is capped at the 10,000 results that can be paged. For company search this is a distinct-company estimate. Null when unknown; absent outside search.
    - `total_is_lower_bound` boolean, nullable: Search endpoints only: true when more records match than `total` shows (the result set is larger than the 10,000 results that can be paged). Absent outside search.
    - `notes` string[], nullable: Search endpoints only: plain-language notes about the result, e.g. that more records match than can be paged, or that a reversed `between` range was put in order.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/graph8/apis/graph8-developer-api.md) · [All operations](https://skmtc.dev/graph8/apis/graph8-developer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/graph8/apis/graph8-developer-api/revisions/2e7cfa0ca921?raw)
