# Conventions

Source: https://help.ruuster.com/api/conventions

> Response envelope, dates and time zone, ranges, granularity, pagination, coverage, identifiers, versioning and the example data.

**Response envelope.** Every success is:

```json
{ "ok": true, "data": { … }, "meta": { … } }
```

`meta` includes `brokerageId`, and where relevant `range`, `granularity`, `coverage`, `total` and
`nextCursor`. Every error is:

```json
{ "ok": false, "error": { "code": "invalid_range", "message": "…" }, "requestId": "req_…" }
```

Every response has an `X-Request-Id` header. Quote it when you contact support.

**Dates and time zone.** Timestamps are ISO 8601 in UTC (`2026-09-30T02:19:03Z`). Days, weeks and
months are calendar periods in **America/Chicago**, the clock Ruuster's weekly analytics email uses.

**Ranges.** `from` and `to` are dates (`YYYY-MM-DD`). The range includes `from` and excludes `to`, so
`from=2026-09-21&to=2026-09-28` is Monday September 21 through Sunday September 27. Without them a call
covers the last 30 complete days, unless the call's own page says otherwise. A reversed or oversized
range returns `400 invalid_range`.

**Granularity.** Series calls take `granularity=day|week|month`. Weeks start on Monday. Each bucket is
labelled with its first day (`"bucket": "2026-09-21"`). A bucket cut off by your range is marked
`"partial": true`.

**Which date a list filters on.** List calls take `dateField` to choose what `from`/`to` apply to:

| Call | `dateField` values (default first) |
|---|---|
| `/properties` | `updated`, `onMarket`, `closed` |
| `/leads` | `created`, `active`, `updated` |

**Pagination.** Lists return up to `limit` rows and `meta.nextCursor`. Pass the cursor back as
`?cursor=` for the next page; `null` means you're done. Cursors stay valid for at least 24 hours and
work for any owner or admin key in your brokerage. There are no page numbers.

**Coverage.** `meta.coverage` says which days are available for your brokerage:
`from`/`to` for Ruuster's own activity data, `ga` for Google Analytics data, `ownershipAsOf` for the
last listing-attribution refresh, and `stableBefore`, the day before which numbers no longer change.
Buckets outside coverage return `null` metrics, never `0`. Totals are computed over covered buckets,
and `bucketsRequested` / `bucketsCovered` show how many that was.

**Comparing periods.** *Planned:* `compare=previous` will add a `previous` object with the same shape
for the range immediately before yours. Until then, request two periods and compare the buckets (see
[Guides](https://help.ruuster.com/api/guides)).

**Incremental sync.** *Planned:* bulk calls will accept `changedSince=<timestamp>` and return only
records whose numbers changed since then. Until then, re-pull the last few days on each sync.

**Caching.** *Planned:* responses for completed periods will carry an `ETag`. A matching
`If-None-Match` will return `304 Not Modified` with no body, which uses none of your daily row quota.

**Methods.** Every data call is `GET`. Other methods return `405 method_not_allowed` with an `Allow`
header. Webhook management uses `POST`, `PATCH` and `DELETE` as documented.

**Identifiers**

| Id | Format | Notes |
|---|---|---|
| `propertyId` | uuid | The id in `…/listings/<propertyId>` on your site. Stable for the life of the listing on Ruuster |
| `mlsRecordId` | MLS number as the MLS publishes it | See [MLS numbers](https://help.ruuster.com/api/mls-numbers) |
| `leadId` | `user:<uuid>` or `unregistered:<uuid>` | `user` has a Ruuster account; `unregistered` is a contact synced from your CRM. Use the whole value and treat it as opaque |
| `crmExternalId` | your CRM's id | For example the Follow Up Boss person id. Your join key to CRM exports |
| `emailId` | `em_…` | One Ruuster email to one recipient. Treat it as opaque |
| `id` on events | `evt_…` | Same in the API and in webhooks |

**Versioning.** Breaking changes ship as `/v2`; `/v1` keeps working. Removing a field, changing what
`null` means, or changing bucket boundaries counts as breaking. Adding fields, optional parameters,
calls or event kinds doesn't; ignore fields you don't use. Ruuster announces deprecations at least 90
days ahead in the [changelog](https://help.ruuster.com/api/changelog) and with `Deprecation` and
`Sunset` headers.

## About the examples

**Fixture.** Every example in these docs uses one fictional brokerage, "Saguaro Realty Group"
(`saguaro.ruuster.com`), observed on Thursday 2026-10-01. All people, addresses, ids and numbers are
fictional. Weeks are Monday–Sunday America/Chicago (2026-09-21 is a Monday).

| Thing | Id |
|---|---|
| Brokerage | `4f6c2a9e-8d31-4b57-9a0e-2c7d5e1b8f43` |
| Jane Doe (admin, the key holder in every example) | user `7c1e9b42-3d85-4a6f-9e07-1b2c8d4f5a69` |
| Marco Ruiz (agent; in the directory and as a lead's assigned `agentId`) | user `e5a7d3c1-9b46-4f28-8c0e-6d2a1f7b3e95` |
| Listing A: 4182 E Saguaro Bloom Dr, Tucson AZ 85718 | `5b7e2d94-1c63-4a8f-b5e0-9d3c7a2f1e86`, MLS `22418765` |
| Listing B: 1920 W Mesquite Ridge Ln, Oro Valley AZ 85737 | `a03c8f61-7e2b-4d95-8a14-6f0b3e9c2d57`, MLS `22419302` |
| Lead Sam Lee (registered) | `user:c0ff3e21-8a4b-4c7d-9e15-2b6f8d3a1c04` |
| Lead Priya Shah (CRM-only) | `unregistered:8d2b6f19-4e37-4a85-b0c9-7f1e3d5a2b68` |
| MLS (MLS of Southern Arizona) | `3f4a7b12-6c85-4d93-a1e0-8b2f5c9d7e31` |

The recipes in [Guides](https://help.ruuster.com/api/guides) use this shell setup:

```bash
B=https://api.ruuster.com/v1
H=(-H "x-api-key: $RUUSTER_API_KEY" -H "x-email: jane@saguarorealty.com")
```

**Response headers on every authenticated call** (shown once):

```
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: no-store
x-request-id: req_01J9ZB4M8Q2C7V5T3K8P6W1N0X
x-ratelimit-limit: 120
x-ratelimit-remaining: 117
x-ratelimit-reset: 1790863260
x-daily-rows-limit: 250000
x-daily-rows-remaining: 248310
```

Lead `email`/`phone` appear in these examples because the fixture brokerage has lead contact
details enabled. With them disabled, those two fields are omitted from every lead object.
Property objects also carry `mlsName` and `mlsRecordIdCore`; they're left out of the examples for length.
