Teasers

Anonymised property profiles attached to projects, properties, and post-NDA rooms — read + design-time write.

A teaser is the anonymised view of a project, property, or deal room. The project and property pre-market teasers are what investors see in their feed before signing an NDA; the room teaser is the post-NDA disclosure surface inside a deal room. All three strip broker-only details (exact address, owner identity, full contact list) and render a curated subset (categories, locality, headline metrics, optional photos) controlled by the broker through a visibility matrix.

The response shape is the same regardless of parent context — the parent determines authorization and lifecycle, not the field shape.

Quick Reference

Feature Details
Parent contexts project (broker-authored), property (investor pre-market), room (post-NDA per-room teaser)
Read endpoints GET /api/v1/projects/{id}/teaser, GET /api/v1/properties/{id}/teaser, GET /api/v1/projects/{id}/rooms/{roomId}/teaser
Write endpoints PATCH on the same three URIs — standard partial-update semantics
Concurrency ETag on every GET; If-Match optional on PATCH — omit to skip the precondition; 412 Precondition Failed only when supplied with a stale token
Stages property_addeddraftingpublishedverifiedcommunicated
Authorization project + room: broker-only; property: portfolio access (investor)
Lifecycle transitions Not available via the API (publish / unpublish / verify / communicate). Perform them in the Propstreet app.

How Teasers Work

A teaser sits between the broker (who owns the source data) and the investor (who consumes it pre-NDA). Three things determine what an investor actually sees:

  1. The parent contextproject, property, or room — picks the lifecycle and the authorization gate.
  2. The visibility matrix (options) — broker decides per field which display mode to expose: actual, hidden, span, or custom_span. On read responses, fields that aren't available for the current plan or property data appear as { "display": "unavailable" } — that value is read-only and cannot be set on a PATCH.
  3. The projection (projection) — what the investor view actually renders, after applying the matrix to the property's stored fields. Hidden fields are absent (not null) so the JSON shape mirrors the investor view.
┌────────────────────────────────────────────────────────────────────┐
│                          PARENT (one of)                            │
│                                                                     │
│  project (broker-authored)   property (pre-market)   room (post-NDA)│
│  ────────────────────────    ──────────────────────  ───────────────│
│  • broker drafts             • investor-initiated    • per-room     │
│  • published / verified      • portfolio teaser      • lives only   │
│  • communicated to           • virtual envelope when   in a deal-   │
│    matched investors           no real teaser yet      room context │
└────────────────────────────────────────────────────────────────────┘


                       TEASER (same DTO shape)
              ┌─────────────────────────────────────┐
              │  stage / publishedUtc / verifiedUtc │
              │  options  (visibility matrix)        │
              │  template (allowed display modes)    │
              │  projection (what investors see)     │
              │  nudge   (optional steerable hint)   │
              │  etag    (optimistic concurrency)    │
              └─────────────────────────────────────┘

Lifecycle stages

Stage Means
property_added Property has been linked but the broker hasn't started editing the teaser. Returned as a thin envelope on the property route when no real teaser row exists yet.
drafting Teaser row exists; not yet published. Investors don't see it.
published Broker has confirmed the matrix and prose. Locked from broad edits — unpublish to change.
verified A reviewer has checked the projection. Adds trust signal; same field shape as published.
communicated Distributed to the matched prospect list. Furthest along — communicatedUtc is the source of truth, takes precedence over earlier timestamps when deriving stage.

Stage precedence on the wire: communicatedUtc > verifiedUtc > publishedUtc > drafting > property_added. Always read the timestamps; don't infer from stage alone.

Nudges

When a prerequisite is missing or the current state blocks the obvious next action, the response includes a steerable hint:

"nudge": {
  "code": "no_property_attached",
  "message": "Attach a property to this project before drafting the teaser.",
  "navigateUri": "propstreet://project/123"
}

Branch on the code field (not the message text) — codes are stable across releases:

Code When
no_property_attached Project has no property linked yet — broker can't start drafting.
no_pre_market_yet Property has no pre-market record — virtual envelope returned.
transferred_to_other_broker The property's latest pre-market record was transferred to another broker — read still works, edits are blocked.
teaser_locked_unpublish_first Teaser is published / verified / communicated — unpublish before edits.
room_teaser_not_designed Room teaser hasn't been designed yet — a virtual default room teaser is returned alongside the nudge; the first PATCH saves a real room teaser.
no_investment_size The project has no investment size yet — set one before designing the teaser.
incomplete_property_data The property's data isn't ready yet — some required values are missing. Wait for data to finish loading, or complete the missing fields on the property.

Authorization by surface

Each teaser surface is gated on a different role:

Surface Required role What you'll see if wrong
GET/PATCH /api/v1/projects/{id}/teaser Broker 403 Forbidden (no body)
GET/PATCH /api/v1/projects/{id}/rooms/{roomId}/teaser Broker 403 Forbidden (no body)
GET/PATCH /api/v1/properties/{id}/teaser Investor 403 Forbidden (no body)

A 403 is returned without a ProblemDetails body — treat it as "the token's role does not match this surface." A 404 with a ProblemDetails body means "id not found or you don't have access to that specific row" and is returned only for callers whose role allows the surface.

If you're integrating from a CRM / back-office, mint a token whose role matches the surface you'll write to: brokers for project + room teasers, investors for property pre-market teasers.

REST API

GET /api/v1/projects/{id}/teaser

Returns the project's teaser. Broker-only.

GET /api/v1/projects/42/teaser HTTP/1.1
Authorization: Bearer ...

Response:

HTTP/1.1 200 OK
ETag: W/"abc123def456"
Content-Type: application/json
{
  "id": "8472",
  "createdUtc": "2026-04-10T08:00:00Z",
  "updatedUtc": "2026-04-12T10:24:00Z",
  "etag": "W/\"abc123def456\"",
  "stage": "published",
  "publishedUtc": "2026-04-12T10:24:00Z",
  "isPreMarket": false,
  "exclusivity": "exclusive",
  "title": "Office building (SE)",
  "tags": ["cbd", "long_leases"],
  "options": { /* visibility matrix */ },
  "template": { /* allowed display modes per field */ },
  "projection": { /* investor-facing rendered view */ },
  "navigateUri": "propstreet://project/42/teaser"
}

id, createdUtc, updatedUtc, and etag are always present on every teaser response. id is "0" on virtual / thin envelopes (e.g. nudge.code = no_pre_market_yet) when no real teaser row exists yet — cache by the parent ID, not by id.

The etag value in the body and the ETag response header are identical — use whichever is easier for your client to read.

GET /api/v1/properties/{id}/teaser

Returns the property's pre-market teaser, or a thin property_added envelope when no pre-market record exists yet (nudge.code = no_pre_market_yet). Investor-readable under the same portfolio access gate as the rest of /api/v1/properties/*.

GET /api/v1/projects/{id}/rooms/{roomId}/teaser

Returns the per-room teaser inside a deal-room. Broker-only. When the room teaser hasn't been designed yet, returns a virtual default room teaser plus nudge.code = room_teaser_not_designed so the client can prompt the broker to design one (the first PATCH pins a real row).

Writing teasers

The write surface is design-time only: brokers shape the visibility matrix, the tag snapshot, and the per-field display modes. Lifecycle transitions (publish, unpublish, verify, communicate, clone) are not available via the API — perform them in the Propstreet app.

PATCH endpoints

PATCH /api/v1/projects/{id}/teaser

PATCH /api/v1/properties/{id}/teaser

PATCH /api/v1/projects/{id}/rooms/{roomId}/teaser

Standard PATCH semantics: send only the fields you want to change. Omitted means "leave unchanged"; an explicit value (including null where allowed) means "set to this".

Set a tag filter and update two option fields:

PATCH /api/v1/projects/42/teaser HTTP/1.1
Authorization: Bearer ...
Content-Type: application/json
If-Match: W/"abc123def456"
{
  "tags": ["cbd", "long_leases"],
  "options": {
    "yield":        { "display": "span" },
    "rentalIncome": { "display": "hidden" }
  }
}

Reset the snapshot to the property's current template pool:

{ "tags": null }

Update a single field without touching anything else:

{
  "options": {
    "locality": { "display": "actual" }
  }
}

Field-by-field semantics on the PATCH body:

  • options — sparse map. Only keys present overwrite the current option for that field. Absent keys are untouched. To revert a single option to the template default, set it explicitly; you cannot "unset" a single option via omission.
  • tags — snapshot, see below.
  • The numeric span payload visible on GET responses is GET-only — it's derived from the property data; PATCH carries the display mode only.

tags — snapshot semantics

tags is a snapshot — the exact set the author chose to disclose to investors at write time. It is stored verbatim and rendered verbatim; the server never resolves it live against the property's current pool. If a tag is later added to or removed from the property, the snapshot on an existing teaser does not change — the author writes a new teaser to pick those changes up. This is load-bearing for the broker audit trail and the legal/disclosure story: what an investor saw at moment X is what was disclosed at moment X.

PATCH body GET reads back Meaning
tags omitted unchanged No change — the field is left alone. Default for any PATCH that doesn't mention it.
"tags": ["a","b"] "tags": ["a","b"] (after dedupe + clamp to the pool) Write snapshot — investor sees exactly these tags. Wire codes outside template.tags are rejected with 400.
"tags": [] "tags": [] Explicit empty snapshot — author chose to show no tags on this teaser.
"tags": null "tags": <materialised pool at write time> Reset to pool — sugar for "snapshot the property's current template.tags as-is." The server materialises the pool and stores it; no "follow live" mode.

Read back the response (tags) rather than assuming the input was preserved exactly — duplicates are deduped, and on the Free plan the pool may be silently clamped to empty before the snapshot lands.

The order of entries in tags is not part of the contract — clients that depend on a specific display order should sort client-side. Treat it as set-shaped.

Free-plan clamp

Brokers on the Free plan may PATCH options or tags that exceed Free-tier capabilities (e.g. span on a plan-restricted metric). The PATCH still returns HTTP 200 with the clamped result — the server silently downgrades the offending option to the closest permitted display mode and the response reflects the clamped state. There is no separate "outside your plan" error; re-read options and tags from the response.

Write preconditions

Precondition Status type
Project or property pre-market teaser has reached published / verified / communicated (room teasers exempt — they keep moving across the full lifecycle, including after verified / communicated) 409 /help/teaser-already-published
Body fails shape validation (unknown field, malformed display mode, etc.) 400 /help/validation-failed (default ValidationProblemDetails)
Body is empty ({} — no options or tags specified) 400 /help/teaser-patch-empty
Project has no property linked 409 /help/teaser-project-has-no-property (GET still 200 with nudge.code = no_property_attached)
Property's latest pre-market record has been transferred to another broker 409 /help/teaser-transferred-to-other-broker (GET still 200 with nudge.code = transferred_to_other_broker)

Auto-create on first edit

If you PATCH a property's teaser and no pre-market record exists yet, the server creates one in the same request and the response carries the new teaser. The same applies to a project room that doesn't have a per-room teaser yet. The operation is idempotent — parallel PATCHes on the same property or room converge on a single teaser.

Concurrency

Teasers use optimistic concurrency. Every read response carries an ETag (in both the HTTP header and the body's etag field). PATCH supports — but does not require — If-Match: send it to gate the write on the version you read, or omit it to skip the precondition. When If-Match is supplied and is stale, you get 412 Precondition Failed and the write is rejected.

How it works

  1. GET returns an ETag header with a version identifier.
  2. PATCH optionally includes the If-Match header carrying that value. Omit If-Match for fire-and-forget writes; include it for safe concurrent edits.
  3. If If-Match is supplied and the teaser changed since you fetched it, you get 412 Precondition Failed.
  4. Refetch, merge your changes, and retry with the new ETag.

Example

# 1. Fetch the teaser
curl "https://app.propstreet.com/api/v1/projects/42/teaser" \
  -H "Authorization: Bearer $TOKEN" \
  -i

Response (headers + body):

ETag: W/"abc123def456"
Content-Type: application/json

{
  "stage": "drafting",
  "options": { /* ... */ },
  "etag": "W/\"abc123def456\"",
  ...
}
# 2. Update with If-Match
curl -X PATCH "https://app.propstreet.com/api/v1/projects/42/teaser" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H 'If-Match: W/"abc123def456"' \
  -d '{ "tags": ["cbd", "long_leases"] }'

Handling 412 Precondition Failed

If someone else updated the teaser:

{
  "type": "urn:propstreet:error:precondition-failed",
  "title": "Precondition failed",
  "status": 412,
  "detail": "ETag mismatch - resource was modified"
}

Resolution:

  1. Refetch the teaser to get the latest ETag and current state.
  2. Merge your intended changes with the refreshed options and tags.
  3. Retry the PATCH with the new If-Match value.

Field reference

options — visibility matrix (broker-authored)

Per-field decision. Each option carries a closed enum of display modes. Examples:

"options": {
  "locality":       { "display": "actual" },
  "yield":          { "display": "span" },
  "investmentSize": { "display": "custom_span" },
  "rentalIncome":   { "display": "hidden" }
}

Each option entry has exactly one field — display. There is no embedded span value on the option itself. The numeric range that backs a span / custom_span display lives on the GET response under projection.<field> as a typed range DTO. For example, with options.yield.display = "span" and options.investmentSize.display = "custom_span":

"projection": {
  "yield":          { "min": 4.5, "max": 5.5 },                                 // PercentOrRangePublicDto
  "investmentSize": { "min": 100, "max": 200, "currency": "SEK", "scale": "millions" }  // PriceOrRangePublicDto
}

Display values on PATCH: actual, hidden, span, custom_span. Read responses additionally include unavailable (read-only) for fields that aren't available given the current plan or property data. The numeric range is GET-only — it's computed from the property data and surfaced on projection; PATCH carries the display mode only.

template — what the broker is allowed to choose

Mirrors options keys but lists the modes that are valid given the active subscription plan, the underlying property data, and the parent context. Use this to surface a UI for editing options without sending invalid selections.

projection — what investors see

Rendered view. Hidden fields are absent (not null) — so the JSON shape mirrors the investor view directly.

exclusivity — sales exclusivity (project parent only)

Mirrored from the parent project. Values: exclusive, non_exclusive, other. Null on property pre-market and room teasers (no parent project exclusivity to mirror).

Naming note. Aligned with the parent project resource (/api/v1/projects/{id}), which renamed mandate to exclusivity in April 2026 (see changelog). The teaser surface emits exclusivity natively — there is no deprecated mandate alias on the teaser response.

Privacy & boundaries

  • No broker identity surfaces. A teaser response never embeds the owning broker's name, contact, or organization. Investors see the projection only.
  • Field hiding is structural. A hidden field is omitted, not nulled — so consumers can't infer "had a value once" from a null. See Data Ownership for the broader rules.
  • Lifecycle transitions are unavailable. State changes (publish, unpublish, verify, communicate, clone) cannot be performed via this API — perform them in the Propstreet app.