## Reservations: properties and stays

Read the homes and stays of the RapidEye customers who connected your integration.

- **Read-only.** You do not create a workspace, send a roster or issue a recording link.
- **One customer is one workspace.** Its `workspace_id` is ours, `rwd_` and 16 characters, and never changes, even when the customer disconnects and connects again.
- **Unlisted until the date we agree.** Your key, these routes and your webhook receiver work from day one, but no customer can connect you until we list you. Until then `GET /v1/workspaces` returns an empty list and any `workspace_id` answers `404 workspace_not_provisioned`.

### Scopes for these routes

| Scope | Grants | On the customer's connection |
|---|---|---|
| `read:workspaces` | `GET /v1/workspaces`, `GET /v1/workspaces/{workspace_id}` | Not needed |
| `read:properties` | `GET /v1/properties`: homes and full addresses | Always |
| `read:reservations` | `GET /v1/reservations` and the two routes under it | Always |

- A read needs the scope on your key **and** on the customer's connection. Otherwise it answers `403 insufficient_scope`, and `detail.required_scope` names the scope.
- Every other route in this guide answers a key with only these scopes with `403 insufficient_scope`.

### Keeping in sync

| Step | Call | Rule |
|---|---|---|
| 1. Customers | `GET /v1/workspaces` | Daily. Keep each `workspace_id`. |
| 2. Backfill | `GET /v1/reservations?workspace_id=...&updated_since=2000-01-01T00:00:00Z&limit=500` | Once per customer, and again after a reconnect. Walk every page, until a page has no `next_cursor`. Backfill customers one after another, not all at once, and back off on a 429. |
| 3. Poll | `GET /v1/reservations?workspace_id=...&updated_since=...` | Every 5 minutes, or every 15 if you take events. `updated_since` = when you started your previous poll, less 5 minutes. Walk every page. Reading a stay twice is harmless. |
| 4. Apply | | Upsert each stay by `reservation_id` when its `revision` is higher than the one you hold; ignore it otherwise. A cancellation is the same stay with a higher `revision` and `status: "cancelled"`, never a deletion. |
| 5. Homes | `GET /v1/properties?workspace_id=...&updated_since=...` | About hourly, with the same 5-minute overlap. Dedupe on `rapideye_property_id`. |
| 6. Events | | Optional. Each one is a nudge to re-read what it names (*Events*). |

### Stubs

- **When:** a stay whose home is switched off, removed or deleted reads as a stub, whatever its status. A live stay there turns `out_of_scope` within about 15 minutes, and `GET /v1/properties` shows the home `inactive` or `removed`.
- **Fields:** only `reservation_id`, `rapideye_property_id`, `source`, `source_reservation_id`, `source_property_id`, `status`, `status_basis`, `first_seen_at`, `last_confirmed_at`, `last_changed_at` and `revision`. Match a stub to your copy by `reservation_id`.
- **Not a cancellation.** If the home is switched back on, its stays come back in full.

### Coverage and freshness

A new booking or change reaches the API within the target below. A cancellation the source does not state takes the time in its row of the source table.

| Stays | Target |
|---|---|
| In house, or arriving or departing from yesterday through 2 days ahead | 20 minutes at p95, 60 at most |
| Arriving within 30 days | About an hour |
| Departing from 30 days ago to arriving within a year | A day |

| Source | Guest name | Status |
|---|---|---|
| Breezeway | Yes | `checked_in` and `checked_out` stated; otherwise `confirmed`, and `cancelled` once the stay leaves Breezeway (`status_basis: inferred`): about 30 minutes within 2 days, about 2 hours within 30 days, up to 2 nights further out |
| OwnerRez | Yes | A new booking or change reaches the API within about 3 minutes, at any date. Stated by OwnerRez: `active` is `confirmed`, a pending booking or quote hold is `hold`, `canceled` is `cancelled`. A booking deleted in OwnerRez becomes `unknown`: about 2 hours within 30 days, up to 2 nights further out |
| Track | Yes | Stated by Track, within 15 minutes. A stay deleted in Track becomes `unknown`: about 3 hours within 30 days, up to 3 nights further out |
| Trellis | No (`source_does_not_provide`) | `confirmed`, and `unknown` once the stay leaves Trellis (`status_basis: inferred`): about 45 minutes within 2 days, about 3 hours within 30 days, up to 3 nights further out. Only stays with a Trellis task are visible. |

Measured freshness per customer is on `GET /v1/workspaces/{workspace_id}`.

### Events

Optional signed deliveries that tell you about a change sooner than your next poll.

- **Register:** send us one HTTPS receiver URL, and the event types you want if not all. We send back its signing secret.
- **Order by `revision`.** Deliveries arrive out of order and more than once; never order by arrival or `occurred_at`.
- **Dedupe on `webhook-id`**, the payload's `event_id`, the same on every retry.

| Event | When | Payload |
|---|---|---|
| `workspace.enabled` | A customer connects you | `workspace`: the workspace object |
| `workspace.disabled` | A customer disconnects you, or RapidEye revokes the connection | `workspace`, with `disabled_at` set |
| `reservation.created` | We first see a stay | `reservation`: the reservation object, and `changes` |
| `reservation.updated` | Anything about the stay changed: dates, times, status, stay type, guest count, channel, home or guest. A cancellation is this event with `status: "cancelled"`; there is no separate one. | `reservation` and `changes`, as in the history |

#### Handling an event

1. **Verify** the signature over the raw body (*Verifying our signature*).
2. **Dedupe** on `webhook-id`.
3. **Answer 2xx within 10 seconds** and do the rest asynchronously. A non-2xx, a redirect or a timeout is retried after 1m, 5m, 30m, 2h and 12h, six attempts in all.
4. **Re-read** what it names. `reservation.*`: `GET /v1/reservations/{reservation_id}?workspace_id=...`, applied by `revision` (skip it when the payload's `revision` is not higher than yours; `changes` is only a hint). `workspace.*`: `GET /v1/workspaces/{workspace_id}`; on a `workspace.enabled` for a workspace you hold as disabled, re-run its backfill. When many events arrive at once, run your poll early instead.

### Errors on these routes

Every error uses the envelope in *The error envelope*. Besides each route's own codes, any route here can return:

| Error | When |
|---|---|
| `401 invalid_key` | The key is missing or not one we recognize. |
| `403 partner_disabled` | We paused your whole integration. Your key is not the problem; email support. |
| `403 insufficient_scope` | Your key, or the customer's connection, does not carry the scope this read needs. `detail.required_scope` names it. |
| `403 workspace_disabled` | This customer's connection to you is off: its data is not readable and its events stop. The `workspace_id` stays the same if it connects again. |
| `404 workspace_not_provisioned` | That `workspace_id` is not one of your customers. `GET /v1/workspaces` lists the ones you can read. |
| `429 too_many_requests` | More than 600 requests from your key in one 5-minute window of the clock. Retry once the next window starts, at most 5 minutes later; nothing is lost. |

### `GET /v1/workspaces`

Lists every customer connected to you (`enabled`) and every customer that was and no longer is (`disabled`). Scope: `read:workspaces`.

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Authorization` | header | string | yes | `Bearer rapideye_pk_...` |
| `status` | query | string | no | `enabled` or `disabled`, comma-separated for both. Default: both. |
| `limit` | query | integer | no | 1 to 500, default 100. |
| `cursor` | query | string | no | The previous page's `next_cursor`. |

```http
GET /v1/workspaces?status=enabled HTTP/1.1
Host: api.rapideyeinspections.com
Authorization: Bearer rapideye_pk_...
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "workspaces": [
    {
      "workspace_id": "rwd_7Hq2c9Lm4TzPx8Ab",
      "customer_name": "Coastal Stays",
      "status": "enabled",
      "scopes": ["read:properties", "read:reservations"],
      "enabled_at": "2026-10-14T15:02:11+00:00",
      "disabled_at": null,
      "active_properties": 142
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `scopes` | What this customer's connection grants (*Scopes for these routes*). |
| `enabled_at` | When the current connection began. |
| `active_properties` | The number of homes the customer shares. Absent on a disabled workspace. |
| `next_cursor` | Absent on the last page. |

| Error | When |
|---|---|
| `400 invalid_request` | `status`, `limit` or `cursor` is malformed. `detail.parameter` names it. |

### `GET /v1/workspaces/{workspace_id}`

Returns one customer's workspace with its measured freshness. Scope: `read:workspaces`.

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Authorization` | header | string | yes | `Bearer rapideye_pk_...` |
| `workspace_id` | path | string | yes | Ours, `rwd_...`. |

```http
GET /v1/workspaces/rwd_7Hq2c9Lm4TzPx8Ab HTTP/1.1
Host: api.rapideyeinspections.com
Authorization: Bearer rapideye_pk_...
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "workspace_id": "rwd_7Hq2c9Lm4TzPx8Ab",
  "customer_name": "Coastal Stays",
  "status": "enabled",
  "scopes": ["read:properties", "read:reservations"],
  "enabled_at": "2026-10-14T15:02:11+00:00",
  "disabled_at": null,
  "active_properties": 142,
  "freshness": {
    "targets": { "p95_minutes": 20, "ceiling_minutes": 60, "applies_to": "stays in house, or arriving or departing from yesterday through 2 days ahead" },
    "sources": [
      {
        "source": "breezeway",
        "reads": [
          { "kind": "window", "covers": "stays in house, or arriving or departing from yesterday through 2 days ahead", "every_minutes": 15, "last_complete_at": "2026-10-20T13:35:02+00:00" },
          { "kind": "upcoming", "covers": "stays in house, or arriving or departing from yesterday through 30 days ahead", "every_minutes": 60, "last_complete_at": "2026-10-20T13:00:41+00:00" },
          { "kind": "full", "covers": "stays departing from 30 days ago to arriving within 365 days, nightly from 06:00 UTC; also run at once whenever a near or upcoming read lacks a stay we hold", "every_minutes": 1440, "last_complete_at": "2026-10-20T06:21:44+00:00" }
        ],
        "last_read_at": "2026-10-20T13:35:02+00:00",
        "last_read_status": "ok",
        "reads_last_24h": 97,
        "failed_reads_last_24h": 0,
        "new_booking_lag_minutes": {
          "window_days": 7,
          "arriving_within_2_days": { "sample": 23, "p50": 7.9, "p95": 15.6 },
          "arriving_later": { "sample": 230, "p50": 38.5, "p95": 702.0 }
        },
        "change_lag_minutes": null,
        "stays_in_window": 1204,
        "stays_without_property": 0,
        "cancellation": "...",
        "exceptions": ["..."]
      }
    ]
  }
}
```

| Field | Meaning |
|---|---|
| `freshness` | Absent on a disabled workspace, as is `active_properties`. A disabled workspace still answers here with its `status`, `scopes` and times. |
| `reads[].kind` | `window` (stays within 2 days), `upcoming` (the next 30 days), `full` (the whole window) or, for Track and OwnerRez, `recent` (everything the source changed since our last read, standing in for `window`). |
| `reads[].last_complete_at` | When that kind of read last finished in full. For `recent`, which is never complete, when it last succeeded. |
| `last_read_at`, `last_read_status` | Our most recent read of the customer's PMS, of any kind: `ok` or `failed`. `reads_last_24h` and `failed_reads_last_24h` count them. |
| `new_booking_lag_minutes` | From when the source recorded a stay to when we first had it, over the last 7 days, split by whether the stay arrives within 2 days of being booked. |
| `change_lag_minutes` | From the source's last-modified time to when we saw the change. `null` for a source that sends no last-modified time. |
| `stays_in_window` | The customer's stays in the default window. |
| `stays_without_property` | Stays whose RapidEye home has since been deleted: we hold them but can no longer place them. A stay at a home the customer never added is not held, so not counted. |
| `exceptions` | In words, where this source falls short of the target. |
| Measured fields | `null`, not zero, if we cannot read our own measurements at that moment. The declared reads, `cancellation` and `exceptions` are still there. |
| `sources`, `note` | `[]` and a `note` for a customer whose PMS we do not read stays from. |

Errors: only the shared ones. This route never answers `workspace_disabled`.

### `GET /v1/properties`

Lists the customer's homes as RapidEye holds them. Scope: `read:properties`.

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Authorization` | header | string | yes | `Bearer rapideye_pk_...` |
| `workspace_id` | query | string | yes | Ours, `rwd_...`. |
| `status` | query | string | no | `active`, `inactive`, `removed`, comma-separated. Default: all three. |
| `updated_since` | query | date-time | no | Homes changed at or after this time. ISO 8601 with an offset. |
| `limit` | query | integer | no | 1 to 500, default 100. |
| `cursor` | query | string | no | The previous page's `next_cursor`. |

```http
GET /v1/properties?workspace_id=rwd_7Hq2c9Lm4TzPx8Ab&status=active HTTP/1.1
Host: api.rapideyeinspections.com
Authorization: Bearer rapideye_pk_...
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "workspace_id": "rwd_7Hq2c9Lm4TzPx8Ab",
  "properties": [
    {
      "rapideye_property_id": "1f0a9c22-6b1e-4a77-9b0f-2d2b0c5f7a10",
      "name": "Beach House Unit 1",
      "status": "active",
      "source": "breezeway",
      "source_property_id": "512345",
      "address": {
        "line1": "18 Ocean Drive",
        "city": "Panama City Beach",
        "region": "FL",
        "postal_code": "32413",
        "country_code": "US"
      },
      "address_complete": true,
      "timezone": "America/Chicago",
      "coordinates": { "latitude": 30.1766, "longitude": -85.8055 },
      "updated_at": "2026-10-15T07:04:12+00:00"
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `rapideye_property_id` | Ours and stable. Every stay names its home by it. |
| `source_property_id` | The home's id in the customer's PMS. |
| `source` | The PMS the home came from: `breezeway`, `ownerrez`, `track`, `trellis`, or `manual` for a home added by hand in RapidEye. |
| `status` | `active`, `inactive` (switched off in RapidEye) or `removed`. An `inactive` or `removed` home carries only `rapideye_property_id`, `status`, `source`, `source_property_id` and `updated_at`. A home the customer deletes from RapidEye no longer appears; its stays turn `out_of_scope` at our next read and still name it by `source_property_id`. |
| `address` | Refreshed from the PMS every night. A field we do not hold is `null`. |
| `address_complete` | `false` when line 1, city, region or postal code is missing. |
| `timezone` | The home's IANA zone. Every stay time is in it. |
| `coordinates` | `null` when the PMS sends none. |

| Error | When |
|---|---|
| `400 missing_parameter` | `workspace_id` is absent or blank. `detail.parameter` names it. |
| `400 invalid_request` | A filter is malformed, such as `updated_since` without an offset. `detail.parameter` names it; for a bad `status`, `detail.allowed` lists the accepted values; for a `limit` out of range, `detail.limit` is the maximum. |

### `GET /v1/reservations`

Lists one page of a customer's stays, oldest change first. Scope: `read:reservations`.

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Authorization` | header | string | yes | `Bearer rapideye_pk_...` |
| `workspace_id` | query | string | yes | Ours, `rwd_...`. |
| `updated_since` | query | date-time | no | Stays that changed at or after this time. ISO 8601 with an offset; a bare date answers `400 invalid_request`. Removes the default window, so a change to an old stay is still returned. |
| `arrival_from`, `arrival_to` | query | date | no | Arrival date range, `YYYY-MM-DD`, inclusive. Removes the default window. Never returns a stub. |
| `rapideye_property_id` | query | string | no | One home. |
| `status` | query | string | no | One or more statuses (*The reservation object*), comma-separated. |
| `stay_type` | query | string | no | One or more of `guest`, `owner`, `owner_guest`, `block`, `unknown`, comma-separated. Never returns a stub. |
| `limit` | query | integer | no | 1 to 500, default 100. |
| `cursor` | query | string | no | The previous page's `next_cursor`. |

- **Default window:** with none of `updated_since`, `arrival_from` or `arrival_to`, stays departing from 30 days ago to arriving within 365 days.
- **Order:** `last_changed_at`, then `reservation_id`. A stay that changes while you walk the pages moves to the end, and you read it again in its newer state. A write that lands just as your walk passes its time can fall behind the cursor; the 5-minute overlap in your next poll catches it.

```http
GET /v1/reservations?workspace_id=rwd_7Hq2c9Lm4TzPx8Ab&updated_since=2026-10-19T09:00:00Z&limit=500 HTTP/1.1
Host: api.rapideyeinspections.com
Authorization: Bearer rapideye_pk_...
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "workspace_id": "rwd_7Hq2c9Lm4TzPx8Ab",
  "reservations": [
    {
      "reservation_id": "8c1e4b7a-90d2-4f55-a3f1-1d2e9b0c6a44",
      "rapideye_property_id": "1f0a9c22-6b1e-4a77-9b0f-2d2b0c5f7a10",
      "source": "breezeway",
      "source_reservation_id": "107053350",
      "source_property_id": "512345",
      "pms_reservation_id": "HMX4QZ8KT2",
      "status": "confirmed",
      "status_basis": "inferred",
      "stay_type": "guest",
      "arrival_date": "2026-10-23",
      "departure_date": "2026-10-27",
      "check_in_at": "2026-10-23T16:00:00-05:00",
      "check_out_at": "2026-10-27T10:00:00-05:00",
      "timezone": "America/Chicago",
      "booked_at": null,
      "source_recorded_at": "2026-10-08T14:21:09+00:00",
      "channel": "unknown",
      "channel_raw": null,
      "guest_count": 4,
      "primary_guest": { "name": "Dana Whitfield" },
      "first_seen_at": "2026-10-08T14:25:41+00:00",
      "last_confirmed_at": "2026-10-20T13:35:02+00:00",
      "last_changed_at": "2026-10-19T09:12:55+00:00",
      "revision": 3,
      "missing": {
        "booked_at": "source_does_not_provide",
        "channel": "source_does_not_provide"
      }
    }
  ],
  "next_cursor": "MjAyNi0xMC0xOVQwOToxMjo1NS4wMDAwMDBafDhjMWU0YjdhLTkwZDItNGY1NS1hM2YxLTFkMmU5YjBjNmE0NA=="
}
```

Each stay is a reservation object (*The reservation object*). `next_cursor` is absent on the last page.

| Error | When |
|---|---|
| `400 missing_parameter` | `workspace_id` is absent or blank. `detail.parameter` names it. |
| `400 invalid_request` | A filter is malformed, such as `updated_since` without an offset. `detail.parameter` names it; for a bad `status` or `stay_type`, `detail.allowed` lists the accepted values; for a `limit` out of range, `detail.limit` is the maximum. |

### `GET /v1/reservations/{reservation_id}`

Returns one stay, in any window. Scope: `read:reservations`.

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Authorization` | header | string | yes | `Bearer rapideye_pk_...` |
| `reservation_id` | path | string | yes | Ours. |
| `workspace_id` | query | string | yes | Ours, `rwd_...`. |

```http
GET /v1/reservations/8c1e4b7a-90d2-4f55-a3f1-1d2e9b0c6a44?workspace_id=rwd_7Hq2c9Lm4TzPx8Ab HTTP/1.1
Host: api.rapideyeinspections.com
Authorization: Bearer rapideye_pk_...
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "reservation_id": "8c1e4b7a-90d2-4f55-a3f1-1d2e9b0c6a44",
  "rapideye_property_id": "1f0a9c22-6b1e-4a77-9b0f-2d2b0c5f7a10",
  "source": "breezeway",
  "source_reservation_id": "107053350",
  "source_property_id": "512345",
  "status": "out_of_scope",
  "status_basis": "inferred",
  "first_seen_at": "2026-10-08T14:25:41+00:00",
  "last_confirmed_at": "2026-10-20T13:35:02+00:00",
  "last_changed_at": "2026-10-22T08:10:31+00:00",
  "revision": 5
}
```

The body is a reservation object. This example is a stub (*Stubs*); a stay at an active home has every field shown under `GET /v1/reservations`.

| Error | When |
|---|---|
| `400 missing_parameter` | `workspace_id` is absent or blank. |
| `404 reservation_not_found` | No stay with that `reservation_id` in that workspace. |

### `GET /v1/reservations/{reservation_id}/history`

Lists every change we observed to one stay, oldest first. Scope: `read:reservations`.

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Authorization` | header | string | yes | `Bearer rapideye_pk_...` |
| `reservation_id` | path | string | yes | Ours. |
| `workspace_id` | query | string | yes | Ours, `rwd_...`. |

```http
GET /v1/reservations/8c1e4b7a-90d2-4f55-a3f1-1d2e9b0c6a44/history?workspace_id=rwd_7Hq2c9Lm4TzPx8Ab HTTP/1.1
Host: api.rapideyeinspections.com
Authorization: Bearer rapideye_pk_...
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "reservation_id": "8c1e4b7a-90d2-4f55-a3f1-1d2e9b0c6a44",
  "changes": [
    { "revision": 1, "event": "created", "observed_at": "2026-10-08T14:25:41+00:00", "source_updated_at": null,
      "fields": { "arrival_date": [null, "2026-10-23"], "departure_date": [null, "2026-10-26"] } },
    { "revision": 2, "event": "updated", "observed_at": "2026-10-19T09:12:55+00:00", "source_updated_at": null,
      "fields": { "departure_date": ["2026-10-26", "2026-10-27"] } }
  ]
}
```

| Field | Meaning |
|---|---|
| `changes` | Oldest first, so the first entry is the stay as we first saw it. |
| `event` | `created`, `updated`, `cancelled`, `reinstated` (a stay we had marked cancelled or missing came back) or `missing`. |
| `fields` | Each changed field with its old and new value. Names are the reservation object's, except that a time changes as `check_in_time` or `check_out_time`, the home's wall-clock time (`"16:00:00"`) that `check_in_at` and `check_out_at` are built from. Guest fields appear as `"changed"`, never with their values. For a stub, cut to the status change. |

| Error | When |
|---|---|
| `400 missing_parameter` | `workspace_id` is absent or blank. |
| `404 reservation_not_found` | No stay with that `reservation_id` in that workspace. |

### The reservation object

Date-times are ISO 8601 in UTC, except `check_in_at` and `check_out_at`, which carry the home's own offset.

| Field | Meaning |
|---|---|
| `reservation_id` | Ours, stable. Key your copy of a stay on it. |
| `rapideye_property_id` | The home, as `GET /v1/properties` names it. |
| `source`, `source_reservation_id` | The system we read the stay from (`breezeway`, `ownerrez`, `track`, `trellis`) and its id there. |
| `source_property_id` | The home's id in that system, the same id `GET /v1/properties` gives. Still set when the home has since been deleted from RapidEye. |
| `pms_reservation_id` | The booking's id in the customer's PMS or channel, usually what you match to your own records. `null` when the source sends none; absent from a stub. |
| `status` | `inquiry`, `hold`, `confirmed`, `checked_in`, `checked_out`, `cancelled`, `no_show`, `unknown` or `out_of_scope` (the stay moved to a home the customer does not run with RapidEye, its home was switched off or removed, or the customer connected a different account of their PMS that does not hold that home; not a cancellation, and always a stub). Refreshed on every read of the PMS. A cancellation is a status, never a deletion. |
| `status_basis` | `source` when the PMS stated the status; `inferred` when we derived it (*Coverage and freshness*). |
| `stay_type` | `guest`, `owner`, `owner_guest` (an owner's guest), `block` (an owner or maintenance hold, no guest) or `unknown`. |
| `arrival_date`, `departure_date` | The home's calendar dates. Do not convert them. |
| `check_in_at`, `check_out_at` | The home's check-in and check-out times with that date's offset, daylight time included. `null` when the source gives no time or we hold no zone for the home. |
| `timezone` | The home's IANA zone. `null` when we hold none, and then both times are `null`. |
| `booked_at` | When the guest booked, when the source says. |
| `source_recorded_at` | When the source first recorded the stay. For Breezeway, the closest thing to a booking time. |
| `channel`, `channel_raw` | `airbnb`, `vrbo`, `booking_com`, `direct`, `owner`, `other` or `unknown`, and the source's own value. Breezeway and Trellis do not send a channel. |
| `guest_count` | As the source states it. |
| `primary_guest` | `{ "name": "..." }`, the primary guest's name. Absent when the source provides no name; `missing.primary_guest` says why. |
| `first_seen_at`, `last_confirmed_at`, `last_changed_at` | When we first saw the stay, when a read last returned it, and when it last changed. `last_confirmed_at` is refreshed at most hourly while a stay is unchanged, so it is accurate to the hour. |
| `revision` | Rises by one on every change; never decreases. |
| `missing` | Each empty field, mapped to one of the reasons below. Absent from a stub. |
| `missing.*`: `source_does_not_provide` | The PMS never sends this field. |
| `missing.*`: `withheld_by_channel` | The booking channel withholds it. |
| `missing.*`: `timezone_unknown` | We hold no zone for the home, so the times are `null`. |
| `missing.*`: `missing` | The source should have sent it and did not; tell us if it is on a field you rely on. Right after a customer connects you, a stay we read before then reads this for `primary_guest` until our next read of it, unless its source sends no name (Trellis); the name then arrives as a higher `revision`. |
