Sendry Docs
API Reference

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

ParameterTypeRequiredDescription
fromstringYesStart date (ISO 8601), e.g., 2025-01-01.
tostringYesEnd date (ISO 8601), e.g., 2025-01-31.
granularitystringNoTime bucket: hour, day (default), week, month.
tagstringNoFilter by tag name.
domainstringNoFilter 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

ParameterTypeDefaultDescription
email_idstringFilter by a specific email ID.
typestringFilter by event type: delivered, bounced, opened, clicked, complained.
tostringFilter by recipient email address.
from_datestringEvents from this date (ISO 8601).
to_datestringEvents until this date (ISO 8601).
limitnumber50Results per page (1–100).
cursorstringPagination 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

ParameterTypeRequiredDescription
fromstringYesStart date (ISO 8601), e.g., 2025-01-01.
tostringYesEnd date (ISO 8601), e.g., 2025-01-31.
granularitystringNoTime bucket: hour, day (default), week, month.
tagstringNoFilter by tag name.
domainstringNoFilter 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

ParameterTypeRequiredDescription
fromstringYesStart date (ISO 8601), e.g., 2025-01-01.
tostringYesEnd date (ISO 8601), e.g., 2025-01-31.
group_bystringYesGrouping dimension: domain or template.
limitnumberNoMaximum 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

ParameterTypeRequiredDescription
fromstringYesStart date (ISO 8601), e.g., 2025-01-01.
tostringYesEnd date (ISO 8601), e.g., 2025-01-31.
granularitystringNoCohort bucket: day (default), week, month.
metricstringNoMetric 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:

ParameterTypeDefaultDescription
formatstringcsvExport 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

ParameterTypeRequiredDescription
dimensionstringYescountry, device, or client.
daysnumberNoLook-back window in days, 1-365. Defaults to 30.
typestringNoRestrict 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

  • device is one of desktop, mobile, tablet, or bot, parsed from the User-Agent header.

  • client is the mail client or browser, for example Gmail, Apple Mail, Outlook, Chrome, or Safari.

  • country is 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's cf-ipcountry), then a MaxMind GeoLite2-Country database if GEOIP_MMDB_PATH is set, then the public-domain ip-location-db range files in GEOIP_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

ParameterTypeRequiredDescription
fromstringYesStart date (ISO 8601), e.g., 2025-01-01.
tostringYesEnd 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:

PlanLog retentionAnalytics retention
Free30 days7 days
Pro90 days90 days
Business180 days1 year
Enterprise365 daysUnlimited

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

FieldTypeRequiredDescription
opt_inbooleanYestrue 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

FieldTypeRequiredDescription
frequencystringYesDelivery cadence: daily, weekly, or monthly.
recipientsstring[]YesEmail 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",
});

On this page