## Setting up a home

Once a property is in the roster you can prepare it for inspections: give it the
checklist the cleaner works through, a Matterport tour, and baseline photos of
the home in good condition. All three are optional, and an inspection works
without any of them; each one makes the report sharper.

Every call here addresses a home by **your** `property_id`, the one you sent in
the roster, and takes `workspace_id` as a query parameter. Any roster state may
be set up, `inactive` and `unconfirmed` included, so you can prepare a home
before it goes live. An id that is not in that workspace's roster answers
`404 property_not_found`, and so does an id that belongs to a different
workspace. The id is matched literally and must survive as one path segment, so
percent-encode anything reserved.

Request bodies on these endpoints are limited to 376,224 bytes; a larger body
answers `413 payload_too_large` before it is read. That is a guard against a
mistaken bulk upload, separate from the field limits below. It sits well above
any realistic body at those limits, and both rules apply.

Progress for every home shows in the `setup` block on
`GET /v1/properties`:

```json
"setup": {
  "checklist": { "items": 12 },
  "matterport": { "state": "imported", "url": "https://my.matterport.com/show/?m=SxQL3iGyoDo" },
  "baseline_photos": { "count": 18, "state": "imported" }
}
```

| Field | Meaning |
|---|---|
| `checklist.items` | How many items the home's own **housekeeping** checklist has (the list a cleaner is served on a turnover). `0` means it has none of its own. The inspection and maintenance lists are not counted here; read them with `GET .../checklist?type=`. |
| `matterport.state` | `none`, `importing`, `imported` or `failed`, for the tour the home currently names. `url` is omitted when no tour is set. |
| `baseline_photos.count` | Every photo in the home's baseline, whatever added it: your imports, and photos the home's managers or we added (uploads, a Google Drive folder, their Airbnb listing, Breezeway listing photos). |
| `baseline_photos.state` | `importing` while a photo import is running, yours or one the home's managers started. Otherwise `imported` when the home has any baseline photos, `failed` when it has none and its most recent import in the last 30 days failed, and `none` otherwise. Read the import itself for which photos were refused. |

### `PUT /v1/properties/{property_id}/checklist?workspace_id=...`

Replace the home's checklist with the list you send. The cleaner sees the items
in your order. It is a full replace, not a merge, and it is idempotent: sending
the same list twice leaves the same checklist. It is safe after the home has
been recorded; items a cleaner already checked off stay on the reports that
recorded them.

| Field | Required | Notes |
|---|---|---|
| `items` | yes | Array of 1 to 50 items. To remove the checklist, use `DELETE`; an empty array is refused. |
| `items[].name` | yes | The step as the cleaner reads it on their phone, one step at a time. 1 to 120 characters; under 55 reads best. Name the area, then the visible results: `Kitchen: counters clear, sink empty, floor swept`. A dash used as punctuation (an em dash, or an en dash with a space beside it) is refused; use a colon. |
| `items[].notes` | no | A short label shown under the step on one truncated line, such as the floor or room. Not a place for instructions. Up to 500 characters. |

Unknown fields, at the top level or on an item, are rejected with
`400 invalid_request`, and `detail` names the field and the item `index`.

**A home has three checklists, and `?type=` picks which one you are writing.**
The values are `housekeeping` (the default when you omit it), `inspection` and
`maintenance`. Each is a separate list: writing one never disturbs the other
two, and clearing one leaves the others alone. Anything outside those three is
`400 invalid_request` with `detail.parameter` set to `type`.

Housekeeping is the list a cleaner is served during a turnover, so it is the
one to use unless you have a reason not to. The response echoes `type` back
even when you left it out, so a stored response always says which list it
describes.

Every item comes back with an **`id`**, which is ours and is how per-item
results are reported (see *What we send back to you*). One thing to know about
it: a replace retires the old items and creates new ones, so an `id` is stable
between writes and not across them. That is deliberate, because a result should
point at the version of the item that was actually checked. Read the ids back
after each `PUT` rather than caching them across an edit.

```bash
curl -sS -X PUT "https://api.rapideyeinspections.com/v1/properties/unit_42/checklist?workspace_id=ws_123" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "name": "Bedrooms: beds made, linens fresh, floors clear", "notes": "Upstairs" },
      { "name": "Hot tub: cover on and latched" }
    ]
  }'
```

```json
{
  "workspace_id": "ws_123",
  "property_id": "unit_42",
  "type": "housekeeping",
  "items": [
    { "id": "c1f0a2e4-...", "name": "Bedrooms: beds made, linens fresh, floors clear", "notes": "Upstairs" },
    { "id": "9b73d5a1-...", "name": "Hot tub: cover on and latched" }
  ],
  "updated_at": "2026-09-19T14:02:11Z"
}
```

### `GET /v1/properties/{property_id}/checklist?workspace_id=...`

The home's own checklist, in order. `items` is an empty array when the home
has none of its own, and `updated_at` is then omitted; a company-wide checklist
set up with us may still apply to that home.

`?type=` reads one of the three lists, same values and same default as the
`PUT`. Read the item `id`s back from here after any edit: they are what
per-item results are keyed on, and a replace issues new ones.

```bash
curl -sS "https://api.rapideyeinspections.com/v1/properties/unit_42/checklist?workspace_id=ws_123&type=maintenance" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME"
```

```json
{
  "workspace_id": "ws_123",
  "property_id": "unit_42",
  "type": "maintenance",
  "items": [
    { "id": "c1f0a2e4-...", "name": "Bedrooms: beds made, linens fresh, floors clear", "notes": "Upstairs" }
  ],
  "updated_at": "2026-09-19T14:02:11Z"
}
```

### `DELETE /v1/properties/{property_id}/checklist?workspace_id=...`

Remove one of the home's own checklists. Idempotent: removing a checklist that
is not there is a `200`. If the customer has a company-wide checklist of that
type on our side, the home goes back to using it. `?type=` picks which list to
clear and leaves the other two alone.

```bash
curl -sS -X DELETE "https://api.rapideyeinspections.com/v1/properties/unit_42/checklist?workspace_id=ws_123" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME"
```

```json
{ "workspace_id": "ws_123", "property_id": "unit_42", "type": "housekeeping", "items": [] }
```

### `PUT /v1/properties/{property_id}/matterport?workspace_id=...`

Set the home's Matterport tour and import it as baseline imagery. Answers `202`
with `state: "importing"`; watch `setup.matterport.state` on
`GET /v1/properties` for `imported` or `failed`.

| Field | Required | Notes |
|---|---|---|
| `url` | yes | A `https://my.matterport.com/show/?m=<model id>` share link (other query parameters are fine) or a `https://discover.matterport.com/space/<model id>` link. We store it as `https://my.matterport.com/show/?m=<model id>`. |

Sending a tour again is safe. The same tour that is already imported answers
`200` with `state: "imported"` and imports nothing; the same tour that is still
importing answers `202` with the import already running. A different tour while
one is importing is `409 matterport_import_in_progress`, and `detail.importing` names
the tour that is running; wait for the first to
finish. A tour whose import failed is imported again. A different tour while one
is already imported replaces it: the home keeps using the imported tour until
the new one finishes importing, and if the new import fails the old tour stays
in use and `state` reads `failed` for the tour you sent.

```bash
curl -sS -X PUT "https://api.rapideyeinspections.com/v1/properties/unit_42/matterport?workspace_id=ws_123" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://my.matterport.com/show/?m=SxQL3iGyoDo&play=1" }'
```

```json
{
  "workspace_id": "ws_123",
  "property_id": "unit_42",
  "matterport": { "url": "https://my.matterport.com/show/?m=SxQL3iGyoDo", "state": "importing" }
}
```

### `DELETE /v1/properties/{property_id}/matterport?workspace_id=...`

Remove the home's tour. Idempotent. Its imported imagery is deleted and stops
being used from the next inspection, and an import that is running is stopped.
Nothing else in the home's baseline (photos, the baseline video) is affected.

```bash
curl -sS -X DELETE "https://api.rapideyeinspections.com/v1/properties/unit_42/matterport?workspace_id=ws_123" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME"
```

```json
{ "workspace_id": "ws_123", "property_id": "unit_42", "matterport": { "state": "none" } }
```

### `POST /v1/properties/{property_id}/baseline-photos?workspace_id=...`

Import baseline photos of the home, in good condition, from URLs. Answers `202`
with an `import_id`; the photos are fetched in the background. Photos add to the
home's baseline, they never replace it, and sending a photo we already hold
adds nothing, whatever URL it arrives from. One import runs per home at a
time: a second batch while one is `importing` is
`409 baseline_import_in_progress`. Baseline imports (photos and tours, from
every workspace) also queue behind each other on our side, so at a busy moment
an import can stay `importing` for some minutes before its first photo is
fetched. Poll the import; do not time it out and send it again.

| Field | Required | Notes |
|---|---|---|
| `photos` | yes | Array of 1 to 25 objects. A repeated photo counts toward the 25 and is fetched, is stored once, and is reported in the import's `duplicates` count. |
| `photos[].url` | yes | An `https` URL on the default port, up to 2,048 characters, with no credentials in it. |

What we fetch, and what we refuse per photo rather than for the whole batch:

- **Public hosts only.** A host that resolves to a private, loopback,
  link-local or otherwise non-public address is refused, and so is a redirect
  to one. We follow at most three redirects, each checked the same way.
- **JPEG, PNG or WebP, up to 15 MB.** The `Content-Type` must say so and the
  bytes must match it.
- **20 seconds per photo.** A slower server fails that photo.
- **Nothing of yours or ours is sent.** No cookies and no authorization
  header, so the URL has to be fetchable on its own; a signed URL that lives
  for a few minutes is fine.

```bash
curl -sS -X POST "https://api.rapideyeinspections.com/v1/properties/unit_42/baseline-photos?workspace_id=ws_123" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
    "photos": [
      { "url": "https://photos.example.com/unit_42/living-room.jpg" },
      { "url": "https://photos.example.com/unit_42/kitchen.jpg" }
    ]
  }'
```

```json
{
  "workspace_id": "ws_123",
  "property_id": "unit_42",
  "import_id": "5d0c7a4e-3b1f-4f7e-9c2a-8e6b1d4f0a37",
  "state": "importing",
  "accepted": 2
}
```

### `GET /v1/properties/{property_id}/baseline-photos/imports/{import_id}?workspace_id=...`

One photo import: its `state` (`importing`, `imported` or `failed`), how many
photos were `stored`, how many were `duplicates` (a photo the home already
holds, from this request or from anywhere else, or one removed from the home
while the import ran), and every photo we refused, with its position in your
request, its URL and a `reason`. `stored`, `duplicates` and the refused photos
add up to the number you sent. The import is `failed` when every photo was
refused, or when it failed before it could report on any photo. The first
carries no `error`: every refused photo is already in `failed` with its own
`reason`. The second carries a short `error`, and sending the same photos again
is safe.

| `reason` | Meaning |
|---|---|
| `invalid_url` | The URL, or a redirect it sent us to, is not one we fetch. |
| `dns_failed` | The host did not resolve. |
| `private_address` | The host resolves to a non-public address. |
| `connect_failed` | We could not connect or the connection broke. |
| `too_many_redirects` | More than three redirects. |
| `http_status` | The server answered something other than `200`. |
| `unsupported_content_type` | Not JPEG, PNG or WebP. |
| `content_mismatch` | The bytes are not a usable image of the type the server declared: another type, truncated or corrupt, or over 100 megapixels. |
| `too_large` | Over 15 MB. |
| `timeout` | The photo took longer than 20 seconds. |
| `upload_failed` | We fetched it and could not store it. Sending it again is safe. |
| `fetch_failed` | Something unexpected went wrong on our side with this photo. Sending it again is safe. |

An `import_id` from another home or workspace answers `404 not_found`,
identical to one that does not exist.

```bash
curl -sS "https://api.rapideyeinspections.com/v1/properties/unit_42/baseline-photos/imports/5d0c7a4e-3b1f-4f7e-9c2a-8e6b1d4f0a37?workspace_id=ws_123" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME"
```

```json
{
  "workspace_id": "ws_123",
  "property_id": "unit_42",
  "import_id": "5d0c7a4e-3b1f-4f7e-9c2a-8e6b1d4f0a37",
  "state": "imported",
  "stored": 1,
  "duplicates": 0,
  "failed": [
    { "index": 1, "url": "https://photos.example.com/unit_42/kitchen.jpg", "reason": "http_status" }
  ]
}
```
