Skip to content

Reports

Reports are read live from your own tables. Opens and clicks count people, not machines: SES’s bot flag, Apple’s mail privacy prefetching and link scanners are filtered out and counted separately.

If you don’t have one yet, send a small campaign:

Terminal window
curl -X POST "$BASE/templates" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"name":"Report demo","version":{"subject":"Hello","html":"<p>Hello, <a href=\"https://example.com/pricing\">see pricing</a></p><p><a href=\"{{ unsubscribe_url }}\">Unsubscribe</a> {{ footer_address }}</p>","fromName":"Example Co","fromEmail":"news@example.com"}}'
curl -X POST "$BASE/campaigns" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"name":"Report demo","templateId":"<tp_...>","templateVersion":1,"segment":{},"stagePlan":{"type":"preset","name":"single"}}'
curl -X POST "$BASE/campaigns/<cm_...>/schedule" -H "Authorization: Bearer $KEY" -H 'Idempotency-Key: report-demo-schedule'
Terminal window
curl "$BASE/campaigns/<cm_...>/report" -H "Authorization: Bearer $KEY"

Check it worked: stats.audience is the number of contacts the campaign is for, and sent and delivered grow as it goes out.

Counter Counts
audience Contacts the campaign is for, fixed when it starts
enqueued, sent Emails queued, and accepted by SES
skipped Contacts who unsubscribed or were suppressed after the campaign started, and, if you cancel a campaign, everyone it hadn’t sent to yet
failed Emails SES refused, with the reason on the send record
unknown Emails whose outcome SES never confirmed, after 30 minutes
delivered, delayed Delivered to the recipient’s mail server; delayed on the way
uniqueOpens, machineOpens People who opened; opens by machines, such as Apple’s prefetching
uniqueClicks, clicks, botClicks People who clicked; every human click; clicks by link scanners
hardBounces, softBounces Permanent and temporary bounces
undeterminedBounces, suppressedBounces Bounces SES couldn’t classify, and addresses on the SES suppression list. Neither counts toward the bounce rate.
complaints, unsubscribes Spam complaints, and unsubscribes from this campaign’s emails

rates gives delivery, open, click, clickToOpen, hardBounce, complaint and unsubscribe, each from human events only.

Terminal window
curl "$BASE/campaigns/<cm_...>/report?by=stage" -H "Authorization: Bearer $KEY"
curl "$BASE/campaigns/<cm_...>/report?by=link" -H "Authorization: Bearer $KEY"

byStage gives the counters for each stage of the campaign’s plan, with each stage’s audience. It’s empty until sending starts, since that’s when the audience is split into stages; from then on it lists every stage, including ones that haven’t started yet. Stages are numbered from 0 here, while the console numbers them from 1. It shows where bounces or complaints came from if a campaign paused. byLink gives clicks and uniqueClicks for each link. We tag every link in your template with its position, the first link being 0, and click events carry only that position, never the URL, since a URL can hold merge values. The report names each link by the href written at that position in the campaign’s template version, Liquid included, such as https://example.com/pricing.

Terminal window
curl "$BASE/campaigns/<cm_...>/recipients?state=sent&limit=100" -H "Authorization: Bearer $KEY"

Each send record has the contact ID, its stage and state (queued, claimed, sent, failed, skipped or unknown), the SES messageId, and when it was sent, delivered, first opened and first clicked, plus any bounce, complaint, unsubscribe or failure reason. Filter by state and stage (numbered from 0, so the console’s stage 1 is stage=0), and page with cursor (see Pagination).

Check it worked: each record’s sentAt is set, and deliveredAt follows within seconds for delivered emails.

To get every record as a file, export it: see Erasure and export. For every raw SES event, query the archive with Athena.