# List a listing's viewers

Source: https://help.ruuster.com/api/properties-viewers

```http
GET https://api.ruuster.com/v1/properties/{propertyId}/viewers
```

Your brokerage's leads who viewed, saved or requested a tour of one listing in a period, with how many times and when. Your own listings by default; any single listing with scope=any.

- Authentication: `x-api-key` and `x-email` headers of an owner or admin
- Rate limit: 120 requests per minute per key
- Rows count against the daily lead-row quota (50,000 by default)

The people behind a listing's engagement, as far as Ruuster can name them: only leads who belong to your brokerage are listed. Anonymous visitors are counted in [Get listing engagement](https://help.ruuster.com/api/properties-engagement) but can't be listed, and another brokerage's lead who viewed your listing on their site never appears here.

By default the listing must be one of yours, and you can pass `mls:<number>` in place of the id to find it by MLS number. With `scope=any` you can ask about **one listing that isn't yours** (a home one of your buyers looked at, say), by `propertyId` only; the answer is still limited to your own leads. It is always one listing per call: there is no call that lists viewers across many listings, and nothing enumerates listings beyond your own inventory.

## Path parameters

| Name | Type | Description |
|---|---|---|
| `propertyId` | string | The listing's `propertyId` (uuid), or `mls:<number>` to find it by MLS number. See [MLS numbers](https://help.ruuster.com/api/mls-numbers) |

## 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 |
| `scope` | string | `brokerage` | `brokerage`: the listing must be attributed to your brokerage (MLS-number lookup allowed). `any`: any single listing your leads viewed, identified by `propertyId` |
| `sort` | string | `views` | `views` (most views first) or `lastViewedAt` (most recent first) |
| `cursor` | string | | `meta.nextCursor` from the previous page. Omit for the first page |
| `limit` | integer | `100` | Rows per page, 1 to 200 |
| `countOnly` | `0` \| `1` | `0` | `1` returns `{ "total": n }` and no rows |
| `agent` | string | your default agent | Whose page the listing links (`url`) open under: an `agentId`, email or landing-page slug from [List agents](https://help.ruuster.com/api/agents-list). Whoever signs up from such a link becomes that agent's lead. Without it, links credit your brokerage's default agent (where unassigned leads go). `meta.agent` names the agent credited |

## Response

`data.property` identifies the listing: `propertyId`, `mlsRecordId`, `mlsName`, `address` (one line), `url` (the listing's page on your Ruuster site, under the page of the agent in `meta.agent`, for example `https://saguaro.ruuster.com/agent/jane-doe/listings/5b7e2d94-…`; see the [property object](https://help.ruuster.com/api/properties-list)), and `ownedByYou`, for your own and other brokerages' listings alike. Use it as the link in a seller report or a CRM task; [Get a listing](https://help.ruuster.com/api/properties-get) has the full details and photos. `meta.scope` echoes the scope used.

`data.viewers` has one row per lead:

| Field | Description |
|---|---|
| `lead` | The short lead form: `leadId`, `firstName`, `lastName`, `email` and `phone` when lead contact details are on, `crmExternalId`, `crmStatus`, `crmTags`, `agentId`, `createdAt`, `lastLoginAt` |
| `views` | Times the lead opened the listing on your sites in the range. Reloads count |
| `favorited` | Whether the lead saved the listing in the range |
| `tourRequested` | Whether the lead requested a tour of the listing in the range |
| `firstViewedAt`, `lastViewedAt` | First and last view in the range. `null` for a lead who saved or requested a tour without a recorded view |

With `scope=brokerage` a listing that isn't attributed to your brokerage returns `404 not_found`; with `scope=any` only an unknown `propertyId` does. Rows count against your daily lead-row quota. `meta.coverage` says which days are available; it's the same coverage as the listing's engagement series.

## Example request

cURL:

```bash
curl -s "https://api.ruuster.com/v1/properties/5b7e2d94-1c63-4a8f-b5e0-9d3c7a2f1e86/viewers?from=2026-09-21&to=2026-09-28&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/properties/5b7e2d94-1c63-4a8f-b5e0-9d3c7a2f1e86/viewers?from=2026-09-21&to=2026-09-28&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/properties/5b7e2d94-1c63-4a8f-b5e0-9d3c7a2f1e86/viewers?from=2026-09-21&to=2026-09-28&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": {
    "propertyId": "5b7e2d94-1c63-4a8f-b5e0-9d3c7a2f1e86",
    "property": {
      "propertyId": "5b7e2d94-1c63-4a8f-b5e0-9d3c7a2f1e86",
      "mlsRecordId": "22418765",
      "mlsName": "MLS of Southern Arizona",
      "address": "4182 E Saguaro Bloom Dr, Tucson, AZ 85718",
      "url": "https://saguaro.ruuster.com/listings/5b7e2d94-1c63-4a8f-b5e0-9d3c7a2f1e86",
      "ownedByYou": true
    },
    "viewers": [
      {
        "lead": { "leadId": "user:c0ff3e21-8a4b-4c7d-9e15-2b6f8d3a1c04", "firstName": "Sam", "lastName": "Lee",
                  "email": "sam.lee@example.com", "phone": "+15205550142", "crmExternalId": "48213",
                  "crmStatus": "Showing Homes Nurture", "crmTags": ["buyer", "tucson"],
                  "agentId": "7c1e9b42-3d85-4a6f-9e07-1b2c8d4f5a69",
                  "createdAt": "2026-09-21T18:20:05Z", "lastLoginAt": "2026-09-30T02:11:40Z" },
        "views": 7, "favorited": true, "tourRequested": true,
        "firstViewedAt": "2026-09-22T02:14:09Z", "lastViewedAt": "2026-09-27T23:40:12Z"
      },
      {
        "lead": { "leadId": "unregistered:8d2b6f19-4e37-4a85-b0c9-7f1e3d5a2b68", "firstName": "Priya", "lastName": "Shah",
                  "email": "priya.shah@example.com", "phone": "+15205550187", "crmExternalId": "51907",
                  "crmStatus": null, "crmTags": ["seller-lead"],
                  "agentId": "e5a7d3c1-9b46-4f28-8c0e-6d2a1f7b3e95",
                  "createdAt": "2026-09-23T15:44:12Z", "lastLoginAt": null },
        "views": 3, "favorited": false, "tourRequested": false,
        "firstViewedAt": "2026-09-24T17:51:33Z", "lastViewedAt": "2026-09-26T15:08:47Z"
      }
    ]
  },
  "meta": {
    "range": { "from": "2026-09-21", "to": "2026-09-28", "tz": "America/Chicago" },
    "scope": "brokerage",
    "coverage": { "from": "2026-07-03", "to": "2026-10-01", "stableBefore": "2026-09-29", "ownershipAsOf": "2026-10-01T06:12:30Z" },
    "nextCursor": "cur_aQ7r2wPq7Lm3XvT8wR"
  }
}
```
