Publishing from your system
The recommended integration is push on change. When a unit is saved in your property system with "publish to Shace" switched on, call PUT /listings/{id} with your reference and the whole unit. When the switch goes off, or the unit is let, call POST /listings/{id}/unpublish. That is the entire integration: one call per event, no diffing on your side, no export job.
The calls are idempotent, so a system without save hooks can run the same calls on a schedule over its flagged units instead. Nothing else changes.
This page covers the rules those two calls rely on: how your references map to our ids, what a push does, and what happens when a unit is not publishable.
Your references and our ids
Every object and listing has two identifiers that do different jobs.
idis Shace's UUID. Always present in responses, generated by us.externalRefis your own id for the same record: the unit id in your property system, a spreadsheet row key, whatever you already have. Optional, and when present it is the identity you push by.
Anywhere a Shace id is accepted, ext: followed by your reference is accepted too. GET /listings/ext:unit-4711 and GET /listings/5c1c8f0e-... return the same listing. Your system therefore never has to store Shace ids.
The endpoint reference uses {id} as the path parameter name for every method. For PUT, send your external reference directly, without adding ext:: PUT /listings/unit-4711. For GET, POST and DELETE, send a Shace UUID or add ext: before your reference: POST /listings/ext:unit-4711/unpublish.
Rules
- Shace never generates a reference. No reference sent means no reference stored. A reference we invented would mean nothing in your system, and copying our UUID into the field would destroy the one question it answers: is this listing linked to a record on your side?
- References are unique per organization and case sensitive, 1 to 128 printable characters. URL-encode them in paths:
unit 12/Abecomes/listings/unit%2012%2FA. - Pushing a reference whose listing was unpublished republishes it (unless the body says
status: "draft"or the listing still has publish blockers (below)). The reference is your identity for the unit; if you send it again, it is live again. - One reference is enough. If you hold several ids for a unit, pick one.
Objects and listings
An object is a building or address. A listing is a unit in it. You can push them together or separately:
- Inline address on the listing (
address): Shace reuses your organization's active object at that address, or creates and geocodes one. Simplest when each unit knows its own address. - By your building reference (
objectRef): push the building first withPUT /objects/bldg-12, then point listings at it. Best when several units share an address, or when you correct an address centrally: onePUTon the object moves every listing with it. - By Shace id (
objectId): a UUID orext:bldg-12.
Exactly one of the three must be present on a listing body.
Listings that already exist in Shace
Landlords already using the app do not need to recreate listings to start publishing from their system. Linking an existing listing to your record by setting its externalRef is coming next; until then, ask Shace and we set the reference for you.
What a push does
An unchanged body is a no-op
Shace compares the incoming content with what it already holds, field by field. If nothing differs, the response says outcome: "unchanged", updatedAt does not move, and no translation or search-index work is queued. Pushing on every save, or your full inventory on a schedule, never generates churn.
One thing does not count as content and is ignored in the comparison: blurPlaceholders, which is derived data. assignedTo is not content either, but sending a member who is not the current assignee reassigns the listing and counts as a change. Every other optional field you leave out is treated as empty, so omitting description clears the stored one: send the whole unit every time.
Send price.amount and areaM2 with at most two decimal places. Extra decimal places are rejected with BAD_REQUEST, so accepted values are stored exactly and an identical push stays unchanged.
Each custom amenity needs a label in the listing's locale. You can also supply a label in the other language; Shace preserves labels you supply and automatically translates languages you omit. Existing automatic translations survive an unchanged push. Omitting a previously supplied label puts that language back under automatic translation.
What a change triggers
- Text changed (
description,customAmenities,locale): the listing is re-translated into the other locale and re-indexed for search. - Media or facts changed (
images,floorPlanUrl,type,amenityIds): re-indexed only. - Price, terms or hours changed (
price,noticePeriod,minimumDuration,areaM2,capacity,furnished,openHours): stored, nothing else.
Translation and indexing only run for published listings. A draft or unpublished listing is stored as sent, and both run when it goes live.
The response carries outcome: "updated" in every case.
Your system owns the listing
A listing with an externalRef is owned by your system. People can still edit it in the app, but your next push overwrites every content field. There is no merge: an edit made in the app lasts until the unit is next saved on your side.
Pending app edits are cleared too. If the committed content already matches your push but app edits are still pending, the response says outcome: "updated" when it clears them.
Two things made in the app survive a push, because they are not content: the assigned agent (unless the body sets assignedTo) and availability blocks.
Taking a unit off the market
POST /listings/ext:unit-4711/unpublish hides the listing from Shace's matchmaking and keeps its content and reference. The response is the full listing with status: "unpublished" and outcome: "updated", or outcome: "unchanged" if it was already off the market. Nothing is deleted, and the next PUT of the same reference puts it back on the market.
Two other things take a listing off: a PUT with status: "draft", and archiving its building with DELETE /objects/{id}.
Listings you do not push are never touched
The API only acts on what it receives. A listing you do not push keeps its state, so a bug on your side that skips units can never wipe your inventory. There is no full-sync mode that removes what is missing; taking a unit off the market is always the explicit call above.
Objects follow the same rules
PUT /objects/{id} with an unchanged address is a no-op and skips geocoding. A changed address is re-geocoded and every listing on the object follows. Pushing the reference of an archived object puts it back in service.
Verifying without pushing
GET /listings?updatedSince=2026-09-01T00:00:00Z returns what changed on Shace's side since a given time, cursor paginated. Use it to reconcile, to spot edits made in the app that your next push will overwrite, or to notice listings that someone unpublished in the app.
Changes to a listing's building or primary-language description also advance its updatedAt and appear in these results. The filter compares the same millisecond timestamps returned by the API: a row whose updatedAt equals updatedSince is excluded.
Publishing and blockers
A listing goes live when it is publishable and the body did not ask otherwise. The rules are the same ones the Shace app enforces:
| Blocker | Rule |
|---|---|
images | At least three images. |
price | A price above zero. Transparent pricing is part of what Shace is, so there is no "price on request". |
noticePeriod | A notice period when price.period is month. |
object | The object the listing sits on must be active, not archived. |
A PUT never fails on these
Field validation errors are 400 BAD_REQUEST: for example noticePeriod or minimumDuration on a day-priced listing, which only apply to month pricing. A body that is valid but not publishable is accepted: the listing is stored, status stays or becomes not live, and publishBlockers lists exactly what is missing.
The one exception is the object blocker on a new listing: creating a listing on an archived object is a 400 BAD_REQUEST, since there is nothing to store it against yet. Reactivate the object with PUT /objects/{id} first. An existing listing whose object is archived is accepted and reports publishBlockers: ["object"].
Code
Your save hook can therefore push whatever the unit holds and show the blockers next to the publish switch in your own UI. Units with a price of 0 in your system come through as drafts with publishBlockers: ["price"], which tells you precisely which units are missing the one thing Shace requires.
Statuses
draft: never been live. New listings that are not publishable, or that were sent withstatus: "draft".published: live and visible to Shace's matchmaking.unpublished: was live and has been taken off, either through/unpublish, because you sentstatus: "draft", because a change made it unpublishable, or because its object was archived.
Send the same reference again with the blockers resolved and it goes live; outcome is updated and status is published.
Holding a listing back
Send status: "draft" to store a listing without publishing it, for example to review it in the app first. The default is published.