Stats a campaign
/api/v1/campaigns/{campaign}/statsParameters
| Name | In | Type | Description |
|---|---|---|---|
campaignrequired | path | string | The campaign identifier. |
Request
/api/v1/campaigns/{campaign}/statscurl -X GET 'https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/stats' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY'const response = await fetch('https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/stats', {
method: 'GET',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
},
});
const { data } = await response.json();import requests
response = requests.get(
"https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/stats",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->get('https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/stats');
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/stats")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer mk_live_YOUR_API_KEY"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }Response
Success.
dataobjectNo rates are published: you have both inputs, and a published percentage bakes in a denominator and a rounding rule we could never change. CORRELATION CAVEAT: only events tagged `campaign:{id}` are exact. Opens, clicks, bounces and unsubscribes arrive from the tracking pipeline without that tag and are matched by recipient address within the campaign's active window, so an address on two overlapping campaigns can have one open counted against both. Counts are unique by recipient.
objectstringcampaign_statscampaign_idstringstatestringdraft | scheduled | sending | paused | sent | canceled | unknownThe campaign's lifecycle state, repeated from the same map `campaign.state` uses so the numbers can be read without a second request. Note that `sent` describes the fan-out finishing, not the numbers below: a campaign can read `sent` with `recipients.sent: 0`.
recipientsobjectWhat the fan-out did with the ledger. EVERY count in this group is `null` until the campaign starts, and an integer from the moment it does. The cut is exact, not a guess: the recipient ledger is materialised and the measurement window opens in the same save that sets `schedule.started_at`, so `started_at === null` is precisely "no ledger, no window, nothing measured". Null here means UNKNOWN, never zero — "nobody opened it" and "it has not been sent" are different facts.
totalintegerEvery row in the recipient ledger. Null until the campaign starts — see the group description.
pendingintegerMaterialised, not yet attempted. Null until the campaign starts — see the group description.
sentintegerHanded to the mail server. NOT a delivery confirmation — that is `engagement.delivered`, and the two are kept separate because a campaign with `sent` above `delivered` has mail in flight or bounced. Null until the campaign starts — see the group description.
failedintegerThe send attempt threw. Null until the campaign starts — see the group description.
skipped_suppressedintegerOn the organization's suppression list. Null until the campaign starts — see the group description.
skipped_unsubscribedintegerOpted out of this organization's mail. Null until the campaign starts — see the group description.
engagementobjectWhat came back. EVERY count in this group is `null` until the campaign starts, and an integer from the moment it does. The cut is exact, not a guess: the recipient ledger is materialised and the measurement window opens in the same save that sets `schedule.started_at`, so `started_at === null` is precisely "no ledger, no window, nothing measured". Null here means UNKNOWN, never zero — "nobody opened it" and "it has not been sent" are different facts.
deliveredintegerConfirmations received from the receiving server. Null until the campaign starts — see the group description.
openedintegerUnique recipients who opened. Null until the campaign starts — see the group description.
clickedintegerUnique recipients who clicked. Null until the campaign starts — see the group description.
bouncedintegerUnique recipients whose message bounced. Null until the campaign starts — see the group description.
unsubscribedintegerUnique recipients who opted out from this message. Null until the campaign starts — see the group description.
generated_atstringWhen this rollup was computed. The endpoint recomputes on every call rather than reading the campaign's cached copy, so it is always "just now" — published anyway, because a caller storing the answer needs to know how old their copy is. In practice NEVER null: the service stamps it on every rollup it returns. The marker is defensive, and a null would mean UNKNOWN — the rollup reached us without a timestamp — never "it has not been computed", because the counts beside it would not exist if it had not been.
Returned inside the standard envelope.
Errors
| Status | When |
|---|---|
401 | The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist. |
403 | The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys. |
404 | No such resource in this organization. |
422 | The request was understood but the values were not acceptable. |
429 | Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait. |
Every status, with what causes it and what to do, is on the error reference.