Projects & Prospects
Manage real estate transaction projects and track investor prospects through your sales pipeline.
Projects represent real estate transactions (single assets or portfolios), and prospects are the investors you're targeting for each project. Use these endpoints to sync deal pipelines between Propstreet and your CRM.
Quick Reference
| Feature | Details |
|---|---|
| Projects | CRUD for real estate transactions |
| Prospects | Investors associated with a project — added by you or matched by Propstreet |
| Capabilities | Actionable flags: chat (messaging available). |
| Classifications | Pipeline stages: not_contacted, contacted, interested, not_interested, bidder |
| External Refs | Link to external CRM systems (HubSpot, Salesforce, etc.) with lookup by external ID |
| Batch Operations | Add up to 100 prospects in a single request |
| Webhooks | Real-time events: project.created/updated/deleted, prospect.created/updated/deleted |
| Pagination | Cursor-based, up to 500 per page |
| Delta Sync | updated_since + include_deleted for incremental sync |
| Idempotency | Idempotency-Key header on POST endpoints |
| Concurrency | ETag-based optimistic locking on PATCH endpoints |
| Linked Properties | GET /api/v1/projects/{id}/properties returns attached property profiles when present |
How Projects & Prospects Work
A project represents a real estate transaction. Prospects are the investors associated with that project. Prospects come from two sources, but the API returns them in a single unified list:
| Source | How they appear | teaser field |
capabilities |
|---|---|---|---|
| Added by you via API or UI | You add contacts/companies from your network | Not present | [] |
| Matched by Propstreet when the teaser is communicated | Platform investors whose investment strategy matches the project | Present with deadlineUtc and acceptedUtc |
May include chat |
How to tell them apart: If a prospect has a
teaserobject, the investor was communicated the teaser. Ifteaseris absent, the prospect was added directly (via API, import, or the Propstreet UI).
┌──────────────────────────────────────────────────────────────────────┐
│ PROJECT │
│ (Real estate transaction — single asset or portfolio) │
│ │
│ PROSPECTS (unified list via API) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Added by you Matched by Propstreet │ │
│ │ ───────────── ───────────────────── │ │
│ │ • No teaser object • teaser.deadlineUtc (response by) │ │
│ │ • classification • teaser.acceptedUtc (if accepted) │ │
│ │ • tags from your network • chat capability (when established)│ │
│ │ │ │
│ │ All prospects share: classification, externalRefs, etag │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Typical workflow
- Create a project — Set up a new real estate transaction
- Add prospects — Add contacts or companies from your network
- Classify prospects — Track them through your pipeline stages (
not_contacted→contacted→interested→bidder) - Communicate the teaser — When the teaser is sent via Propstreet, matching investors appear as new prospects with a
teaserobject - Monitor responses — Check
teaser.acceptedUtcto see who accepted; usecapabilities: ["chat"]to know when messaging is available - Sync with CRM — Use delta sync to keep your systems aligned
Query Parameter Naming Convention
Multi-word query parameters use snake_case (e.g., page_size, updated_since, teaser_stage, include_deleted). Single-word parameters use lowercase (e.g., cursor, status, classification).
Optimistic Concurrency (ETags)
Projects and prospects support optimistic concurrency control using ETags to prevent lost updates when multiple clients modify the same resource.
How It Works
Response: ETag is provided in both the HTTP ETag header and the etag body field (identical values):
HTTP/1.1 200 OK
ETag: W/"abc123"
Content-Type: application/json
{
"id": "123",
"name": "Stockholm Office Portfolio",
"etag": "W/\"abc123\"",
...
}
Request: Use the If-Match header with the ETag value when updating:
curl -X PATCH "https://app.propstreet.com/api/v1/projects/123" \
-H "If-Match: W/\"abc123\"" \
-d '{"name": "Updated Name"}'
Why Both Header and Body?
- Header (
ETag)—Standard HTTP mechanism; works with caching infrastructure and conditional requests - Body (
etag)—Convenience for clients that process JSON but don't easily access response headers
Both contain identical values. Use whichever is easier for your integration. For updates, always use the If-Match header.
Conflict Resolution
If another client modified the resource since you fetched it, you'll receive 412 Precondition Failed:
{
"type": "urn:propstreet:error:precondition-failed",
"title": "Precondition failed",
"detail": "ETag mismatch - resource was modified"
}
Resolution: Refetch the resource to get the latest ETag and data, then retry your update.
Linked Properties
Use GET /api/v1/projects/{id}/properties when you need the property profiles attached to a project.
- Returns the same property schema as
/api/v1/properties/{id} - Returns
200 OKwith an emptydataarray when the project has no linked properties - Useful for CRM/deal integrations that sync project metadata and need the attached asset profile in one additional read
Projects
List Projects
Fetch all projects with cursor-based pagination:
curl "https://app.propstreet.com/api/v1/projects?page_size=100" \
-H "Authorization: Bearer $TOKEN"
Response:
{
"data": [
{
"id": "123",
"name": "Stockholm Office Portfolio",
"asset": { "type": "portfolio", "propertyCount": 5 },
"status": "open",
"classification": "active",
"teaser": {
"stage": "published",
"publishedUtc": "2026-01-14T09:00:00Z"
},
"mandate": "exclusive",
"price": { "value": 150, "currency": "SEK", "scale": "millions" },
"externalRefs": [{ "namespace": "hubspot", "id": "deal-123456" }],
"createdUtc": "2026-01-15T10:00:00Z",
"updatedUtc": "2026-01-15T10:00:00Z",
"etag": "W/\"abc123\""
}
],
"page": {
"nextCursor": "eyJ1IjoiMjAyNi0wMS0xNVQxMDowMDowMFoiLCJpIjoxMjN9",
"pageSize": 100,
"hasMore": true
}
}
Note: Null fields are omitted from responses, not sent as
null. For example, a project with no teaser will not include ateaserkey at all, rather than"teaser": null.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
updated_since |
datetime | Filter by update time (ISO 8601 UTC) |
include_deleted |
boolean | Include soft-deleted projects (default: false) |
page_size |
integer | Results per page, 1-500 (default: 500) |
cursor |
string | Pagination cursor from previous response |
status |
string | Filter by lifecycle status: open, closed, deleted |
classification |
string | Filter by work stage: draft, active, inactive |
teaser_stage |
string | Filter by teaser stage: property_added, drafting, published, verified, communicated |
Project Fields
| Field | Type | Description |
|---|---|---|
id |
string | Unique project identifier |
name |
string | Project name |
asset |
object | Asset info: type and property count (see below) |
status |
string | Lifecycle: open, closed, deleted |
classification |
string | Work stage: draft, active, inactive |
teaser |
object | Teaser info with stage and timestamps (see below) |
mandate |
string | Sales mandate: exclusive, non_exclusive, other |
price |
object | Asking price (see Price below) |
priceRange |
object | Custom price range override (see Price Range below) |
transactions |
array | Completed transactions (only when status=closed) |
externalRefs |
array | External CRM references (see External Refs) |
createdUtc |
datetime | When the project was created |
updatedUtc |
datetime | When the project was last modified |
deletedUtc |
datetime | When the project was deleted (only present for deleted projects) |
etag |
string | ETag for optimistic concurrency (see ETags) |
Asset Object
{ "type": "portfolio", "propertyCount": 5 }
| Field | Type | Description |
|---|---|---|
type |
string | single (one property) or portfolio (multiple properties) |
propertyCount |
integer | Number of properties (only present when type is portfolio) |
For a non-portfolio project: { "type": "single" }.
Status Values
| Status | Description |
|---|---|
open |
Active project (read-only, set by system) |
closed |
Transaction completed (read-only, set by system) |
deleted |
Soft-deleted |
Classification Values
| Classification | Description |
|---|---|
draft |
Work in progress, not yet active |
active |
Actively being marketed |
inactive |
Paused or on hold |
Teaser Object
{
"stage": "communicated",
"publishedUtc": "2026-01-10T10:00:00Z",
"verifiedUtc": "2026-01-12T14:00:00Z",
"communicatedUtc": "2026-01-15T09:00:00Z"
}
| Field | Type | Description |
|---|---|---|
stage |
string | Current stage (see Stage Values below) |
publishedUtc |
datetime | When teaser was published (omitted if not yet) |
verifiedUtc |
datetime | When teaser was verified (omitted if not yet) |
communicatedUtc |
datetime | When teaser was first sent to prospects (omitted if not yet) |
The teaser object is omitted entirely if the teaser workflow has not started.
Teaser Stage Values
| Stage | Description |
|---|---|
property_added |
Property details added |
drafting |
Teaser being drafted |
published |
Teaser published internally |
verified |
Teaser verified and ready |
communicated |
Teaser sent to at least one prospect |
Price Object
{ "value": 150, "currency": "SEK", "scale": "millions" }
| Field | Type | Description |
|---|---|---|
value |
decimal | Price value (multiply by scale) |
currency |
string | ISO 4217 currency code (SEK, EUR, USD, etc.) |
scale |
string | none (1), thousands, millions, billions |
Price Range Object
{ "min": 100, "max": 200, "currency": "SEK", "scale": "millions" }
| Field | Type | Description |
|---|---|---|
min |
decimal | Minimum price (optional) |
max |
decimal | Maximum price (optional) |
currency |
string | ISO 4217 currency code |
scale |
string | none, thousands, millions, billions |
Get a Project
curl "https://app.propstreet.com/api/v1/projects/123" \
-H "Authorization: Bearer $TOKEN"
Response includes both the ETag header and etag body field. See Optimistic Concurrency for details.
Create a Project
curl -X POST "https://app.propstreet.com/api/v1/projects" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Stockholm Office Portfolio",
"asset": { "type": "portfolio", "propertyCount": 5 },
"mandate": "exclusive",
"price": { "value": 150, "currency": "SEK", "scale": "millions" }
}'
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Project name (1-200 characters) |
asset |
object | No | Asset info: { "type": "single" } or { "type": "portfolio", "propertyCount": 5 }. Defaults to single property if omitted. |
mandate |
string | No | Sales mandate: exclusive, non_exclusive, other |
price |
Price | No | Asking price (see Price Object above) |
priceRange |
PriceRange | No | Custom price range override (see Price Range Object above) |
externalRefs |
ExternalRef[] | No | Link to external CRM systems (see External Refs) |
[!important] Use the
Idempotency-Keyheader to safely retry failed requests. See Idempotency Guide for details.
Update a Project
curl -X PATCH "https://app.propstreet.com/api/v1/projects/123" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: W/\"abc123\"" \
-d '{
"name": "Updated Project Name",
"asset": { "type": "portfolio", "propertyCount": 3 }
}'
When providing asset in a PATCH request, the full asset object is replaced. The type field is always required when asset is provided (e.g., { "type": "single" } to change from portfolio to single).
The If-Match header is recommended for concurrent update safety. See Optimistic Concurrency.
Delete a Project
curl -X DELETE "https://app.propstreet.com/api/v1/projects/123" \
-H "Authorization: Bearer $TOKEN"
Projects are soft-deleted. Use include_deleted=true to retrieve deleted projects.
Prospects
Prospects are investors (contacts or companies) associated with a project. There are two types:
- Prospects you add — contacts or companies from your network, added via the API or Propstreet UI. These have no
teaserobject. - Prospects matched by Propstreet — investors whose investment strategy matches the project. These appear automatically when the teaser is communicated and include a
teaserobject with response timestamps.
The API returns both types in a single list. Use the presence of the teaser field to distinguish them.
List Prospects
curl "https://app.propstreet.com/api/v1/projects/123/prospects?page_size=100" \
-H "Authorization: Bearer $TOKEN"
Response:
{
"data": [
{
"id": "456",
"projectId": "123",
"contactId": "789",
"companyId": "321",
"classification": "interested",
"capabilities": ["chat"],
"tags": ["tier-1", "repeat-buyer"],
"externalRefs": [{ "namespace": "salesforce", "id": "006XXXXXXXXXXXX" }],
"teaser": {
"acceptedUtc": "2026-01-20T14:30:00Z",
"deadlineUtc": "2026-02-15T23:59:59Z"
},
"createdUtc": "2026-01-15T10:00:00Z",
"updatedUtc": "2026-01-20T14:30:00Z",
"etag": "W/\"def456\""
}
],
"page": {
"nextCursor": "...",
"pageSize": 100,
"hasMore": false
}
}
Note on contactId/companyId: Create requires exactly one of
contactIdorcompanyId(mutually exclusive), but responses may return both when a contact has an associated company.Best practice: Prefer
contactId— contacts have email and phone for direct outreach and support more capabilities (activity logging, chat). UsecompanyIdonly as a placeholder when the specific person at the company is not yet known.
Prospect Fields
| Field | Type | Description |
|---|---|---|
id |
string | Unique prospect identifier |
projectId |
string | Parent project ID |
contactId |
string | Contact ID (if linked to a contact) |
companyId |
string | Company ID (if linked to a company) |
classification |
string | Pipeline stage (see Classifications below) |
capabilities |
array | Capability flags (see Capabilities below) |
tags |
array | Tags from the associated contact or company |
teaser |
object | Teaser info with acceptedUtc and deadlineUtc (omitted if not communicated) |
externalRefs |
array | External CRM references (see External Refs) |
createdUtc |
datetime | When the prospect was created |
updatedUtc |
datetime | When the prospect was last modified |
deletedUtc |
datetime | When the prospect was deleted (only present for deleted prospects) |
etag |
string | ETag for optimistic concurrency (see ETags) |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
updated_since |
datetime | Filter by update time (ISO 8601 UTC). Includes created, modified, and deleted prospects. |
include_deleted |
boolean | Include removed prospects (default: false) |
page_size |
integer | Results per page, 1-500 (default: 500) |
cursor |
string | Pagination cursor from previous response |
capability |
string | Filter by capability: chat |
classification |
string | Filter by classification (see Classifications below) |
tags |
array | Filter by tags with OR semantics. Repeat the query parameter, e.g. ?tags=vip&tags=nordic |
Capabilities
Capabilities indicate what actions are available for a prospect:
| Capability | Description |
|---|---|
chat |
Chat/messaging is available (only for prospects who have accepted the teaser) |
Teaser Object (prospect-level)
Present only for prospects who were communicated the teaser. Omitted entirely for prospects you added directly.
| Field | Type | Description |
|---|---|---|
teaser.acceptedUtc |
datetime | When the investor accepted the teaser (null if not yet) |
teaser.deadlineUtc |
datetime | Deadline for the investor to respond |
Classifications
Track prospects through your sales pipeline:
| Classification | Description |
|---|---|
not_contacted |
Not yet reached out (default) |
contacted |
Reached out, awaiting response |
interested |
Expressed interest in the project |
not_interested |
Declined or not interested |
bidder |
Active bidder in the process |
Get a Prospect
curl "https://app.propstreet.com/api/v1/projects/123/prospects/456" \
-H "Authorization: Bearer $TOKEN"
Add a Prospect
Add a contact or company from your network to a project. Prefer contacts — they have email/phone for direct outreach. Use a company only as a placeholder when the specific person is unknown:
# Add a contact
curl -X POST "https://app.propstreet.com/api/v1/projects/123/prospects" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"contactId": "789"}'
# Add a company
curl -X POST "https://app.propstreet.com/api/v1/projects/123/prospects" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"companyId": "456"}'
| Field | Type | Description |
|---|---|---|
contactId |
string | Contact ID to add — preferred (mutually exclusive with companyId) |
companyId |
string | Company ID to add — use when person unknown (mutually exclusive with contactId) |
externalRefs |
ExternalRef[] | Link to external CRM systems (see External Refs) |
[!warning] Provide exactly one of
contactIdorcompanyId, not both. Responses may include both fields when a contact has an associated company.
Update a Prospect
Update a prospect's classification or external references:
# Update classification
curl -X PATCH "https://app.propstreet.com/api/v1/projects/123/prospects/456" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: W/\"def456\"" \
-d '{"classification": "bidder"}'
# Update external refs
curl -X PATCH "https://app.propstreet.com/api/v1/projects/123/prospects/456" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: W/\"def456\"" \
-d '{"externalRefs": [{"namespace": "salesforce", "id": "006XXXXXXXXXXXX"}]}'
The If-Match header is recommended for concurrent update safety. See Optimistic Concurrency.
Remove a Prospect
curl -X DELETE "https://app.propstreet.com/api/v1/projects/123/prospects/456" \
-H "Authorization: Bearer $TOKEN"
Associated Projects
Find which projects a contact or company appears in as a prospect. Useful for answering "where is this investor active?" across your deal pipeline.
List Projects for a Contact
curl "https://app.propstreet.com/api/v1/network/contacts/789/projects?page_size=100" \
-H "Authorization: Bearer $TOKEN"
List Projects for a Company
curl "https://app.propstreet.com/api/v1/network/companies/321/projects?page_size=100" \
-H "Authorization: Bearer $TOKEN"
Response:
{
"data": [
{
"id": "123",
"prospectId": "456",
"name": "Stockholm Office Portfolio",
"status": "open",
"classification": "active",
"prospectClassification": "interested",
"prospectCount": 12,
"createdUtc": "2026-01-15T10:00:00Z",
"updatedUtc": "2026-01-20T14:30:00Z",
"etag": "W/\"abc123\""
}
],
"page": {
"nextCursor": "eyJ1IjoiMjAyNi0wMS0xNVQxMDowMDowMFoiLCJpIjoxMjN9",
"pageSize": 100,
"hasMore": true
}
}
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page_size |
integer | Results per page, 1-500 (default: 100) |
cursor |
string | Pagination cursor from previous response |
status |
string | Filter by project status: open or closed |
classification |
string | Filter by prospect stage: not_contacted, contacted, interested, not_interested, bidder |
Associated Project Fields
| Field | Type | Description |
|---|---|---|
id |
string | Project identifier |
prospectId |
string | Prospect identifier for this contact/company on the project |
name |
string | Project name |
status |
string | Project lifecycle: open or closed |
classification |
string | Project work stage: draft, active, inactive |
prospectClassification |
string | Prospect pipeline stage on this project |
prospectCount |
integer | Total number of prospects on this project |
createdUtc |
datetime | When the project was created |
updatedUtc |
datetime | When the project was last modified |
deletedUtc |
datetime | When the project was deleted (only present for deleted) |
etag |
string | ETag for the project |
External References
External references let you link projects and prospects to records in external systems (CRMs, ERPs, etc.). This enables bidirectional sync by looking up Propstreet resources using your existing IDs.
External Ref Structure
Each external ref has a namespace (system identifier) and an id (the record ID in that system):
{
"externalRefs": [
{ "namespace": "hubspot", "id": "deal-123456" },
{ "namespace": "salesforce", "id": "006XXXXXXXXXXXX" }
]
}
Rules:
- Namespace is case-insensitive (
HubSpot=hubspot=HUBSPOT) - Each namespace can only appear once per resource (no duplicate namespaces)
- Project external refs must be unique within your tenant (same namespace+id can't exist on multiple projects)
- Prospect external refs must be unique within a project (same namespace+id can't exist on multiple prospects in the same project, but can be reused across different projects)
Create with External Refs
curl -X POST "https://app.propstreet.com/api/v1/projects" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Stockholm Office Portfolio",
"externalRefs": [
{ "namespace": "hubspot", "id": "deal-123456" }
]
}'
Update External Refs
External refs are replaced entirely on update (not merged):
curl -X PATCH "https://app.propstreet.com/api/v1/projects/123" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"externalRefs": [
{ "namespace": "salesforce", "id": "006XXXXXXXXXXXX" },
{ "namespace": "pipedrive", "id": "12345" }
]
}'
To remove all external refs, send an empty array:
curl -X PATCH "https://app.propstreet.com/api/v1/projects/123" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"externalRefs": []}'
Lookup by External Ref
Find a project by its external reference instead of Propstreet ID:
# Find project by HubSpot deal ID
curl "https://app.propstreet.com/api/v1/projects/external/hubspot/deal-123456" \
-H "Authorization: Bearer $TOKEN"
Find a prospect within a project:
# Find prospect by Salesforce opportunity ID
curl "https://app.propstreet.com/api/v1/projects/123/prospects/external/salesforce/006XXXXXXXXXXXX" \
-H "Authorization: Bearer $TOKEN"
Returns 404 Not Found if no resource matches the external reference.
CRM Sync with External Refs
External refs enable efficient bidirectional sync:
class PropstreetCRMSync:
def sync_from_crm(self, crm_deal):
"""Sync CRM deal to Propstreet, creating or updating as needed."""
# Check if project already exists using CRM ID
response = requests.get(
f"{self.base_url}/projects/external/hubspot/{crm_deal.id}",
headers=self.headers
)
if response.status_code == 200:
# Update existing project
project = response.json()
requests.patch(
f"{self.base_url}/projects/{project['id']}",
headers=self.headers,
json={"name": crm_deal.name}
)
elif response.status_code == 404:
# Create new project with external ref
requests.post(
f"{self.base_url}/projects",
headers=self.headers,
json={
"name": crm_deal.name,
"externalRefs": [
{"namespace": "hubspot", "id": crm_deal.id}
]
}
)
Batch Operations
Batch Add Prospects
Add multiple prospects to a project in a single request (max 100 per request):
curl -X POST "https://app.propstreet.com/api/v1/projects/123/prospects:batch" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "contactId": "101" },
{ "contactId": "102", "externalRefs": [{ "namespace": "hubspot", "id": "deal-789" }] },
{ "companyId": "201" },
{ "contactId": "103" }
]
}'
Each item in the batch accepts the same fields as POST /prospects (contactId/companyId + optional externalRefs). External refs are validated per-item and across the batch before any prospects are created.
Response includes both successful and failed items:
{
"created": [
{ "id": "456", "contactId": "101" },
{
"id": "457",
"contactId": "102",
"externalRefs": [{ "namespace": "hubspot", "id": "deal-789" }]
},
{ "id": "458", "companyId": "201" }
],
"failed": [
{
"index": 3,
"contactId": "103",
"error": "duplicate",
"message": "Contact already exists as prospect in this project"
}
]
}
Error codes:
| Error | Description |
|---|---|
validation_error |
Missing or invalid contactId/companyId |
invalid_id |
ID format is invalid |
duplicate |
Duplicate entry in batch request |
not_added |
Could not add (already exists, blocked, not found) |
Webhooks
Receive real-time notifications when projects or prospects change. See the Webhooks Guide for setup instructions.
Project Events
| Event | Triggered When |
|---|---|
project.created |
New project created |
project.updated |
Project fields change (name, classification, teaser, price, etc.) |
project.deleted |
Project soft-deleted |
Prospect Events
| Event | Triggered When |
|---|---|
prospect.created |
Prospect added to project |
prospect.updated |
Classification or tags change |
prospect.deleted |
Prospect deleted from project |
Webhook Payload Example
{
"event": "project.updated",
"timestamp": "2026-01-15T10:30:00Z",
"data": {
"id": "123",
"name": "Stockholm Office Portfolio",
"asset": { "type": "portfolio", "propertyCount": 5 },
"status": "open",
"classification": "active",
"teaser": {
"stage": "communicated",
"publishedUtc": "2026-01-10T10:00:00Z",
"verifiedUtc": "2026-01-12T14:00:00Z",
"communicatedUtc": "2026-01-15T09:00:00Z"
},
"changeOrigin": "<opaque>",
"externalRefs": [{ "namespace": "hubspot", "id": "deal-123456" }],
"changedFields": ["teaser.stage"]
}
}
Filter by External Ref
Subscribe to changes for specific CRM records:
# Webhook subscription with external ref filter
curl -X POST "https://app.propstreet.com/api/v1/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-system.com/webhooks/propstreet",
"events": ["project.updated", "prospect.updated"],
"filter": {
"externalRef.namespace": "hubspot"
}
}'
Sync Patterns
Initial Sync
Fetch all projects and their prospects:
async function initialSync(token) {
// 1. Fetch all projects
let cursor = null;
const projects = [];
do {
const url = new URL("https://app.propstreet.com/api/v1/projects");
url.searchParams.set("page_size", "500");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
const { data, page } = await response.json();
projects.push(...data);
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);
// 2. Fetch prospects for each project
for (const project of projects) {
const prospects = await fetchAllProspects(token, project.id);
await saveToDatabase(project, prospects);
}
return projects.length;
}
Delta Sync
Keep your CRM in sync with incremental updates:
async function deltaSync(token, lastSyncTime) {
const url = new URL("https://app.propstreet.com/api/v1/projects");
url.searchParams.set("updated_since", lastSyncTime.toISOString());
url.searchParams.set("include_deleted", "true");
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
const { data } = await response.json();
for (const project of data) {
if (project.deletedUtc) {
await markAsDeleted(project.id);
} else {
await upsertProject(project);
// Also sync prospects for updated projects
const prospects = await fetchAllProspects(
token,
project.id,
lastSyncTime
);
await syncProspects(project.id, prospects);
}
}
}
Filtering Prospects
Use filters to fetch specific subsets of prospects:
# Get only prospects with chat enabled
curl "https://app.propstreet.com/api/v1/projects/123/prospects?capability=chat" \
-H "Authorization: Bearer $TOKEN"
# Get only interested prospects
curl "https://app.propstreet.com/api/v1/projects/123/prospects?classification=interested" \
-H "Authorization: Bearer $TOKEN"
# Get prospects with a specific tag
curl "https://app.propstreet.com/api/v1/projects/123/prospects?tags=tier-1" \
-H "Authorization: Bearer $TOKEN"
CRM Integration Example
Sync Propstreet projects with your CRM deals/opportunities:
import requests
from datetime import datetime, timezone
class PropstreetSync:
def __init__(self, token):
self.base_url = "https://app.propstreet.com/api/v1"
self.headers = {"Authorization": f"Bearer {token}"}
def sync_projects_to_crm(self, last_sync: datetime):
"""Sync Propstreet projects to CRM deals."""
params = {
"updated_since": last_sync.isoformat(),
"include_deleted": "true"
}
response = requests.get(
f"{self.base_url}/projects",
headers=self.headers,
params=params
)
projects = response.json()["data"]
for project in projects:
if project.get("deletedUtc"):
self.crm.archive_deal(project["id"])
else:
asset = project["asset"]
self.crm.upsert_deal({
"external_id": project["id"],
"name": project["name"],
"stage": self.map_to_crm_stage(project),
"is_portfolio": asset["type"] == "portfolio",
"property_count": asset.get("propertyCount")
})
# Sync prospects as deal contacts
self.sync_prospects(project["id"])
def sync_prospects(self, project_id: str):
"""Sync prospects to CRM deal contacts."""
response = requests.get(
f"{self.base_url}/projects/{project_id}/prospects",
headers=self.headers
)
prospects = response.json()["data"]
for prospect in prospects:
contact_id = prospect.get("contactId") or prospect.get("companyId")
teaser = prospect.get("teaser")
self.crm.link_contact_to_deal(
deal_id=project_id,
contact_id=contact_id,
role=prospect.get("classification"),
is_communicated=teaser is not None and teaser.get("acceptedUtc") is not None
)
def map_to_crm_stage(self, project: dict) -> str:
"""Map project fields to CRM pipeline stage.
Uses teaser.stage for progress tracking since status is lifecycle only.
"""
if project["status"] == "closed":
return "Closed Won"
teaser = project.get("teaser")
teaser_stage = teaser.get("stage") if teaser else None
return {
None: "Qualification",
"property_added": "Qualification",
"drafting": "Qualification",
"published": "Proposal",
"verified": "Proposal",
"communicated": "Negotiation"
}.get(teaser_stage, "Qualification")
Error Handling
| Status | Meaning | Action |
|---|---|---|
| 400 | Invalid request (validation failed) | Check request body/parameters |
| 401 | Unauthorized | Check API credentials |
| 404 | Project or prospect not found | Verify IDs exist |
| 409 | Conflict (duplicate prospect) | Investor already exists as a prospect in this project |
| 412 | Precondition failed (ETag mismatch) | Refetch and retry with new ETag |
| 422 | Unprocessable entity (data integrity) | Contact support |
Related Documentation
- Sync Patterns Guide — Pagination and delta sync details
- Webhooks Guide — Real-time event notifications setup
- Idempotency Guide — Safe retries with idempotency keys
- Errors & Retries — Error handling patterns
- API Reference — Full endpoint specifications