Listings & Ownership
Build listing, ownership, contact, and owner-access workflows
Mental Model
A listing is the shared property context for ownership, reservations, fees, transactions, statements, and reporting. Ownership periods define who owns it over a date range. Contacts provide owner, vendor, payout, tax, and portal identity.
Resources and Lifecycle
Listings keep platform identity even when PMS references, status, groups, or owners change. Inactive listings and contacts remain in accounting history.
Ownership periods combine a listing, date range, business model, owner members
and splits, reserve behavior, and optional setListingInactive. Periods cannot
overlap. A changed owner constellation or allocation creates a new period.
Owner access is separate from contact identity. Invitations and viewer changes can affect an external identity provider and send notifications.
Read Model
Render returned listing status, active ownership, fee periods, parent and
children, upcoming deactivation, and issues[]. These fields are the server's
setup view; do not rebuild completeness queries in the client.
Keep inactive entities available when rendering history, but exclude them from normal new-entry selectors.
Decision Table
| Decision | Rule |
|---|---|
| Business model | managed, co_host, or co_host_airbnb |
| End date | Null means ongoing; start must precede a non-null end |
| Members | One or more unique owner contacts unless inactive period |
| Splits | Explicit plus distributed omitted values form full ownership |
| Deactivation | setListingInactive=true; do not collect owner members |
| Overlap default | onConflict=error |
| Remove locked history | Offer archive only when error context recommends it |
| Remove last period | Defaults to onLast=extendPrevious; send onLast=none unless the extension is confirmed |
| Payout account | Accounting account reference, never bank credentials |
Conflict strategies are error, updateExisting, and
adjustInsertingItem. Explain that the latter two can change a neighboring
stored period or the proposed range.
The onLast=extendPrevious default stretches the previous period over the
removed range, so keep it only when the user has confirmed that outcome;
otherwise pass onLast=none.
Contacts and Owner Access
Contacts are owners or vendors. Owners can join ownership periods and receive statements or portal access; vendors are normally selected on expenses and recurring expense templates.
An empty PUT /contacts/{id} body is an idempotent no-op. It returns the
current contact without writing an empty SQL update or emitting a contact
change.
Owner users can read GET /contacts, GET /contacts/{id}, and
GET /contacts/csv. List and export results include only contacts directly
owned by the authenticated user; requesting a contact outside the user's
owner-visible contact set by ID returns 404.
Invitations require an owner contact with an email. Admin and viewer access are mutually exclusive for one owner/user pair; remove the existing role before inviting the other.
Contact search matches both contact fields and secondary emails on the linked
admin owner user. Contact access and GET /contacts/{id}/viewers responses
return secondaryEmails alongside the primary email so clients can explain
which owner identity an alias belongs to.
Invites resolve email against the global platform identity before adding
regional owner access. A user whose former team membership was removed can be
invited into another region with the same email; the existing identity is
reused. If a non-owner team membership still exists in any region, the owner
invite remains blocked with 400 BAD_REQUEST. Ask for another email address;
a copy-link invite cannot bypass the account-type conflict. Legacy
identity-provider IDs are not part of the user API or copied into another
region.
Files linked to owner contacts use the same owner-access scope. Existing file downloads remain readable when a team is not initialized for the general ledger; GL-only file management operations remain unavailable to that team.
User Self-Deletion
DELETE /me always targets the authenticated interactive
user and accepts no user ID. It removes the platform identity, team
memberships, owner access, API tokens, and linked identity-provider account.
Deletion returns 409 if any active property-manager membership would be left
without another active admin or user member. Owner memberships do not
satisfy that safeguard. Otherwise the user may delete the account while team
memberships still exist; those memberships are removed by the same lifecycle.
API keys and embedded sessions cannot call this endpoint.
Editability
Ownership dates and listing groups can change journal attribution across many resources. Books closing, statement periods, statement attachment, or locked ownership history can prevent moving or shortening a period.
A listing group parent must have active ownership. A child with active ownership can join only when its active owner constellation and business model match the parent's. Grouping removes the child's separate ownership periods, so any child journal reference before books closing blocks the move, including zero-value entries. Support must migrate locked accounting history before the group can change.
Ownership updates validate only date boundaries that actually change. An unchanged mid-month start does not block an end-only edit when its statement month begins earlier, but the new end must still be on or after the locked range end.
An inactive period rejects owner-side postings in that range. Manager-side records can retain listing attribution when the operation permits it.
Listing deletion preserves historical accounting identity. Any journal entry
that references a listing directly or uses it as its listing-group parent
blocks hard deletion. The delete response includes journal entries in
context.lockReasons and suggests onLocked=archive; archive keeps the
listing available to its historical journals while making it inactive.
Invitations, viewer creation, reinvites, and access removal are external identity workflows. Do not present them as ordinary reversible contact edits.
Preview and Preflight
Dry-run listing, ownership-period, group, contact, and supported delete/archive mutations when their generated operations declare support. Always preflight an ownership range or parent change because one edit can affect reservations, fees, statements, and journal attribution outside the visible row.
Mutation Recipe
- Load listing setup state, ownership periods, contacts, and returned issues.
- Validate date ordering, unique owners, splits, and business model locally.
- Select and explain an overlap strategy.
- Render current locks and any suggested archive outcome.
- Dry-run the complete proposed listing or period mutation.
- Show every adjusted neighboring period returned by the API.
- Confirm and apply the same payload.
- Re-read listing setup and affected periods.
Failure and Recovery
On overlap, keep the proposed dates and let the user choose a documented
strategy. When a lock blocks the change, show the structured lock context and
the first open date, and offer archive only if the error context returns
suggestedOnLocked: "archive". After a concurrent edit, reload all periods
before offering conflict resolution again.
Common Recipes
Mid-month ownership change
End the current period and create the new owner constellation with a boundary that does not overlap. Expect separate statements for the same listing and month when both periods contain activity.
For example, this preflight starts a new ownership period on July 15 and lets the API trim the overlapping current period:
POST /listings/ownership-periods?dryRun=true
Content-Type: application/json
{
"listingId": "11111111-1111-4111-8111-111111111111",
"startAt": "2026-07-15",
"endAt": null,
"members": [
{
"contactId": "33333333-3333-4333-8333-333333333333",
"split": 60000
},
{
"contactId": "44444444-4444-4444-8444-444444444444",
"split": 40000
}
],
"onConflict": "updateExisting"
}Deactivate a listing
Create or update the relevant ownership period with
setListingInactive=true. Do not delete historical owners or prior periods.
PUT /listings/ownership-periods/{ownershipPeriodId}?dryRun=true
Content-Type: application/json
{
"setListingInactive": true
}Webhook Reconciliation
listing.changed invalidates the canonical listing after creation, public
configuration or hierarchy changes, archive/reactivation, and deletion.
Compare resourceVersion and fetch href; a deletion tombstone may return
404. Ownership-period changes emit only when the canonical listing response
changes.
API Reference
- List listings — GET /listings
- Create a listing — POST /listings
- Update a listing —
PUT /listings/{id} POST /listings/ownership-periods— Create an ownership periodPUT /listings/ownership-periods/{id}— Update one- Create a contact — POST /contacts
- Invite an owner contact —
POST /contacts/{id}/invite
