Reporting
Three read-only views over broadcast performance. All three require:
reports:read
| Endpoint | Answers |
|---|---|
GET /api/broadcasts/{broadcast_id}/report | How did this send do? |
GET /api/broadcasts/{broadcast_id}/links | Which link did the work? |
GET /api/broadcasts/metrics-bulk | The headline numbers for many sends at once. |
These are the aggregate figures a dashboard, a warehouse job, or a weekly digest needs. The deeper analysis SonarSend runs on a send — bounce and failure breakdowns, engaged-contact lists, variant significance testing, delivery timelines, receiving-provider and device mix, and a single contact’s engagement history — is read in the app on the broadcast’s report page, and is not part of the API.
Common reporting workflow#
- store broadcast IDs from the broadcast API as you create sends
- poll
metrics-bulkfor the KPI row of everything you track - call
/reportfor the full numbers on one send, and/linkswhen the question is which call to action worked
Broadcast summary#
GET /api/broadcasts/:broadcast_id/report
Use this as the primary broadcast performance snapshot.
Rates use total_recipients — the intended audience — as the denominator, not
what the provider accepted, so they stay comparable with the list page and read
slightly lower than a provider-side dashboard.
There are three open counts on purpose. apparent_opened is every contact with
any open event, the number other tools report. verified_opened is the headline:
non-bot openers plus anyone who clicked, since a person cannot click without
opening — which is what guarantees open rate ≥ click rate. confirmed_opened is
the strict high-confidence floor. total_opened and total_clicked are
high-confidence only and already exclude bots.
pending_opened and pending_clicked are forced to 0 once the send is no
longer live, so a zero there does not mean nothing is waiting on the classifier.
classifier_last_run_at and classifier_interval_ms are likewise only populated
while the send is live.
Engagement here reflects the settled confidence tier rather than raw opens and clicks: a click is recorded immediately and classified shortly after, so a report read minutes into a send can still move. Expect the numbers to be lower — and truer — than a raw count of engagement.
Link performance#
GET /api/broadcasts/:broadcast_id/links
Every tracked destination with its click counts, busiest first. Links that were
never clicked still appear, with zeroes and null timestamps — the universe comes
from what was sent, not from what was clicked, which is what makes this usable as
a “which call to action failed” view. click_rate is per link over the whole
audience, so the link rates do not sum to the broadcast’s click rate.
Bulk metrics for many broadcasts#
GET /api/broadcasts/metrics-bulk
This is useful for dashboards that need high-level KPIs for many broadcasts at once.
It is served from a cache with a long TTL. For a send that just started, or
after an action you expect to have moved the numbers, pass ?refresh=true to
invalidate and recompute before reading. It must be exactly true; 1 is
ignored.
Leaner than /report: opened and clicked here are the verified (bot-filtered)
counts, with no apparent/confirmed split and no total_failed.
Integration guidance#
- Treat these as read-only analytical views.
- Start with
metrics-bulkand drop to/reportonly for the sends whose numbers you display in full. A dashboard covering fifty broadcasts is one request, not fifty. - Poll at a sensible cadence. Nothing here changes second by second once a send has finished.
- Store broadcast IDs from the creation flow so reporting calls stay simple later.
- For contact-level data, use a contact export rather than a reporting call: one export produces one CSV for a whole audience, and can include engagement rollup columns.