Analytics
Query delivery stats, event logs, and cohort analysis via the API.
The analytics API provides delivery statistics, event logs, and cohort analysis for your email sends.
Get stats
GET /v1/analytics
Requires read_only scope.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Start date (ISO 8601), e.g., 2025-01-01. |
to | string | Yes | End date (ISO 8601), e.g., 2025-01-31. |
granularity | string | No | Time bucket: hour, day (default), week, month. |
tag | string | No | Filter by tag name. |
domain | string | No | Filter by sending domain. |
Response
{
"summary": {
"sent": 15000,
"delivered": 14700,
"opened": 6174,
"clicked": 1764,
"bounced": 300,
"complained": 15,
"delivery_rate": 0.98,
"open_rate": 0.42,
"click_rate": 0.12,
"bounce_rate": 0.02,
"complaint_rate": 0.001
},
"timeseries": [
{
"date": "2025-01-01",
"sent": 500,
"delivered": 490,
"opened": 205,
"clicked": 60,
"bounced": 10,
"complained": 0
}
]
}
Rates are decimal values between 0 and 1 (e.g., 0.42 = 42%).
Get event logs
GET /v1/logs
Requires read_only scope.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
email_id | string | Filter by a specific email ID. | |
type | string | Filter by event type: delivered, bounced, opened, clicked, complained. | |
to | string | Filter by recipient email address. | |
from_date | string | Events from this date (ISO 8601). | |
to_date | string | Events until this date (ISO 8601). | |
limit | number | 50 | Results per page (1–100). |
cursor | string | Pagination cursor. |
Response
{
"data": [
{
"id": "evt_abc123",
"email_id": "email_abc123",
"type": "delivered",
"recipient": "user@example.com",
"metadata": null,
"created_at": "2025-03-12T09:00:05Z"
},
{
"id": "evt_abc124",
"email_id": "email_abc123",
"type": "opened",
"recipient": "user@example.com",
"metadata": {
"user_agent": "Mozilla/5.0...",
"ip": "1.2.3.4"
},
"created_at": "2025-03-12T09:15:30Z"
}
],
"has_more": true,
"next_cursor": "evt_abc100"
}
Compare to previous period
GET /v1/analytics/comparison
Requires read_only scope. Returns the same aggregates as Get stats for both the requested period and the period immediately before it, plus the percentage change between them.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Start date (ISO 8601), e.g., 2025-01-01. |
to | string | Yes | End date (ISO 8601), e.g., 2025-01-31. |
granularity | string | No | Time bucket: hour, day (default), week, month. |
tag | string | No | Filter by tag name. |
domain | string | No | Filter by sending domain. |
Response
{
"current": {
"sent": 15000,
"delivered": 14700,
"opened": 6174,
"clicked": 1764,
"bounced": 300,
"complained": 15,
"inbound_received": 0,
"delivery_rate": 0.98,
"open_rate": 0.42,
"click_rate": 0.12,
"bounce_rate": 0.02,
"complaint_rate": 0.001
},
"previous": {
"sent": 12000,
"delivered": 11700,
"opened": 4680,
"clicked": 1320,
"bounced": 300,
"complained": 12,
"inbound_received": 0,
"delivery_rate": 0.975,
"open_rate": 0.40,
"click_rate": 0.11,
"bounce_rate": 0.025,
"complaint_rate": 0.001
},
"changes": {
"sent_pct": 25.0,
"delivered_pct": 25.64,
"opened_pct": 31.92,
"clicked_pct": 33.64,
"bounced_pct": 0.0,
"complained_pct": 25.0,
"delivery_rate_delta": 0.005,
"open_rate_delta": 0.02,
"click_rate_delta": 0.01,
"bounce_rate_delta": -0.005,
"complaint_rate_delta": 0.0
}
}
*_pct values are percentage changes; *_rate_delta values are absolute changes in the rate.
Get breakdowns
GET /v1/analytics/breakdowns
Requires read_only scope. Aggregates delivery metrics grouped by sending domain or by template.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Start date (ISO 8601), e.g., 2025-01-01. |
to | string | Yes | End date (ISO 8601), e.g., 2025-01-31. |
group_by | string | Yes | Grouping dimension: domain or template. |
limit | number | No | Maximum groups to return. Default 10, max 50. |
Response
{
"data": [
{
"id": "dom_abc123",
"name": "acme.com",
"sent": 9000,
"delivered": 8820,
"opened": 3704,
"clicked": 1058,
"bounced": 180,
"complained": 9,
"delivery_rate": 0.98,
"open_rate": 0.42,
"click_rate": 0.12
}
],
"group_by": "domain"
}
id is null for rows whose dimension value could not be resolved; name is the human-readable group label.
Get cohort analysis
GET /v1/analytics/cohorts
Requires read_only scope. Groups sends into cohorts by send date and reports a rate for each period offset after the send.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Start date (ISO 8601), e.g., 2025-01-01. |
to | string | Yes | End date (ISO 8601), e.g., 2025-01-31. |
granularity | string | No | Cohort bucket: day (default), week, month. |
metric | string | No | Metric to track: open_rate (default), click_rate, delivery_rate. |
Response
{
"cohorts": [
{
"cohort_date": "2025-01-01",
"period_offset": 0,
"total_sent": 500,
"metric_value": 0.42
},
{
"cohort_date": "2025-01-01",
"period_offset": 1,
"total_sent": 500,
"metric_value": 0.08
}
],
"granularity": "day",
"metric": "open_rate"
}
period_offset is the number of granularity periods after the send date (0, 1, 2, ...). metric_value is a decimal between 0 and 1.
Export logs
GET /v1/analytics/export
Requires sending_access scope. Available on Pro and higher plans. Returns a CSV or JSON export of event logs for a date range.
Query parameters
Same as event logs, plus:
| Parameter | Type | Default | Description |
|---|---|---|---|
format | string | csv | Export format: csv or json. |
Engagement breakdown
GET /v1/analytics/engagement-breakdown
Requires read_only scope.
Groups open and click events by the context Sendry derives from the request that fired the tracking pixel or the click redirect: the device type, the mail client or browser, and the country.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
dimension | string | Yes | country, device, or client. |
days | number | No | Look-back window in days, 1-365. Defaults to 30. |
type | string | No | Restrict to opened or clicked. Both are counted when omitted. |
Response
{
"data": [
{ "value": "US", "count": 1840 },
{ "value": "GB", "count": 612 },
{ "value": "unknown", "count": 97 }
],
"total": 2549,
"dimension": "country",
"days": 30
}
Buckets are ordered by count, descending.
How the context is derived
-
deviceis one ofdesktop,mobile,tablet, orbot, parsed from the User-Agent header. -
clientis the mail client or browser, for exampleGmail,Apple Mail,Outlook,Chrome, orSafari. -
countryis an ISO-3166 alpha-2 code, resolved from the first source that answers: the country header your edge proxy sets (GEO_COUNTRY_HEADER, defaulting to Cloudflare'scf-ipcountry), then a MaxMind GeoLite2-Country database ifGEOIP_MMDB_PATHis set, then the public-domain ip-location-db range files inGEOIP_CSV_DIR, which the Docker image ships at/app/geoip. Private and reserved addresses are never geolocated, so a hit that arrives with a LAN address reports no country rather than a wrong one.Read country on clicks with more confidence than on opens. A click redirect is fetched by the reader's own browser, so the address is theirs. An open pixel is often fetched by the mailbox provider instead: Gmail proxies images through Google, so a Gmail open usually reports Google's country rather than the reader's.
Raw IP addresses are never stored: the address is used only to look up the country code and is then discarded.
Events recorded before this data existed, and hits Sendry could not classify,
are grouped under unknown.
Get region distribution
GET /v1/analytics/regions
Requires read_only scope. Breaks down sent emails by the SES region that delivered them.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Start date (ISO 8601), e.g., 2025-01-01. |
to | string | Yes | End date (ISO 8601), e.g., 2025-01-31. |
Response
{
"data": [
{ "region": "us-east-1", "count": 12000, "percentage": 80.0 },
{ "region": "eu-west-1", "count": 3000, "percentage": 20.0 }
]
}
Emails with no recorded region are grouped under unknown.
Analytics retention
Log retention varies by plan:
| Plan | Log retention | Analytics retention |
|---|---|---|
| Free | 30 days | 7 days |
| Pro | 90 days | 90 days |
| Business | 180 days | 1 year |
| Enterprise | 365 days | Unlimited |
Benchmarks
GET /v1/analytics/benchmarks
Requires read_only scope. Available on Pro and higher plans.
Returns anonymized cross-organization benchmark data to compare your delivery rates, open rates, and click rates against industry averages by domain category.
Toggle benchmark opt-in
POST /v1/analytics/benchmarks/opt-in
Requires full_access scope. Opts the organization in or out of contributing anonymized data to cross-organization benchmarks.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
opt_in | boolean | Yes | true to contribute data, false to opt out. |
Response
{
"benchmark_opt_in": true
}
List scheduled reports
GET /v1/analytics/scheduled-reports
Requires read_only scope. Returns every scheduled analytics report for the organization, newest first.
Response
{
"data": [
{
"id": "srep_abc123",
"orgId": "org_abc123",
"frequency": "weekly",
"recipients": ["ops@acme.com"],
"enabled": true,
"lastSentAt": null,
"createdAt": "2025-03-12T09:00:00.000Z",
"updatedAt": "2025-03-12T09:00:00.000Z"
}
]
}
Create scheduled report
POST /v1/analytics/scheduled-reports
Requires full_access scope.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
frequency | string | Yes | Delivery cadence: daily, weekly, or monthly. |
recipients | string[] | Yes | Email addresses to send the report to. 1–10 addresses. |
Example request
{
"frequency": "weekly",
"recipients": ["ops@acme.com", "growth@acme.com"]
}
Response
Returns the created report object, same shape as the items in List scheduled reports.
Delete scheduled report
DELETE /v1/analytics/scheduled-reports/:id
Requires full_access scope. Returns 404 not_found if the report does not belong to the organization.
Response
{
"deleted": true
}
SDK example
const { summary, timeseries } = await sendry.analytics.stats({
from: "2025-01-01",
to: "2025-01-31",
granularity: "week",
});
console.log(`Delivery rate: ${(summary.delivery_rate * 100).toFixed(1)}%`);
console.log(`Open rate: ${(summary.open_rate * 100).toFixed(1)}%`);
const logs = await sendry.analytics.logs({
email_id: "email_abc123",
});