Engagement (report)
/api/v1/reports/engagementRequest
/api/v1/reports/engagementcurl -X GET 'https://app.mailyte.com/api/v1/reports/engagement' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY'const response = await fetch('https://app.mailyte.com/api/v1/reports/engagement', {
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/reports/engagement",
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/reports/engagement');
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/reports/engagement")
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.
dataobjectobjectstringengagement_reportgenerated_atstringWhen this response was computed, ISO 8601 UTC. Every report answers it, so "is this figure stale?" never depends on which report you asked.
periodobjectThe window this report actually covers, which is not always the window you asked for: a `date_range` outside `day`, `week`, `month` and `quarter` is silently replaced with `month`, so a request for `year` returns thirty days and this object is the only place that says so. Null when the report covers no window -- a stored result read back from `/reports/history` keeps no record of the window it was built over, and inventing one from your current query would describe the query rather than the data.
start_atstringend_atstringgranularitystringdayThe width of one `series` bucket. Always `day`: the `granularity` query parameter is accepted and ignored.
totalsobjectEngagement measured against DELIVERED mail. `delivered` is the denominator -- an open rate over messages that bounced is meaningless -- and it is the same number, under the same name, that `/reports/email-delivery` publishes. No `open_rate`, `click_rate` or `unsubscribe_rate`: divide by `delivered`.
deliveredintegerMessages a receiving server accepted (`email.delivered`). This is the denominator to divide the engagement counts by. Null means the figure is unavailable, never that it is zero.
openedintegerOpen events (`tracking.open`). Events, not people: one recipient opening twice counts twice. Null means the figure is unavailable, never that it is zero.
clickedintegerClick events (`tracking.click`). Events, not people. Null means the figure is unavailable, never that it is zero.
unsubscribedintegerUnsubscribe events (`tracking.unsubscribe`). Null means the figure is unavailable, never that it is zero.
seriesarray<object>The same counts bucketed over the period, oldest first. Rows are SPARSE: a bucket in which nothing happened has no row at all, rather than a row of zeros, so do not index this array by day offset.
start_atstringThe instant this bucket opens, midnight UTC. Replaces the bare `2026-09-17` these rows used to carry, which named no timezone. NULL MEANS UNKNOWN -- we could not read this bucket's boundary -- and never that the bucket has no start or is empty. A LIVE report cannot produce it: the rows are grouped by `delivery_events.created_at`, which is NOT NULL, so every bucket is keyed by a real date. It is reachable only through `GET /reports/history`, where the row is JSON written by whichever version of the report ran at the time and re-rendered through this same resource WITHOUT being re-validated; a stored row carrying no date becomes null here rather than being silently dated today. Drop such a row or read the report live.
deliveredintegerMessages a receiving server accepted (`email.delivered`). This is the denominator to divide the engagement counts by. Null means the figure is unavailable, never that it is zero.
openedintegerOpen events (`tracking.open`). Events, not people: one recipient opening twice counts twice. Null means the figure is unavailable, never that it is zero.
clickedintegerClick events (`tracking.click`). Events, not people. Null means the figure is unavailable, never that it is zero.
unsubscribedintegerUnsubscribe events (`tracking.unsubscribe`). Null means the figure is unavailable, never that it is zero.
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.