# Get email stats

Source: https://help.ruuster.com/api/emails-stats

```http
GET https://api.ruuster.com/v1/emails/stats
```

Ruuster email sent to your leads (saved-search alerts and listing updates): sends, deliveries, human opens and clicks, optionally per saved search. These rates are stricter than the open and click rates in Ruuster's weekly analytics email, which count automatic and repeat opens; for a headline rate that matches that email, use summary's weeklyEmail figures, and present these as the detail, not as a discrepancy.

- Authentication: `x-api-key` and `x-email` headers of an owner or admin
- Rate limit: 120 requests per minute per key

## Query parameters

| Name | Type | Default | Description |
|---|---|---|---|
| `from` | date | 30 days before `to` | First day, `YYYY-MM-DD` in America/Chicago, included |
| `to` | date | today | Day after the last day, `YYYY-MM-DD`, excluded |
| `granularity` | string | `week` | `day`, `week` (Monday start) or `month` |
| `groupBy` | string | | `savedSearch` adds `bySavedSearch[]` to each bucket: `savedSearchId`, `name`, `sent`, `openedHuman`, `clicked` |
| `cursor` | string | | With `groupBy`, `meta.nextCursor` from the previous page of saved searches |
| `limit` | integer | `100` | With `groupBy`, saved searches per page, 1 to 500 |

## Response

Each bucket: `sent`, `delivered`, `uniqueRecipients`, `opened` (all recorded opens, including automated ones), `openedHuman` (opens that don't look automated; best-effort), `openRateHuman`, `clicked`, `clickRate`. Rates are over `delivered`.

Emails are counted on the Central day they were sent; opens and clicks are added to that day as they arrive, so the last 14 days can still increase. `uniqueRecipients` is distinct per day and summed across the bucket's days.

## Example request

cURL:

```bash
curl -s "https://api.ruuster.com/v1/emails/stats?from=2026-09-21&to=2026-09-28&granularity=week&groupBy=savedSearch&limit=2" \
  -H "x-api-key: $RUUSTER_API_KEY" \
  -H "x-email: $RUUSTER_EMAIL"
```

Node.js:

```javascript
const res = await fetch("https://api.ruuster.com/v1/emails/stats?from=2026-09-21&to=2026-09-28&granularity=week&groupBy=savedSearch&limit=2", {
  headers: {
    "x-api-key": process.env.RUUSTER_API_KEY,
    "x-email": process.env.RUUSTER_EMAIL,
  },
});
const body = await res.json();
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.requestId})`);
console.log(body.data);
```

Python:

```python
import os
import requests

res = requests.get(
    "https://api.ruuster.com/v1/emails/stats?from=2026-09-21&to=2026-09-28&granularity=week&groupBy=savedSearch&limit=2",
    headers={"x-api-key": os.environ["RUUSTER_API_KEY"], "x-email": os.environ["RUUSTER_EMAIL"]},
    timeout=30,
)
body = res.json()
if not body["ok"]:
    raise RuntimeError(f'{body["error"]["code"]}: {body["error"]["message"]} ({body.get("requestId")})')
print(body["data"])
```

## Example response

```json
{
  "ok": true,
  "data": {
    "series": [
      { "bucket": "2026-09-21", "sent": 13054, "delivered": 13014, "uniqueRecipients": 9593,
        "opened": 3489, "openedHuman": 1433, "openRateHuman": 0.1101,
        "clicked": 385, "clickRate": 0.0296,
        "bySavedSearch": [
          { "savedSearchId": "b81f4d20-6c3e-4a97-9e15-0d7c2a5f8b36", "name": "Catalina Foothills 3bd", "sent": 412, "openedHuman": 61, "clicked": 19 },
          { "savedSearchId": "3c9e1a74-5b20-4f86-ad31-8e6b0f2c7d95", "name": "Oro Valley under 650k", "sent": 377, "openedHuman": 44, "clicked": 12 }
        ] }
    ],
    "totals": { "sent": 13054, "delivered": 13014, "uniqueRecipients": 9593, "opened": 3489, "openedHuman": 1433, "clicked": 385 }
  },
  "meta": { "range": { "from": "2026-09-21", "to": "2026-09-28", "tz": "America/Chicago" }, "granularity": "week", "groupTotal": 8734, "nextCursor": "cur_Ql0s4MPq7Lm3XvT8wR" }
}
```
