# Listings and synchronization

Synchronize advertisements and edit their supported content fields.

## REST operations and key permissions

| Task | Key permissions | Start here |
| --- | --- | --- |
| Read published and unpublished listings; synchronize adverts | `read:properties`; `write:properties` for the corresponding mutation | `GET /api/v1/landlord/listings`, `GET /api/v1/landlord/listings/changes`, `PATCH /api/v1/landlord/listings/{unitId}/content`, `POST /api/v1/landlord/listings/{unitId}/media/uploads`, `POST /api/v1/landlord/listings/{unitId}/media/uploads/{uploadId}/complete`, `PATCH /api/v1/landlord/listings/{unitId}/media`, `PATCH /api/v1/landlord/listings/{unitId}/publication` |

## Bootstrap and replay listing changes

For listing full sync, retain the `syncCursor` from the **first bootstrap page** unchanged until every bootstrap page has been applied to a temporary dataset. Follow the bootstrap `nextCursor` through all pages; only then start `/api/v1/landlord/listings/changes` from that retained first cursor. Apply each change page atomically with its `nextCursor`; continue while `hasMore=true`. Replace the previous cache only after the full bootstrap and initial change replay succeed. Webhooks wake that change reader; periodic polling recovers missed notifications.

## Edit an existing listing's name or unit text

Use a unit ID returned by the company listing feed. With `write:properties`, send `PATCH /api/v1/landlord/listings/{unitId}/content` with an `Idempotency-Key` and this JSON body:

```json
{"name":"Bright two-room apartment","otherFacts":"Two rooms and a kitchen with a balcony."}
```

The response is the same `{"listing": ...}` projection as the listing detail read. `name` and `otherFacts` are the only accepted fields; both are optional strings and trimmed, and at least one must be supplied. The limits are 200 and 10000 characters respectively, with a 64 KiB request-body cap. Omitted fields stay unchanged. Empty `name` removes the optional unit name. Empty `otherFacts` clears the unit text, so public `description` falls back to legacy unit `facts` and then the property's description. It does not erase those fallback sources. `null` and unknown fields are rejected.

Read the result back and apply listing-change events as usual. Replays still require the key's current permission and ownership; a later denial does not undo an earlier successful operation. Different operation keys changing the same field follow last-writer semantics; `listingVersion` is not a write precondition here.

Media, publication, availability and private tenant fields have no path through this content operation. Their separate administration boundaries still apply. The content write, required semantic audit and replay result commit atomically. The audit records key/operation IDs and changed field names, without storing your credential or the advertisement text in that audit record.

## Upload listing images and publish through the API

Activate an explicit listing delegation for the exact integration key in **Settings → API & agents**. The administrator signs in again with password, passkey or an eID method enabled in Homito and grants `media` and/or `publication` with an expiry. Activation uses the currently authenticated person's own company membership and configured publication trust. `createdBy`, wildcard permission, a replacement key or a person ID supplied by a client cannot replace that delegation. It can be read or revoked at the authenticated web-app control plane `/api/v1/settings/api-keys/{keyId}/listing-grant`; that address is not on the external API origin.

Every operation below requires `write:properties`, its delegated capability and a distinct `Idempotency-Key`. Keep the exact key and body after an interrupted response; replay rechecks current authority before returning a receipt.

1. Send `POST /api/v1/landlord/listings/{unitId}/media/uploads` with `{"images":[{"bytes":12345,"sha256":"<64 lowercase hexadecimal characters>","mimeType":"image/png"}]}`. Compute size and SHA-256 from the actual original bytes. Limits: ten images per reservation and five MiB per image. The reservation expires in fifteen minutes; a returned PUT URL lasts at most five minutes.
2. PUT those exact bytes to the returned `upload.url`, with **every** returned `upload.headers` value and no Homito Authorization header. Use binary HTTP upload, reject redirects, and check the HTTP result before completion. This private staging object is not a listing image. Replaying the reservation does not extend its URLs; after expiry create a new reservation with a new operation key.
3. Send `POST /api/v1/landlord/listings/{unitId}/media/uploads/{uploadId}/complete` with `{}` or `{"setAsFront":true}`. The server verifies actual length, SHA-256, MIME, decoding and pixel limits, writes a different immutable public image, then commits the gallery, audit, change journal, durable webhook intent and receipt together. Read the returned listing back. GIF and animated WebP may retain embedded metadata; completion does not promise complete metadata stripping.
4. Send `PATCH /api/v1/landlord/listings/{unitId}/publication` with `{"isPublic":true,"allocationMethod":"individual_assessment","creditCheckMode":"without_credit_check"}` to publish, or `{"isPublic":false}` to unpublish. Allocation can also be `first_come`. Maintenance, company freeze, round rules and the delegated person's configured publication trust still apply. Unpublishing keeps its existing publication-trust exemption and still needs a live key, delegation, person, membership and ownership. Arbitrary slugs, credit checks and private tenant fields are not accepted.

Follow `listing.changed`/`listing.unpublished` notifications through the ordered changes feed. Abandoned raw uploads and unattached derived objects have durable scheduled cleanup. A successful write receipt records the mutation and delivery intent; verify actual webhook delivery separately.

After an approved exact MCP distribution advertises them, `homito.reserve_listing_media`, `homito.complete_listing_media` and `homito.set_listing_publication` call these same REST operations with `reservation`, `completion` and `publication` objects respectively. Each also takes `unitId` and `idempotencyKey`; completion takes `uploadId`. Enable the ordinary local write gate; you approve each call in your MCP client before it is sent. The binary PUT remains a separate HTTP upload using the returned instructions.

Manage existing images with `PATCH /api/v1/landlord/listings/{unitId}/media`: `{"removeUrls":["<exact URL from the current gallery>"],"frontImageUrl":"<exact retained gallery URL>"}`. Either field can be supplied separately; use `frontImageUrl:null` to clear the explicit front selection. The operation can only remove or select existing images, never import a new URL. Managed images removed from the gallery become eligible for fenced physical cleanup. The corresponding approved MCP tool is `homito.update_listing_gallery` with a `gallery` object and the ordinary write gates.

## Continue reads safely

Follow `nextCursor` until it is `null`, keeping the same list operation and filters. Use cursors only with the same company and list operation. Import previews, jobs and their history belong to the API key that created them, including access by job ID. A green public `/health` response alone does not verify your key or company access.

## Keep private data out of public caches

Keep personal data, receipts and upload capabilities out of public listing caches. Tenant and application reads intentionally return minimized operational projections; do not infer missing private fields or screening conclusions. Keep nullable/unknown values distinct from zero or a completed result.

## Use the published contract version

Use the API version and lifecycle shown in the API reference, the schemas for each operation, and any required form or resource version when updating an integration. Preserve opaque versions unchanged and pin an approved MCP release. Event names come from the same catalog as the API; arrival order and a product version are not substitutes for current resource readback.
