curl --request GET \
--url https://api.goyappr.com/campaigns/{id}/stats \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/stats")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.goyappr.com/campaigns/{id}/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{
"campaign_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "<string>",
"leads_by_status": {
"pending": 812,
"awaiting_disposition": 2,
"completed_success": 141,
"exhausted": 45
},
"leads_total": 123,
"attempts_total": 123,
"attempts_in_flight": 123,
"calls_today": 123,
"max_calls_per_day": 123,
"spent_cents": 123,
"reserved_cents": 123,
"estimate_cents": 123,
"budget_cents": 123,
"last_tick_at": "2023-11-07T05:31:56Z",
"last_tick_result": "<string>",
"last_error": "<string>",
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}{
"error": "<string>",
"code": "<string>"
}Campaign stats
Progress counters plus last_tick_result — the engine’s own account of what it
last did with this campaign, and the field to read when a running campaign looks
idle. Required scope campaigns:read.
curl --request GET \
--url https://api.goyappr.com/campaigns/{id}/stats \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/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.goyappr.com/campaigns/{id}/stats")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.goyappr.com/campaigns/{id}/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{
"campaign_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "<string>",
"leads_by_status": {
"pending": 812,
"awaiting_disposition": 2,
"completed_success": 141,
"exhausted": 45
},
"leads_total": 123,
"attempts_total": 123,
"attempts_in_flight": 123,
"calls_today": 123,
"max_calls_per_day": 123,
"spent_cents": 123,
"reserved_cents": 123,
"estimate_cents": 123,
"budget_cents": 123,
"last_tick_at": "2023-11-07T05:31:56Z",
"last_tick_result": "<string>",
"last_error": "<string>",
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}{
"error": "<string>",
"code": "<string>"
}GET /campaigns/{id}/leads.
Required scope: campaigns:read.
Path parameters
| Parameter | Type | Notes |
|---|---|---|
id | uuid | Campaign ID. Must belong to the API key’s workspace. |
Response fields
| Field | Type | Meaning |
|---|---|---|
campaign_id | uuid | Echo of the path parameter. |
status | string | Current campaign status. |
leads_by_status | object | Contact counts keyed by contact status. Statuses with no contacts are omitted, so treat a missing key as 0. |
leads_total | integer | Contacts enrolled. |
attempts_total | integer | Dial attempts made across all contacts, including retries. |
attempts_in_flight | integer | Attempts not yet settled — calls ringing, talking, or waiting to be categorized. Compare against max_in_flight. |
calls_today | integer | Calls started today, against max_calls_per_day. Resets at midnight in your workspace timezone. |
max_calls_per_day | integer | The configured daily cap, for convenience. |
spent_cents | integer | Settled spend for this campaign. |
reserved_cents | integer | Worst-case cost of the calls currently in flight. |
estimate_cents | integer | Pre-launch estimate, when one was computed. |
budget_cents | integer | null | The configured cap, or null for no cap. Enforced against spent_cents + reserved_cents. |
last_tick_at | timestamp | When the campaign was last evaluated. While running this should be within the last couple of minutes. |
last_tick_result | string | Outcome of that evaluation. See the table below. |
last_error | string | null | Last internal error message, truncated. Normally null. |
company_id | uuid | Workspace this response belongs to. |
Contact statuses in leads_by_status
| Status | Meaning |
|---|---|
pending | Waiting for its turn. |
scheduled | Waiting for a retry timer to elapse. |
dialing | A call is being placed or is in progress. |
awaiting_disposition | The call ended and is waiting to be categorized. Never re-dialed in this state. |
completed_success | A stop rule fired — this contact is done. |
completed_failed | Finished without a successful outcome. |
exhausted | Hit max_attempts without hitting a stop rule. |
excluded | Removed by you, or retired by a stop / archive. |
dnc | On the do-not-call list. Never called again. |
pending, scheduled, dialing and awaiting_disposition are the live states. When all four reach zero the campaign auto-completes.
Reading last_tick_result
This is the field to check first when a running campaign appears idle. Most values are normal pacing, not faults.
| Value | Meaning |
|---|---|
admitted | Calls were started on the last evaluation. Everything is working. |
no_eligible_leads | Nothing is due right now — every remaining contact is finished, in flight, or waiting on a retry timer. |
spacing | min_seconds_between_calls has not elapsed yet. |
daily_cap_reached | max_calls_per_day hit for today. |
max_in_flight | This campaign already has its maximum number of calls outstanding. |
outside_call_window | Outside your workspace calling hours. |
no_reachable_call_window | Your calling hours leave no upcoming window at all. The campaign is paused as paused_config. |
from_number_unavailable | The from-number is no longer active. Paused as paused_config. |
agent_has_no_duration_cap | The agent’s maximum call duration was removed, so worst-case spend is unbounded. Paused as paused_config. |
insufficient_credit | Balance below the minimum needed to place a call. Paused as paused_insufficient_credit; auto-resumes on top-up. |
credit_reserve_would_breach_floor | The balance covers a call today but not the worst case of the calls already in flight. Clears on its own as they settle, or after a top-up. |
budget_exhausted | budget_cents reached, counting in-flight worst case. Paused as paused_budget. |
resumed_credit_ok | Funds arrived and the campaign resumed itself. |
platform_admission_disabled | Campaign dialing is paused platform-wide. Nothing to fix on your side. |
completed | Every contact reached a terminal state. |
error | An internal error occurred; see last_error. |
last_tick_at is more than a few minutes old on a running campaign, the campaign is not being evaluated at all — that is an infrastructure problem rather than a configuration one.
Example request
curl "https://api.goyappr.com/campaigns/CAMPAIGN_ID/stats" \
-H "Authorization: Bearer $YAPPR_API_KEY"
Example response
{
"campaign_id": "b3f1c0d2-5a44-4f0e-9c11-7a2e8d3f0001",
"status": "running",
"leads_by_status": {
"pending": 291,
"scheduled": 44,
"dialing": 1,
"awaiting_disposition": 1,
"completed_success": 58,
"exhausted": 12,
"excluded": 3,
"dnc": 2
},
"leads_total": 412,
"attempts_total": 194,
"attempts_in_flight": 2,
"calls_today": 37,
"max_calls_per_day": 150,
"spent_cents": 8140,
"reserved_cents": 1000,
"estimate_cents": 41200,
"budget_cents": 50000,
"last_tick_at": "2026-07-28T11:59:00.412Z",
"last_tick_result": "spacing",
"last_error": null,
"company_id": "fe493f11-0000-0000-0000-000000000001"
}
attempts_total exceeds completed_success + exhausted because retries count as attempts: with max_attempts: 3, one contact can contribute up to three.
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | INSUFFICIENT_SCOPE | API key lacks campaigns:read. |
| 404 | — | No campaign with that ID in this workspace, or it has been archived. |
| 500 | — | The counters could not be computed. Safe to retry. |
Authorizations
Your Yappr API key (e.g. ypr_live_...). Generate one in the dashboard under Settings → API Keys.
Path Parameters
Response
Campaign stats
Live progress for one campaign, plus the engine's own account of what it last did. When a running campaign appears to be doing nothing, last_tick_result and last_error are the fields that explain it.
Same value as Campaign.status.
Contact counts keyed by CampaignContact.status. Statuses with no contacts are omitted.
Show child attributes
Show child attributes
{
"pending": 812,
"awaiting_disposition": 2,
"completed_success": 141,
"exhausted": 45
}
Every dial attempt this campaign has made, including platform-side retries.
Attempts not yet settled — dialing, live, or awaiting classification.
Calls handed to the queue in the current day window.
Machine-readable reason the last evaluation admitted, or did not admit, a call.