Report a campaign
/api/v1/campaigns/{campaign}/reportParameters
| Name | In | Type | Description |
|---|---|---|---|
campaignrequired | path | string | The campaign identifier. |
Request
/api/v1/campaigns/{campaign}/reportcurl -X GET 'https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/report' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY'const response = await fetch('https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/report', {
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/report",
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/report');
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/report")
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.
dataobjectobjectstringcampaign_reportcampaign_idstringstatestringdraft | scheduled | sending | paused | sent | canceled | unknownThe campaign's lifecycle state, from the same map `campaign.state` uses.
timelineobjectEngagement per bucket from the send start to the last observed event. BOTH members are null for a campaign that never started — an empty `points` array beside a confident bucket size would read as "we measured and nothing happened", which is a different fact from "there is no window to measure over". Same cut as the stats counts, from the same column.
bucketstringhour | day`hour` while the window is short enough to read that way (72 hours), `day` after. Null when there is no window.
pointsarray<object>Null when the campaign never started; never `[]` in that case.
atstringThe bucket boundary as an INSTANT, ISO 8601 UTC. The underlying value is a bucket label with no timezone and no `T`; publishing it unchanged meant every date parser read it as local time. `null` means UNKNOWN — the stored bucket label was empty or did not parse against this row's `bucket` format, so we could not say which instant it is. It never means "no time": the row's counts are still real, they just cannot be placed on the axis. Drop such a point rather than guessing where it goes.
deliveredintegeropenedintegerclickedinteger
linksarray<object>Clicks grouped by DESTINATION, most-clicked first — a per-destination table, not a list of click events. At most 50 rows; no campaign at today's volumes comes near that. No ratio is published: you have both inputs.
urlstringCan be the literal string `(unknown link)`: click events from before the tracking pipeline echoed the original URL are grouped under it rather than dropped, so `total_clicks` still sums to `engagement.clicked`. It is a display sentinel in a data field and a known wart.
total_clicksintegerEvery click on this destination.
unique_clickersintegerDistinct recipients who clicked it. Five clicks from one person is not five interested readers, which is why both numbers are kept.
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.