Analytics Insights
curl --request GET \
--url https://app.tuco.ai/api/analytics/insights \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/analytics/insights"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.tuco.ai/api/analytics/insights', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.tuco.ai/api/analytics/insights",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/analytics/insights"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.tuco.ai/api/analytics/insights")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/analytics/insights")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"insights.channel": {},
"insights.unique": {},
"insights.cadence.netNew": 123,
"insights.cadence.nudge": 123,
"insights.cadence.response": 123,
"insights.cadence.sentByStep": [
123
],
"insights.cadence.firstReplyByStep": [
123
],
"insights.cadence.firstReplies": 123,
"insights.cadence.laterReplies": 123,
"insights.cadence.avgThreadLength": 123,
"insights.conversations": 123,
"insights.daily": [
{}
],
"insights.hourly": [
{}
],
"insights.engagement": {},
"insights.truncated": true,
"sources": [
{}
]
}Analytics
Analytics Insights
Conversation-shaped analytics for the Tuco AI iMessage API: channel and direction split, a cadence-step funnel with reply attribution, and per-day/hour series.
GET
/
api
/
analytics
/
insights
Analytics Insights
curl --request GET \
--url https://app.tuco.ai/api/analytics/insights \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/analytics/insights"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.tuco.ai/api/analytics/insights', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.tuco.ai/api/analytics/insights",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/analytics/insights"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.tuco.ai/api/analytics/insights")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/analytics/insights")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"insights.channel": {},
"insights.unique": {},
"insights.cadence.netNew": 123,
"insights.cadence.nudge": 123,
"insights.cadence.response": 123,
"insights.cadence.sentByStep": [
123
],
"insights.cadence.firstReplyByStep": [
123
],
"insights.cadence.firstReplies": 123,
"insights.cadence.laterReplies": 123,
"insights.cadence.avgThreadLength": 123,
"insights.conversations": 123,
"insights.daily": [
{}
],
"insights.hourly": [
{}
],
"insights.engagement": {},
"insights.truncated": true,
"sources": [
{}
]
}The companion to Analytics. Where that endpoint answers “how many messages”, this one answers “what happened across the conversation”.Every send is classified by its role in the thread — a net-new first touch, a cold follow-up nudge (the lead hasn’t replied yet), or a response (you replied to their reply). Each conversation’s first reply is credited to the cadence step that earned it, and later inbound messages are counted as reply-to-reply turns.Roles are computed over each conversation’s entire history, then counted inside your window — so a step-4 send still reads as step 4 even when you only look at a single day.Scope is a single workspace (the one behind your key). Agency roll-ups are not offered here yet.
Authentication
Authorization: Bearer tuco_sk_xxxxxxxxxxxxx
Query parameters
string
Inclusive start of the window — an ISO 8601 timestamp (
2026-08-01T00:00:00Z) or a calendar day (2026-08-01). Date-only bounds cover whole calendar days in the workspace timezone. Defaults to the trailing 30 days.string
Inclusive end of the window — ISO timestamp or
YYYY-MM-DD.string
Restrict to messages sent on a single Tuco line (24-char ObjectId hex).
string
Restrict to one attributed origin (
gohighlevel, campaign, api, dashboard, autopilot, unattributed, …). The sources breakdown always lists every origin so you can switch.string
IANA zone to cut days and hours in (e.g.
America/New_York). Defaults to the workspace timezone. tz is accepted as an alias.Example
curl "https://app.tuco.ai/api/analytics/insights?dateFrom=2026-07-13&dateTo=2026-08-17&lineId=6a550c00d3c70557446422af" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
Success response (200)
{
"success": true,
"filter": {
"lineId": "6a550c00d3c70557446422af",
"source": null,
"dateFrom": "2026-07-13T04:00:00.000Z",
"dateTo": "2026-08-18T03:59:59.999Z",
"timezone": "America/New_York"
},
"insights": {
"channel": { "attempted": 1986, "imessageDelivered": 948, "smsFallbackSent": 0, "notOnImessage": 1038, "notReached": 1038, "failed": 0, "reached": 948, "imessageReplied": 418, "smsReplied": 0, "totalReplied": 418 },
"unique": { "attempted": 586, "reached": 254, "notOnImessage": 332, "replied": 148 },
"cadence": {
"netNew": 329, "nudge": 491, "response": 480,
"sentByStep": [329, 154, 137, 85, 115],
"firstReplyByStep": [149, 14, 23, 9, 6],
"inboundFirst": 4, "firstReplies": 205, "laterReplies": 484,
"avgThreadLength": 5.96
},
"engagement": { "delivered": 389, "read": 177, "voiceNotes": 150, "attachments": 150, "scheduled": 84, "failed": 13 },
"conversations": 333,
"daily": [{ "day": "2026-07-13", "imessageDelivered": 2, "smsFallbackSent": 0, "notReached": 0, "replied": 2, "activeConversations": 2 }],
"hourly": [{ "hour": 0, "sent": 23, "replied": 10 }],
"truncated": false
},
"sources": [{ "source": "gohighlevel", "sent": 495, "reached": 210 }],
"explainer": { "netNew": "First outbound message ever sent to a lead …" }
}
object
Delivery-aware split.
attempted = all sends; imessageDelivered = reached over iMessage; smsFallbackSent = fell back and an SMS was actually dispatched; notOnImessage = recipient has no iMessage (status fallback), a list-quality signal distinct from failed (technical); notReached = notOnImessage with no SMS dispatched (reached nobody); reached = delivered + dispatched SMS. Plus imessageReplied / smsReplied / totalReplied.object
Distinct recipients, not message counts:
attempted = tried, reached = a message actually got to, notOnImessage = leads never delivered an iMessage (the headline when there is no SMS fallback), replied = replied. Reply rate should use replied / reached so it’s comparable across workspaces regardless of iMessage delivery.number
First outbound message ever sent to a lead (their first touch).
number
Cold follow-up sends — outbound to a lead who has not replied yet (cadence step 2+).
number
Outbound sent after the lead had already replied (“I replied to their reply”).
number[]
Cold outbound sends by cadence step — index 0..4 = step 1, 2, 3, 4, 5+.
number[]
Each conversation’s first reply, credited to the step that earned it (same 1..5+ index).
number
Conversations that opened — one per lead that replied at least once.
number
Reply-to-reply turns — inbound after the conversation had already opened.
number
Average messages per conversation touched in the window.
number
Distinct leads with at least one message in the window.
object[]
Per-day iMessage delivered / SMS fallback / not-reached / replies, plus that day’s distinct
activeConversations (drives the day drill-down).object[]
Sent and replies by hour of day (0..23) in the workspace timezone.
object
Delivery/read receipts, voice notes, attachments, scheduled sends.
boolean
true when the workspace history was too large to replay for this request; cadence and daily fields are then empty. Narrow the window or filter by line.object[]
Where the sending came from. A message carrying a GoHighLevel location counts as GoHighLevel even when sent through the API. Messages with no marker are reported as
unattributed.Error responses
| Status | When |
|---|---|
401 | Missing or invalid API key |
500 | Server error computing insights |