Analytics
Campaign and workspace performance, engagement, deliverability and reply classifications, plus your own analytics queries.
Return campaign or workspace performance for a reporting window
/v1/analytics/campaign-performanceanalytics.get-campaign-performancegroupBy is day, step, segment, sender_route, campaign, or language. channel is email or linkedin. With campaignId and neither from nor to, the window is the campaign to date: from its first send or first imported day, whichever is earlier, in timeZone (default UTC), to today, up to 3660 days; from and to in the response hold it. Without campaignId, from and to are required. groupBy=sender_route (email, campaignId required) splits the campaign by the sender rule each contact's first email went out under: one row per provider rule (key the provider slug, label its name), everyone_else and not_routed (sent with no rules or with sender routing off), each with routeKey equal to key. A contact stays in one row; its sends, contacted, distinct repliers, interested replies and bounces in the window count there. Opens, clicks, unsubscribes, bounceBreakdown and the opportunity and meetings fields are not split by route (0 or absent on the rows). The window metrics are the campaign's groupBy=day ones, opens and clicks included, so the rows need not add up to them: a contact with no accepted send is in no row. groupBy=language (needs campaignId) returns one row per language the campaign's emails went out in, keyed by that language, sorted by sends with unknown (sends that recorded no language) last: sends, contacted, replies and interested (distinct contacts), the reply rate over contacted. A reply counts in the language of the send it answers. Opens, clicks and bounces are not split by language, and the LinkedIn channel has no language rows. The window metrics are the campaign's groupBy=day ones, where a contact emailed in two languages counts once. groupBy=campaign (email, no campaignId, a window up to 3660 days) returns one row per campaign with email activity in the window, with campaignId on each row, in one read: a row's counts are the window metrics that campaign's own groupBy=day read returns with the same non-UTC timeZone, so lifetime numbers for every campaign take one call. Without a timeZone (or with UTC) the rows add up UTC days, while the campaign's own read counts contacted and replies once across the window, so there a contact active on several days counts once per day. Its unique opens add up the campaign's days, its unique clicks count each human clicker once over the UTC window as the campaign's own read does, the opportunity, meetings and bounceBreakdown fields are left out, and the window metrics add up the rows. Rates are fractions from 0 to 1. Day buckets are UTC calendar dates unless timeZone is an IANA zone that is not UTC: then email day rows (groupBy=day), their meetings and opportunity cohort count that zone's calendar days, from its midnight on from to its midnight after to, and the window metrics add up the rows; for one campaign, unique opens and unique clicks are the campaign window's, as without a zone. Opens, unique opens and unsubscribes count that zone's days too. Imported history and days before 2026-08-24 stay on UTC days: imported history arrives from each vendor as date-only day buckets (Instantly yyyy-MM-dd, Smartlead and Apollo day buckets, SourceDailyStat.Date is a DateOnly), so it has no time of day to move. Step, segment and sender_route rows and the LinkedIn channel count the window from the zone's midnights too; a segment row's opens and unsubscribes stay on their stored UTC days. groupBy=inbox is served by deliverability. Email openCount, uniqueOpenCount and openRate count human opens; machineOpenCount (window metrics and step rows) counts contacts whose every open was a machine open (Apple MPP, a crawler, or an open within a minute of the send). Opens are an estimate and a floor, never the headline: reply rate is. Email clickCount is Kenso-tracked human clicks; botClickCount is separate. The window metrics carry bounceBreakdown (type and reason split) when a categorized bounce exists.
Parameters
400 workspace_required).Response 200
data is a CampaignPerformanceModel.
curl -X GET "https://api.ken.so/v1/analytics/campaign-performance" \ -H "Authorization: Bearer $KEN_API_KEY" \ -H "Ken-Version: 2026-11-02" \ -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID"
{
"success": true,
"data": {
"campaignId": 812,
"from": "2026-09-23T15:02:11Z",
"to": "2026-09-23T15:02:11Z",
"timeZone": "string",
"groupBy": "string",
"channel": "string",
"metrics": {
"sentCount": 1,
"deliveredCount": 1,
"bounceCount": 1,
"openCount": 1,
"uniqueOpenCount": 1,
"clickCount": 1,
"uniqueClickCount": 1,
"botClickCount": 1,
"machineOpenCount": 1,
"replyCount": 1,
"unsubscribeCount": 1,
"interestedCount": 1,
"contactedCount": 1,
"inviteCount": 1,
"acceptedCount": 1,
"messageCount": 1,
"deliveryRate": 0.5,
"openRate": 0.5,
"clickRate": 0.5,
"replyRate": 0.5,
"bounceRate": 0.5,
"bounceBreakdown": {
"hardCount": 1,
"softCount": 1,
"unknownCount": 1,
"byReason": {
"key": 1
}
},
"opportunitiesCreated": 1,
"pipelineAmount": 0.5,
"wonAmount": 0.5,
"wonCount": 1,
"meetingsBooked": 1
},
"rows": [
{
"key": "string",
"date": "string",
"stepOrder": 1,
"segmentId": 812,
"campaignId": 812,
"label": "string",
"routeKey": "string",
"metrics": {
"sentCount": 1,
"deliveredCount": 1,
"bounceCount": 1,
"openCount": 1,
"uniqueOpenCount": 1,
"clickCount": 1,
"uniqueClickCount": 1,
"botClickCount": 1,
"machineOpenCount": 1,
"replyCount": 1,
"unsubscribeCount": 1,
"interestedCount": 1,
"contactedCount": 1,
"inviteCount": 1,
"acceptedCount": 1,
"messageCount": 1,
"deliveryRate": 0.5,
"openRate": 0.5,
"clickRate": 0.5,
"replyRate": 0.5,
"bounceRate": 0.5,
"bounceBreakdown": {
"hardCount": 1,
"softCount": 1,
"unknownCount": 1,
"byReason": {}
},
"opportunitiesCreated": 1,
"pipelineAmount": 0.5,
"wonAmount": 0.5,
"wonCount": 1,
"meetingsBooked": 1
},
"variants": [
{
"variant": "string",
"sentCount": 1,
"acceptedCount": 1,
"replyCount": 1,
"failedCount": 1,
"acceptRate": 0.5
}
],
"notesWaiting": 1,
"notesSkippedFreeAccount": 1,
"notesFailed": 1
}
]
}
}Return a ranked engagement breakdown for one campaign
/v1/analytics/engagementanalytics.get-engagementdimension is device, browser, os, country, region, city, referrer, network, link, or source. Blank defaults to device. limit is a top-N cap from 1 to 200 (default 50). country filters the city dimension. segmentId filters region, network, referrer, and source.
Parameters
400 workspace_required).Response 200
data is a EngagementModel.
curl -X GET "https://api.ken.so/v1/analytics/engagement" \ -H "Authorization: Bearer $KEN_API_KEY" \ -H "Ken-Version: 2026-11-02" \ -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID"
{
"success": true,
"data": {
"campaignId": 812,
"from": "2026-09-23T15:02:11Z",
"to": "2026-09-23T15:02:11Z",
"dimension": "string",
"rows": [
{
"key": "string",
"label": "string",
"count": 1,
"humanCount": 1,
"botCount": 1,
"uniqueContactCount": 1
}
]
}
}Return workspace deliverability for a reporting window
/v1/analytics/deliverabilityanalytics.get-deliverabilitygroupBy is day, domain, or inbox. Rates are fractions from 0 to 1. Day buckets are UTC calendar dates. With a timeZone that is not UTC, day, domain and inbox rows count sequencer sends, replies, bounces and out-of-office replies on that zone's days, from its midnight on from to its midnight after to, and the window sentCount, replyCount, bounceCount, clickCount and rates are the sums of the day rows. Complaints are kept per domain per UTC day with no time of day, so they stay on UTC days. Clicks carry no sending inbox, so delivery rows hold none to move. limit caps domain and inbox rows (1 to 200, default 50). Domain and inbox rows carry reputation (paused, at_risk, watch, good or unknown) and alerts, plain-language alerts with a code, scope, severity, message and recommended action, built from stored signals: enforced send pauses, blocklists, SPF, DKIM and DMARC checks, placement tests, complaints, recipient refusals and, on inbox rows, bounce rates, Microsoft tenant bans and sender rejections. An inbox row also carries its domain's alerts. Day rows carry neither. New alert codes may be added. Reputation and alerts describe the state now; only complaint_rate and high_bounce_rate count the window.
Parameters
400 workspace_required).Response 200
data is a DeliverabilityModel.
curl -X GET "https://api.ken.so/v1/analytics/deliverability" \ -H "Authorization: Bearer $KEN_API_KEY" \ -H "Ken-Version: 2026-11-02" \ -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID"
{
"success": true,
"data": {
"from": "2026-09-23T15:02:11Z",
"to": "2026-09-23T15:02:11Z",
"groupBy": "string",
"timeZone": "string",
"metrics": {
"sentCount": 1,
"replyCount": 1,
"bounceCount": 1,
"clickCount": 1,
"replyRate": 0.5,
"bounceRate": 0.5,
"clickRate": 0.5,
"inboxCount": 1,
"activeInboxCount": 1,
"domainCount": 1,
"inboxesPerDomain": 0.5,
"healthyInboxCount": 1,
"lowActivityInboxCount": 1,
"inactiveInboxCount": 1,
"complaintCount": 1,
"complaintRate": 0.5
},
"rows": [
{
"date": "string",
"domainId": 812,
"domainName": "string",
"inboxId": 812,
"email": "[email protected]",
"inboxCount": 1,
"sentCount": 1,
"replyCount": 1,
"bounceCount": 1,
"clickCount": 1,
"replyRate": 0.5,
"bounceRate": 0.5,
"clickRate": 0.5,
"complaintCount": 1,
"oooCount": 1,
"oooRate": 0.5,
"reputation": "string",
"alerts": [
{
"code": "string",
"scope": "string",
"severity": "string",
"message": "string",
"action": "string",
"since": "2026-09-23T15:02:11Z",
"until": "2026-09-23T15:02:11Z",
"rate": 0.5,
"threshold": 0.5
}
]
}
]
}
}Return the bounce gates in force and each domain's and inbox's verdict for the last 24 hours
/v1/analytics/deliverability-gatesanalytics.get-deliverability-gatesRead-only. Counts accepted cold and 1:1 sends and hard bounces over the last 24 hours, as the sequencer's bounce signal does, and judges each row by the gate the sequencer enforces (Sequencer:SendBlocks:Bounce): inbox rows by the inbox block gate, domain rows by the campaign pause gate (there is no domain bounce block). A row is over when its rate is strictly above threshold with at least minSends sends and minBounces hard bounces, watch when it is not over and its rate is at or above 80% of the threshold, and ok otherwise. isSmallSample marks a row with fewer sends than minSends, which cannot be over. The sequencer itself judges over enforcedWindowHours; isEnforced is false while it only records. A mailbox with no managed domain counts toward its address's domain, except on a free-mail provider (gmail.com), where it has an inbox row only. Rates are fractions from 0 to 1. Rows are worst first; limit caps each list (1 to 200, default 50).
Parameters
400 workspace_required).Response 200
data is a DeliverabilityGatesModel.
limit.limit.curl -X GET "https://api.ken.so/v1/analytics/deliverability-gates?limit=1" \ -H "Authorization: Bearer $KEN_API_KEY" \ -H "Ken-Version: 2026-11-02" \ -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID"
{
"success": true,
"data": {
"from": "2026-09-23T15:02:11Z",
"to": "2026-09-23T15:02:11Z",
"windowHours": 1,
"isEnforced": true,
"enforcedWindowHours": 1,
"domainGate": {
"threshold": 0.5,
"minSends": 1,
"minBounces": 1
},
"inboxGate": {
"threshold": 0.5,
"minSends": 1,
"minBounces": 1
},
"domainCount": 1,
"inboxCount": 1,
"domains": [
{
"domainId": 812,
"domainName": "string",
"inboxCount": 1,
"sentCount": 1,
"bounceCount": 1,
"bounceRate": 0.5,
"verdict": "string",
"isSmallSample": true
}
],
"inboxes": [
{
"inboxId": 812,
"email": "[email protected]",
"domainId": 812,
"domainName": "string",
"sentCount": 1,
"bounceCount": 1,
"bounceRate": 0.5,
"verdict": "string",
"isSmallSample": true
}
]
}
}Return reply classification counts for a reporting window
/v1/analytics/reply-classificationsanalytics.get-reply-classificationsClassification names are lowercase snake_case. interestedCount is the interested class. Counts are people, not messages: a contact counts once per row, in one class (interested if any of its messages is, else its latest reply's class, else its latest message's). Optional campaignId must belong to the workspace. byCampaign, bySegment, and byConfiguration add those breakdowns. Replies count by the time they came in, from UTC midnight on from to UTC midnight after to, unless timeZone is an IANA zone that is not UTC: then from that zone's midnight on from to its midnight after to, totals and breakdowns alike.
Parameters
400 workspace_required).Response 200
data is a ReplyClassificationsModel.
curl -X GET "https://api.ken.so/v1/analytics/reply-classifications" \ -H "Authorization: Bearer $KEN_API_KEY" \ -H "Ken-Version: 2026-11-02" \ -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID"
{
"success": true,
"data": {
"from": "2026-09-23T15:02:11Z",
"to": "2026-09-23T15:02:11Z",
"campaignId": 812,
"totalCount": 1,
"interestedCount": 1,
"counts": [
{
"classification": "string",
"count": 1
}
],
"byCampaign": [
{
"campaignId": 812,
"segmentId": 812,
"configurationId": 812,
"name": "string",
"totalCount": 1,
"interestedCount": 1,
"counts": [
{
"classification": "string",
"count": 1
}
]
}
],
"bySegment": [
{
"campaignId": 812,
"segmentId": 812,
"configurationId": 812,
"name": "string",
"totalCount": 1,
"interestedCount": 1,
"counts": [
{
"classification": "string",
"count": 1
}
]
}
],
"byConfiguration": [
{
"campaignId": 812,
"segmentId": 812,
"configurationId": 812,
"name": "string",
"totalCount": 1,
"interestedCount": 1,
"counts": [
{
"classification": "string",
"count": 1
}
]
}
]
}
}Run an analytics query: metrics split by up to two dimensions over a window, with an optional previous period
/v1/analytics/queryanalytics.queryNeeds the generative_ui feature on your workspace. Until it is on, calls answer 404.
Needs the generative_ui flag (404 while it is off). metrics holds 1 to 8 names, groupBy at most 2 dimensions, limit at most 200 rows, and the window at most 400 days. A null dimension value is Unknown. By default (rateBasisevent_time) rates are event-time ratios: numerator and denominator are counted by event time inside the same window and group, so a rate is usually 0 to 1 but can exceed 1 in short windows, day or week buckets or by step when replies or accepts answer earlier sends (a verdict on such counts is too_early); it is null on a zero denominator. rateBasiscohort counts each reply and accept against the send it answers, in that send's group and day, whenever it arrived, so replies, interested, not interested, accepted and their rates stay within the group's contacted and invites (0 to 1); every row and the totals then carry stillFilling, true while most of their sends are under 7 days old. Other counts are event-time on both bases. previous is null without compareTo. With it, rows grouped by day or week get previous null (the previous window holds other dates; only a week straddling both windows matches its earlier days), so only the totals compare. Each call counts on the workspace's analyticsQueries quota.
Parameters
400 workspace_required).Request body
application/json, optional.
Response 200
data is a AnalyticsQueryResult.
curl -X POST "https://api.ken.so/v1/analytics/query" \
-H "Authorization: Bearer $KEN_API_KEY" \
-H "Ken-Version: 2026-11-02" \
-H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"metrics": [
"string"
],
"groupBy": [
"string"
],
"filters": {
"key": null
},
"window": {
"preset": "string",
"from": "2026-09-23",
"to": "2026-09-23"
},
"compareTo": "string",
"timeZone": "string",
"limit": 1,
"rateBasis": "string"
}'{
"success": true,
"data": {
"window": {
"from": "2026-09-23",
"to": "2026-09-23",
"timeZone": "string"
},
"previousWindow": {
"from": "2026-09-23",
"to": "2026-09-23",
"timeZone": "string"
},
"rows": [
{
"dimensions": {
"key": null
},
"labels": {
"key": "string"
},
"metrics": {
"key": 0.5
},
"previous": {
"key": 0.5
},
"sample": {
"key": 1
},
"stillFilling": true
}
],
"totals": {
"metrics": {
"key": 0.5
},
"previous": {
"key": 0.5
},
"sample": {
"key": 1
},
"stillFilling": true
},
"truncated": true,
"asOf": "2026-09-23T15:02:11Z"
}
}Return one campaign's contacts, replies and interested replies split by step and variant
/v1/analytics/variant-performanceanalytics.get-variant-performanceNeeds the variant_performance feature on your workspace. Until it is on, calls answer 404.
The analytics query with a fixed shape, so a member, an editor and a client seat read which variant of a step wins without the generative_ui flag. Behind its own variant_performance flag (404 while it is off for the workspace). Metrics are contacted, replies, interested, reply_rate and positive_reply_rate, grouped by step and variant, counted the query's way: distinct contacts per row; replies come from the inbox, and out-of-office and the other non-replies do not count. It reads on the query's cohort basis: each reply counts against the step and variant of the send it answers, whenever it arrived, so replies and interested stay within contacted and the rates are 0 to 1 (null when nobody was contacted). Every row and the totals carry stillFilling, true while most of their sends are under 7 days old and replies to them are still arriving. from and to are inclusive calendar days in timeZone (default the campaign's sending zone, else UTC), at most 400 days; leave both out for the campaign to date. Email and LinkedIn rows both come back: LinkedIn rows read "LinkedIn step N" and "LinkedIn variant X", and a contact with no variant is the Unknown row. A campaign of another workspace is 404. Each call counts on the workspace's analyticsQueries quota. Campaigns view.
Parameters
YYYY-MM-DD, inclusive). With to; neither is the campaign to date.YYYY-MM-DD, inclusive).400 workspace_required).Response 200
data is a AnalyticsQueryResult.
curl -X GET "https://api.ken.so/v1/analytics/variant-performance" \ -H "Authorization: Bearer $KEN_API_KEY" \ -H "Ken-Version: 2026-11-02" \ -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID"
{
"success": true,
"data": {
"window": {
"from": "2026-09-23",
"to": "2026-09-23",
"timeZone": "string"
},
"previousWindow": {
"from": "2026-09-23",
"to": "2026-09-23",
"timeZone": "string"
},
"rows": [
{
"dimensions": {
"key": null
},
"labels": {
"key": "string"
},
"metrics": {
"key": 0.5
},
"previous": {
"key": 0.5
},
"sample": {
"key": 1
},
"stillFilling": true
}
],
"totals": {
"metrics": {
"key": 0.5
},
"previous": {
"key": 0.5
},
"sample": {
"key": 1
},
"stillFilling": true
},
"truncated": true,
"asOf": "2026-09-23T15:02:11Z"
}
}