@propertysimple/client 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/API.md +27 -73
  2. package/README.md +200 -57
  3. package/client/src/index.d.ts +257 -2422
  4. package/client/src/index.js +6 -69
  5. package/contracts/src/index.d.ts +0 -7
  6. package/contracts/src/index.js +0 -7
  7. package/contracts/src/openapi.js +1 -1
  8. package/contracts/src/operations.d.ts +950 -4678
  9. package/contracts/src/operations.js +6 -36
  10. package/contracts/src/opportunity-events.d.ts +2 -1
  11. package/contracts/src/opportunity-events.js +6 -1
  12. package/contracts/src/property-features.d.ts +2 -2
  13. package/contracts/src/property-features.js +62 -19
  14. package/contracts/src/routes.d.ts +28 -305
  15. package/contracts/src/routes.js +82 -354
  16. package/contracts/src/schemas.d.ts +1114 -1355
  17. package/contracts/src/schemas.js +45 -223
  18. package/contracts/src/search.d.ts +1366 -253
  19. package/contracts/src/search.js +117 -2
  20. package/openapi.json +19288 -38596
  21. package/package.json +1 -9
  22. package/client/src/directory-sync.d.ts +0 -123
  23. package/client/src/directory-sync.js +0 -404
  24. package/client/src/managed-sync.d.ts +0 -129
  25. package/client/src/managed-sync.js +0 -410
  26. package/contracts/src/directory-execution.d.ts +0 -320
  27. package/contracts/src/directory-execution.js +0 -122
  28. package/contracts/src/directory-identity.d.ts +0 -1255
  29. package/contracts/src/directory-identity.js +0 -129
  30. package/contracts/src/directory-sales.d.ts +0 -333
  31. package/contracts/src/directory-sales.js +0 -102
  32. package/contracts/src/directory.d.ts +0 -1088
  33. package/contracts/src/directory.js +0 -179
  34. package/contracts/src/intelligence.d.ts +0 -366
  35. package/contracts/src/intelligence.js +0 -157
  36. package/contracts/src/professional-intelligence.d.ts +0 -256
  37. package/contracts/src/professional-intelligence.js +0 -90
  38. package/contracts/src/publication.d.ts +0 -44
  39. package/contracts/src/publication.js +0 -42
  40. package/contracts/src/smart-feed.d.ts +0 -3731
  41. package/contracts/src/smart-feed.js +0 -109
package/API.md CHANGED
@@ -2,78 +2,32 @@
2
2
 
3
3
  Generated from the shared route registry. Run `npm run sdk:docs` in the platform repository to update it.
4
4
 
5
- Every operation below is available through `ps.call(operation, input, options)`, with inferred input and response types. The [quickstart](README.md) documents convenient namespace helpers such as `ps.listings.get(id)` and `ps.search.listings(query)`. Request fields use the API's snake_case names. Supply `{ idempotencyKey: 'stable-request-id' }` in options for mutating POST requests; search POSTs are read-only.
5
+ Every operation below is available through `ps.call(operation, input, options)`, with inferred input and response types. The [quickstart](README.md) documents convenient namespace helpers such as `ps.listings.get(id)` and `ps.search.listings(query)`. Request fields use the API's snake_case names. Every POST in this registry is a read-only search request and needs no idempotency key; the SDK requires `{ idempotencyKey: 'stable-request-id' }` only for a mutating POST.
6
6
 
7
- This reference describes the SDK contract, not a claim that every feature is enabled or accepted in production. Credential scopes, dataset grants, source freshness and index readiness still govern access. Legacy `/api/*`, admin pages, public health/OpenAPI routes and internal jobs are outside this versioned SDK registry. The package also includes `openapi.json` with full input/output schemas.
7
+ This reference describes the SDK contract, not a claim that every feature is enabled or accepted in production. Credential scopes, dataset grants, source freshness and index readiness still govern access. Legacy `/api/*`, admin pages, public health/OpenAPI routes and internal jobs are outside this versioned SDK registry. The package also includes `openapi.json` with full input/output schemas. Operations retired in October 2026 (directory, catalog snapshots and changes, publications, judgments, opportunity plans and actions) are no longer in the SDK; the API answers their routes with `410 RETIRED`.
8
8
 
9
- | Operation for `ps.call` | HTTP endpoint | Required scope | Purpose |
10
- | ---------------------------------- | ---------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------- |
11
- | `searchFeatures` | `GET /v1/search/features` | `catalog:read` | Read typed feature definitions and supported search operators. |
12
- | `searchListings` | `POST /v1/search/listings` | `catalog:read` | Search current inventory with typed hard constraints and evidence. |
13
- | `searchFacets` | `POST /v1/search/facets` | `catalog:read` | Read authorized refinement counts for the same normalized search. |
14
- | `searchMap` | `POST /v1/search/map` | `catalog:read` | Read bounded map points and clusters with matching search semantics. |
15
- | `searchSuggest` | `GET /v1/search/suggest` | `catalog:read` | Suggest registered features and permitted listing locations. |
16
- | `searchInterpret` | `POST /v1/search/interpret` | `catalog:read` | Interpret a bounded search vocabulary into editable filters and unresolved text. |
17
- | `directoryGetCanonicalIdentity` | `GET /v1/directory/canonical-identities/{id}` | `directory:read` | Resolve reviewed identity links using only permitted source members. |
18
- | `directoryMergeIdentities` | `POST /v1/directory/identity-merges` | `directory:identity:write` | Record an audited identity merge with expected source bindings. |
19
- | `directorySplitIdentity` | `POST /v1/directory/identity-splits` | `directory:identity:write` | Split a reviewed identity without moving account or action history. |
20
- | `directoryListInventory` | `GET /v1/directory/entities/{id}/inventory` | `directory:read` | Read represented or current-roster inventory with optional verified corporate rollup. |
21
- | `discoverDirectorySalesCandidates` | `GET /v1/directory/sales-candidates` | `directory:sales` | Discover a bounded authorized page of subjects for sales-plan evaluation. |
22
- | `claimDirectoryActions` | `POST /v1/directory/action-claims` | `directory:execute` | Reserve directory recommendations under a separately reviewed execution policy. |
23
- | `startDirectoryAttempt` | `POST /v1/directory/actions/{id}/attempts` | `directory:execute` | Recheck authority and start one attempt, or return reconciliation-only replay. |
24
- | `getDirectoryAction` | `GET /v1/directory/actions/{id}` | `directory:execute` | Read a product-scoped directory action and its execution state. |
25
- | `recordDirectoryActionOutcomes` | `POST /v1/directory/action-outcomes` | `directory:execute` | Record authenticated provider receipts without retrying ambiguous effects. |
26
- | `directoryRecordProfile` | `POST /v1/directory/profile-observations` | `directory:write` | Record a trusted producer’s scoped profile evidence. |
27
- | `directoryRecordRelationship` | `POST /v1/directory/relationship-observations` | `directory:write` | Record a trusted producer’s evidenced relationship. |
28
- | `directoryListProfessionals` | `GET /v1/directory/professionals` | `directory:read` | Read permitted source-backed professionals. |
29
- | `directoryListOffices` | `GET /v1/directory/offices` | `directory:read` | Read permitted source offices. |
30
- | `directoryListOrganizations` | `GET /v1/directory/organizations` | `directory:read` | Read evidenced business and brand observations. |
31
- | `directoryListTeams` | `GET /v1/directory/teams` | `directory:read` | Read evidenced teams. |
32
- | `directoryGetEntity` | `GET /v1/directory/entities/{id}` | `directory:read` | Read a scoped professional, office, organization or team. |
33
- | `directoryResolveAgent` | `GET /v1/directory/legacy-agents/{id}` | `directory:read` | Resolve a listing agent ID to its permitted public directory identity. |
34
- | `directoryResolveIdentity` | `GET /v1/directory/identities` | `directory:read` | Resolve a typed directory source identity. |
35
- | `directoryListRelationships` | `GET /v1/directory/relationships` | `directory:read` | Read bounded evidenced relationships. |
36
- | `directoryGetContacts` | `GET /v1/directory/entities/{id}/contacts` | `directory:contacts` | Read purpose-permitted directory contact endpoints. |
37
- | `directoryGetMetrics` | `GET /v1/directory/entities/{id}/metrics` | `directory:read` | Read authorized observed inventory metrics. |
38
- | `createDirectorySnapshot` | `POST /v1/directory/snapshots` | `directory:sync` | Capture a directory or combined feed boundary. |
39
- | `getDirectorySnapshot` | `GET /v1/directory/snapshots/{id}` | `directory:sync` | Read snapshot scope, boundary and expiry. |
40
- | `listDirectorySnapshotItems` | `GET /v1/directory/snapshots/{id}/items` | `directory:sync` | Page immutable typed snapshot resources. |
41
- | `listDirectoryChanges` | `GET /v1/directory/changes` | `directory:sync` | Replay typed changes with complete commit-group boundaries. |
42
- | `createDirectorySalesPlan` | `POST /v1/directory/sales-plans` | `directory:sales` | Prepare bounded professional or organization sales recommendations. |
43
- | `getDirectorySalesPlan` | `GET /v1/directory/sales-plans/{id}` | `directory:sales` | Read a scoped recommendation-only directory sales plan. |
44
- | `requestDirectoryAssessment` | `POST /v1/directory/assessments` | `intelligence:judge` | Queue a canonical-evidence directory assessment. |
45
- | `latestDirectoryAssessment` | `GET /v1/directory/assessments/latest` | `intelligence:read` | Read prepared directory intelligence with current evidence checks. |
46
- | `getDirectoryAssessment` | `GET /v1/directory/assessments/{id}` | `intelligence:read` | Read a purpose-scoped directory assessment. |
47
- | `reportPublication` | `POST /v1/publications` | `publications:write` | Acknowledge this product’s current listing publication. |
48
- | `resolveDestination` | `GET /v1/listings/{id}/destination` | `catalog:read` | Resolve a current product-reported destination. |
49
- | `evaluateJudgment` | `POST /v1/judgments` | `intelligence:judge` | Evaluate a bounded versioned task against caller-supplied evidence. |
50
- | `getJudgment` | `GET /v1/judgments/{id}` | `intelligence:read` | Read this credential subject’s durable judgment audit. |
51
- | `intelligenceCapabilities` | `GET /v1/intelligence-capabilities` | `intelligence:read` | Read implemented judgment tasks and explicit unsupported capabilities. |
52
- | `capabilities` | `GET /v1/capabilities` | Authenticated caller | Read this caller’s supported capabilities. |
53
- | `getCatalogHealth` | `GET /v1/catalog-health` | `catalog:read` | Read authorized source freshness independently of catalog changes. |
54
- | `listListings` | `GET /v1/listings` | `catalog:read` | Search the current authorized catalog. |
55
- | `listListingMap` | `GET /v1/listings/map` | `catalog:read` | Read bounded map markers within a required geographic box. |
56
- | `listSimilarListings` | `GET /v1/listings/{id}/similar` | `catalog:read` | Rank a bounded local shortlist using transparent fact similarity. |
57
- | `getListing` | `GET /v1/listings/{id}` | `catalog:read` | Read a current listing. |
58
- | `getListingContacts` | `GET /v1/listings/{id}/contacts` | `contacts:read` | Read protected source contact routing for a current active listing. |
59
- | `resolveListingIdentity` | `GET /v1/listing-identities` | `catalog:read` | Resolve a namespaced source alias. |
60
- | `resolveAgentIdentity` | `GET /v1/agent-identities` | `agents:read` | Resolve a namespaced agent source alias. |
61
- | `getAgent` | `GET /v1/agents/{id}` | `agents:read` | Read permitted agent attribution. |
62
- | `listAgentListings` | `GET /v1/agents/{id}/listings` | `catalog:read` | Read an agent’s authorized inventory. |
63
- | `classificationSchema` | `GET /v1/classification-schema` | `catalog:read` | Read the current classification vocabulary. |
64
- | `createSnapshot` | `POST /v1/catalog-snapshots` | `catalog:sync` | Create a consistent snapshot with its replay cursor. |
65
- | `getSnapshot` | `GET /v1/catalog-snapshots/{id}` | `catalog:sync` | Read snapshot readiness and expiry. |
66
- | `listSnapshotItems` | `GET /v1/catalog-snapshots/{id}/items` | `catalog:sync` | Page an immutable snapshot. |
67
- | `listCatalogChanges` | `GET /v1/catalog-changes` | `catalog:sync` | Replay authorized projection changes and removals. |
68
- | `listEvents` | `GET /v1/events` | `catalog:sync` | Replay committed domain occurrences. |
69
- | `listActionProfiles` | `GET /v1/action-profiles` | `opportunities:read` | Read published action profiles. |
70
- | `listOpportunities` | `GET /v1/opportunities` | `opportunities:read` | Read current candidates without reserving actions. |
71
- | `createPlan` | `POST /v1/opportunity-plans` | `plans:create` | Create or reuse an actual persisted selection. |
72
- | `getPlan` | `GET /v1/opportunity-plans/{id}` | `opportunities:read` | Read plan progress, coverage and execution counts. |
73
- | `listPlanItems` | `GET /v1/opportunity-plans/{id}/items` | `opportunities:read` | Read stable selection pages. |
74
- | `claimDispatchBatch` | `POST /v1/dispatch-batches` | `actions:dispatch` | Reserve executable actions after critical revalidation. |
75
- | `startAttempt` | `POST /v1/actions/{id}/attempts` | `actions:dispatch` | Fence and record dispatch before an external effect. |
76
- | `recordActionOutcomes` | `POST /v1/action-outcomes` | `actions:dispatch` | Record idempotent sender receipts with per-item results. |
77
- | `recordProductContextEvents` | `POST /v1/product-context-events` | `product-events:write` | Ingest owned product context snapshots and deltas. |
78
- | `createWebhookSubscription` | `POST /v1/webhook-subscriptions` | `webhooks:manage` | Register a subscription only where delivery support is configured. |
79
- | `readiness` | `GET /v1/operations/readiness` | `operations:read` | Inspect configured operational dependencies. |
9
+ | Operation for `ps.call` | HTTP endpoint | Required scope | Purpose |
10
+ | ------------------------ | -------------------------------- | -------------------- | -------------------------------------------------------------------------------- |
11
+ | `searchFeatures` | `GET /v1/search/features` | `catalog:read` | Read typed feature definitions and supported search operators. |
12
+ | `searchListings` | `POST /v1/search/listings` | `catalog:read` | Search current inventory with typed hard constraints and evidence. |
13
+ | `searchFacets` | `POST /v1/search/facets` | `catalog:read` | Read authorized refinement counts for the same normalized search. |
14
+ | `searchStats` | `POST /v1/search/stats` | `catalog:read` | Read count, price, price per area and days-on-market statistics for a search. |
15
+ | `searchIndex` | `POST /v1/search/index` | `catalog:read` | Walk compact entries for every listing a search matches, for sitemaps. |
16
+ | `searchMap` | `POST /v1/search/map` | `catalog:read` | Read bounded map points and clusters with matching search semantics. |
17
+ | `searchSuggest` | `GET /v1/search/suggest` | `catalog:read` | Suggest registered features and permitted listing locations. |
18
+ | `searchInterpret` | `POST /v1/search/interpret` | `catalog:read` | Interpret a bounded search vocabulary into editable filters and unresolved text. |
19
+ | `capabilities` | `GET /v1/capabilities` | Authenticated caller | Read this caller’s supported capabilities. |
20
+ | `getCatalogHealth` | `GET /v1/catalog-health` | `catalog:read` | Read authorized source freshness independently of catalog changes. |
21
+ | `listListings` | `GET /v1/listings` | `catalog:read` | Search the current authorized catalog. |
22
+ | `listListingMap` | `GET /v1/listings/map` | `catalog:read` | Read bounded map markers within a required geographic box. |
23
+ | `listSimilarListings` | `GET /v1/listings/{id}/similar` | `catalog:read` | Rank a bounded local shortlist using transparent fact similarity. |
24
+ | `getListing` | `GET /v1/listings/{id}` | `catalog:read` | Read a current listing. |
25
+ | `getListingContacts` | `GET /v1/listings/{id}/contacts` | `contacts:read` | Read protected source contact routing for a current active listing. |
26
+ | `resolveListingIdentity` | `GET /v1/listing-identities` | `catalog:read` | Resolve a namespaced source alias. |
27
+ | `resolveAgentIdentity` | `GET /v1/agent-identities` | `agents:read` | Resolve a namespaced agent source alias. |
28
+ | `getAgentEmailSharing` | `GET /v1/agent-email-sharing` | `contacts:read` | Count known source-agent identities sharing an exact stored email. |
29
+ | `getAgent` | `GET /v1/agents/{id}` | `agents:read` | Read permitted agent attribution. |
30
+ | `listAgentListings` | `GET /v1/agents/{id}/listings` | `catalog:read` | Read an agent’s authorized inventory. |
31
+ | `listEvents` | `GET /v1/events` | `catalog:sync` | Replay committed domain occurrences. |
32
+ | `listListingChanges` | `GET /v1/listing-changes` | `catalog:sync` | Follow every listing change, including removals, from a durable cursor. |
33
+ | `readiness` | `GET /v1/operations/readiness` | `operations:read` | Inspect configured operational dependencies. |
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @propertysimple/client
2
2
 
3
3
  The typed **server-side PropertySimple SDK** for listing details, property search,
4
- agents, catalog synchronization, directory data and the platform's other public
5
- API operations. Configure one client in your backend, then use `ps.listings`,
6
- `ps.search`, `ps.agents` and `ps.catalog` throughout your application.
4
+ agents, source health, listing synchronization and the catalog event stream.
5
+ Configure one client in your backend, then use `ps.listings`, `ps.search`, `ps.agents` and `ps.catalog`
6
+ throughout your application.
7
7
 
8
8
  This package is distributed publicly for **authenticated PropertySimple API
9
9
  integrations**. An API key is required to access data; no credentials or listing
@@ -12,11 +12,21 @@ is separate and unchanged.
12
12
 
13
13
  **Service readiness:** SDK availability and API feature readiness are separate.
14
14
  Start direct catalog integrations with `ps.listings` and `ps.agents`. Managed
15
- search uses `ps.search` and requires a ready, authorized index on the API server.
16
- At the initial 0.1.0 release, full production search acceptance is still pending;
17
- do not treat this package release as approval to switch search traffic. Installing
18
- the SDK does not activate imports, indexing, synchronization schedules, Jev,
19
- geocoding or outbound actions.
15
+ search uses `ps.search` with an authorized index on the API server; responses
16
+ identify their backend and consistency (see [Search](#search)). Check the target
17
+ environment's capabilities and readiness before switching search traffic.
18
+ Installing the SDK does not activate imports, indexing, schedules,
19
+ geocoding or any outbound contact.
20
+
21
+ **Breaking changes from 0.1.0 to 0.2.0:** the directory, catalog snapshots and
22
+ changes, publications, judgments, opportunity plans and actions were retired from
23
+ the platform. Their SDK helpers (`ps.directory`, `ps.publications`, `ps.judgments`,
24
+ `ps.opportunityPlans`, `ps.actions`, `ps.directoryActions`, the catalog snapshot
25
+ and change helpers) and the `@propertysimple/client/managed-sync` and
26
+ `/directory-sync` exports are gone; the API answers their routes with
27
+ `410 RETIRED`. Use the new `ps.catalog.listingChanges` helper to keep a local copy
28
+ current, or follow events to react to listing occurrences (see
29
+ [Keeping a copy current](#keeping-a-copy-current)).
20
30
 
21
31
  ## Installation
22
32
 
@@ -26,7 +36,7 @@ included. TypeScript applications should use their normal `@types/node` setup.
26
36
  CommonJS and browser clients are not supported by this release.
27
37
 
28
38
  ```sh
29
- npm install @propertysimple/client
39
+ npm install @propertysimple/client@0.2.0
30
40
  ```
31
41
 
32
42
  The archive contains compiled JavaScript, TypeScript declarations, the shared
@@ -65,7 +75,7 @@ instances can coexist without changing another tenant's configuration.
65
75
  Request a service key and dataset ID from your PropertySimple platform
66
76
  administrator. Keys grant specific datasets and operations: catalog reads use
67
77
  `catalog:read`, agent profiles use `agents:read`, and protected contacts need their
68
- own scope. Choose the minimum permissions your integration needs. A dataset ID is
78
+ own scope; listing synchronization and the event stream use `catalog:sync`. Choose the minimum permissions your integration needs. A dataset ID is
69
79
  an authorization boundary, not an MLS number or a value to guess.
70
80
 
71
81
  Keep the key in your backend's secret manager or an ignored local `.env` file.
@@ -144,29 +154,78 @@ const results = await ps.search.listings({
144
154
  Search responses include compact listing cards with a cover photo, `photo_count`,
145
155
  match reasons and index/freshness metadata. Use `ps.listings.get(id, options)` for
146
156
  complete details. The SDK calls the shared API; it never exposes Typesense keys.
147
- Source facts can be searched without Jev; interpreted features require their own
148
- current evidence. Missing evidence is not a negative match.
157
+ Missing feature evidence is not a negative match.
158
+
159
+ With a loaded, promoted index, managed search reports `meta.backend: 'typesense'`
160
+ and `meta.consistency: 'current_index'`. Listing results are rechecked against the
161
+ current catalog; totals, facets, statistics and index entries describe the search
162
+ index and can briefly lag catalog changes.
163
+
164
+ When the API uses the PostgreSQL fallback, `ps.search.listings` supports plain
165
+ filters (text, offer, price, beds/baths/area, city/region/ZIP, property type, bounds)
166
+ and the `price_asc`, `price_desc` and `newest` sorts, with
167
+ `meta.backend: 'postgres'` and `meta.consistency: 'degraded'`; `relevance` returns
168
+ newest first. Feature predicates, `facet_fields`, radius, polygon and the portal
169
+ filters below require the managed index and return HTTP 503 when it is unavailable.
170
+ `ps.search.facets`, `stats`, `index`, `map` and `suggest` also require the
171
+ index. `ps.search.features` and `interpret` remain available, but an interpreted
172
+ query can still require an unavailable index. Surface persistent search failures;
173
+ never drop filters and present a broader result. The catalog's bounded
174
+ `ps.listings.map` remains available independently of the search index.
175
+
176
+ The search map helper takes `{ query, viewport, zoom, limit }`.
177
+ Use the same applied query for listings, facets and maps, and honor
178
+ generation/checkpoint, coverage, truncation and expiry metadata.
179
+
180
+ `ps.search.stats({ query })` returns count, price, price-per-area and days-on-market
181
+ statistics for a query (place and feature pages); pass `currency` and
182
+ `transaction_type` (and `price_period` for rentals) to get prices; price-per-area
183
+ statistics also require `living_area_unit`. Missing context produces a null
184
+ statistic and a warning. Queries also take a
185
+ `polygon`, and `filters` take `county`, `days_on_market`, `year_built`, `lot_size`,
186
+ `has_photos`, `has_virtual_tour`, `property_types` and `exclude_property_types`
187
+ (`capabilities.features` lists `search_stats` and `search_portal_filters`).
188
+ `ps.search.features()` lists only the features that can match.
189
+
190
+ `ps.search.index({ query, limit, after })` pages compact entries for sitemaps and
191
+ link indexes, up to 5,000 entries per page (default 1,000). Pass the previous
192
+ page's `next` as `after` until `has_more` is false. Index entries can briefly be
193
+ stale; use `ps.listings.get` for current listing details. The index walk uses its
194
+ own stable order regardless of the query's `sort` and is not a snapshot.
195
+
196
+ <!-- sdk-example: search-index.ts -->
149
197
 
150
- The map helper takes `{ query, viewport, zoom, limit }`. Use the same applied
151
- query for listings, facets and maps, and honor generation/checkpoint, coverage,
152
- truncation and expiry metadata. Search readiness or stale-index errors must not
153
- silently become empty inventory or a broader query with filters removed.
198
+ ```ts
199
+ import ps from './ps.js';
200
+
201
+ const query = {
202
+ dataset_id: 'your-authorized-dataset',
203
+ filters: { region: 'TX', has_photos: true },
204
+ };
205
+ const page = await ps.search.index({ query, limit: 1000 });
206
+
207
+ if (page.has_more && page.next) {
208
+ const nextPage = await ps.search.index({ query, limit: 1000, after: page.next });
209
+ console.log(nextPage.data.map((entry) => entry.id));
210
+ }
211
+ ```
212
+
213
+ `ps.search.facets` accepts a `limit` of up to 1,000 values per field; honor each
214
+ field's `truncated` flag. `ps.search.suggest({ q, include_listings: true })` can
215
+ include street-address suggestions when the query starts with a house number and
216
+ the listing's address may be shown.
154
217
 
155
218
  ## Methods and complete API coverage
156
219
 
157
- | Namespace | Common methods |
158
- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
159
- | `ps.listings` | `list`, `get`, `resolveIdentity`, `contacts`, `map`, `similar` |
160
- | `ps.search` | `listings`, `suggest`, `facets`, `map`, `features`, `interpret` |
161
- | `ps.agents` | `get`, `resolveIdentity`, `listings` |
162
- | `ps.catalog` | `health`, `createSnapshot`, `snapshot`, `snapshotItems`, `changes`, `events`, `opportunityObservations` |
163
- | `ps.directory` | `professionals`, `offices`, `organizations`, `teams`, `get`, `inventory`, identity, contact and synchronization helpers |
164
- | `ps.publications` | `report`, `destination` |
165
- | `ps.judgments` | `capabilities`, `evaluate`, `get` |
166
- | `ps.opportunityPlans` | `create`, `get`, `items`, `claimDispatchBatch`, `waitUntilReady` |
167
- | `ps.actions` / `ps.directoryActions` | Explicit action claims, attempts and outcomes |
168
-
169
- All 69 registered versioned public operations are available through
220
+ | Namespace | Common methods |
221
+ | ------------- | --------------------------------------------------------------------------------- |
222
+ | `ps.listings` | `list`, `get`, `resolveIdentity`, `contacts`, `map`, `similar` |
223
+ | `ps.search` | `listings`, `suggest`, `facets`, `stats`, `index`, `map`, `features`, `interpret` |
224
+ | `ps.agents` | `get`, `resolveIdentity`, `listings`, `emailSharing` |
225
+ | `ps.catalog` | `health`, `listingChanges`, `events`, `opportunityObservations` |
226
+
227
+ `ps.iterateListings(query)` pages through live listing results. Every registered
228
+ versioned public operation is available through
170
229
  `ps.call(operation, input, options)`, including operations without a shortcut.
171
230
  Input and output types are inferred from the operation name. Input fields use the
172
231
  API's snake_case names; helpers use camelCase. For example:
@@ -218,19 +277,102 @@ if (firstPage.has_more && firstPage.next_cursor) {
218
277
  }
219
278
  ```
220
279
 
221
- For a consistent replica, create a catalog snapshot, read its immutable pages,
222
- then replay `ps.catalog.changes` from the supplied cursor. Persist the checkpoint
223
- atomically with applied updates/removals. An expired cursor requires a new
224
- snapshot; resuming from an invented timestamp loses removals.
280
+ ### Keeping a copy current
281
+
282
+ `ps.catalog.listingChanges` (`GET /v1/listing-changes`, scope `catalog:sync`)
283
+ returns each changed listing's current state, including identities for hidden
284
+ and removed records. Start with `start: 'all'` to walk the complete catalog and
285
+ then catch changes made during that walk. The default `start: 'now'` starts with
286
+ an empty page and a cursor for future changes; it does not load existing listings.
287
+
288
+ Apply `visible` items as upserts and take `hidden` or `removed` items down. Removal
289
+ is not evidence of a sale; a restored source listing returns under the same ID.
290
+ A listing can appear more than once during the initial walk; apply items in order
291
+ and keep the last state. This is a feed of current states, not every intermediate
292
+ revision. Store each page's changes and `next_cursor` in one transaction. Continue
293
+ with that cursor while `has_more` is true, then poll from the saved cursor.
294
+
295
+ <!-- sdk-example: sync.ts -->
296
+
297
+ ```ts
298
+ import ps from './ps.js';
299
+
300
+ const datasetId = 'your-authorized-dataset';
301
+ let cursor: string | undefined = undefined; // load the cursor stored with your copy
302
+
303
+ const page = await ps.catalog.listingChanges({
304
+ dataset_id: datasetId,
305
+ ...(cursor ? { cursor } : { start: 'all' }),
306
+ limit: 200,
307
+ });
308
+ for (const change of page.data) {
309
+ if (change.status === 'visible') {
310
+ if (!change.listing) throw new Error('Visible change is missing its listing.');
311
+ console.log('upsert', change.listing.id, change.listing.revision);
312
+ } else {
313
+ console.log('take down', change.listing_id, change.status);
314
+ }
315
+ }
316
+ if (!page.next_cursor) throw new Error('Listing changes did not return a cursor.');
317
+ cursor = page.next_cursor;
318
+ // Replace logging with writes; commit those writes and this cursor in one transaction.
319
+ // Request the next page only after that commit; continue while page.has_more.
320
+ ```
321
+
322
+ Listing-change cursors expire seven days after issue. Poll at least daily; on
323
+ `410 CURSOR_EXPIRED`, rebuild the copy with `start: 'all'` and reconcile its
324
+ visible, hidden and removed states. Never skip or invent a cursor to repair lag.
325
+ `include_contacts: true` additionally requires `contacts:read`; contacts follow
326
+ the current listing's visibility and freshness rules. Respect location expiry
327
+ and address privacy in stored copies and caches.
328
+
329
+ ### Reacting to events
330
+
331
+ `ps.catalog.events` (`GET /v1/events`, scope `catalog:sync`) replays committed
332
+ listing occurrences in order: additions, price and status changes, open houses,
333
+ removals and others. Each event names a `listing_id`; re-read that listing rather
334
+ than patching your copy from event evidence. To start, page events until
335
+ `has_more` is false and keep that cursor, then read the catalog with
336
+ `ps.listings.list` or `ps.iterateListings`, and store the rows and the cursor
337
+ together. Anything that changes during the read is replayed from the cursor;
338
+ re-reading a listing twice is harmless. Then poll from the stored cursor:
339
+
340
+ <!-- sdk-example: events.ts -->
341
+
342
+ ```ts
343
+ import { ApiError } from '@propertysimple/client';
344
+ import ps from './ps.js';
345
+
346
+ const datasetId = 'your-authorized-dataset';
347
+ let cursor: string | undefined = undefined; // load the cursor stored with your copy
348
+
349
+ const page = await ps.catalog.events({ dataset_id: datasetId, cursor, limit: 100 });
350
+ for (const listingId of new Set(page.data.map((event) => event.listing_id))) {
351
+ try {
352
+ const listing = await ps.listings.get(listingId, { dataset_id: datasetId });
353
+ console.log('upsert', listing.id, listing.revision);
354
+ } catch (error) {
355
+ // No longer readable under this grant. That is not evidence of a sale.
356
+ if (error instanceof ApiError && error.status === 404) console.log('unavailable', listingId);
357
+ else throw error;
358
+ }
359
+ }
360
+ cursor = page.next_cursor ?? cursor;
361
+ // Commit the changed rows and this cursor in one transaction; continue while page.has_more.
362
+ ```
225
363
 
226
- The optional `@propertysimple/client/managed-sync` export supplies
227
- `CatalogSyncWorker` and `PostgresCatalogTarget`. The
228
- `@propertysimple/client/directory-sync` export supplies `DirectorySyncWorker`
229
- and `PostgresDirectoryTarget` for directory or combined feeds. They use an
230
- explicitly supplied database adapter and a separate consumer-owned schema;
231
- installation does not create tables, overwrite product data or install a schedule.
232
- Keep the database wrappers, checkpoints and cache invalidation handling required
233
- by the [managed synchronization guide](https://github.com/PropertySimple/ps-listings-platform/blob/main/docs/managed-sync.md).
364
+ Events are served about a minute after they are written, so poll on a schedule.
365
+ Event cursors expire seven days after issue. Retention windows are eight days for
366
+ `listing_updated` and fourteen days for other events when retention is enabled.
367
+ A cursor behind deleted history gets `410 EVENT_HISTORY_PRUNED`; re-read the
368
+ listings you keep and restart from retained history instead of skipping the gap.
369
+ Use `listingChanges` for a catalog copy and events for occurrence history; event
370
+ evidence is not a full historical listing snapshot.
371
+ `ps.catalog.opportunityObservations`
372
+ returns the same page reduced to typed observations (new to market, price drop,
373
+ open house, market-age milestone, source removal).
374
+ The platform repository's `npm run demo:sync` runs the event loop against a disposable
375
+ fictional catalog.
234
376
 
235
377
  ## Errors, requests and retries
236
378
 
@@ -268,20 +410,21 @@ For an integration timeout spanning retries, pass
268
410
  HTTP status, problem code and request ID; exclude API keys, contact details and
269
411
  complete response bodies from ordinary logs. In particular:
270
412
 
271
- | Result | Integration behavior |
272
- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
273
- | HTTP 401/403 | Check the service key, dataset grant and operation scope; do not retry with broader credentials automatically. |
274
- | HTTP 404 | Handle the unavailable resource explicitly; do not infer that a listing sold. |
275
- | HTTP 429/503 | Honor the SDK's bounded retry behavior and surface persistent unavailability. |
276
- | `SEARCH_INDEX_NOT_READY` / `SEARCH_SNAPSHOT_TOO_OLD` | Keep search unavailable until a valid generation is ready; do not show an empty inventory as a successful response. |
277
- | `ContractError` | Check API/SDK compatibility and report the contract mismatch. |
278
-
279
- Mutating POST operations require a caller-supplied
280
- `{ idempotencyKey: 'stable-request-id' }` request option. Retries keep the same key
281
- and body. Search POSTs are read-only and need no key. An action attempt with
282
- `replayed: true` is an existing attempt to reconcile, not permission to send again.
283
- The SDK does not call email, advertising or payment providers. A plan does not
284
- constitute permission to contact someone or make a purchase.
413
+ | Result | Integration behavior |
414
+ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
415
+ | HTTP 401/403 | Check the service key, dataset grant and operation scope; do not retry with broader credentials automatically. |
416
+ | HTTP 404 | Handle the unavailable resource explicitly; do not infer that a listing sold. |
417
+ | HTTP 429/503 | Honor the SDK's bounded retry behavior and surface persistent unavailability. |
418
+ | `SEARCH_UNAVAILABLE` (HTTP 503) | The query needs the managed index. Show that feature as unavailable; never retry it as a broader plain search. |
419
+ | `SEARCH_INDEX_NOT_READY` / `SEARCH_PROVIDER_UNAVAILABLE` (HTTP 503) | A required search index is not ready or its provider failed. Surface persistent unavailability without dropping filters. |
420
+ | `CURSOR_EXPIRED` (HTTP 410) | Restart a listing-change copy with `start: 'all'`; rebuild an event cursor from retained history after reconciling current listings. |
421
+ | `EVENT_HISTORY_PRUNED` (HTTP 410) | Event history has a gap. Reconcile current listings and restart from retained events. |
422
+ | `ContractError` | Check API/SDK compatibility and report the contract mismatch. |
423
+
424
+ Every POST operation is a read-only search request and needs no idempotency key.
425
+ The SDK requires a caller-supplied `{ idempotencyKey: 'stable-request-id' }` for any
426
+ mutating POST, keeping the same key and body across retries. The SDK does not call
427
+ email, advertising or payment providers.
285
428
 
286
429
  ## Types, validation and compatibility
287
430
 
@@ -331,11 +474,11 @@ npm run sdk:pack
331
474
  ```
332
475
 
333
476
  For local integration testing, install the archive from
334
- `packages/client/dist/propertysimple-client-0.1.0.tgz` into a separate application.
477
+ `packages/client/dist/propertysimple-client-<version>.tgz` into a separate application.
335
478
  Publish the exact inspected archive to avoid including workspace configuration:
336
479
 
337
480
  ```sh
338
- npm publish ./packages/client/dist/propertysimple-client-0.1.0.tgz --access public --registry https://registry.npmjs.org/ --ignore-scripts
481
+ npm publish ./packages/client/dist/propertysimple-client-<version>.tgz --access public --registry https://registry.npmjs.org/ --ignore-scripts
339
482
  ```
340
483
 
341
484
  `npm run sdk:test` builds and packs the release, installs it in a temporary project