---
title: "Get engagement by location"
method: GET
path: "/v1/email/stats/locations"
tags: ["email-stats"]
---

# Get engagement by location

`GET /v1/email/stats/locations`

## Request

- Base URL: `https://us1.platform.bird.com`
- URL: `https://us1.platform.bird.com/v1/email/stats/locations`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `bird_session`

Returns engagement counts (opens and clicks) for the requested period, grouped by the location they were recorded from. Use it to see where your audience engages, for example the top countries by unique opens. The reading location is only known from open and click events, so rows have engagement counts but no delivery counts or rates.

Use `group_by` to choose the granularity: `country` (the default), `region`, or `city`. Each row has the location hierarchy down to the requested level, so a `city` grouping also reports that row's region and country. Rows are ranked by the `sort` metric, `unique_opens` by default, and paginated with the requested `limit` (50 by default, 200 at most).

Rows are computed against event time rather than send time. The window can span at most 365 days. Ask for more and you get a `422`.

## Query parameters

- `starting_after` string
- `ending_before` string
- `from` string, date
- `to` string, date
- `timezone` string
- `category` string
- `group_by` 'country' | 'region' | 'city'
- `sort` 'opens' | 'opens_non_prefetched' | 'unique_opens' | 'unique_opens_non_prefetched' | 'clicks' | 'unique_clicks': Metric to rank rows by, applied descending. Shared by the engagement-only breakdowns (locations, email clients), which report open and click counts but no rates because delivery events provide no per-dimension denominator.
- `limit` integer

## Headers

- `X-Workspace-Id` string
- `X-Organization-Id` string

## Response `200`

Per-location engagement breakdown for the requested period.

- EmailStatsByLocationResponse: Per-location engagement breakdown for the requested period, grouped at the requested `group_by` granularity, ranked by the `sort` metric (default `unique_opens`) descending and paginated with the requested `limit` (default 50, max 200).
  - `period` EmailStatsPeriod, required: The date range this response was actually computed against. Echoed back so clients can render the period without tracking it themselves and so cached responses can be keyed by what was queried.
    - `from` string, date, required: Inclusive start date the response covers (YYYY-MM-DD).
    - `to` string, date, required: Inclusive end date the response covers (YYYY-MM-DD).
    - `data_as_of` string, date-time, nullable: The instant the statistics in this response are current to: events recorded up to roughly this time are reflected, while more recent events may not be yet. Statistics are served from a rolling aggregation that refreshes every few seconds, so a response reflects data from up to a few seconds ago. Use this field to label data freshness (for example "as of 14:03") rather than assuming the numbers are to-the-second. Null when the freshness boundary is not being reported.
  - `data` EmailLocationStatsPoint[], required: Location breakdown rows, ranked by the `sort` metric (default `unique_opens`) descending. Empty when no opens or clicks with a resolved location occurred in the period.
    - `country` string, required: The country this row aggregates, as a two-letter country code (ISO 3166-1 alpha-2) resolved from the open or click event. Always present.
    - `region` string, nullable, required: The region (state or province) within the country. Populated when `group_by` is `region` or `city`; null at coarser groupings.
    - `city` string, nullable, required: The city within the region. Populated when `group_by` is `city`; null at coarser groupings.
    - `engagement` EmailEngagementCounts, required: Open and click counts for a breakdown row whose dimension is resolved from engagement events only. `opens`, `opens_non_prefetched`, and `clicks` count each event. The same recipient opening or clicking more than once counts each time. The `unique_*` fields count distinct recipients instead, so a recipient who opened five times only counts once there. Rates and unsubscribe counts are not included here. A per-dimension delivered count is unavailable as a rate's denominator, and an unsubscribe event has none of the information this breakdown is grouped by, so it cannot be placed on a row.
      - `opens` integer, required: Distinct open events, counting repeat opens from the same recipient and opens auto-fetched by inbox privacy features (such as Apple Mail Privacy Protection and the Gmail image proxy).
      - `opens_non_prefetched` integer, required: Distinct open events excluding those auto-fetched by inbox privacy features. Same event-counting semantics as `opens`, with prefetched opens removed.
      - `unique_opens` integer, required: Distinct recipients who opened at least once, including opens auto-fetched by inbox privacy features.
      - `unique_opens_non_prefetched` integer, required: Distinct recipients who opened at least once, excluding opens auto-fetched by inbox privacy features.
      - `clicks` integer, required: Distinct click events, counting repeat clicks from the same recipient.
      - `unique_clicks` integer, required: Distinct recipients who clicked at least once.
  - `total` integer, required: Total number of distinct locations at the requested `group_by` level with activity in the period, regardless of `limit`. Pass `next_cursor` as `starting_after` to request the next page.
  - `next_cursor` string, nullable, required: Cursor for the next page. Pass back as `starting_after` to advance forward. `null` when no next page exists.
  - `prev_cursor` string, nullable, required: Cursor for the previous page. Pass back as `ending_before` to step backward. `null` when no previous page exists.
  - `refresh_cursor` string, nullable, required: Refresh anchor, the first row of this response. Pass back as `ending_before` to fetch what precedes it in the current sort order. On a newest-first sort those are the items that have appeared since; on any other sort they are the items that sort earlier, so refreshing such a list means re-fetching it instead. Non-`null` whenever `data` is non-empty; `null` only on an empty page. Distinct from `prev_cursor`.

## Other responses

- `400`: Bad request
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Resource not found
- `422`: The request has invalid field values, violates a business rule, or carries a query parameter the endpoint does not declare. Field validation errors use `type: validation_error` and include the affected fields in `details`. Business-rule errors identify the failed rule in `type`.
- `429`: Rate limit exceeded
- `500`: Internal server error
- `503`: The service is temporarily unavailable. If `Retry-After` is present, wait for that delay before retrying; otherwise, retry with exponential backoff. Reuse the same idempotency key and request when retrying a mutation.

---

[API](https://skmtc.dev/messagebird/apis/bird-api.md) · [All operations](https://skmtc.dev/messagebird/apis/bird-api/llms.txt) · [OpenAPI document](https://skmtc.dev/messagebird/apis/bird-api/revisions/6dfce1179799?raw)
