API Changelog

Stay up to date with API changes and improvements.

All notable changes to the Propstreet Public API are documented here. This project uses Semantic Versioning.


[1.8.0] - September 2026

Added

Interest and stage on prospects — the two axes beside the single classification:

  • interest on the prospect response — your judgement of the prospect's appetite: interested, maybe, not_interested; null when none is recorded. maybe is new: it had no word in the old vocabulary.
  • stage on the prospect response — how far the process has come, derived from what has actually happened: not_contacted, teaser, im, data_room, bid. Never lowered by later events.
  • stageOverride on the prospect response — the latest manual stage correction, { stage, setBy, setAtUtc, reason } where setBy is { id, name }; omitted when the stage was never overridden.
  • PATCH /api/v1/projects/{projectId}/prospects/{id} accepts interest (send null to clear), stage (a manual correction, returned as stageOverride; later events can still raise it) and stageReason (only together with stage).
  • DELETE /api/v1/projects/{projectId}/prospects/{id}/stage-override withdraws the manual stage correction (204, also when there is none). Every correction on the prospect is withdrawn, so stage is derived from what has happened again and stageOverride is omitted.
  • GET /api/v1/projects/{projectId}/prospects accepts interest= (interested, maybe, not_interested, or none for prospects without a judgement) and stage= filters. They combine with each other and with capability / tags / classification.
  • New action-timeline events on GET .../prospects/{id}/actions and the project / offer timelines: prospect.interest_changed (an interest judgement recorded or cleared) and prospect.stage_overridden (a manual stage correction; details.message carries the reason when one was given). Both are type=audit rows.
  • MCP: set_prospect_interest (interest interested / maybe / not_interested, or none to clear; optional note) and override_prospect_stage (stage + optional reason); list_prospects gains interest and stage filters; get_prospect returns the three new fields.

Follow-up tasks on prospects — dated calls, meetings and emails you can complete, cancel and reopen:

  • nextTask on the prospect response — the nearest-due open follow-up task, { id, type, dueDateUtc, assignee, overdue }; omitted when the prospect has no open task. type is call, meeting or email; overdue is true once the due date is before today (UTC).
  • openTaskCount on the prospect response — how many open follow-up tasks the prospect carries.
  • POST /api/v1/projects/{projectId}/prospects/{id}/tasks adds a task (type, dueDate as today / tomorrow / 7d / an ISO date, optional assignee and note) and returns it with 201. A dueDate in the past is accepted and records a task that is already overdue. A prospect can carry any number of open tasks. Send an Idempotency-Key header to make a retry safe: the same key returns the task that was created instead of adding a second one.
  • POST /api/v1/projects/{projectId}/prospects/{id}/tasks/{taskId}/complete completes a task (204). Completing records who and when; a task can be completed once, and completing a canceled task is refused (400).
  • POST /api/v1/projects/{projectId}/prospects/{id}/tasks/{taskId}/cancel cancels a follow-up task (204). The task no longer counts in nextTask / openTaskCount and can no longer be completed; canceling it again is harmless. A completed task cannot be canceled (400); reopen it first.
  • POST /api/v1/projects/{projectId}/prospects/{id}/tasks/{taskId}/reopen undoes a task's completion (204), so the task is open again and can be completed again. A task that is not completed, or that was canceled, returns 400.
  • Task ids. Every task verb accepts either the task's id — returned when it was added and served as nextTask.id — or the id of its prospect.task_added row on the prospect's activity timeline. Either id is looked up on the prospect in the URL only; a task that is not on this prospect returns 404.
  • Canceling a task, reopening one and withdrawing a stage correction change what the prospect answers, so they also advance the prospect's updatedUtc and ETag: a delta sync on updated_since returns the prospect, and a stale If-Match is refused.
  • A canceled task, an undone completion and a withdrawn correction leave the default activity timeline; include_archived=true still returns them as audit rows carrying details.archivedAt — when the entry was archived, the same field archived notes carry. Active rows carry no archivedAt.
  • GET /api/v1/projects/{projectId}/prospects accepts task_due= (overdue, today, this_week) to find prospects with an open task due in that window.
  • New action-timeline events on GET .../prospects/{id}/actions: prospect.task_added (the note, when given, in details.message) and prospect.task_completed; type=tasks lists just the task rows.
  • MCP: add_prospect_task (type, due date, optional assignee and note, one or many prospects) and complete_prospect_task (by the task's id or its timeline id, looked up on that prospect only; not_found otherwise); list_prospects gains a taskDue filter; get_prospect returns nextTask and openTaskCount.

Project-level tags on prospects — your labels on an investor in one deal, separate from the contact's own tags:

  • projectTags on the prospect response — your labels on this prospect within this project, for example ["Single tenant", "Follow up Q4"]. They live on the prospect, not on the contact or company, so they never show on the same investor in another project, and they are never shown to the investor. The existing tags field keeps serving the contact's or company's network-wide tags.
  • PATCH /api/v1/projects/{projectId}/prospects/{id} accepts projectTags — the whole list (send [] to clear). Values are trimmed and de-duplicated (case-sensitive) and a multi-word value stays one label. tags is untouched.
  • GET /api/v1/projects/{projectId}/prospects accepts project_tags= (repeat the parameter; OR semantics, case-insensitive). It combines with every other filter, including tags=.
  • Webhooks: prospect.updated fires when the project tags change, with project_tags in changed_fields.
  • MCP: update_prospect gains addProjectTags / removeProjectTags (the existing tags param still replaces the contact's or company's network tags); list_prospects gains a projectTags filter; get_prospect returns projectTags.

Classification comments on the action timelines:

  • details.message on prospect.classified rows — the comment the broker wrote with the classification. Present only when a comment was written; a classification without one keeps omitting details entirely.

Changed

  • A note sent with a classification is now its own timeline entry. POST /api/v1/projects/{projectId}/prospects/classify and MCP classify_prospects accept the same optional note as before, but the words are logged as a note on each prospect instead of being folded into the classification entry. A commented classification therefore produces two rows on the action timelines — a note row carrying the text in details.message, and the prospect.classified audit row, which no longer repeats it. The note behaves like every other prospect note: it has its own id and ETag, appears under type=notes, and can be archived or superseded through PATCH /api/v1/projects/{projectId}/prospects/{id}/notes/{noteId}. If you read the comment from the classification row, read it from the note row instead — entries written before this release keep the comment where it was.
  • A prospect whose note could not be logged is reported as failed. If the stage moves but the note cannot be written, the prospect is returned in failed with the note's reason instead of being counted in classified, so a partial result is never reported as a complete one. The stage that was accepted stays applied — retry the request to add the note. Prospects you can classify at all can take a note, so this is a fault path rather than an everyday outcome.
  • type=notes now also returns commented classifications. On GET /api/v1/projects/{id}/actions, GET /api/v1/projects/{projectId}/prospects/{id}/actions, and GET /api/v1/offers/{id}/actions, the note filter returns the same set the prospect's note count counts: note rows plus pipeline classifications carrying a broker comment. Such a row keeps the shape it always had — type stays audit, event stays prospect.classified — and is still returned by type=audit, so the two filters overlap on it. It is the same row in both: dedupe by id if you union the filters, and read the comment from details.message.
  • MCP: update_activity now names the record the row belongs to — networkRef for a contact or company, projectRef alone for a project note, projectRef + prospectRef for a prospect, or offerRef for an offer, the same scope log_activity takes. The activity id is looked up on that record only; an id from any other record is not found and nothing is changed.

Unchanged

  • classification keeps its meaning and its values. It is still the single-value classification you last wrote (not_contacted by default), read back exactly as written, and classification= still filters on it; the two new axes are additive beside it. Writing classification never lowers a stage or clears an interest — use the stage / interest fields for that. One addition: writing bidder also records interested, since a bid is interest. When a PATCH sends classification together with interest / stage, the two axes win.
  • The mapping runs one way only — the two axes never write back to classification. Setting interest or overriding stage leaves classification exactly as you last wrote it, so the two drift apart as soon as you use the axes. A prospect you classified interested in the spring and marked not_interested last week still answers classification=interested, and a prospect whose recorded interest is interested can still carry the classification not_contacted. Filter, report and route on interest and stage; read classification as the last word that was written, not as a summary of where the prospect stands today.
  • prospectClassification on the associated-projects lists (GET /api/v1/network/contacts/{id}/projects, .../companies/{id}/projects) keeps reporting the single-value classification recorded on that project; read the prospect itself for interest and stage.

[1.7.0] - June 2026

Added

Project restore:

  • POST /api/v1/projects/{id}/restore — restore a deleted project within 30 days of deletion. Restores the prospects and external references removed by the same delete. Returns 409 when the project is not deleted.
  • New webhook event project.restored, emitted when a deleted project is restored. See Webhooks.
  • Deleting a project is now offered only for a true draft. A project that has been shared with investors (a teaser was communicated), or that has active Case Room members, is refused (409) and must be closed instead — closing preserves the full history and winds the rooms down.

Prospect owner & room-access expiry on PATCH /api/v1/projects/{projectId}/prospects/{id}:

  • owner — assign a team member as the prospect's owner. Send the member's user id, or "me" for the caller; send null to unassign. Validated against the project's team (the user must belong to the project's billing group).
  • accessExpiry — schedule when the prospect's room access expires. Accepts a relative ("7d", "1w", "1m"), named ("tomorrow", "next_week", "next_month"), or ISO date/datetime value, and must be in the future. Only valid on a prospect connected to a room (otherwise 400); send null to clear a scheduled expiry.
  • The prospect response (ProspectPublicDto) now includes a read-only owner object ({ id, name }) when an owner is assigned.

Optional note on batch classification:

  • POST /api/v1/projects/{projectId}/prospects/classify accepts an optional note, attached to each prospect's classification action and surfaced in the prospect note column. Whitespace-only notes are rejected.

MCP Q&A Tools (broker):

  • list_questions — list a Case Room's Q&A threads (subject, status, category, priority, assignee, prospect), with optional status and prospect filters.
  • get_question — read one thread in full: every post, including internal drafts not yet released to the prospect. Posts anchored to a room document carry a file object (stable file id + download path), directly usable for download.
  • draft_answer — write an internal draft answer on a thread. Drafts are never sent to the prospect — release stays a human decision in the app.
  • update_question — triage a thread: set category and/or priority, or route it to a team member via assigneeRef.

MCP Q&A rollups:

  • list_rooms items now include openQuestionCount.
  • get_room now includes a qa block with per-status thread counts and the room's Q&A window.

Offer inbox categories:

  • GET /api/v1/offers and MCP list_offers now include a category on each row: buying, invitation, or selling. filter=all returns the complete visible offer inbox across those categories. filter=invitations returns pending Case Room / Seller Portal invitations with category=invitation and status=pending; filter=selling returns accepted Seller Portal offers where the caller is the seller with category=selling.

[1.6.0] - May 2026

Added

Note revisions (archive / unarchive / supersede):

  • PATCH /api/v1/network/contacts/{id}/notes/{noteId} — archive, unarchive, or supersede a note on a contact.
  • PATCH /api/v1/network/companies/{id}/notes/{noteId} — same for a company.
  • PATCH /api/v1/projects/{projectId}/prospects/{id}/notes/{noteId} — same for a prospect note inside a project.
  • PATCH /api/v1/projects/{id}/notes/{noteId} — same for a project-level note.
  • Body is body-discriminated on action: archive, unarchive, or supersede.
    • archive soft-hides the note and stamps archivedAt. Idempotent — re-archiving an already-archived note is a no-op 204.
    • unarchive clears archivedAt. Returns 400 when the note has already been superseded (unarchive the head of the chain instead).
    • supersede expires the predecessor and creates a new revision that points back at it. Send a supersede object with the fields to change; unset fields inherit from the predecessor. The supersede object also accepts contentFormat (plaintext or markdown) on every note type — contact, company, prospect, and project notes.
  • If-Match is required on these four endpoints. Unlike every other V1 PATCH/DELETE (where If-Match is best-effort), a missing or blank If-Match returns 428 Precondition Required (/help/precondition-required). A stale If-Match still returns 412 Precondition Failed (/help/precondition-failed). Fetch the resource, copy the ETag, then retry. A lost optimistic-concurrency race — a concurrent revision committed between your read and write — returns 409 Conflict (/help/concurrency-conflict); re-fetch the ETag and retry. These PATCH endpoints are not replay-cached by Idempotency-Key; use the ETag precondition for retry safety.

Project notes (create):

  • POST /api/v1/projects/{id}/notes — log a broker-authored note directly on a project, mirroring the existing prospect-note create endpoint.

Note response fields (read):

  • GET action/note responses now expose supersedesActivityId (the predecessor a revision replaced), supersededByActivityId (the successor that replaced this note), archivedAt (when the note was archived; null when active), and contentFormat (markdown for markdown notes; null for plain text).
  • NetworkAction.details is now a typed OpenAPI union. Use details.kind (note or audit) to discriminate the payload; generated clients expose the note revision fields on the note details type.
  • The contact/company action endpoints (GET /api/v1/network/contacts/{id}/actions, GET /api/v1/network/companies/{id}/actions), the project action endpoint (GET /api/v1/projects/{id}/actions), and the prospect action endpoint (GET /api/v1/projects/{projectId}/prospects/{id}/actions) accept two new opt-in query params: include_archived (default false) surfaces archived notes alongside their archivedAt, and include_superseded (default false) surfaces the full revision chain with the entries linked via supersedesActivityId / supersededByActivityId. Both default off, so existing integrations see no change.

Project & prospect activity timelines (new):

  • GET /api/v1/projects/{id}/actions and GET /api/v1/projects/{projectId}/prospects/{id}/actions are new full activity timelines. Each row carries a type of note, chat, or audit and a typed details union discriminated by details.kind (which mirrors type). chat rows are broker↔counterparty conversation entries (a message, a shared file, or a logged call) — the message body itself is private and is never returned; only a shared file is exposed (on details.kind = chat). audit rows carry a stable dot-notation event string (e.g. project.created, prospect.accepted, room.file_uploaded) — the action event vocabulary is a superset of the webhook event vocabulary, so the same names you subscribe to also appear inline on the timeline. (chat rows carry an event too, e.g. prospect.message_sent / prospect.file_shared / prospect.call_logged.)
  • New query params on both endpoints: type (all | notes | chat | audit, default all) filters by row type, and include_rollups (default true) controls cross-entity interleaving. On the project timeline include_rollups=true interleaves the project's prospect and Case Room activity (each rolled-up row carries a subject of prospect or room); on the prospect timeline it interleaves the prospect's Case Room activity and its linked contact/company notes (subject of room, contact, or company). Set include_rollups=false to scope the timeline to the entity's own actions (no subject).
  • The row id (and the supersedesActivityId / supersededByActivityId link fields) on every action timeline — project, prospect, offer, and the contact/company (network) endpoints — is now a globally-unique, opaque action id, uniform across the whole API and MCP. Treat it as opaque and pass it back verbatim — it is the same id the note PATCH endpoints accept and the same id MCP list_activities / update_activity use.

New endpoint — offer activity timeline (investor/seller-facing):

  • GET /api/v1/offers/{id}/actions — the recipient's read of an offer's activity timeline (the counterparty view of the prospect timeline the broker sees in-app). The offer is the caller's own identity, so its Case Room activity folds in with no subject, and conversation entries appear as type = chat. Only what the recipient is entitled to see appears — broker-private notes and broker-internal events never surface here. Read-only: rows carry no etag. Supports the same type, include_rollups, include_archived, and include_superseded params.

Changed

  • V1 ETag headers are now strong ("…") rather than weak (W/"…"). Strong ETags are usable with If-Match strong comparison per RFC 9110. If you previously stripped a leading W/ before sending If-Match, that step is no longer needed.

[1.5.0] - May 2026

Added

Teasers:

  • GET /api/v1/projects/{id}/teaser — broker-authored teaser for a project. Returns the visibility matrix (options), the allowed display modes per field (template), and the investor-facing projection.
  • GET /api/v1/properties/{id}/teaser — pre-market teaser for a property. Returns a thin property_added envelope (with nudge.code = no_pre_market_yet) when no pre-market record exists yet.
  • GET /api/v1/projects/{id}/rooms/{roomId}/teaser — per-room teaser inside a Case Room. Returns a virtual default room teaser plus nudge.code = room_teaser_not_designed when the room teaser hasn't been designed yet (the first PATCH pins a real row).
  • PATCH /api/v1/projects/{id}/teaser, PATCH /api/v1/properties/{id}/teaser, PATCH /api/v1/projects/{id}/rooms/{roomId}/teaser — design-time write surface. Standard PATCH semantics: send only the fields you want to change; omitted fields are unchanged. Auto-creates the pre-market record or room teaser on first PATCH. Free-plan callers receive clamped results.
  • Snapshot tags field — what investors see on the teaser. Send a list to write that snapshot verbatim (deduped and clamped to template.tags); [] writes an explicit empty snapshot; null resets the snapshot to the property's current template pool. Snapshots are byte-stable: subsequent property-tag edits do not change what investors see on an existing teaser — the author writes a new teaser to pick them up.
  • If-Match / ETag parity with the rest of V1 — every GET returns an ETag header and etag body field; every PATCH supports If-Match and returns 412 Precondition Failed on stale; omit If-Match to skip the precondition check (V1 convention).
  • All three routes return the same teaser shape: stage, timestamps (publishedUtc, verifiedUtc, communicatedUtc), options, template, projection, optional nudge, and an etag field.
  • Stage precedence: communicatedUtc > verifiedUtc > publishedUtc > drafting > property_added.
  • Lifecycle transitions (publish, unpublish, verify, communicate) are not available via the API. Perform them in the Propstreet app.

[1.4.0] - April 2026

Added

Identity & Teams:

  • GET /api/v1/auth/me — fetch the authenticated caller's identity, capabilities, primary team, and any other teams the caller belongs to or has been invited to — all in a single call. Returns primaryTeam: null and secondaryTeams: [] for callers with no active team (e.g. pre-onboarding or service accounts).
  • GET /api/v1/teams/{id} — fetch a team. Returns team details plus an inline roster (up to 50 active members) when the caller is an active member. Callers with a pending invite see the team's details without the member list.
  • GET /api/v1/teams/{id}/members — paginated list of a team's active members, filterable by role=admin|member. Stable keyset ordering for cursor pagination.

MCP Identity Tool:

  • get_me — same payload as /api/v1/auth/me, returned as structured content so AI agents can bootstrap with a single tool call.

Offers:

  • GET /api/v1/offers — list offers the investor has received. Filter by all, active, ongoing, inactive, removed, unread, or unanswered. filter=unanswered returns the single teaser currently awaiting a response.
  • GET /api/v1/offers/{id} — fetch a single offer with the anonymized teaser (asset categories, locality, country, price / yield / area / year ranges, zoning, tags, conversions). The broker contact card is populated after the investor accepts. Unknown or inaccessible ids return 404 Not Found.
  • teaser.title — a short human-readable label composed from asset categories and locality in the caller's UI language (e.g. "Industrial, Tullinge (SE)"). Null when both are unknown.
  • status: unanswered, accepted, rejected, expired, removed, closed.
  • capabilities: "chat" when broker messaging is available.
  • Teaser range fields expose min / max (and currency / scale where applicable). Exact values appear as min == max.
  • Cursor pagination via the shared data + page.{nextCursor, pageSize, hasMore} envelope.

MCP Offer Tools:

  • list_offers — list offers with the same filters as the REST endpoint.
  • get_offer — fetch a single offer by Smart Ref (offer id or id= / ext= selector).

Mandates:

  • /api/v1/mandates — full CRUD for investor acquisition mandates, including cursor pagination, delta sync (updated_since, include_deleted), ETag-based concurrency, and idempotent create support.
  • POST /api/v1/mandates/search — search mandates by geography or location.
  • POST /api/v1/mandates/batch/get — fetch up to 100 mandates by id in a single call.
  • Mandate responses expose asset criteria (categories, investmentSize, yearBuilt, parcelArea), geographic area, country, and two parallel tag arrays — positiveTags (desirable attributes) and negativeTags (deal-breakers) — which are mutually exclusive.
  • Geographic input accepts geography (GeoJSON Polygon / MultiPolygon), near + radius (point and radius), or location (free-text locality resolved server-side). Exactly one mode is allowed per request. Write responses echo a resolvedLocation hint; GET responses do not.
  • Mandate responses include the standard audit/sync fields: etag, changeType, changeOrigin, and deletedUtc.
  • External reference support: attach third-party ids via externalRefs (namespace + externalId pairs) for linking mandates to records in your CRM or other systems.
  • PATCH is partial: omitted fields are unchanged; sent fields replace. Categories are full-replace.

MCP Mandate Tools:

  • list_mandates — list mandates with optional country, category, tag, name, and updatedSince filters.
  • search_mandates — geography- or location-based search.
  • get_mandate — fetch a single mandate by Smart Ref (mandate id, name, id= / name= / ext= selector).
  • create_mandate — create a mandate with categories, ranges, tags, external refs, and geographic criteria. Supports idempotency.
  • update_mandate — partial update with the same semantics as the REST endpoint.
  • delete_mandate — delete a mandate.

Deprecated

Project field mandateexclusivity:

  • project.mandate is now aliased by the new project.exclusivity field. Both names carry identical values (exclusive, non_exclusive, other) and are populated on every read response. Webhook project.updated events list both names in changes when the value changes.
  • Write requests (POST /api/v1/projects, PATCH /api/v1/projects/{id}) accept either field. Sending both mandate and exclusivity with non-null values on the same request is rejected with 422 validation_failed; sending neither leaves the value unchanged.
  • MCP tools create_project and update_project accept only exclusivity on write. Reads still return both mandate and exclusivity on every project object for now.
  • mandate will be removed entirely in a future minor version. Migrate integrations to exclusivity now.

[1.3.0] - March 2026

Added

Properties:

  • /api/v1/properties - Full CRUD for property assets, including cursor pagination, delta sync (updated_since, include_deleted), ETag-based concurrency, and idempotent create support.
  • GET /api/v1/projects/{id}/properties - Returns properties linked to a project, using the same public property schema as the standalone property endpoints.
  • Property responses now expose audit/sync fields consistent with other versioned v1 resources: etag, changeType, changeOrigin, and deletedUtc.

Company Duplicate Detection:

  • POST /api/v1/companies/find-duplicates — Check for potential duplicate companies before creating. Accepts name (required), domain, countryCode, and linkedInUrl. Returns matching candidates ordered by relevance (same-country matches first when countryCode is provided). Each result includes id, name, domain, and countryCode.

Company Fields:

  • linkedInUrl on companies — Company LinkedIn URL, available on read (GET), create (POST), and update (PATCH). Searchable and tracked in audit history.

MCP Property Tools:

  • list_properties - List broker properties with optional country/category filters.
  • get_property - Fetch a property by smart reference (id, name, id=, name=).
  • create_property - Create a property with idempotency support, description, location, and tenure fields.
  • update_property - Update name, description, country, locality, coordinates, and tenure, including coordinate clearing.

MCP Server:

  • create_company now performs duplicate detection before creation and returns a conflict with candidate details unless force=true. This prevents accidental duplicate company creation by AI agents.

Changed

Property Contract Clarifications:

  • Area write payloads must include unit: "sqm" on parcelArea, zoning.gfa, rentedArea, and vacantArea. Read responses remain normalized to sqm.
  • Property tags are documented enum-backed snake_case values; unknown tags are rejected with 400 Validation failed instead of being silently dropped.
  • Property PATCH location updates preserve tri-state semantics for nested country and locality, and late write collisions now return 409 Conflict.

Prospect Tag Filters:

  • Breaking change: GET /api/v1/projects/{id}/prospects now accepts repeatable tags query parameters with OR semantics instead of a single tag string. Example: ?tags=vip&tags=nordic.

Property Tag Filters:

  • Breaking change: GET /api/v1/properties now accepts repeatable tags query parameters with OR semantics instead of repeatable tag. Example: ?tags=core&tags=value_add.

MCP Tool Tag Filters:

  • Breaking change: MCP list tools now use array-based tags parameters consistently. list_companies, list_contacts, and list_prospects no longer accept string/CSV tag filters.

See the MCP Guide for tool-level details and the generated OpenAPI schema for the full property resource contract.


[1.2.1] - February 2026

Changed

Contact/Company Deletion — Automatic Prospect Removal:

  • DELETE /api/v1/network/contacts/{id} and DELETE /api/v1/network/companies/{id} no longer reject deletion when the entity is a prospect on active projects. Instead, the entity is automatically removed as a prospect from all active projects before deletion.
  • Each prospect removal fires a prospect.updated webhook with action.type: "removed". The contact/company deletion itself fires contact.deleted / company.deleted.
  • Timeline history for affected projects is preserved — prospects remain visible in project activity logs.
  • MCP delete_contact / delete_company preview (preview=true) now includes affected_projects showing which projects will be affected.

Fixed

Delete Webhook Payloads:

  • project.deleted and prospect.deleted webhook payloads now include external_refs — the list of active external references at deletion time. CRM integrators need these to identify and clean up the corresponding record in their system, since GET returns 404 after deletion.
  • Prospects are now cascade-deleted when their parent project is deleted, ensuring prospect.deleted webhooks fire for each affected prospect.
  • Re-adding a previously removed prospect to a project now works correctly. Previously, the unique index blocked re-addition of deleted prospects.

[1.2.0] - February 2026

Added

Projects & Prospects:

  • /api/v1/projects - Full CRUD for real estate transaction projects
  • /api/v1/projects/{id}/prospects - Manage investor prospects on project prospect lists
  • Unified prospects API - Returns both prospect-list and communicated prospects in one endpoint
  • Capability flags - communicated, accepted, chat, invitation indicate prospect status
  • Classification pipeline - Track prospects: not_contactedcontactedinterestedbidder
  • Filtering - Query prospects by capability, classification, or tag
  • Cursor-based pagination with delta sync (updated_since, include_deleted)
  • Idempotency support via Idempotency-Key header
  • ETag-based optimistic concurrency control

Associated Projects:

  • GET /api/v1/network/contacts/{contactId}/projects — List projects where a contact is a prospect
  • GET /api/v1/network/companies/{companyId}/projects — List projects where a company is a prospect
  • Cursor-based pagination with page_size and cursor parameters
  • Filter by status (open/closed) and classification (prospect pipeline stage)

Webhook Events:

  • project.created - Fired when a new project is created
  • project.updated - Fired when project fields change (name, status, classification, etc.)
  • project.deleted - Fired when a project is deleted
  • prospect.created - Fired when a prospect is added to a project
  • prospect.updated - Fired when prospect classification or other fields change
  • prospect.deleted - Fired when a prospect is deleted from a project
  • Webhook payloads include action field when changes were triggered by business actions (e.g., {"action": {"type": "classified"}})

See the Projects & Prospects Guide for complete documentation. See the Webhooks Guide for webhook payload reference.

MCP Server:

  • Structured content — All MCP tool responses now include structuredContent (typed JSON element, MCP 2025-06-18+) alongside the existing text content. Agents can parse responses programmatically without re-deserializing from text. No client-side changes required.

Link Webhook Payloads:

  • Link event webhooks (link.created, link.updated, link.deleted) now include contact_id and company_id fields in the payload. This is critical for link.deleted events where the link resource is no longer retrievable via the API.
  • Re-creating a previously deleted link (same contact-company pair) now correctly triggers a link.created webhook event. Previously, reactivating a deleted link was silently treated as an update with no webhook fired.

Changed

OpenAPI Schema Cleanup:

  • Removed Dto suffix from schema component names: WebhookDtoWebhook, WebhookCreatedDtoWebhookCreated, WebhookDeliveryDtoWebhookDelivery, EmailEntryRequestDtoEmailEntryRequest, PhoneEntryRequestDtoPhoneEntryRequest, CompanyLinkDtoCompanyLink, ContactLinkDtoContactLink
  • Added property descriptions to all public API schema components
  • Cleaned up internal terminology in API descriptions

Note: JSON property names and HTTP contracts are unchanged. If your code generator produces class names from OpenAPI schema IDs, update your generated code.


[1.1.0] - February 2026

Added

Company Fields:

  • homepageUrl - Company homepage URL
  • domain - Company domain, can be:
    • Set directly via API (e.g., "domain": "acme-corp.com") when only domain is known
    • Auto-extracted from homepageUrl when URL is provided (e.g., "acme-corp.com" from "https://www.acme-corp.com/about")
    • When both are provided, homepageUrl takes precedence for domain derivation
  • Both fields now searchable and sortable via /api/v1/network/companies:search
  • Domain changes tracked in audit history and webhook payloads

Profile Pictures:

  • profilePictureUrl on contacts and companies - Download URL when profile picture is set
  • Profile picture changes now tracked in webhook changed_fields as profile_picture

Webhooks:

  • Real-time event notifications for contacts, companies, and links
  • 9 event types: contact.created/updated/deleted, company.created/updated/deleted, link.created/updated/deleted
  • HMAC-SHA256 signature verification via Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature)
    • Note: Initially shipped as X-Propstreet-Signature; migrated to Standard Webhooks in v1.2.0
  • Exponential backoff retry (9 attempts over ~35 hours)
  • Auto-disable after 10 consecutive failures
  • Delivery history tracking via /api/v1/webhooks/{id}/deliveries
  • Secret regeneration endpoint
  • changed_fields reference documentation for all resource types

See the Webhooks Guide for complete documentation.


[1.0.0] - December 2025

Added

Core Resources:

  • Contacts - Full CRUD, search, batch operations
  • Companies - Full CRUD, search, batch operations
  • Links - Contact-Company relationships
  • Network Actions - Activity notes on contacts and companies
  • Files - Document attachments (up to 100MB)

Authentication:

  • OAuth 2.0 with Client Credentials flow for production integrations
  • Personal Access Tokens for testing and scripts
  • Bot Users for dedicated integration accounts

Sync Features:

  • Delta sync with updated_since and include_deleted
  • Cursor-based pagination (up to 500 per page)
  • ETag-based concurrency for safe updates
  • External references for CRM ID tracking

Rate Limits:

  • OAuth Clients: 600 requests/minute
  • Personal Access Tokens: 60 requests/minute

Documentation:

  • Comprehensive guides at docs.propstreet.com
  • Interactive API explorer
  • Code samples for TypeScript, C#, Python

Roadmap

Upcoming Features

We're continuously improving the API. Contact feedback@propstreet.com to share your integration needs.


Deprecation Policy

We follow a deprecation policy to ensure stability:

  1. Deprecation notice - Announced in changelog and API responses
  2. Migration period - Minimum 6 months before removal
  3. Removal - After migration period ends

We'll always provide migration guidance and support.


Subscribe to Updates

For API announcements: