GET /analytics/campaigns
Returns aggregated UTM campaign performance for the active Organizational Unit. Each row in the response represents a unique UTM value (campaign, source, or medium) discovered from the metadata of ingested engagement events. Use this endpoint to power campaign performance dashboards, validate channel ROI, or pull attribution signals into your own BI stack.
When to Use This
- Channel performance reporting: pull weekly or monthly campaign rollups into a BI tool or shared dashboard.
- Attribution review: see which campaigns, sources, or mediums are tied to MQLs, pipeline, and closed-won revenue.
- Tagging hygiene audits: list every distinct UTM value to spot typos or casing drift in your campaign tagging.
- Lead drilldown automation: combine the aggregate endpoint with the lead drilldown endpoint to pull contact-level detail for a specific campaign.
Authentication
Required: Yes. Use a Personal Access Token or a Service Account credential. All results are scoped to the active Organizational Unit.
Request
Endpoint: GET /analytics/campaigns
Headers:
Accept: application/json
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
group_by | string | No | One of campaign, source, or medium. Selects which UTM key to aggregate on (utm_campaign, utm_source, or utm_medium). Defaults to campaign. Unknown values fall back to campaign. |
start_date | string | No | Lower bound for event occurred_at. Accepts YYYY-MM-DD or RFC3339. Omit for no lower bound. |
end_date | string | No | Upper bound for event occurred_at. Accepts YYYY-MM-DD or RFC3339. Omit for no upper bound. |
Example:
curl -u "CLIENT_ID:CLIENT_SECRET" \
-H "Accept: application/json" \
"https://your-instance.com/analytics/campaigns?group_by=campaign&start_date=2026-03-01&end_date=2026-03-31"
Response
Success Response
Status: 200 OK
The response is an envelope containing the requested grouping and an array of campaign rows, sorted by total event count descending.
| Field | Type | Description |
|---|---|---|
group_by | string | The grouping that was applied: campaign, source, or medium. |
campaigns | array | One row per unique UTM value with at least one matching event in the requested window. Empty array when no events match. |
Each entry in campaigns has the following fields:
| Field | Type | Description |
|---|---|---|
name | string | The UTM value (e.g. spring-2026-launch, linkedin, email). |
total_events | integer | Count of events whose metadata contains the chosen UTM key. Counts only the canonical event in a normalized pair (duplicates are excluded). |
unique_leads | integer | Distinct leads with at least one event tagged to this UTM value. |
mql_count | integer | Distinct leads whose current engagement_level is Warm or Hot. This is a current snapshot, not historical — leads who have since cooled off are excluded. |
total_points | integer | Sum of metadata.score_delta across the campaign's events. Events that did not trigger scoring contribute 0. |
deals_count | integer | Distinct deals reachable from any of the campaign's leads via the lead → account → deal join. Requires the lead to have an account_id. |
pipeline_value | number | Sum of distinct deal amount values across all deals reachable from the campaign's leads, regardless of stage. Multi-touch and unweighted: the same deal counts in full toward every campaign that touched the account. |
closed_won_value | number | Subset of pipeline_value filtered to deals whose stage matches closedwon or closed_won (case-insensitive). Same multi-touch caveat applies. |
first_event_at | string or null | RFC3339 timestamp of the earliest event for this campaign, or null when no events fall in the window. |
last_event_at | string or null | RFC3339 timestamp of the most recent event for this campaign, or null when no events fall in the window. |
Example Response:
{
"group_by": "campaign",
"campaigns": [
{
"name": "spring-2026-launch",
"total_events": 1284,
"unique_leads": 312,
"mql_count": 47,
"total_points": 6420,
"deals_count": 12,
"pipeline_value": 485000,
"closed_won_value": 120000,
"first_event_at": "2026-03-01T08:14:22Z",
"last_event_at": "2026-03-31T19:42:01Z"
},
{
"name": "evergreen-content",
"total_events": 642,
"unique_leads": 198,
"mql_count": 18,
"total_points": 2310,
"deals_count": 4,
"pipeline_value": 95000,
"closed_won_value": 0,
"first_event_at": "2026-03-02T11:01:08Z",
"last_event_at": "2026-03-30T22:17:55Z"
}
]
}
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 401 | Unauthorized | Check your API credentials. |
| 403 | Forbidden | Ensure your account has access to analytics data for the active Organizational Unit. |
| 500 | Server Error | Retry later or contact support if the issue persists. |
GET /analytics/campaigns/{campaign_name}/leads
Returns the individual leads attributed to a single UTM value. Use this when you have a campaign of interest from the aggregate endpoint and want to pull contact-level detail for outreach, QA, or reporting.
The response is capped at 500 leads, ordered by earliest first touch (most recent first).
Authentication
Required: Yes. Same as the aggregate endpoint.
Request
Endpoint: GET /analytics/campaigns/{campaign_name}/leads
| Path Parameter | Type | Required | Description |
|---|---|---|---|
campaign_name | string | Yes | The exact UTM value to look up. URL-encode if it contains spaces or special characters. |
| Query Parameter | Type | Required | Description |
|---|---|---|---|
group_by | string | No | One of campaign, source, or medium. Selects which UTM key to match campaign_name against. Defaults to campaign. Must match the grouping used to discover the value. |
start_date | string | No | Lower bound for event occurred_at. Accepts YYYY-MM-DD or RFC3339. |
end_date | string | No | Upper bound for event occurred_at. Accepts YYYY-MM-DD or RFC3339. |
Example:
curl -u "CLIENT_ID:CLIENT_SECRET" \
-H "Accept: application/json" \
"https://your-instance.com/analytics/campaigns/spring-2026-launch/leads?group_by=campaign&start_date=2026-03-01"
Response
Status: 200 OK
| Field | Type | Description |
|---|---|---|
campaign_name | string | Echoes the path parameter so the caller can confirm what was queried. |
leads | array | Lead records attributed to the campaign. Up to 500 entries. Empty array when no leads match. |
Each entry in leads has the following fields:
| Field | Type | Description |
|---|---|---|
lead_id | string | UUID of the lead. |
display_name | string or null | Best display value for the lead, derived from email, phone, social, external ID, or cookie alias in that order. |
email | string or null | Primary email alias when the lead has one. |
first_name | string or null | Lead's first name from metadata (first_name or firstname). |
last_name | string or null | Lead's last name from metadata (last_name or lastname). |
title | string or null | Job title from lead metadata. |
company | string or null | Company name from lead metadata. |
score | integer | Total engagement score for the lead. Defaults to 0 when no score has been calculated. |
level | string or null | Current engagement level (Cold, Warm, Hot, etc.). |
account_id | string or null | UUID of the lead's associated account, when one is matched. |
account_name | string or null | Display name of the matched account. |
deal_stage | string or null | Stage of the most recent associated deal, when an account-linked deal exists. |
deal_amount | number or null | Amount of the most recent associated deal. |
first_touch_at | string or null | RFC3339 timestamp of this lead's earliest event tagged to the campaign. |
last_touch_at | string or null | RFC3339 timestamp of this lead's most recent event tagged to the campaign. |
Example Response:
{
"campaign_name": "spring-2026-launch",
"leads": [
{
"lead_id": "9b1a72c2-2d2c-4d8b-9a5e-2e3f4a1b9c10",
"display_name": "rachel.kim@example.com",
"email": "rachel.kim@example.com",
"first_name": "Rachel",
"last_name": "Kim",
"title": "Director of Demand Generation",
"company": "Acme Co",
"score": 78,
"level": "Hot",
"account_id": "0f3c4d5e-6789-4abc-9def-0123456789ab",
"account_name": "Acme Co",
"deal_stage": "negotiation",
"deal_amount": 75000,
"first_touch_at": "2026-03-04T13:21:08Z",
"last_touch_at": "2026-03-31T17:05:42Z"
}
]
}
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 400 | campaign_name is required | Pass a non-empty value in the path. |
| 401 | Unauthorized | Check your API credentials. |
| 403 | Forbidden | Ensure your account has access to analytics data for the active Organizational Unit. |
| 500 | Server Error | Retry later or contact support if the issue persists. |
Important Notes
Auto-Discovery From UTMs
Campaigns are not stored as standalone records — they are discovered at query time from the utm_campaign, utm_source, or utm_medium keys in event metadata. Any unique UTM string becomes its own row. Casing differences (Spring2026 vs spring2026) and whitespace differences create separate campaigns. Cleanup must happen upstream in the link generators producing the events; kenbun does not offer rename, alias, or merge for campaigns.
Deduplication Of Normalized Events
The aggregator filters out events where normalized = true AND normalized_parent = false, so a normalized event pair counts only once (the canonical row). Events that are not part of a normalization pair are counted as-is.
MQL Is Current, Not Historical
mql_count reflects each lead's current engagement_level, not their level at the time of the campaign event. A lead who was Hot during the campaign and has since cooled to Cold will not be counted. This makes the MQL number useful for "how many leads from this campaign are still warm?" but not for "how many leads did this campaign ever heat up?"
Multi-Touch, Unweighted Pipeline
pipeline_value and closed_won_value are computed by joining each campaign's leads to their accounts and then to deals on those accounts. The full deal amount is credited to every campaign that touched the account. Two campaigns that each generated one event for the same account both show the deal's full amount. Summing pipeline columns across rows will overcount; treat each row as "deals this campaign was involved in" rather than a clean attribution slice.
Pipeline Requires Account Linkage
Pipeline metrics flow through the lead → account → deal join. Leads without an account_id contribute to total_events, unique_leads, and mql_count but never to deals_count, pipeline_value, or closed_won_value.
Organizational Unit Scoping
All results are scoped to the active Organizational Unit determined by the request context (typically the active-OU cookie set by POST /org-units/active). Events tagged to the same UTM value in different OUs do not aggregate together.
Related Endpoints
- GET /events -- The underlying event stream that feeds campaign aggregation
- GET /events-analytics -- Time-series counts of events
- GET /deals/attribution-insights -- Score-by-milestone attribution across closed-won deals
- GET /score-history -- Historical score for an individual lead
See Also
- Campaigns analytics page -- The end-user view backed by these endpoints
- Main API Documentation