curl --request GET \
--url https://api.upliftai.org/v1/campaigns/{campaignId}/stats \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.upliftai.org/v1/campaigns/{campaignId}/stats"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.upliftai.org/v1/campaigns/{campaignId}/stats', 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://api.upliftai.org/v1/campaigns/{campaignId}/stats",
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://api.upliftai.org/v1/campaigns/{campaignId}/stats"
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://api.upliftai.org/v1/campaigns/{campaignId}/stats")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.upliftai.org/v1/campaigns/{campaignId}/stats")
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{
"dialed": 412,
"connected": 168,
"booked": 74,
"notInterested": 51,
"wrongPerson": 9,
"voicemail": 96,
"noAnswer": 121,
"silentPickup": 19,
"failed": 6,
"analysisFailed": 2,
"callbacks": 12,
"scored": 168,
"avgScore": 76,
"great": 83,
"ok": 61,
"low": 24,
"totalDurationSec": 9284,
"avgDurationSec": 55,
"totalContacts": 850,
"attemptedContacts": 318,
"resolvedContacts": 262,
"retrying": 44,
"exhausted": 33,
"projectedRevenue": 333000,
"sentiment": {
"happy": 91,
"neutral": 52,
"annoyed": 14,
"unknown": 11
}
}{
"message": "invalid authorization",
"error": "Unauthorized",
"statusCode": 401
}{
"message": "Forbidden resource",
"error": "Forbidden",
"statusCode": 403
}{
"message": "Campaign not found: 00000000-0000-4000-8000-000000000000",
"error": "Not Found",
"statusCode": 404
}Aggregate stats for a campaign
Counters for every run of the campaign, written by the post-call pipeline — all zero until the first call finishes.
Note: Scores, conversions, and sentiment are computed by LLMs — best effort, with room to be wrong. We are continuously closing the tracking gaps, but for sensitive workloads we recommend running your own analysis on the transcripts delivered over webhooks.
curl --request GET \
--url https://api.upliftai.org/v1/campaigns/{campaignId}/stats \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.upliftai.org/v1/campaigns/{campaignId}/stats"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.upliftai.org/v1/campaigns/{campaignId}/stats', 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://api.upliftai.org/v1/campaigns/{campaignId}/stats",
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://api.upliftai.org/v1/campaigns/{campaignId}/stats"
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://api.upliftai.org/v1/campaigns/{campaignId}/stats")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.upliftai.org/v1/campaigns/{campaignId}/stats")
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{
"dialed": 412,
"connected": 168,
"booked": 74,
"notInterested": 51,
"wrongPerson": 9,
"voicemail": 96,
"noAnswer": 121,
"silentPickup": 19,
"failed": 6,
"analysisFailed": 2,
"callbacks": 12,
"scored": 168,
"avgScore": 76,
"great": 83,
"ok": 61,
"low": 24,
"totalDurationSec": 9284,
"avgDurationSec": 55,
"totalContacts": 850,
"attemptedContacts": 318,
"resolvedContacts": 262,
"retrying": 44,
"exhausted": 33,
"projectedRevenue": 333000,
"sentiment": {
"happy": 91,
"neutral": 52,
"annoyed": 14,
"unknown": 11
}
}{
"message": "invalid authorization",
"error": "Unauthorized",
"statusCode": 401
}{
"message": "Forbidden resource",
"error": "Forbidden",
"statusCode": 403
}{
"message": "Campaign not found: 00000000-0000-4000-8000-000000000000",
"error": "Not Found",
"statusCode": 404
}Authorizations
Project API key (sk_api_…).
Path Parameters
Response
The campaign's counters — zero across the board before the first call completes.
Counters for one campaign. A dial lands in at most one of connected, voicemail, noAnswer, silentPickup, failed and analysisFailed, so those sum to dialed or just under. Grading, sentiment and revenue figures cover the connected subset.
Call attempts placed. A contact retried twice counts twice.
Attempts that reached a person and were graded.
Conversions recorded — appointments booked, leads qualified, payments promised, depending on the campaign's goal.
Answered, but the callee never spoke — a dead line, an unannounced machine, or a carrier answering on the callee's behalf. Not graded.
Attempts that never reached the callee (carrier rejection, no route, no credit). Busy and unreachable dials count in dialed but land in no bucket here.
Calls that reached a person but could not be scored. They count in no other counter here, not even connected.
Calls where the contact asked to be rung back. The campaign callbacks endpoint lists them.
Graded calls with a score, equal to great + ok + low. A call whose every rule was N/A has score: null and is left out.
Mean score over scored, rounded. null until a call has a score.
Scored 80 or above.
Scored 60 to 79.
Scored under 60.
Rounded mean over the calls that reported a duration.
Contacts enrolled, counted across every run.
Contacts dialed at least once.
Contacts the campaign is finished with — reached, out of retries, or skipped.
Contacts waiting out a retry backoff before the next attempt.
Contacts that used every retry without reaching anyone. Counted in resolvedContacts too.
booked priced with the campaign's config.economics, in the same unit those values used. 0 when economics were never configured.
How post-call analysis read the contact's mood.
Show child attributes
Show child attributes
